Table of contents
Introduction
This article has almost the same content as Genuilder's documentation. But CodeProject members are
not developers like other... We are curious, we hack, combine, create, criticize ideas in order to find better solutions, for ourselves and for others. That's why I'll also talk about
the implementation details of Genuilder at the end of this article.
In one sentence, Genuilder is a Build time development framework... Remember, it took me a long time to find how to describe Genuilder in one sentence.
It means that you will be able to run your own code when developers build your project, emit error messages, generate code on the fly (for example, it is how WCF RIA services works), and more.
...or you can just use the built-in features, like AppConfig
compile time verification, or async contract generators for WCF.
Installation
Go to genuilder.codeplex.com and download the Genuilder Visual Studio extension.
Run it.
Restart Visual Studio, you should then see the new project item.
Upgrading Genuilder
You have to uninstall the previous version of Genuilder in the Extension Manager (in Visual Studio "Tools/Extension manager").
Common features
Common features are accessible through the CommonFeature
enumeration in your Genuilder project.
You need to run your Genuilder project each time you modify it to install or remove features from your projects.
AppConfigVerifier
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. For example, the AppConfig
:
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 also have 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:
InterfaceExtractor
InterfaceExtractor is a code generator which extracts an interface from 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. Here is some other cool things to do with an interface. For example, this class:
will create 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
Like 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 the 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 from your config file
and somewhere your code has a dependency on it, it will break at compile time.
For example, for this config file:
You will be able to access sections like this:
Extensions
Installing an extension in your project
An article has been written on CodeProject to introduce the Extensibility framework of Genuilder,
but I will refresh your memory. To install an extension, you need to use ExtensibilityFeature
, add extensions, and install it on the project. In this example,
you install AsyncServiceContractsExtension
on the file Program.cs of every project.
Built-in Extensions
AsyncServiceContractsExtension
This extension creates a ServiceContract
given a synchronous ServiceContract
, useful for Silverlight projects. Given:
Will generate:
An example is given here.
StronglyTypedReflectionExtension
It generates classes to access a type's member in a strongly typed way. Given:
It will generate a class to access PropertyInfo
, MehtodInfo
, EventInfo
this way:
ProxyGeneratorExtension
The goal and usage of this IExtension
is explained in this CodeProject article.
Custom Extensions
Object model
To create your own extension, you have to implement IExtension
, here is the object model of Genuilder.Extensibility
:
GenItems
represents code files of your project, you can use ExecutionContext.CreateChild(string name)
and GenItem.CreateChild(string name)
to create new ones. When you use GenItem.CreateChild
, the child will be deleted when the parent is deleted.
MSBuild logging
You can easily send warning, error, and info messages to Visual Studio by using ExecutionContext.Logger
and GenItem.Logger
.
For example, in this example, I output an error on Program.cs line 5 column 6 in Visual Studio.
Once installed on my project, here is what happens at compile time:
Debugging an Extension
You can easily debug your extensions by calling the Project.Build()
method on your project.
In this case, you will be able to break in the MyExtension
class to see what is going on during the build.
Easy code parsing and generation
Genuilder has a built-in object extension to easily parse CodeItem
s with NRefactory.
You can get it this way:
var compilationExtension = genItem.GetExtension<CompilationUnitExtension>()
Then you can use the CompilationUnit
property to walk through the Abstract Syntax Tree (AST).
Or, more easily, use the Visitor pattern by implementing AbstractASTVisitor
, overriding methods called during the visit
of the tree, then calling CompilationUnit.Visit(visitor,null);
. Take a look at the code source
of ProxyGeneratorExtension.
In this example, you'll see how to use the CodeWriter
class to generate readable code.
Resharper
Currently, the intellisense of Resharper doesn't work on generated classes. But the code will compile.
Under the hood: All is feature
In Genuilder philosophy, all is feature and features are... Chunk of MSBuild XML in your project, nothing more, nothing else. And there is actually two types of features.
Under the hood, a CommonFeature
is just a TargetFeature
, as you can see in the implementation of InstallCommonFeature
.
public void InstallCommonFeature(CommonFeature feature)
{
InstallFeature(new TargetFeature(
GetFullPath(_CommonFeature.GetFileName(feature), feature)));
}
For example, when you install a CommonFeature
and an IExtension
in your project:
foreach(var project in Projects.InSubDirectories("../..").ExceptForThisAssembly())
{
var ex = new ExtensibilityFeature();
ex.AddExtension(new MyExtension());
project.InstallFeature(ex);
project.InstallCommonFeature(CommonFeature.AppConfigVerifier);
project.Save();
}
You will see in the csproj file the following XML :
Red elements are the delimiters of Genuilder's sections. Genuilder guarantees that everything it adds to your csproj is between these two elements.
Blue is the extensibility feature's section.
Cyan is your extension section, composed in three parts delimited by pipe '|' parts:
- The full class name of your extension
- The assembly of your extension
- The DataContract serialization of your extension
Orange we ask to not use the Visual Studio compiler. Without it, auto completion on generated classes is buggy.
Green is the CommonFeature's section, as you can see it only includes an import to a target file.
A feature is really easy to implement, I use the MSBuild framework along with some of my own Extension Methods to modify the csproj file easily.
You can use the msBuildData
parameter to add things just after the Genuilder's section delimiter; for example, this is how TargetFeature
is implemented:
public override void LoadFeature(Project project, MSBuildData msBuildData)
{
var importPath = PathUtil.RelativePathTo(
new FileInfo(project.Path).Directory.FullName,
new FileInfo(_Path).FullName);
msBuildData.ProjectRootElement.AddImport(importPath).After(
msBuildData.StartImportGenuilder);
}
MSBuild integration of Extensions
As you can see in the Blue section, the Extensibility feature imports Genuilder.Extensibility.targets
.
It just inserts its own Msbuild Task
during compilation, and during cleaning.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<UsingTask TaskName="Genuilder.Extensibility.MSBuild.ExtensibilityTask"
AssemblyFile="Genuilder.Extensibility.dll"/>
<UsingTask TaskName="Genuilder.Extensibility.MSBuild.ExtensibilityCleanTask"
AssemblyFile="Genuilder.Extensibility.dll"/>
<PropertyGroup>
<CompileDependsOn>
GenuilderExtensibility;
$(CompileDependsOn)
</CompileDependsOn>
</PropertyGroup>
<PropertyGroup>
<CleanDependsOn>
GenuilderExtensibilityClean;
$(CleanDependsOn)
</CleanDependsOn>
</PropertyGroup>
<Target Name="GenuilderExtensibility">
<Genuilder.Extensibility.MSBuild.ExtensibilityTask
CompileIn="@(Compile)"
Extensions="@(GenuilderExtension)"
OutputPath="$(IntermediateOutputPath)">
<Output TaskParameter="CompileOut"
ItemName="Compile"></Output>
</Genuilder.Extensibility.MSBuild.ExtensibilityTask>
</Target>
<Target Name="GenuilderExtensibilityClean">
<Genuilder.Extensibility.MSBuild.ExtensibilityCleanTask
OutputPath="$(IntermediateOutputPath)" />
</Target>
</Project>
The implementation of this task has been kept as small as possible, because it was really hard to test. The core logic
of Genuilder.Extensibility
is in the GenEngine
.
I'll explain some lines of the ExtensibilityTask.Execute
method.
First GenEngine
needs an IGenLogger
to add logging capability to IExtension
and GenItem
. ExtensibilityTask
just provides an implementation of this interface for MSBuild logging, the MsBuildLogger
.
That's what we find in the implementation of ExtensibilityTask
.
GenEngine engine = new GenEngine();
engine.Logger = new MSBuildLogger(this.Log);
Then GenEngine
needs your extensions.
engine.Extensions.AddRange(GetExtensions());
GetExtensions
loads assemblies of every IExtension
, and deserializes every extension. The problem is that these extensions were loaded
in the Visual Studio process when compiling.
The implication is that when you try to debug an IExtension
and recompile it, you got an error like this one:
Unable to copy file reference.dll to bin/reference.dll.
The process cannot access the file reference.dll because it is being used by another process
The fix is simple, I just need to load my ExtensibilityTask
in a separate AppDomain, and unload it when the task is executed. That's why ExtensibilityTask
inherits from AppDomainIsolatedTask
instead of Task
.
GenEngine.StartInfo.GenerationDirectory
is the directory where new GenItem
s are generated.
engine.StartInfo.GenerationDirectory = OutputPath;
As you see in the Target file, this maps to IntermediateOutputDirectory
. That is, by default, the "obj/Debug|Release" folder.
RunHistory
is a file which keeps track of generated files after every GenEngine.Run
, it keeps track of generated files and timestamp. Its goal is to see if files
were modified between two runs so that GenEngine
can set the value of GenItem.Modified
before every run.
The other goal is to include the generated files of the last run to the list of GenEngine.SourceFiles
before each run.
Conclusion
I worked hard to make the simplest design possible to implement Genuilder. And I'm happy to see there is not much lines of code.
I hope that Genuilder will make your projects a little more simple, and that its design is simple enough so that you don't need to know MSBuild to understand how Genuilder works.