Click here to Skip to main content
Click here to Skip to main content

Strongly typed AppSettings with MSBuild

By , 18 Oct 2009
 

Introduction

I see that code generation is fashion out there... But there is something most developers overlook. And it's called MSBuild. What does MSBuild have to do with code generation? In fact, most of you already know that when you add the x:Name attribute to a XAML object, you can access it from the code-behind, it's like magic. And obviously, there is no magic. What if you could do that yourself easily? I'm excited to know what you will imagine with that possibility. 

My goal is to show you how cool MSBuild is with code generation. For that, I will create a strongly typed appSettings.

to:

There is no magic

I recommend the book Build Engine: Using MSBuild and Team Foundation Build, by Sayed Ibrahim Hashimi, if you are interested in MSBuild. It's a cool compilation of tips, tricks, and references in a single place. This gave me a lot of ideas. 

When you add the x:Name on a XAML element, you can access this objet in the code behind. And in fact, the build process generates a *.g.cs in the intermediate directory of your project (/obj), and links these files dynamically to your intellisense.

In fact, you can do that yourself in three lines with MSBuild. Just create a .cs (MyMagicalClass.cs) file, and put it in the same directory as your project. Then, add this at the end of your .csproj file (at the end, and that is important). Do not include the code file in your project. I just want you to copy it to the same directory as the project file.

<target name="BeforeBuild">
    <ItemGroup>
        <Compile Include="MyMagicalClass.cs" />
    </ItemGroup>
</target>

Now, compile the first time, and you'll have the intellisense on this file, even if it's not shown in your project in the Solution Explorer.

Quickly, I will explain how this is possible. When you hit "Build" in Visual studio, Visual studio calls the target called "Build" on your project file. But the build target has some dependencies which will be executed before the Build target executes. Let's see what the dependencies are in C:\Windows\Microsoft.NET\Framework\v3.5\Microsoft.Common.targets.

<PropertyGroup>
    <BuildDependsOn>
    BeforeBuild;
    CoreBuild;
    AfterBuild
    </BuildDependsOn>
</PropertyGroup>
<target name="Build" 
   condition=" '$(_InvalidConfigurationWarning)' != 'true' " 
   dependsontargets="$(BuildDependsOn)"
outputs="$(TargetPath)" />

It seems that the build depends on BeforeBuild.

<Target Name="BeforeBuild"/>

And that this target is empty.

In reality, when you add a Target called BeforeBuild at the end of your project, you tell MSBuild to override any previously defined Target called BeforeBuild (you can even override what a "Build" is).

The code we wrote earlier in the .csproj, just tells MSBuild to "add the file in the compilation". Every time you add a .cs file in your project, a new Compile item is included in your .csproj, this is why they are shown in the Solution Explorer.

However, compile items added dynamically through the invocation of a target are not shown in the Solution Explorer... Nevertheless, they are compiled, and interpreted by the intellisense.

So now, I will show you a cool thing: generate a class during compilation depending on the app.config file, and integrate the class in the compilation. Let's go...

Now you get the idea, let's try to create an AppSettings class generator...

Our goal will be to be able to reference, with a strongly typed fashion, AppSettings items in the App.config file.

I will not enter into the nasty details of the implementation of my class, the interesting point remains the integration with MSBuild. The interesting thing in this code is the WriteClass method witch generates the code with CodeDOM from the config file passed to the constructor.

public enum Language
{
    CSharp,
    VB
}
public class Generator
{
    readonly string _ConfigFile;
    public Generator(string configFile)
    {
        if(configFile == null)
            throw new ArgumentNullException("configFile");
        if(!File.Exists(configFile))
            throw new FileNotFoundException(configFile, configFile);
        _ConfigFile = configFile;
        NameSpace = "";
        ClassName = "Settings";
        Language = Language.CSharp;
    }

    public String NameSpace
    {
        get;
        set;
    }

    public String ClassName
    {
        get;
        set;
    }

    public Language Language
    {
        get;
        set;
    }
    CodeDomProvider CreateProvider()
    {
        switch(Language)
        {
            case Language.CSharp:
                return new Microsoft.CSharp.CSharpCodeProvider();
            case Language.VB:
                return new Microsoft.VisualBasic.VBCodeProvider();
            default:
                throw new NotSupportedException();
        }
    }

    public void WriteClass(TextWriter writer)
    {
        var config = 
            ConfigurationManager.OpenMappedExeConfiguration(
            new ExeConfigurationFileMap()
        {
            ExeConfigFilename = _ConfigFile
        }, ConfigurationUserLevel.None);

        CodeNamespace ns = new CodeNamespace(NameSpace);
        CodeTypeDeclaration type = new CodeTypeDeclaration(ClassName);
        ns.Types.Add(type);

        foreach(KeyValueConfigurationElement setting in config.AppSettings.Settings)
        {
            CodeMemberProperty property = new CodeMemberProperty();
            property.HasSet = false;
            property.HasGet = true;
            property.Attributes = MemberAttributes.Static | MemberAttributes.Public;
            property.Type = new CodeTypeReference(typeof(String));
            property.Name = setting.Key;

            var getSettingCall =
                new CodeIndexerExpression(
                    new CodePropertyReferenceExpression(
                        new CodeTypeReferenceExpression(typeof(ConfigurationManager)),
                        "AppSettings"),
                    new CodePrimitiveExpression(setting.Key));
            property.GetStatements.Add(new CodeMethodReturnStatement(getSettingCall));
            type.Members.Add(property);
        }

        var provider = CreateProvider();
        provider.GenerateCodeFromNamespace(ns, writer, new CodeGeneratorOptions());
    }
}

For example, this config file:

<configuration>
    <appSettings>
        <add key="val1" value="value"/>
        <add key="val2" value="value2"/>
        <add key="val3" value="value2"/>
        <add key="hello" value="value2"/>
        <add key="hello3" value="value2"/>
        <add key="hello6" value="value2"/>
    </appSettings>
</configuration>

will generate this class:

public class Settings {
    
    public static string val1 {
        get {
            return System.Configuration.ConfigurationManager.AppSettings["val1"];
        }
    }
    
    public static string val2 {
        get {
            return System.Configuration.ConfigurationManager.AppSettings["val2"];
        }
    }
    
    public static string val3 {
        get {
            return System.Configuration.ConfigurationManager.AppSettings["val3"];
        }
    }
    
    public static string hello {
        get {
            return System.Configuration.ConfigurationManager.AppSettings["hello"];
        }
    }
    
    public static string hello3 {
        get {
            return System.Configuration.ConfigurationManager.AppSettings["hello3"];
        }
    }
    
    public static string hello6 {
        get {
            return System.Configuration.ConfigurationManager.AppSettings["hello6"];
        }
    }
}

...And integrate it in the build process

First, I just need to create a custom Task in MSBuild and implement the Execute method. Every logged error will appear in the error window of Visual Studio. (To build a Task in MsBuild, you need to reference the Microsoft.Build.* assemblies.)

As you can see... it's a wrapper around my Generator:

public class ClassGeneratorTask : Task
{
    public ClassGeneratorTask()
    {
        Language = "CS";
    }
    [Required]
    public ITaskItem FilePath
    {
        get;
        set;
    }


    public String Language
    {
        get;
        set;
    }

    [Required]
    public ITaskItem ConfigFile
    {
        get;
        set;
    }

    Language? GetLanguage()
    {
        if("CS".Equals(Language, StringComparison.InvariantCultureIgnoreCase))
            return SettingsClassGenerator.Language.CSharp;
        if("VB".Equals(Language, StringComparison.InvariantCultureIgnoreCase))
            return SettingsClassGenerator.Language.VB;
        return null;
    }

    public override bool Execute()
    {
        var language = GetLanguage();
        if(language == null)
        {
            Error(Language + " is not a valid language, specify CS or VB");
            return false;
        }
        try
        {
            string text = File.ReadAllText(ConfigFile.ItemSpec);
        }
        catch(IOException ex)
        {
            Error("Error when trying to open config file : " + " " + 
                  ConfigFile.ItemSpec + " " + ex.Message);
            return false;
        }
        try
        {
            using(var generatedWriter = 
                  new StreamWriter(File.Open(FilePath.ItemSpec, FileMode.Create)))
            {
                Generator gen = new Generator(ConfigFile.ItemSpec);
                gen.Language = language.Value;
                gen.WriteClass(generatedWriter);
                generatedWriter.Flush();
            }
        }
        catch(IOException ex)
        {
            Error("Error when accessing to " + FilePath.ItemSpec + 
                  " " + ex.Message);
            return false;
        }
        return true;
    }

    public void Error(string message)
    {
        Log.LogError(message);
    }
}

ConfigFile, FilePath, and Language are properties passed when we call the task. FilePath is the path to the generated file. You can see that the type of ConfigFile and FilePath are ITaskItem, and it would work if it was just a string. However, ITaskItem has more information, like the MSBuild metadata. In reality, I don't need it, and I just could use string instead, but I feel it's a good practice to use ITaskItem. After all, it's closer to the domain of my class (which is MSBuild).

Now, we want a simple way to execute this task at the right moment on the app.config file. For that, I just need to create a .targets file which is just a classic MSBuild project file we'll import later in the csproj files.

Note: SettingsGeneratorTask.dll is the assembly containing the MSBuild task shown earlier. This assembly must be in the same directory as the .targets file.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
    <UsingTask AssemblyFile="SettingsGeneratorTask.dll" TaskName="ClassGeneratorTask"/>

    <ItemGroup>
        <SettingsGenerated Include="$(IntermediateOutputPath)AppSettings.g.cs">
        </SettingsGenerated>
    </ItemGroup>
    <Target Name="GenerateSetting" 
           Inputs="@(AppConfigWithTargetPath)" 
           Outputs="@(SettingsGenerated)">
        <ClassGeneratorTask Language="$(Language)" ConfigFile="@(AppConfigWithTargetPath)" 
           FilePath="@(SettingsGenerated)"></ClassGeneratorTask>
        <ItemGroup>
            <Reference Include="System.Configuration"></Reference>
            <Reference Include="System"></Reference>
            <Compile Include="@(SettingsGenerated)"></Compile>
            <FileWrites Include="@(SettingsGenerated)"/>
        </ItemGroup>
    </Target>
    <PropertyGroup>
        <ResolveReferencesDependsOn>
            GenerateSetting;
            $(ResolveReferencesDependsOn)
        </ResolveReferencesDependsOn>
    </PropertyGroup>
</Project>

As you can see here:

<PropertyGroup>
    <ResolveReferencesDependsOn>
        GenerateSetting;
        $(ResolveReferencesDependsOn)
    </ResolveReferencesDependsOn>
</PropertyGroup>

I insert my custom task as a dependency of ResolveReferences. I've done that because after investigating Microsoft.Common.targets, I've seen that this step was the only one:

  • Before the compilation.
  • After the resolution of the item AppConfigWithTargetPath that I use in my task to locate the app.config file.

Now here:

<ItemGroup>
    <Reference Include="System.Configuration"></Reference>
    <Reference Include="System"></Reference>
    <Compile Include="@(SettingsGenerated)"></Compile>
    <FileWrites Include="@(SettingsGenerated)"/>
</ItemGroup>

You can see that in my task, I add References on System and System.Configuration dynamically because the generated code depends on it. I don't want to bother the user if he forgets to add these references to the project.

So, I add two references, the generated class to compile items and the FileWrites items. FileWrites items is used to say to MSBuild that the file should be deleted when you clean the project.

Another tip:

<Target Name="GenerateSetting" Inputs="@(AppConfigWithTargetPath)" 
        Outputs="@(SettingsGenerated)">

This means that the target depends on the AppConfig file, and creates the @(SettingsGenerated) item. In fact, before executing the task, MSBuild will compare the timestamps of @(SettingsGenerated) and the config file. If @(SettingsGenerated) was modified after the config file, this mean that the config file has not changed since the last time and that the task can be skipped; this way, there is no useless overhead during the compilation.

So, what's left? Just including the targets file in your csproj, after the import of Microsoft.Common.targets.

<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\SettingsGeneratorTask\bin\Debug\SettingsClassGenerator.targets" />

Create your app.config with your favorite settings, and compile... you will get intellisense on the AppSettings of your config file!

Conclusion

This project is done to show you how MSBuild and code generation are powerful... As you can see, what I've done is really simple to code, and yet incredibly useful. Now that you know this, what will you imagine?

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

Nicolas Dorier
Software Developer Freelance
France France
Member
I develop to make you forget what's between you and your work.
 
I teach and, with delight, you'll see that the best castles are done with the dumbest concepts.

Curiosity is the best teacher.
 
If you are interested for working with me, this way Smile | :)

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionDoesn't Visual Studio already do this?memberWes Jones28 Jun '11 - 8:44 
Doesn't Visual Studio kind of already do this for us?
In the IDE, go to Project->Properties->Settings. Anything you add there, you can then access w/ Properties.Settings.Default.MySetting, which is a class which gets code generated for us.
AnswerRe: Doesn't Visual Studio already do this?memberNicolas Dorier28 Jun '11 - 9:36 
Visual studio use the same technique to do that with settings.
However AppSettings and .settings are not the same thing.
 
AppSettings are created so that an administrator can easily modify the configuration of your application, just by modifying the app.config file.
 
Settings are not editable by hand -settings are created so that the developer can easily store user preferences in his code-. Settings can be both global or by user.
GeneralRe: Doesn't Visual Studio already do this?memberWes Jones28 Jun '11 - 16:05 
Ah, I see. Well, great job & thanks for sharing your hard work!
GeneralGood workmemberOmarGamil10 Nov '09 - 4:21 
good job, been having the same problem too
thanks for sharing Wink | ;)
GeneralAmazingmentorNick Butler19 Oct '09 - 9:12 
You guys are really pushing out some impressive stuff Smile | :)
 
Thanks for sharing!
 
Nick
 
----------------------------------
Be excellent to each other Smile | :)

GeneralRe: AmazingmemberNicolas Dorier7 Nov '09 - 22:19 
thanks it's encouraging ! If you liked this article you should appreciate Genuilder which apply the principles of this article Wink | ;)
GeneralVery nice but could you explainmvpSacha Barber17 Oct '09 - 19:42 
Nicolas, very nice work.
 
But you know where you say
 
"ConfigFile, FilePath, and Language are properties passed when we call the task. FilePath is the path to the generated file. You can see that the type of ConfigFile and FilePath are ITaskItem, and it would work if it was just a string. However, ITaskItem has more information, like the MSBuild metadata. In reality, I don't need it, and I just could use string instead, but I feel it's a good practice to use ITaskItem. After all, it's closer to the domain of my class (which is MSBuild)."
 
It is not clear to me where language is passed. You have the following
 
<ClassGeneratorTask ConfigFile="@(AppConfigWithTargetPath)" 
FilePath="@(SettingsGenerated)"></ClassGeneratorTask>
 
So where is language provided there
 
Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue
 
My Blog : sachabarber.net

GeneralRe: Very nice but could you explainmemberNicolas Dorier18 Oct '09 - 0:27 
Effectively, it's not really clear I'll fix that.
Language has a default value hard coded to 'C#'.
 
In that this should be :
<ClassGeneratorTask Language="$(Language)" ConfigFile="@(AppConfigWithTargetPath)" 
FilePath="@(SettingsGenerated)"></ClassGeneratorTask>
 
The "Language" property is specified to VB or CSharp inside every .csproj or .vbproj.
GeneralRe: Very nice but could you explainmvpSacha Barber18 Oct '09 - 2:22 
Oh, ok, that makes sense.
 
Hey man where did you pick up your WCF knowledge, you have done some very advanced WCF stuff in your other articles man.
 
What did you do to learn that?
 
Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue
 
My Blog : sachabarber.net

QuestionWhy we need?memberR i c k C o de7 Oct '09 - 16:40 
Why would I do that? I dont understand any practical need of this... Am i missing some point?
AnswerRe: Why we need?memberNicolas Dorier7 Oct '09 - 22:23 
Imagine you remove a settings from you app.config file or rename it. You have to update manually every references to the key of the appsetting in your code. And if you forgot one, you have a bug not found during compile time.
With what I've done, you can't mispell an AppSetting key in your code, and if the key change, the compiler will catch the error for you.
 
The main point of this article is that you can easily compile generated classes transparently during the build Process, and gain intellisense on it.
 
A good example is if you have a bunch of xsd file, and you want classes automatically generated during the compilation every time you modify an xsd file.
This way if you modify the xsd and break something in your code, the compiler will be able to catch it, without worrying about anything. The code is also more easy to code, thanks to the intellisense.
The XML team of Microsoft did that, I think they call it Linq to XSD.
 
But now you can imagine to generate classes from a DSL this way.
GeneralRe: Why we need?membertwebb7217 Oct '09 - 20:52 
I think this article is very interesting and very creative.
That being said, I agree with the top of this thread that it doesn't serve most applications useful purpose.
If you need to change settings based upon environment or database connections; you have a problem because you'll need to re-issue another build. Minimizing the number of builds is usually the best strategy for release management. TFS makes it very easy to re-build a specific version, however it tends to just clog the drop folder and make version numbers less meaningful.
Ultimately, if you need your release code portable (which is 99% of the apps out there), you need to leave settings TBD at runtime.
Uber-respect for all your articles! I'm working my way through reading them all!
Thanks. Thumbs Up | :thumbsup:
 
"I say we take off and nuke it from orbit. It's the only way to be sure."

AnswerWhy not create a settings file and move it to the properties foldermemberNatza Mitzi18 Oct '09 - 9:58 
create a settings file and move it to the properties folder
Once you do that, all of your settings are fully typed, controlled and can be changed using the settings designer.
The code class is auto created and can be accessed via Properties name space.
 
Natza Mitzi

AnswerRe: Why not create a settings file and move it to the properties foldermemberNicolas Dorier18 Oct '09 - 11:59 
The settings are saved on the user machine in C:\Users\[User]\AppData\Local and C:\Users\[User]\AppData\Roaming, not in the config file.
Settings are used to mostly save preferences of your user whereas the AppSettings in the config file are use to configure easily some key/value pair used by your application.
But maybe I should have done that article on the ConnectionString section instead of the AppSettings section. (You just have to change 2 lines of code in the generator to do that)
GeneralNice workmemberDaniel Vaughan6 Oct '09 - 11:03 
I really like what you’re doing with MSBuild Nicolas. Very cool.
I wonder if the article might benefit from more explanation in the introduction; painting a clearer picture about what the code does and what you are trying to achieve. I think that extra clarity could allow your code to shine more, and it should because it’s great work. Have a 5 from me.
 
Cheers,
Daniel
 
Daniel Vaughan
Blog: DanielVaughan.Orpius.com


Company: Outcoder

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 18 Oct 2009
Article Copyright 2009 by Nicolas Dorier
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid