Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Genuilder

5.00/5 (30 votes)
10 Nov 2009Ms-PL9 min read 73.9K   437  
Enhance Visual Studio and your build process in just two clicks. This tool does its best to be out of your way, while improving your development experience seemlessly.

Image 1

Table of contents

Introduction

First of all, I am very excited to talk about this project; this is maybe the most useful project I have ever done (it's not hard stuff, because I love doing useless things too).

Genuilder (the name is a mix between "Generator" and "Builder") is the beginning of a new way to easily extend Visual Studio and your build process in 8 mouse clicks (yeah, it's a power of two, a plain round integer).

This idea comes from previous works I've done on MSBuild: the AppConfigVerifier done with Daniel, the AppSettingsGenerator, and a little tool I've done at work, EnvironmentFile, that I'll present during this article.

I thought that these articles haven't the popularity they deserve; I was very happy with them, but not so many people have shared their enthusiasm in vote. My guess is that people don't care about MSBuild, so this article is split in two parts.

  1. What Genuilder does, how to use it
  2. Internals

You can read the first part in 2 or 3 minutes. In 10 minutes from now, your project will be ready to use Genuilder.

I've done everything to make this project to "just work" in 8 clicks without MSBuild knowledge.

This project will evolve depending on the enthusiasm and ideas you have, I believe there is much potential for future improvements.

Genuilder is on CodePlex; I recommend you to download sources and binaries there!

Installation

Imagine you want to install Genuilder on MyCorpProject:

Image 2

Download the latest release.

Copy the Genuilder directory next to your solution:

Image 3

In the Genuilder directory, execute Genuilder.GUI.exe, open the solution, and select your project, then click on the button:

Image 4

Now, MyCorpProject has all the features of Genuilder. Yes, it's that simple...

Note: The project must not be in read-only mode.

Features

AppConfigVerifier

The AppConfigVerifier is an integration in Genuilder of this project. It scans all 'type=".*"' in the config file, and checks if the type is referenced in the project. The documentation can be found on CodeProject. Here is a quick example, where in the config file, you misspelled the MembershipProvider type:

Image 5

Genuilder will find the error at compile time:

Image 6

You can exclude types or assemblies from the verification, just check this article.

AppSettingsGenerator

This feature comes from one of my articles on CodeProject. With it, you can access your AppSettings and ConnectionStrings in a strongly typed way. An example app.config:

Image 7

Auto completion on AppSettings:

Image 8

Auto completion on ConnectionStrings:

Image 9

EnvironmentFile

EnvironmentFile is just a pre-processor for your config file. For example, you have this app.config with a token:

Image 10

You have also this EnvironmentFile.env file which is just a key=value pair list delimited by line breaks:

Image 11

After compilation, the output config file (for example, MyProject.exe.config) will be:

Image 12

This is particularly useful when developers in a team do not have the same development environment, they don't need to checkout their own version of the app.config. (EnvironmentFile should be unique for each developer and should not be in your source control.)

If one of your fellow developers changes the config file like that:

Image 13

After the next Get latest version, the project will not compile because the new token is not set in EnvironmentFile.env:

Image 14

Note: this feature doesn't work for web.config files. (Don't worry, .NET 4.0 is coming with a new feature for web.config files.)

InterfaceExtractor

Interface extractor is a code generator which extracts an interface from the public members of classes and structs. This feature is really useful when you want to create static or dynamic proxies around a service or a controller for authentication or logging, for example. I'll create an article on CodeProject on a nice Use Case with ASP.NET MVC to control authorized access of a controller. (I have not done and I've not eaten my own dog food yet... so be really careful with this feature ! ;))

For example, this class:

Image 15

will extract this interface:

Image 16

Note that the parameter of ExtractInterface is optional; the default interface name is IClassName. Also, you don't have to implement the interface on your class.

Warning: [ExtractInterface(typeof(SomeNamespace.Interface))] is currently not allowed, you cannot specify the namespace of the extracted interface... not a big deal to add though.

PropertiesExtractor

As InterfaceExtractor, PropertiesExtractor is a class generator which generates a static class to access property names in a strongly typed way. For example, this class implements INotifyPropertyChanged and does not hard code the property name with a string:

Image 17

Because PropertiesExtractor will generate this other class:

Image 18

This way, if you refactor the property name, the code will break at compile time instead of during runtime.

As with InterfaceExtractor, the parameter of ExtractProperties is optional, the default class name is ClassNameProperties. Warning: [ExtractProperties(typeof(SomeNamespace.MyClassProperties))] is currently not allowed, you cannot specify the namespace of the extracted class properties... not a big deal to add though.

StrongConfigGenerator

This feature generates a class to access your configuration file in a strongly typed way. If you remove a section while your code somewhere has a dependency on it, it will break the code at compile time instead of during runtime.

For example, for this config file:

Image 19

you will be able to access sections like this:

Image 20

Known problems with Visual Studio

These bugs are fixed in version 1.1.

My code doesn't compile the first time

There is a bug in Visual Studio 2008. When you compile, generated classes are generated before the compilation, but for some obscure reasons, Visual studio will fail the compilation if you already use these generated classes in your code, although Intellisense works well and the classes are generated. But the next compilation will not fail.

For example, you code this class in one step before compiling the first time:

Image 21

Visual Studio says:

Image 22

The next time you modify the file where Person is and rebuild, you won't see the error.

This is clearly a bug, which doesn't appear when you build with MSBuild in command line... It seems that Visual Studio compiles the class generated before you decide to build.

My code should break at compile time, but it doesn't

This bug is caused by the same root as this bug.

You have this class:

Image 23

You refactor the property's name to 'Year'. The constructor should break since it references PersonProperties.Age instead of PersonProperties.Year.

But Visual Studio doesn't complain:

Image 24

The next time you modify the file where Person is and rebuild, it will break as expected.

How I've fixed these bugs

These bugs were caused by Visual Studio which uses an in-process compiler instead of Csc.exe, source: MSBuild Team. The trick is to just add this line in the AllTargets.targets to force Visual Studio to use Csc.exe.

XML
<PropertyGroup>
    <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
</PropertyGroup>

Internals

How do you do that? It's awesome!

I hope you are as enthusiastic as me on this project; in fact, it is stupid simple. When you run the Genuilder GUI on your project, there are only two lines in your project file.

I import AllTargets.targets:

XML
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\bin\AllTargets.targets" /> <!-- This line ! -->

And I add a reference to Genuilder.dll (for now, it contains only two classes, ExtractInterface and ExtractProperties).

XML
<Reference Include="Genuilder">
    <HintPath>..\bin\Genuilder.dll</HintPath>
</Reference>

That's it! And, here is the AllTargets.targets file. If you want to disable a feature, just comment a line.

XML
<Project ToolsVersion="3.5" 
         DefaultTargets="Build" 
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="AppConfigVerifier.targets" />
    <Import Project="AppSettingsGenerator.targets" />
    <Import Project="EnvironmentFile.targets" />
    <Import Project="InterfaceExtractor.targets" />
    <Import Project="PropertiesExtractor.targets" />
    <Import Project="StrongConfigGenerator.targets" />
</Project>

Most of these targets files are not complicated, they insert their targets in the build process and invoke a task. For example, here is AppConfigVerifier.targets (as documented in my previous article on AppSettingsGenerator). Note that StrongConfigGenerator and AppSettingsGenerator are very similar, you can also change the name and the namespace of generated classes in these files.

XML
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" 
        xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask TaskName="AppConfigVerifier.MSBuild.Verifier" 
         AssemblyFile="AppConfigVerfier.MSBuild.dll" />
    <PropertyGroup>
        <BuildDependsOn>
            $(BuildDependsOn);
            AppConfigVerifier_GetConfigFile;
            VerifyAppConfig
        </BuildDependsOn>
    </PropertyGroup>

    <Target Name="AppConfigVerifier_GetConfigFile">
        <ItemGroup>
            <_ConfigFile Include="@(AppConfigWithTargetPath)" 
               Condition="Exists('@(AppConfigWithTargetPath)')" />
            <_ConfigFile Include="Web.config" 
               Condition="Exists('Web.config') And '@(_ConfigFile)' == ''" />
        </ItemGroup>
    </Target>

    
    <Target Name="VerifyAppConfig" Inputs="@(_ConfigFile)" 
               Outputs="$(IntermediateOutputPath)Verifier_LastCheck.txt" 
               Condition="Exists('@(_ConfigFile)')">
        <ItemGroup>
            <DefaultReferencePath 
               Include=" @(MainAssembly->'%(FullPath)')">
            </DefaultReferencePath>
        </ItemGroup>
        <AppConfigVerifier.MSBuild.Verifier References="@(ReferencePath)" 
           DefaultReferences="@(DefaultReferencePath)" 
           Config="@(_ConfigFile)">
        </AppConfigVerifier.MSBuild.Verifier>

        <WriteLinesToFile 
            File="$(IntermediateOutputPath)Verifier_LastCheck.txt" 
            Lines="LastModified" Overwrite="true">
        </WriteLinesToFile>
        
        <ItemGroup>
            <FileWrites 
              Include="$(IntermediateOutputPath)Verifier_LastCheck.txt"/>
        </ItemGroup>
    </Target>
</Project>

I use the log of MSBuild to send messages to the Error List of Visual Studio. In the near future, I will add MSBuild properties to easily disable some targets for some projects.

So that's it, as you see, it's stupid, simple. Why hasn't anybody thought about this earlier?

Interesting stuff I have done with MSBuild during the development of this project.

My favorite principle in development is DRY, Don't Repeat Yourself. I'll show you how MSBuild is all about that.

This solution has three projects:

  • Genuilder.GUI is just a WPF application, nothing really interesting (except it's sexy baby!).
  • Genuilder.sln has all the projects related to targets files, it's the core project.
  • GenuilderSample.sln is a solution with one or two project samples per targets file.

I wanted to automate three things:

  1. Every targets files should be added to AllTargets.targets. (Do you remember? It's the file which is imported in your project file with my GUI.)
  2. Every project with a Genuilder feature (i.e., a targets file) must generate itsr output in a common directory.
  3. Every project sample must import the targets file it is supposed to test.

I adopted a convention with the name of my projects.

Image 25

All projects which contain a new feature to include in AllTargets.targets are called *.Targets.

Image 26

All project samples end with XXXXX.TestProject(x?), where XXXXXX is the name of the tested project "XXXXX.Targets".

For example, if I create a new Targets project called NewFeature.Targets, and a new project sample called NewFeature.TestProject, they will be configured in one keystroke.

With MSBuild, I will decrease the manual work to do when I create a new project, and improve readability by the following conventions.

The project "SetCommonConfigOnTargets" in Genuilder.sln, is a command line utility which:

  1. Import a targets file called "Common.Before.targets" in all .Targets projects.
  2. Import the targets file XXXX.targets in every XXXX.TestProject.csproj.

Here is "Common.Before.targets"; it sets the output directory of all .Targets projects to a common directory.

XML
<Project 
        xmlns="http://schemas.microsoft.com/developer/msbuild/2003" 
        ToolsVersion="3.5">
    <PropertyGroup>
        <OutDir>..\bin</OutDir>
    </PropertyGroup>
</Project>

During the compilation of the project CommonConfig, SetCommonConfigOnTargets.exe is called, the projects are configured, and AllTargets.targets is created.

CommonConfig will automatically reference all .Targets projects. This way, they are compiled before CommonConfig. Yeah, it's automatic, and it's one line in the project file:

XML
<ProjectReference Include="..\*.Targets*\*.csproj" />

I also store every targets file in an MSBuild item called "TargetsFiles".

XML
<TargetsFiles Include="..\**\*.targets" 
   Exclude="..\**\bin\**\*;..\$(AssemblyName)\*.targets;..\*TestProject*\**\*.targets" />

Then, I redefine what it means to "Compile" the project, i.e., create the AllTargets.targets file, and execute SetCommonConfigOnTargets.exe, to ensure that all projects which follow the convention are configured.

XML
<Target Name="Compile">
    <Message Text="@(TargetsFiles)">
    </Message>
    <ItemGroup>
        <_LinesToWrite Include="<Project ToolsVersion="3.5" 
           DefaultTargets="Build" 
           xmlns="http://schemas.microsoft.com/developer/msbuild/2003">" />
        <_LinesToWrite 
          Include="@(TargetsFiles -> '<Import Project="%(FileName)%(Extension)" />')" />
        <_LinesToWrite Include="</Project>" />
    </ItemGroup>
    <WriteLinesToFile File="AllTargets.targets" Lines="@(_LinesToWrite)" Overwrite="true">
    </WriteLinesToFile>
    <Message Text="$(Language)">
    </Message>
    <Copy SourceFiles="AllTargets.targets" DestinationFolder="$(OutDir)">
    </Copy>
    <Exec Command="SetCommonConfigOnTargets.exe" 
      WorkingDirectory="..\SetCommonConfigOnTargets\bin\$(Configuration)">
    </Exec>
</Target>

I also had to override a build step called CopyFilesToOutputDirectory to make it work, because CommonConfig doesn't generate an assembly.

XML
<Target Name="CopyFilesToOutputDirectory" />

This way, when I compile CommonConfig.csproj, everything is well configured without manual intervention.

Conclusion

Well, that's it. I will wait for your feedback and some ideas on new features I should add to Genuilder. If you like it and if you use it, please spread the word. :D

License

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