This article summarizes some thoughts about the potentials
of PostSharp with Dependency Injection frameworks. The aim is to support some
ongoing or future discussions about loose integration of PostSharp into major DI
frameworks. It does not describe something that already exist "off-the-shelf" as
a downloadable plug-in.
Container or Service Locator
Most dependency injection (DI) frameworks today require instances to be
constructed using a "factory", "object builder" or "container".
This approach also uses a lot of System.Reflection to set up properties. And
eventually "bad" reflection: the one that breaks the object encapsulation and
accesses private members. You may care or not. I do.
Another major drawback of the "factory" approach is that you cannot use the
standard constructors.
This may be just annoying, but can become really blocking if your constructor
is called from a part of the program that you don't have control on. For
instance, what is you want to inject dependencies into a WinForms user
object? When the user object will be added into a form, Visual Studio will
instantiate it using the default constructor.
Anyway, why does the consumer of an object need to know that a special
factory method should be used? Is our abstraction so shiny?
Of course, there is a solution: inside the constructor, initialize each
dependency by a call to a service locator. It is not
literally an "injection" of dependencies: dependencies are created from
outside.
Container or Context
How does the service locator know how to create the dependency? Where does it
take the configuration from, since the object got no reference to its
container? Well, actually, the object has no container! The service locator
has simply context-sensitive configuration. Each thread has its own context
from which the service locator know how to create the dependency. Even better
than contexts, stacks of contexts allow to have nested contexts just
like we have nested transactions.
An elegant solution is to distinguish between global contexts
(static ones) and local contexts (thread-specific). When a
dependency is resolved, the service locator always looks for a match from the
top of the context stack to the bottom. The stack of local contexts is always
on the top of the stack of global contexts.
The use of local contexts makes it possible to execute unit tests in multiple
threads while using different mocking implementations of dependencies.
For instance, the configuration of a local context may look like this:
using ( new LocalContext("Test") )
{
Context.CurrentContext.Bind<ICustomerProcesses>().To<CustomerProcesses>();
Context.CurrentContext.Bind<IRentalProcesses>().To<RentalProcesses>();
TestRental test = new TestRental();
test.Go();
}
Et PostSharp dans tout ça?
The drawback of the Service Locator pattern is that you need to write
boiler-plate code. For each field, you have to call the service locator.
This is true for those who don't know PostSharp. But friends of
aspect-oriented programming surely noticed an opportunity to let the machine
generate code for you!
Wouldn't it be nice to be able to declare a dependency just like this:
class CustomerProcesses : ICustomerProcesses
{
[Inject]
private IRentalProcesses rentalProcesses;
}
Wouldn't it be nice to have a custom attribute that adds in all constructors
calls to the service locator?
Well, for those who are comfortable with PostSharp Core, it is a matter of
hours to develop it. Users of PostSharp Laos will unfortunately have to wait
until they get an off-the-shelf implementation. But, promised, it will come
soon!
What it means for Dependency Injection frameworks?
What does it mean for DI frameworks to be compatible with this approach?
Apart from these relatively small side requirements (only container nesting
is a little extravagant, but it's a nice-to-have both for the
container- and the context- based approach), the use of an aspect weaver does
not interfere too much with the design of the DI framework. So it is
perfectly possible to have to have a DI framework that is AOP-enabled, but
still excellent without AOP.
In this
presentation from QCon San Francisco 2007, SpringSource CTO and AspectJ project lead
Adrian Colyer discusses where Aspect-Oriented Programming (AOP) should be used, practical applications of AOP in enterprise situations such as Hibernate exception translation and automatic operation retry on nonfatal exceptions, and AOP mechanisms in
Spring 2.5.
Watch Using AOP in the Enterprise (62 minutes).
[from infoQ.com]
If you already use PostSharp Laos in any project, internal or external,
I would like to hear from you and there is a chance for your
company to get some (limited) visibility!
I am writing an article for the AOSD 2008
conference and I would like to augment my paper with case study (in
technical, not commercial wordings). The case study will then be published
separately on the website and eventually mentioned at other conferences.
So, if you think you did something interesting with PostSharp, please
get back to me, I really need your feedback!
Thank you,
Gael
All works so magically. Once you have PostSharp installed on your computer, your build process automatically invokes PostSharp when it thinks it should. Most of the cases, it works and it's enough. But if you have a complex build process (especially one that does not use MSBuild), you could ask: how does this work? And how can I integrate PostSharp in my non-MSBuild projects?
Getting Inserted in the MSBuild Process
Most of the stuff is in the file PostSharp-1.0.targets, a file typically installed in the directory C:\Program Files\PostSharp 1.0. From the moment you install PostSharp on your system (that is, using the Microsoft Installer package), this file is included in any project.
There is absolutely no hack in that; it uses extension points of MSBuild especially designed for this purpose (although badly documented). At the very beginning of the file C:\Windows\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets, you will see the following code:
<PropertyGroup>
<CustomBeforeMicrosoftCommonTargets Condition="'$(CustomBeforeMicrosoftCommonTargets)'==''">
$(MSBuildExtensionsPath)\v2.0\Custom.Before.Microsoft.Common.targets
</CustomBeforeMicrosoftCommonTargets>
<CustomAfterMicrosoftCommonTargets Condition="'$(CustomAfterMicrosoftCommonTargets)'==''">
$(MSBuildExtensionsPath)\v2.0\Custom.After.Microsoft.Common.targets
</CustomAfterMicrosoftCommonTargets>
<ReportingServicesTargets Condition="'$(ReportingServicesTargets)'==''">
$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\ReportingServices\Microsoft.ReportingServices.targets
</ReportingServicesTargets>
</PropertyGroup>
<Import Project="$(CustomBeforeMicrosoftCommonTargets)"
Condition="Exists('$(CustomBeforeMicrosoftCommonTargets)')"/>
This means that the build process loads the file c:\Program Files\MSBuild\v2.0\Custom.After.Microsoft.Common.targets, if present. He some program wants to be plugged into the standard MSBuild process, it just have to modify this file. And that's exactly what does PostSharp!
Our installer just edits this file and imports our PostSharp-1.0.targets, which gives us the opportunity to insert PostSharp tasks in the build process. How? Here also, using quite standard extension points.
Have a look, in the file Microsoft.Common.targets, at the
target Compile.
All its dependencies are contained in the
property CompileDependsOn. And we want to modify the
compilation process, we modify this property from our PostSharp-1.0.targets:
<PropertyGroup>
<CompileDependsOn>
$(CompileDependsOn);
PostSharpInspectConstants;
PostSharpInspectReferences;
PostSharp
</CompileDependsOn>
</PropertyGroup>
As you can see, we have inserted three targets after other
compilation targets: PostSharpInspectConstants,
PostSharpInspectReferences, and PostSharp.
Invoking PostSharp Only Relevantly
We could invoke PostSharp in every single project you build on your machine, but it would not last a long time before you uninstall it, isn't it? So we have to decide, in some way, whether PostSharp should be invoked.
This is the role of the targets PostSharpInspectConstants and
PostSharpInspectReferences. Let's begin with the last one, which is also
the most important.
The objective of the PostSharpInspectReferences target is to enable
PostSharp whenever the current project is has direct or indirect references to
the assembly PostSharp.Public.dll. The heuristic works in most cases:
if your assembly references PostSharp.Public.dll, it's probably because
it uses an aspect, so it requires to be post-processed. But it does not work
always: if you develop a PostSharp plug-in or aspect library, you will
surely reference PostSharp.Public.dll, but your plug-in won't need to
be transformed. True, but if you develop a plug-in or an aspect library for
PostSharp, you are more probable to have read the documentation, and you know
how to disable automatic enabling of PostSharp based on references.
And this is one of the role of the target PostSharpInspectConstants:
detecting the presence of the constant SkipPostSharp in your
project. So if you want to skip PostSharp in your project, or only in a
particular build configuration, simply define the SkipPostSharp
constant (aka compilation symbol), and you are done!
PostSharp Has Projects, Too
Like MSBuild, PostSharp has its own notion of project: *.psproj
files. And like MSBuild, PostSharp projects are based on tasks and plug-ins: a
PostSharp project describes all steps that need to be performed to properly
analyze and transform assemblies. You cannot just "execute PostSharp against an
assembly", you always need to provide it a project.
Hopefully, PostSharp comes with a default project: Default.psproj.
The default project automatically detects what is has to do based on custom
attributes present in the assembly. That's why the default project is so
flexible that most users don't need to be aware of this concept.
But this is an advanced post, so let's go on.
What's important here is not how automatic detection of tasks works (it is
documented), but to point out that the default project must take a lot of
properties from the environment, in this case from MSBuild. Here are they:
| Property |
Meaning |
| Output (required) |
Full path of the output assembly. |
| ReferenceDirectory |
Directory by reference to which relative paths in the psproj
will be resolved. We pass the root directory of the project (i.e. the
directory containing your csproj
file). |
| Configuration |
Debug or Release, typically. It is
not used by Default.psproj. |
| Platform |
Any, typically. It is not used by Default.psproj. |
| SearchPath |
Comma-separated list of directories that have to be added to the
search path while looking for plug-ins or referenced assemblies. We pass
the output directory (i.e. bin\debug) as well as all
reference paths defined in user-level project properties. |
| IntermediateDirectory |
Tells PostSharp where to put its own intermediate stuff. We pass
typically obj\debug\PostSharp. |
| CleanIntermediate |
Whether PostSharp should clean its intermediate files after execution.
We pass false. |
| MSBuildFullProjectPath |
Full path of the MSBuild project file (the csproj or
vbproj). This is to solve relative paths that users could
pass in custom attributes. |
| SignAssembly |
Determines whether the assembly should be signed. We take this value
from MSBuild. |
| PrivateKeyLocation |
Location of the private key. We take this value from MSBuild. |
All this stuff is of course passed automatically to PostSharp, so most of the
time you don't have to worry about this. However, if you need to invoke
PostSharp manually, this will surely interest you.
Invoking
PostSharp Manually
If you are not happy with the default integration of PostSharp in the build
process, there are many ways to do it differently.
If your project is built using MSBuild, you have the following options:
- Do not install PostSharp globally, but insert it manually in each
project. You will have to import PostSharp-1.0.targets
in all C# or VB (or other) project requiring PostSharp. Yup, there is
currently no binary package other than the installer
performing global registration. But you can
do this package yourself by zipping C:\Program Files\PostSharp 1.0
-- simply!.
- Do not use our MSBuild targets, but use our
MSBuild task.
- Use the command-line utility (see below).
If you don't use MSBuild you can use the command line utility. And there is
already a NAnt task in the 1.1 branch!
PostSharp from the Command Line
PostSharp comes with a command line utility. Its syntax is very simple,
because nearly everything is passed as named properties:
Usage: postsharp [<options>] <project> <input> [<options>]
Options:
/P:name=value Sets a property.
/Attach Gives the opportunity to attach a debugger to the process.
So here is how a call to the command line may look like:
PostSharp.exe
Default.psproj
.\obj\Debug\PostSharp.Samples.Binding.exe
/P:Output=.\obj\Debug\PostSharp\PostSharp.Samples.Binding.exe
/P:ReferenceDirectory=P:\open\branches\1.0\Samples\PostSharp.Samples.Binding
/P:SearchPath=bin\Debug /P:IntermediateDirectory=obj\Debug\PostSharp
/P:CleanIntermediate=False
/P:MSBuildProjectFullPath=P:\\Samples\PostSharp.Samples.Binding\PostSharp.Samples.Binding.csproj
Summary
I've explained how PostSharp is invoked from MSBuild and/or Visual Studio,
and described the properties that are passed from MSBuild to PostSharp.
Finally, I've shown how to invoke PostSharp on your own, eventually from the
command line.
I hope this has been usefull to some of you and...
Happy Aspecting!
-Gael
An interesting post of David DeWinter showing how difficult it is to use FxCop/VSTS Code Analysis when PostSharp is enabled. Indeed, PostSharp does not respect all FxCop rules, and does not emit rule-disabling custom attributes.
David shows how to run FxCop before PostSharp, which involves some serious changes in MSBuild files.
Seems like a worry for the next version of PostSharp...