4

I've a problem where I want to design a settings structure for my app that needs to be as optimal as possible in terms of localization, extension and grouping. I want to group settings per entity type (you can think of it as grouping settings per controller). Settings will get displayed to the user so each setting needs a pretty title and a description both of which need to be localized. New settings will only be introduced by the developers and a recompile will be needed.

What I came up with is a class that exposes settings as static properties so they are easily accessible to be used throughout the application in a static manner. The settings get loaded the first time the class is constructed (which happens when setting is requested) and I use database to store the settings and use reflection to assign them to their corresponding properties at runtime.

It looks like this

public class FirmSettings
{
    private static IFirmSettingsRepository _repository { get; set; }

    public static bool ShowInvoicePaymentDetails { get; set; }

    public static bool ShowInvoiceDiscountValue { get; set; }

    public static bool ShowDocumentComment { get; set; }

    public static bool ShowDocumentTaxStatement { get; set; }

    public static bool ShowDocumentAuthor { get; set; }

    #region Constructors

    /// <summary>
    ///     Initializes a new instance of the <see cref = "FirmSettings" /> class.
    /// </summary>
    static FirmSettings()
    {
        Load();
    }

    #endregion

    #region Load Settings

    public static void Load()
    {
        _repository = MvcApplication.Container.Get<IFirmSettingsRepository>();
        Type settingsType = typeof (FirmSettings);

        //------------------------------------------------------------
        //  Enumerate through individual settings nodes
        //------------------------------------------------------------
        StringDictionary dic = _repository.LoadSettings();

        if (dic == null)
        {
            Save(); // prepares the settings with blank settings
            dic = _repository.LoadSettings(); // reload
        }

        foreach (string key in dic.Keys)
        {
            //------------------------------------------------------------
            //  Extract the setting's name/value pair
            //------------------------------------------------------------
            string name = key;
            string value = dic[key];

            //------------------------------------------------------------
            //  Enumerate through public properties of this instance
            //------------------------------------------------------------
            foreach (PropertyInfo propertyInformation in settingsType.GetProperties(BindingFlags.Public |
                                                                                    BindingFlags.Static))
            {
                //------------------------------------------------------------
                //  Determine if configured setting matches current setting based on name
                //------------------------------------------------------------
                if (propertyInformation.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
                {
                    //------------------------------------------------------------
                    //  Attempt to apply configured setting
                    //------------------------------------------------------------
                    try
                    {
                        if (propertyInformation.CanWrite)
                        {
                            propertyInformation.SetValue(typeof (FirmSettings),
                                                         Convert.ChangeType(value, propertyInformation.PropertyType,
                                                                            CultureInfo.CurrentCulture), null);
                        }
                    }
                    catch
                    {
                        // TODO: Log exception to a common logging framework?
                    }
                    break;
                }
            }
        }

        // perform resave if there are any new settings
        Save();
    }

    #endregion

    #region Save settings

    /// <summary>
    ///     Saves the settings to disk.
    /// </summary>
    public static void Save()
    {
        StringDictionary dic = new StringDictionary();
        Type settingsType = typeof (FirmSettings);

        //------------------------------------------------------------
        //  Enumerate through settings properties
        //------------------------------------------------------------
        foreach (PropertyInfo propertyInformation in settingsType.GetProperties(BindingFlags.Public |
                                                                                BindingFlags.Static))
        {
            //------------------------------------------------------------
            //  Extract property value and its string representation
            //------------------------------------------------------------
            object propertyValue = propertyInformation.GetValue(typeof (FirmSettings), null);

            string valueAsString;
            //------------------------------------------------------------
            //  Format null/default property values as empty strings
            //------------------------------------------------------------
            if (propertyValue == null || propertyValue.Equals(Int32.MinValue) || propertyValue.Equals(Single.MinValue))
            {
                valueAsString = String.Empty;
            }
            else
            {
                valueAsString = propertyValue.ToString();
            }
            //------------------------------------------------------------
            //  Write property name/value pair
            //------------------------------------------------------------
            dic.Add(propertyInformation.Name, valueAsString);
        }

        _repository.SaveSettings(dic);
    }

    #endregion
}

Each setting is stored in DB as lowercased version of the property name (for loading we disregard the case). The same will go for localization string which will be stored as, for instance, FirmSettings_ShowDocumentTaxStatement_Title and FirmSettings_ShowDocumentTaxStatement_Desc. (convention)

This approach does not solve grouping however. In the UI, some sort of settings grouping will be required, so the Invoice settings would get displayed in a group. I could introduce a prefix for certain settings, then render it out based on the prefix (another convention).

Do you like this approach? If not, how do you do it? There's lots of conventions in this approach and that's what's bothering me, just a little.

John Farrell
  • 24,673
  • 10
  • 77
  • 110
mare
  • 13,033
  • 24
  • 102
  • 191

2 Answers2

1

You lost me here...

I see you use a Container of some sort, so why don't you just inject a singleton instance of that settings class every time you need reference to it? Static class + methods is bad for unit testing (which you will need for this).

Also, I don't get why you want to use Reflections/string matching for setting storage/retrieval. If you truly have significant amount of settings with complex grouping between them, you will need to invest the time to come up with a proper DAL.

Just a note, your "key" (eg. FirmSettings_ShowDocumentTaxStatement_Title) doesn't contain namespace, so if two classes have the same name and same method, you will end up with a bug that will be hard to catch. That's just a simple scenario. My point is, string matching your class + method name for the purpose of identification isn't a good idea. (Because I am assuming you have a big complicated project to resort to this kind of settings management.)

Lastly, "I don't know how else would you assign a value to static (or regular) properties at runtime other than with the use of reflection." You can use class/method/property attributes and have a factory class pump out (in your case) singleton of your desired settings class. The appropriate DB column/row association information can be contained in the attributes.

P.S. Reflection is fine in terms of performance. Just don't use static class, use singleton instead, and do a background task on initialization of the settings classes. Once the singleton are initiated, you won't have to initialize it again. But whatever you do, I strongly advise you to lose the string matching with class/method names.

P.P.S. Look into AoP / policy injection (or was it intervention in Microsoft Unity DI container). I believe those maybe of help to you?

P.P.P.S. Lastly into 3 post script is bad English...

Sleeper Smith
  • 3,212
  • 4
  • 28
  • 39
  • Thanks for your input about singleton, definitely worth checking it out and trying to go that route. I actually opened a new question about conccurency issue with singleton or static classes http://stackoverflow.com/questions/4382537/singleton-class-or-a-class-with-static-methods-in-asp-net-mvc-application – mare Dec 07 '10 at 22:57
0

IMHO the solution is too complicated. Try to come up with something simpler. First don't use reflection(performance reasons) and second I would use configuration files (xml) instead of DBs fo application/user settings.

See this comprehensive article about reflection and performance.

Liviu Mandras
  • 6,540
  • 2
  • 41
  • 65
  • 1
    I can't see the use of reflection causing performance problems here – Kev Hunter Dec 06 '10 at 14:54
  • @Kev In general reflection is slower and is to be avoided. Storing settings in my optinion is not something that requires reflection. It can be done in many different other ways. – Liviu Mandras Dec 06 '10 at 14:59
  • Decision has already been made to use DB so there will be no changes here. We want everything in DB because of easiear maintainence. – mare Dec 06 '10 at 17:46
  • Now for the reflection: I don't know how else would you assign a value to static (or regular) properties at runtime other than with the use of reflection. And we do need properties for settings because using magic strings and reading them from configuration files by accessing ConfigurationManager.AppSettings is error prone and undesirable. I should noted before that having strongly typed settings is one of the requirements. – mare Dec 06 '10 at 17:47