Como implementar um ConfigurationSection com um ConfigurationElementCollection

166

Estou tentando implementar uma seção de configuração personalizada em um projeto e continuo correndo contra exceções que não entendo. Espero que alguém possa preencher os espaços em branco aqui.

Eu tenho App.configque se parece com isso:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="ServicesSection" type="RT.Core.Config.ServicesConfigurationSectionHandler, RT.Core"/>
    </configSections>
    <ServicesSection type="RT.Core.Config.ServicesSection, RT.Core">
            <Services>
                <AddService Port="6996" ReportType="File" />
                <AddService Port="7001" ReportType="Other" />
            </Services>
        </ServicesSection>
</configuration>

Eu tenho um ServiceConfigelemento definido assim:

public class ServiceConfig : ConfigurationElement
  {
    public ServiceConfig() {}

    public ServiceConfig(int port, string reportType)
    {
      Port = port;
      ReportType = reportType;
    }

    [ConfigurationProperty("Port", DefaultValue = 0, IsRequired = true, IsKey = true)]
    public int Port 
    {
      get { return (int) this["Port"]; }
      set { this["Port"] = value; }
    }

    [ConfigurationProperty("ReportType", DefaultValue = "File", IsRequired = true, IsKey = false)]
    public string ReportType
    {
      get { return (string) this["ReportType"]; }
      set { this["ReportType"] = value; }
    }
  }

E eu tenho um ServiceCollectiondefinido assim:

public class ServiceCollection : ConfigurationElementCollection
  {
    public ServiceCollection()
    {
      Console.WriteLine("ServiceCollection Constructor");
    }

    public ServiceConfig this[int index]
    {
      get { return (ServiceConfig)BaseGet(index); }
      set
      {
        if (BaseGet(index) != null)
        {
          BaseRemoveAt(index);
        }
        BaseAdd(index, value);
      }
    }

    public void Add(ServiceConfig serviceConfig)
    {
      BaseAdd(serviceConfig);
    }

    public void Clear()
    {
      BaseClear();
    }

    protected override ConfigurationElement CreateNewElement()
    {
      return new ServiceConfig();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
      return ((ServiceConfig) element).Port;
    }

    public void Remove(ServiceConfig serviceConfig)
    {
      BaseRemove(serviceConfig.Port);
    }

    public void RemoveAt(int index)
    {
      BaseRemoveAt(index);
    }

    public void Remove(string name)
    {
      BaseRemove(name);
    }
  }

A parte que falta é o que fazer para o manipulador. Originalmente, tentei implementar um, IConfigurationSectionHandlermas encontrei duas coisas:

  1. não deu certo
  2. está obsoleto.

Agora estou completamente perdido sobre o que fazer para poder ler meus dados na configuração. Qualquer ajuda por favor!

Chris Holmes
fonte
Não consigo fazer isso funcionar. Eu adoraria ver RT.Core.Config.ServicesSection. Acabei de receber o elemento não reconhecido 'AddService', apesar de usar o código da resposta aceita também.
sirdank
Também perdi isso no começo - esta parte: [ConfigurationCollection (typeof (ServiceCollection), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")] O AddItemName deve corresponder, portanto, se você alterou "add" para "addService" funcionaria #
3037 HeatherD as

Respostas:

188

A resposta anterior está correta, mas também darei todo o código.

Seu app.config deve ficar assim:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
   <configSections>
      <section name="ServicesSection" type="RT.Core.Config.ServiceConfigurationSection, RT.Core"/>
   </configSections>
   <ServicesSection>
      <Services>
         <add Port="6996" ReportType="File" />
         <add Port="7001" ReportType="Other" />
      </Services>
   </ServicesSection>
</configuration>

Suas aulas ServiceConfige ServiceCollectionpermanecem inalteradas.

Você precisa de uma nova classe:

public class ServiceConfigurationSection : ConfigurationSection
{
   [ConfigurationProperty("Services", IsDefaultCollection = false)]
   [ConfigurationCollection(typeof(ServiceCollection),
       AddItemName = "add",
       ClearItemsName = "clear",
       RemoveItemName = "remove")]
   public ServiceCollection Services
   {
      get
      {
         return (ServiceCollection)base["Services"];
      }
   }
}

E isso deve fazer o truque. Para consumi-lo, você pode usar:

ServiceConfigurationSection serviceConfigSection =
   ConfigurationManager.GetSection("ServicesSection") as ServiceConfigurationSection;

ServiceConfig serviceConfig = serviceConfigSection.Services[0];
Russell McClure
fonte
10
As [Add|Remove|Clear]ItemNamepropriedades no ConfigurationCollectionatributo não são realmente necessárias neste caso, porque "add" / "clear" / "remove" já são os nomes padrão dos elementos XML.
Wim Coenen
2
Como posso fazê-lo funcionar para que as tags não sejam adicionadas? Parece funcionar apenas se forem adicionados. Não funcionaria se fosse <Service Port = "6996" ReportType = "File" /> ou <Service Port = "7001" ReportType = "Other" />
JonathanWolfson
7
@ JonathanWolfson: basta alterar AddItemName = "add" para AddItemName = "Service"
Mubashar
Essa ainda é a abordagem para o .NET 4.5?
crush
6
@ Crush: sim, não há muitas mudanças neste canto empoeirado do .NET.
22419 Russell McClure
84

Se você estiver procurando por uma seção de configuração personalizada, como a seguir

<CustomApplicationConfig>
        <Credentials Username="itsme" Password="mypassword"/>
        <PrimaryAgent Address="10.5.64.26" Port="3560"/>
        <SecondaryAgent Address="10.5.64.7" Port="3570"/>
        <Site Id="123" />
        <Lanes>
          <Lane Id="1" PointId="north" Direction="Entry"/>
          <Lane Id="2" PointId="south" Direction="Exit"/>
        </Lanes> 
</CustomApplicationConfig>

então você pode usar minha seção de implementação da configuração, para começar, adicione System.Configuration referência de montagem ao seu projeto

Olhe para cada elemento aninhado que eu usei. O primeiro é Credenciais com dois atributos, então vamos adicioná-lo primeiro

Elemento Credenciais

public class CredentialsConfigElement : System.Configuration.ConfigurationElement
    {
        [ConfigurationProperty("Username")]
        public string Username
        {
            get 
            {
                return base["Username"] as string;
            }
        }

        [ConfigurationProperty("Password")]
        public string Password
        {
            get
            {
                return base["Password"] as string;
            }
        }
    }

PrimaryAgent e SecondaryAgent

Ambos têm os mesmos atributos e parecem um endereço para um conjunto de servidores para um primário e um failover; portanto, você só precisa criar uma classe de elemento para os dois, como seguir

public class ServerInfoConfigElement : ConfigurationElement
    {
        [ConfigurationProperty("Address")]
        public string Address
        {
            get
            {
                return base["Address"] as string;
            }
        }

        [ConfigurationProperty("Port")]
        public int? Port
        {
            get
            {
                return base["Port"] as int?;
            }
        }
    }

Explicarei como usar dois elementos diferentes com uma classe posteriormente neste post. Vamos pular o SiteId, pois não há diferença. Você só precisa criar uma classe igual à anterior com apenas uma propriedade. vamos ver como implementar a coleção Lanes

ele é dividido em duas partes, primeiro você precisa criar uma classe de implementação de elemento e depois criar uma classe de elemento de coleção

LaneConfigElement

public class LaneConfigElement : ConfigurationElement
    {
        [ConfigurationProperty("Id")]
        public string Id
        {
            get
            {
                return base["Id"] as string;
            }
        }

        [ConfigurationProperty("PointId")]
        public string PointId
        {
            get
            {
                return base["PointId"] as string;
            }
        }

        [ConfigurationProperty("Direction")]
        public Direction? Direction
        {
            get
            {
                return base["Direction"] as Direction?;
            }
        }
    }

    public enum Direction
    { 
        Entry,
        Exit
    }

você pode observar que um atributo de LanElementé uma enumeração e se tentar usar qualquer outro valor na configuração que não esteja definido no aplicativo de enumeração ativará uma System.Configuration.ConfigurationErrorsExceptioninicialização. Ok, vamos para a definição de coleção

[ConfigurationCollection(typeof(LaneConfigElement), AddItemName = "Lane", CollectionType = ConfigurationElementCollectionType.BasicMap)]
    public class LaneConfigCollection : ConfigurationElementCollection
    {
        public LaneConfigElement this[int index]
        {
            get { return (LaneConfigElement)BaseGet(index); }
            set
            {
                if (BaseGet(index) != null)
                {
                    BaseRemoveAt(index);
                }
                BaseAdd(index, value);
            }
        }

        public void Add(LaneConfigElement serviceConfig)
        {
            BaseAdd(serviceConfig);
        }

        public void Clear()
        {
            BaseClear();
        }

        protected override ConfigurationElement CreateNewElement()
        {
            return new LaneConfigElement();
        }

        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((LaneConfigElement)element).Id;
        }

        public void Remove(LaneConfigElement serviceConfig)
        {
            BaseRemove(serviceConfig.Id);
        }

        public void RemoveAt(int index)
        {
            BaseRemoveAt(index);
        }

        public void Remove(String name)
        {
            BaseRemove(name);
        }

    }

você pode perceber que eu defini o que AddItemName = "Lane"você pode escolher o que quiser para o item de entrada da coleção. Prefiro usar "add" o padrão, mas o alterei apenas para fins deste post.

Agora todos os nossos elementos aninhados foram implementados agora, devemos agregar todos os da classe que precisa implementar System.Configuration.ConfigurationSection

CustomApplicationConfigSection

public class CustomApplicationConfigSection : System.Configuration.ConfigurationSection
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(CustomApplicationConfigSection));
        public const string SECTION_NAME = "CustomApplicationConfig";

        [ConfigurationProperty("Credentials")]
        public CredentialsConfigElement Credentials
        {
            get
            {
                return base["Credentials"] as CredentialsConfigElement;
            }
        }

        [ConfigurationProperty("PrimaryAgent")]
        public ServerInfoConfigElement PrimaryAgent
        {
            get
            {
                return base["PrimaryAgent"] as ServerInfoConfigElement;
            }
        }

        [ConfigurationProperty("SecondaryAgent")]
        public ServerInfoConfigElement SecondaryAgent
        {
            get
            {
                return base["SecondaryAgent"] as ServerInfoConfigElement;
            }
        }

        [ConfigurationProperty("Site")]
        public SiteConfigElement Site
        {
            get
            {
                return base["Site"] as SiteConfigElement;
            }
        }

        [ConfigurationProperty("Lanes")]
        public LaneConfigCollection Lanes
        {
            get { return base["Lanes"] as LaneConfigCollection; }
        }
    }

Agora você pode ver que temos duas propriedades com o nome PrimaryAgent eSecondaryAgent ambas têm o mesmo tipo. Agora você pode entender facilmente por que tínhamos apenas uma classe de implementação nesses dois elementos.

Antes de poder usar esta seção de configuração recém-inventada em seu app.config (ou web.config), você só precisa informar ao aplicativo que você inventou sua própria seção de configuração e respeitar um pouco, para isso, é necessário adicionar as seguintes linhas no app.config (pode ser logo após o início da marca raiz).

<configSections>
    <section name="CustomApplicationConfig" type="MyNameSpace.CustomApplicationConfigSection, MyAssemblyName" />
  </configSections>

NOTA: MyAssemblyName deve ficar sem .dll, por exemplo, se o nome do arquivo de montagem for myDll.dll, use myDll em vez de myDll.dll

para recuperar essa configuração, use a seguinte linha de código em qualquer lugar do seu aplicativo

CustomApplicationConfigSection config = System.Configuration.ConfigurationManager.GetSection(CustomApplicationConfigSection.SECTION_NAME) as CustomApplicationConfigSection;

Espero que o post acima o ajude a começar com um tipo um pouco complicado de seções de configuração personalizadas.

Happy Coding :)

**** Editar **** Para ativar o LINQ, LaneConfigCollectionvocê precisa implementarIEnumerable<LaneConfigElement>

E adicione a seguinte implementação de GetEnumerator

public new IEnumerator<LaneConfigElement> GetEnumerator()
        {
            int count = base.Count;
            for (int i = 0; i < count; i++)
            {
                yield return base.BaseGet(i) as LaneConfigElement;
            }
        }

para as pessoas que ainda estão confusas sobre como o rendimento realmente funciona, leia este belo artigo

Dois pontos-chave extraídos do artigo acima são

realmente não termina a execução do método. yield return pausa a execução do método e na próxima vez que você o chamar (para o próximo valor de enumeração), o método continuará sendo executado a partir da última chamada de retorno de rendimento. Parece um pouco confuso, eu acho ... (Shay Friedman)

O rendimento não é um recurso do tempo de execução .Net. É apenas um recurso da linguagem C # que é compilado no código IL simples pelo compilador C #. (Lars Corneliussen)

Mubashar
fonte
3
Obrigado por fornecer um exemplo completo, isso realmente ajuda muito!
John Leidegren
46

Este é um código genérico para a coleção de configurações:

public class GenericConfigurationElementCollection<T> :   ConfigurationElementCollection, IEnumerable<T> where T : ConfigurationElement, new()
{
    List<T> _elements = new List<T>();

    protected override ConfigurationElement CreateNewElement()
    {
        T newElement = new T();
        _elements.Add(newElement);
        return newElement;
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return _elements.Find(e => e.Equals(element));
    }

    public new IEnumerator<T> GetEnumerator()
    {
        return _elements.GetEnumerator();
    }
}

Depois de ter GenericConfigurationElementCollection, você pode simplesmente usá-lo na seção de configuração (este é um exemplo do meu Dispatcher):

public class  DispatcherConfigurationSection: ConfigurationSection
{
    [ConfigurationProperty("maxRetry", IsRequired = false, DefaultValue = 5)]
    public int MaxRetry
    {
        get
        {
            return (int)this["maxRetry"];
        }
        set
        {
            this["maxRetry"] = value;
        }
    }

    [ConfigurationProperty("eventsDispatches", IsRequired = true)]
    [ConfigurationCollection(typeof(EventsDispatchConfigurationElement), AddItemName = "add", ClearItemsName = "clear", RemoveItemName = "remove")]
    public GenericConfigurationElementCollection<EventsDispatchConfigurationElement> EventsDispatches
    {
        get { return (GenericConfigurationElementCollection<EventsDispatchConfigurationElement>)this["eventsDispatches"]; }
    }
}

O elemento Config é config Here:

public class EventsDispatchConfigurationElement : ConfigurationElement
{
    [ConfigurationProperty("name", IsRequired = true)]
    public string Name
    {
        get
        {
            return (string) this["name"];
        }
        set
        {
            this["name"] = value;
        }
    }
}

O arquivo de configuração ficaria assim:

<?xml version="1.0" encoding="utf-8" ?>
  <dispatcherConfigurationSection>
    <eventsDispatches>
      <add name="Log" ></add>
      <add name="Notification" ></add>
      <add name="tester" ></add>
    </eventsDispatches>
  </dispatcherConfigurationSection>

Espero que ajude!

Mzf
fonte
Legal! Estava pensando o mesmo e descobri que não estou sozinha. Gostaria que o MS implementasse isso para todas as configurações do FCL
abatishchev
Alguma sugestão sobre como fazer isso com um BasicMap para os itens? Não quero implementar o Add se puder evitá-lo.
SpaceCowboy74
28

Uma alternativa mais fácil para quem prefere não escrever manualmente toda essa configuração manualmente ...

1) Instale o Nerdle.AutoConfig partir do NuGet

2) Defina seu tipo de ServiceConfig (uma classe concreta ou apenas uma interface, também serve)

public interface IServiceConfiguration
{
    int Port { get; }
    ReportType ReportType { get; }
}

3) Você precisará de um tipo para armazenar a coleção, por exemplo

public interface IServiceCollectionConfiguration
{
    IEnumerable<IServiceConfiguration> Services { get; } 
}

4) Adicione a seção de configuração da seguinte maneira (observe a nomeação camelCase)

<configSections>
  <section name="serviceCollection" type="Nerdle.AutoConfig.Section, Nerdle.AutoConfig"/>
</configSections>

<serviceCollection>
  <services>
    <service port="6996" reportType="File" />
    <service port="7001" reportType="Other" />
  </services>
</serviceCollection>

5) Mapa com AutoConfig

var services = AutoConfig.Map<IServiceCollectionConfiguration>();
fearofawhackplanet
fonte
5
Graças a Deus por esta resposta
Svend
Para as pessoas que querem apenas fazê-lo e não necessariamente criar tudo do zero, esta é a verdadeira resposta :)
CodeThief
5

Tente herdar do ConfigurationSection . Esta postagem no blog de Phil Haack tem um exemplo.

Confirmado, de acordo com a documentação para IConfigurationSectionHandler :

No .NET Framework versão 2.0 e superior, você deve derivar da classe ConfigurationSection para implementar o manipulador de seção de configuração relacionado.

Jeff Ogata
fonte