views:

171

answers:

1

I've searched the site and haven't found exactly what I'm looking for. Close, but no cigar.

Basically I want to have a config section like this:

    <configSections>
        <section name="PhoneNotificationsSection" type="Alerts.PhoneAlertConfigSection,Alerts,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"/>    
    </configSections>
    <PhoneNotificationsSection>
        <phones>
            <add phone="MyMobile" value="[email protected]" />
            <add phone="OtherMobile" value="[email protected]" />
        </phones>
    </PhoneNotificationsSection>

Then I'd like to, in my appSettings consuming code, be able to write something like this (pseudo code):

foreach (phone p in phones)
{
   //'phone' attribute is just helpful/descriptive
   DoSomething(p.value);
}

I've done enough research to know I probably need a few of my own classes that implement and/or inherit from certain Configuration classes to make the above code possible. I just haven't found anything that clearly demonstrates this scenario and how to code for it - and when I try to learn the whole .NET configuration world my brain starts to hurt. Anyone have some code like what I'm looking for that they can share?

+1  A: 

I've written something similar once, as an example for a C# course. In my opinion it mainly demonstrates how awful the .NET configuration subsystem is, although the code does work. I've not adapted it to your settings, as it's fairly easy to introduce a mistake and so far the SO editor does not validate posted code samples ;)

First, the configuration section declaration:

<configSections>
    <section name="passwordSafe"
             type="Codeworks.PasswordSafe.Model.Configuration.PasswordSafeSection, Codeworks.PasswordSafe.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</configSections>

<passwordSafe hashAlgorithm="SHA256">
    <users>
        <user name="mm" password="Jok2eyBcFs4y7UIAlCuLix4mLfxw2byfvHfElpmk8d8=" />
        <user name="joe" password="Jok2eyBcFs4y7UIAlCuLix4mLfxw2byfvHfElpmk8d8=" />
    </users>
</passwordSafe>

To match the above snippet we first need the configuration section:

public class PasswordSafeSection : ConfigurationSection
{
    #region Static Accessors
    /// <summary>
    /// Gets the configuration section using the default element name.
    /// </summary>
    public static PasswordSafeSection GetSection()
    {
        return GetSection( "passwordSafe" );
    }

    /// <summary>
    /// Gets the configuration section using the specified element name.
    /// </summary>
    public static PasswordSafeSection GetSection( string sectionName )
    {
        PasswordSafeSection section = ConfigurationManager.GetSection( sectionName ) as PasswordSafeSection;
        if( section == null )
        {
            string message = string.Format( "The specified configuration section (<{0}>) was not found.", sectionName );
            throw new ConfigurationErrorsException( message );
        }    
        return section;
    }
    #endregion

    #region Configuration Properties
    [ConfigurationProperty( "hashAlgorithm" )]
    public string HashAlgorithm
    {
        get { return (string) this[ "hashAlgorithm" ]; }
        set { this[ "hashAlgorithm" ] = value; }
    }

    [ConfigurationProperty( "users", IsDefaultCollection=true )]
    public UserElementCollection Users
    {
        get { return (UserElementCollection) this[ "users" ]; }
        set { this[ "users" ] = value; }
    }

    public override bool IsReadOnly()
    {
        return false;
    }
    #endregion
}

We are using a custom element collection, so let's declare that too:

[ConfigurationCollection( typeof(UserElement), CollectionType = ConfigurationElementCollectionType.BasicMap )]
public class UserElementCollection : ConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new UserElement();
    }

    protected override string ElementName
    {
        get { return "user"; }
    }
    public override ConfigurationElementCollectionType CollectionType
    {
        get { return ConfigurationElementCollectionType.BasicMap; }
    }

    public override bool IsReadOnly()
    {
        return false;
    }

    #region Indexers
    public UserElement this[ int index ]
    {
        get { return BaseGet( index ) as UserElement; }
        set
        {
            if( BaseGet( index ) != null )
            {
                BaseRemoveAt( index );
            }
            BaseAdd( index, value );
        }
    }

    public new UserElement this[ string name ]
    {
        get { return BaseGet( name ) as UserElement; }
    }
    #endregion

    #region Lookup Methods
    protected override object GetElementKey( ConfigurationElement element )
    {
        UserElement user = element as UserElement;
        return user != null ? user.UserName : "error";
    }

    public string GetKey( int index )
    {
        return (string) BaseGetKey( index );
    }
    #endregion

    #region Add/Remove/Clear Methods
    public void Add( UserElement item )
    {
        BaseAdd( item );
    }

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

    public void Remove( UserElement item )
    {
        BaseRemove( GetElementKey( item ) );
    }

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

    public void Clear()
    {
        BaseClear();
    }
    #endregion
}

And finally we need to declare the custom element used in the element collection:

public class UserElement : ConfigurationElement
{
    #region Constructors
    public UserElement()
    {
    }

    public UserElement( string userName, string passwordHash )
    {
        UserName = userName;
        PasswordHash = passwordHash;
    }
    #endregion

    #region Configuration Properties
    [ConfigurationProperty( "name", IsKey = true )]
    public string UserName
    {
        get { return (string) this[ "name" ]; }
        set { this[ "name" ] = value; }
    }

    [ConfigurationProperty( "password", IsRequired = true )]
    public string PasswordHash
    {
        get { return (string) this[ "password" ]; }
        set { this[ "password" ] = value; }
    }

    public override bool IsReadOnly()
    {
        return false;
    }
    #endregion
}

Now, having all this in place we're ready to access the configuration file. I'm using a Configurator helper class to make this slightly less cumbersome:

public static class Configurator
{
    #region AppSettings Helpers
    public static int SplashScreenDisplayTime
    {
        get { return Convert.ToInt32( ConfigurationManager.AppSettings[ "splash.display.msecs" ] ); }
    }
    #endregion

    #region User Helpers
    public static bool TryGetUserPasswordHash( string userName, out string passwordHash )
    {
        UserElement user = GetUser( userName );
        passwordHash = user != null ? user.PasswordHash : null;
        return ! string.IsNullOrEmpty( passwordHash );
    }

    private static UserElement GetUser( string userName )
    {
        SystemConfiguration config = GetConfiguration( ConfigurationUserLevel.PerUserRoamingAndLocal );
        PasswordSafeSection section = config.Sections[ "passwordSafe" ] as PasswordSafeSection;
        return section.Users[ userName ];
    }
    public static void AddUser( string userName, string passwordHash, string encryptionKey )
    {
        SystemConfiguration config = GetConfiguration( ConfigurationUserLevel.PerUserRoamingAndLocal );
        PasswordSafeSection section = config.Sections[ "passwordSafe" ] as PasswordSafeSection;
        UserElement user = section.Users[ userName ];
        if( user == null )
        {
            user = new UserElement( userName, passwordHash, encryptionKey );
            section.Users.Add( user );
            config.Save( ConfigurationSaveMode.Modified );
        }
    }
    public static void RemoveUser( string userName )
    {
        SystemConfiguration config = GetConfiguration( ConfigurationUserLevel.PerUserRoamingAndLocal );
        PasswordSafeSection section = config.Sections[ "passwordSafe" ] as PasswordSafeSection;
        section.Users.Remove( userName );
        config.Save( ConfigurationSaveMode.Modified );
    }
    public static void UpdateUser( string userName, string passwordHash )
    {
        SystemConfiguration config = GetConfiguration( ConfigurationUserLevel.PerUserRoamingAndLocal );
        PasswordSafeSection section = config.Sections[ "passwordSafe" ] as PasswordSafeSection;
        UserElement user = section.Users[ userName ];
        if( user != null )
        {
            user.PasswordHash = passwordHash;
            config.Save( ConfigurationSaveMode.Modified );
        }
    }
    #endregion

    #region Configuration Helpers
    private static SystemConfiguration GetConfiguration( ConfigurationUserLevel userLevel )
    {
        SystemConfiguration config = InitializeConfiguration( userLevel );
        return config;
    }

    private static SystemConfiguration InitializeConfiguration( ConfigurationUserLevel userLevel )
    {    
        SystemConfiguration config = ConfigurationManager.OpenExeConfiguration( userLevel );
        PasswordSafeSection section = config.Sections[ "passwordSafe" ] as PasswordSafeSection;
        if( section == null )
        {
            section = new PasswordSafeSection();
            section.SectionInformation.AllowExeDefinition = ConfigurationAllowExeDefinition.MachineToLocalUser;
            section.SectionInformation.ForceSave = true;
            config.Sections.Add( "passwordSafe", section );
            config.Save( ConfigurationSaveMode.Full );
        }
        return config;
    }
    #endregion
}

Hope this helps.

Morten Mertner
Having trouble getting this code to compile - the last 2 methods in the last code snippet that return 'SystemConfiguration' objects - compiler doesn't recognize the SystemConfiguration class and thinks I'm missing an assembly reference. Where is that class defined? Do I need a certain 'Using' declaration?
jamauss
Also - Can you show me an example of how you use this code to access the <user> elements in a foreach loop/statement? What I've tried so far is returning an empty ElementCollection
jamauss
Nevermind, I got it working - just had to change all instances of 'SystemConfiguration' to 'Configuration'. Thanks!
jamauss
Glad you got it worked out. Let me know if you hit another snag.
Morten Mertner