views:

317

answers:

2

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?

+1  A: 

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?

itowlson
Thank you for your response! Realistically I'd rather that whatever I do, the whole process of populating the facade class is automatic and no work is required by the developer. Are there any pre-existing resources that you know of that can do this?
Dan Atkinson
Hmm, since you're in ASP.NET, you might be able to do this using the System.Web.Compilation namespace, which I think is what ASP.NET Web Site projects use to create their profile classes. Unfortunately, this is way outside my area of knowledge, and I'm not even sure if it works in Web Application (.csproj-based) projects. Sorry. Your other option is to look at VS custom tools (IVsSingleFileGenerator) but they're designed for a different scenario and will work only if your setting definitions are kept in their own special file.
itowlson
This - http://blog.jqweb.ca/?p=44 - looks like something I could use, but I'll need to adapt if to pull from the db, rather than the web.config... I'll update if/when I find a solution.
Dan Atkinson
Also, as I mentioned, T4 templates don't auto-update the generated code (at least so I believe -- correction requested if I'm wrong!). So you will need to remember to re-run the template when you change the settings schema.
itowlson
J Wynia has created a T4 template for this - see http://wynia.org/wordpress/2010/04/t4-template-for-appsettings-access-in-config-filesFor auto running the T4 template, you could possibly use Chirpy http://chirpy.codeplex.com/
Oskar Austegard
+1  A: 

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

#>
Cheburek
This is an interesting idea, but I'm not a big fan of CodeSmith to be honest. In the end, I wrote a class of my own which was required in any case as there was no way for my application to infer what type my appsettings were.
Dan Atkinson
I've added a bit of code to my post. Hope it could help you.
Cheburek
That's a really interesting solution! One thing it seemed to have trouble with (and this is VBs IsNumeric) is "0,5,0" which VB believes is a numeric value, although I'm not sure how!
Dan Atkinson
Interesting to know about such IsNumeric behavior.It seems as a solution to allow user input like that "1,000,000" but it's ugly. If you dealing with strings like "0,5,0" then it is possible to enhance number check. I.e. you could use following Regex pattern \d+(?:[,\.]\d+)?It must allow only single dot or comma
Cheburek