Click here to Skip to main content
15,881,089 members
Articles / Programming Languages / XML

Strongly typed AppSettings with MSBuild

Rate me:
Please Sign up or sign in to vote.
4.89/5 (16 votes)
18 Oct 2009Ms-PL5 min read 50.9K   153   37   17
How to extend MSBuild to dynamically compile stuff during a build.

Image 1

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.

Image 2

to:

Image 3

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.

XML
<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.

XML
<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.

XML
<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.

C#
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:

XML
<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:

C#
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:

C#
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.

XML
<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:

XML
<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:

XML
<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:

XML
<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.

XML
<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)


Written By
Software Developer Freelance
France France
I am currently the CTO of Metaco, we are leveraging the Bitcoin Blockchain for delivering financial services.

I also developed a tool to make IaaS on Azure more easy to use IaaS Management Studio.

If you want to contact me, go this way Smile | :)

Comments and Discussions

 
QuestionNot getting intellisense Pin
pkfox6-Apr-14 23:15
professionalpkfox6-Apr-14 23:15 
AnswerRe: Not getting intellisense Pin
Nicolas Dorier7-Apr-14 0:23
professionalNicolas Dorier7-Apr-14 0:23 
QuestionDoesn't Visual Studio already do this? Pin
Wes Jones28-Jun-11 8:44
Wes Jones28-Jun-11 8:44 
AnswerRe: Doesn't Visual Studio already do this? Pin
Nicolas Dorier28-Jun-11 9:36
professionalNicolas Dorier28-Jun-11 9:36 
GeneralRe: Doesn't Visual Studio already do this? Pin
Wes Jones28-Jun-11 16:05
Wes Jones28-Jun-11 16:05 
GeneralGood work Pin
Omar Gameel Salem10-Nov-09 4:21
professionalOmar Gameel Salem10-Nov-09 4:21 
GeneralAmazing Pin
Nicholas Butler19-Oct-09 9:12
sitebuilderNicholas Butler19-Oct-09 9:12 
GeneralRe: Amazing Pin
Nicolas Dorier7-Nov-09 22:19
professionalNicolas Dorier7-Nov-09 22:19 
GeneralVery nice but could you explain Pin
Sacha Barber17-Oct-09 19:42
Sacha Barber17-Oct-09 19:42 
GeneralRe: Very nice but could you explain Pin
Nicolas Dorier18-Oct-09 0:27
professionalNicolas Dorier18-Oct-09 0:27 
GeneralRe: Very nice but could you explain Pin
Sacha Barber18-Oct-09 2:22
Sacha Barber18-Oct-09 2:22 
QuestionWhy we need? Pin
mrcodebased7-Oct-09 16:40
mrcodebased7-Oct-09 16:40 
AnswerRe: Why we need? Pin
Nicolas Dorier7-Oct-09 22:23
professionalNicolas Dorier7-Oct-09 22:23 
GeneralRe: Why we need? Pin
twebb7217-Oct-09 20:52
professionaltwebb7217-Oct-09 20:52 
AnswerWhy not create a settings file and move it to the properties folder Pin
Natza Mitzi18-Oct-09 9:58
Natza Mitzi18-Oct-09 9:58 
AnswerRe: Why not create a settings file and move it to the properties folder Pin
Nicolas Dorier18-Oct-09 11:59
professionalNicolas 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 work Pin
Daniel Vaughan6-Oct-09 11:03
Daniel Vaughan6-Oct-09 11:03 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.