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.
- What Genuilder does, how to use it
- 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:
Download the latest release.
Copy the Genuilder directory next to your solution:
In the Genuilder directory, execute Genuilder.GUI.exe, open the solution, and select your project, then click on the button:
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:
Genuilder will find the error at compile time:
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:
Auto completion on AppSettings:
Auto completion on ConnectionStrings:
EnvironmentFile
EnvironmentFile is just a pre-processor for your config file. For example, you have this app.config with a token:
You have also this EnvironmentFile.env file which is just a key=value pair list delimited by line breaks:
After compilation, the output config file (for example, MyProject.exe.config) will be:
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:
After the next Get latest version, the project will not compile because the new token is not set in EnvironmentFile.env:
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:
will extract this interface:
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:
Because PropertiesExtractor will generate this other class:
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:
you will be able to access sections like this:
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:
Visual Studio says:
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:
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:
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.
<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:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\bin\AllTargets.targets" />
And I add a reference to Genuilder.dll (for now, it contains only two classes, ExtractInterface
and ExtractProperties
).
<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.
<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.
="1.0"="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:
- 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.)
- Every project with a Genuilder feature (i.e., a targets file) must generate itsr output in a common directory.
- Every project sample must import the targets file it is supposed to test.
I adopted a convention with the name of my projects.
All projects which contain a new feature to include in AllTargets.targets are called *.Targets.
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:
- Import a targets file called "Common.Before.targets" in all .Targets projects.
- 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.
<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:
<ProjectReference Include="..\*.Targets*\*.csproj" />
I also store every targets file in an MSBuild item called "TargetsFiles
".
<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.
<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.
<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