Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Auto-generate a strongly-typed AppSettings class

Here's the question first:

Is this possible? I'm taking my inspiration from Joe Wrobel's work (a redux of the forgotten Codeplex project). Here, you do your work on creating your profile for the provider, and it does the legwork of creating the strong typing for it, effectively creating a facade for the Profile class.

And now the back story!

I really don't like magic strings. They're pretty bad and can cause some serious issues when it comes to updating your application. Having worked in languages like PHP and ColdFusion, I know that it's easy to put them into your application and forget about them until you need to change one. And then you have to hunt each and every variation of them down and alter them accordingly.

.NET really isn't that much better if you follow the 'out of the box' application templates. Lots of examples out there use the appsettings in the web.config to store various settings. This is indeed a fine place to store, and is perfect for most applications. Problems start to arise however, when you start calling these directly - for example ConfigurationManager.AppSettings["MyAppSetting"]. Then you're not really any better off than a PHP user as you're back to using magic strings.

This is where facades come in. Facades offer a way of creating a strongly-typed object from a magic string in one place, and having the developer reference that from the rest of the application.

Now, instead of using a web.config to contain my appsettings, I use a database to hold them all. On application start, the name/value combos are retrieved, and are then sequentially added to the ConfigurationManager.AppSettings via Set. No biggie (apart from the problem I had earlier!).

This 'application facade' is accessible by my data layer, service layer and presentation layer and holds things like the application mode, which service endpoint to use yada yada yada and limits the need for having to hunt for many magic strings, down to two magic strings - one (the name) in the facade, and the other (the name and value) in the point of creation (which, for me is the db).

This facade class will eventually get pretty big and I'll eventually get tired of having to update both of them.

So what I'd like to do is have an ApplicationFacade class which auto-generates every time a build is done. And now back to the beginning... Is this possible?

like image 439
Dan Atkinson Avatar asked Oct 18 '09 00:10

Dan Atkinson


3 Answers

You could also use CodeSmith templates for this purpose. Advantage is that you can set in template file properties to be regenerated on each build (set BuildAction = "Complile")

Edited I also looked for such solution. After googling I found base T4 template to generate such a class. I have redesigned it and you can find it below.

Template is generating wrapper class for appSetting section from your Web.config/App.config file

Suppose you have following lines of settings in config file

  <appSettings>
    <add key="PageSize" value="20" />
    <add key="CurrentTheme" value="MyFavouriteTheme" />
    <add key="IsShowSomething" value="True" />
  </appSettings>

After processing template you will get following class

namespace MyProject.Core
{
    /// <remarks>
    /// You can create partial class with the same name in another file to add custom properties
    /// </remarks>
    public static partial class SiteSettings 
    {
        /// <summary>
        /// Static constructor to initialize properties
        /// </summary>
        static SiteSettings()
        {
            var settings = System.Configuration.ConfigurationManager.AppSettings;
            PageSize = Convert.ToInt32( settings["PageSize"] );
            CurrentTheme = ( settings["CurrentTheme"] );
            IsShowSomething = Convert.ToBoolean( settings["IsShowSomething"] );
        }

        /// <summary>
        /// PageSize configuration value
        /// </summary>
        public static readonly int PageSize;

        /// <summary>
        /// CurrentTheme configuration value
        /// </summary>
        public static readonly string CurrentTheme;

        /// <summary>
        /// IsShowSomething configuration value
        /// </summary>
        public static readonly bool IsShowSomething;

    }
}

Save following code to *.tt file and include to your project where you want to put generated file. To regenerate class on each build see my answer here Template recognize string, datetime, int and bool types from values

<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="Microsoft.VisualBasic" #>
<#@ template language="VB" debug="True" hostspecific="True"  #>
<#@ output extension=".Generated.cs" #>
<#
    Dim projectNamespace as String = "MyProject.Core"
    Dim className as String = "SiteSettings"
    Dim fileName as String = "..\..\MyProject.Web\web.config"

    Init(fileName)  

#>
//------------------------------------------------------------------------------
// FileName = <#= path #>
// Generated at <#= Now.ToLocaltime() #>
//
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
//     
//    NOTE: Please use the Add a Reference to System.Configuration assembly if 
//          you get compile errors with ConfigurationManager
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Configuration;

namespace <#= projectNamespace #>
{
    /// <remarks>
    /// You can create partial class with the same name in another file to add custom properties
    /// </remarks>
    public static partial class <#= className #> 
    {
        /// <summary>
        /// Static constructor to initialize properties
        /// </summary>
        static <#= className #>()
        {
            var settings = System.Configuration.ConfigurationManager.AppSettings;
<#= AddToCostructor(path) #>        }

<#= RenderApplicationSettings(path) #>  }
}

<#+ 
    Dim path as String = ""
    Dim doc as XDocument = Nothing

    Public Sub Init(fileName as String)
        Try
            path = Host.ResolvePath(fileName)
            If File.Exists(path) Then
                doc = XDocument.Load(path)
            End If
        Catch
            path = "<< App.config or Web.config not found within the project >>"
        End Try     
    End Sub

    Public Function AddToCostructor(ByVal path as String) as String                 
        If doc Is Nothing Then Return ""

        Dim sb as New StringBuilder()

        For Each result as XElement in doc...<appSettings>.<add>            
            sb.Append(vbTab).Append(vbTab).Append(vbTab)
            sb.AppendFormat("{0} = {1}( settings[""{0}""] );", result.@key, GetConverter(result.@value))
            sb.AppendLine()
        Next

        Return sb.ToString()

    End Function

    Public Function RenderApplicationSettings(ByVal path as String) as String
        If doc Is Nothing Then Return ""

        Dim sb as New StringBuilder()       

        For Each result as XElement in doc...<appSettings>.<add>    
            dim key = result.@key
            sb.Append(vbTab).Append(vbTab)
            sb.Append("/// <summary>").AppendLine()
            sb.Append(vbTab).Append(vbTab)
            sb.AppendFormat("/// {0} configuration value", key).AppendLine()            
            sb.Append(vbTab).Append(vbTab)
            sb.Append("/// </summary>").AppendLine()
            sb.Append(vbTab).Append(vbTab)
            sb.AppendFormat("public static readonly {0} {1}; ", GetPropertyType(result.@value), key)    
            sb.AppendLine().AppendLine()
        Next

        Return sb.ToString()

    End Function

    Public Shared Function GetConverter(ByVal prop as String) as String     
        If IsNumeric(prop) Then Return "Convert.ToInt32"
        If IsDate(prop) Then Return "Convert.ToDateTime"
        dim b as Boolean
        If Boolean.TryParse(prop, b) Then Return "Convert.ToBoolean"        
        Return ""
    End Function

    Public Shared Function GetPropertyType(ByVal prop as String) as String
        If IsNumeric(prop) Then Return "int"
        If IsDate(prop) Then Return "DateTime"
        dim b as Boolean
        If Boolean.TryParse(prop, b) Then Return "bool"
        Return "string"
    End Function

#>
like image 144
Cheburek Avatar answered Nov 20 '22 01:11

Cheburek


You could do this with a pre-build step. This is reasonably easy to do -- just write a program or script or template that regenerates the class, and call it in your pre-build event -- but this will give you red wigglies and no intellisense on any new members until the class gets regenerated.

A slightly more manual, but probably more convenient, approach would be to create a T4 template and include that in your project. You would however need to remember to re-transform the template every time you added a new setting. Would this be too onerous?

like image 39
itowlson Avatar answered Nov 19 '22 23:11

itowlson


@Cheburek Great work

Here is C# port

<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ template language="C#" debug="True" hostspecific="True"  #>
<#@ output extension=".Generated.cs" #>
<#
    var projectNamespace = "SandBoxLib";
    var className  = "AppSettings";
    var fileName  = "app.config";

    Init(fileName);

#>
//------------------------------------------------------------------------------
// FileName = <#= path #>
// Generated at <#= DateTime.UtcNow.ToLocalTime() #>
//
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
//     
//    NOTE: Please use the Add a Reference to System.Configuration assembly if 
//          you get compile errors with ConfigurationManager
// </auto-generated>
//------------------------------------------------------------------------------

using System;

namespace <#= projectNamespace #>
{
    /// <remarks>
    /// You can create partial class with the same name in another file to add custom properties
    /// </remarks>
    public static partial class <#= className #> 
    {
        /// <summary>
        /// Static constructor to initialize properties
        /// </summary>
        static <#= className #>()
        {
            var settings = System.Configuration.ConfigurationManager.AppSettings;
<#= AddToCostructor() #>        }

<#= RenderApplicationSettings() #>  }
}

<#+ 
    private string path = "";
    private XDocument doc;

    public void Init(string fileName){
        try{
            path = Host.ResolvePath(fileName);
            if (File.Exists(path)){
                doc = XDocument.Load(path);
            }
        }
        catch{
            path = "<< App.config or Web.config not found within the project >>";
        }
    }

    public string AddToCostructor(){
        if (doc == null) return "";

        var sb = new StringBuilder();

        foreach (var elem in doc.Descendants("appSettings").Elements()){
            var key = GetAttributeValue(elem, "key");
            var val = GetAttributeValue(elem, "value");
            sb.Append("\t").Append("\t").Append("\t");
            sb.AppendFormat("{0} = {1}( settings[\"{0}\"] );", key, GetConverter(val));
            sb.AppendLine();
        }

        return sb.ToString();
    }

    public string RenderApplicationSettings(){
        if (doc == null) return "";

        var sb = new StringBuilder();

        foreach (var elem in doc.Descendants("appSettings").Elements()){    
            var key = GetAttributeValue(elem, "key");
            var val = GetAttributeValue(elem, "value");

            sb.Append("\t").Append("\t");
            sb.AppendFormat("public static readonly {0} {1}; ", GetPropertyType(val), key);
            sb.AppendLine().AppendLine();
        }

        return sb.ToString();
    }

    public string GetConverter(string value){
        if (IsNumeric(value)) return "Convert.ToInt32";
        if (IsDate(value)) return "Convert.ToDateTime";
        if (IsBool(value)) return "Convert.ToBoolean";
        return "string";
    }

    public string GetPropertyType(string value){
        if (IsNumeric(value)) return "int";
        if (IsDate(value)) return "DateTime";
        if (IsBool(value)) return "bool";
        return "string";
    }

    private string GetAttributeValue(XElement elem, string attributeName){
        return elem.Attribute(attributeName).Value;
    }

    private bool IsNumeric(string value){
        return int.TryParse(value, out var r);
    }

    private bool IsDate(string value){
        return DateTime.TryParse(value, out var r);
    }

    private bool IsBool(string value){
        return Boolean.TryParse(value, out var r);
    }
#>
like image 24
Sherlock Avatar answered Nov 20 '22 00:11

Sherlock