I put in a request for one-way multi associations here (you may need to register to view). The response I got back was basically that implementing one-way multi associations would "break existing functionality and this approach contradicts with the XPO concepts". I think that this is a shame, but there you go.
So, given that I want this feature, I'm going to find the middle ground: let's have one-way associations, but use PostSharp to implement the extra code to keep XPO happy.
The Problem:
I want to be able to write this as valid XPO code:
public class Customer : XPObject
{
public XPCollection
}
public class Order : XPObject
{
}
But XPO wants me to add the other end of the link to Order and to also add associated attributes:
public class Customer : XPObject
{
[Association("CustomerOrders")]
public XPCollection
}
public class Order : XPObject
{
[Association("CustomerOrders")]
public Customer Customer;
}
So, for my one-way association there must be another property or field on the linked class as well as marking each participting property(or field with the Association attribute. Not a huge amount of extra code and not an unreasonable amount either!
(Also, it's worth noting here that I am again using PostSharp to let me use automatic properties with XPO here - there is a little more required for normal XPO code. See my previous posts for more details on how this is done.)
A Solution:
We're going to use PostSharp to generate the reverse link for us and to add the associations that XPO requires. Basically, I am going to write code as in the first section above and get PostSharp to enhance it at compile time so that it is actually like the second section. Specifically, I'm going to use PostSharp to:
- Add a public field to a class.
- Add an attribute to this new field.
- Add an attribute to an existing property.
If you care about the reverse link, you will want to implement it yourself as is usual in XPO. The way we are doing it here means that the generated reverse link will not be visible at design time from inside of its own assembly (but it will be from outside the assembly).
Also, the current implementation is not compatible with the Compact Framework or Silverlight. For this you will need to use the upcoming version 1.5 of PostSharp - more details here.
Approach
As you will see from the PostSharp web site, PostSharp is made up of two main sections - PostSharp.Core and PostSharp.Laos. PostSharp.Laos is a plugin on top of PostSharp.Core that simplifies a lot of the common tasks in AOP. PostSharp.Core is the main engine that enchances your code at compile time. It is both powerful and deep, and I am certainly not an expert in its use! However, despite all the power and depth, it is also fairly accesssible.
The PostSharp website uses the lovely term "Pay-As-You-Use Complexity" for PostSharp - you can acheive pretty much anything you want and the common tasks are made fairly simple (by PostSharp.Laos), while other tasks may require you to gain some more knowledge of the inner workings of PostSharp (meaning PostSharp.Core). In my previous articles about PostSharp, we used PostSharp.Laos, for this task we are going to use PostSharp.Core.
In this article, I'm going to show how to add fields and attributes to classes using PostSharp.Core. I was inspired by Ruurd Boeke's articles, which you can find here. Ruurd uses PostSharp to enchance POCO classes for the Entity Framework use and goes a lot further with PostSharp than I do here. I highly recommend reading Ruurd's articles for more detailed information.
Let's now have a look what we need to do to get PostSharp to do what we want. I'm going to break this down into three sections:
Stage 1 - Attach Aspects
The first step is to create a CompoundAspect as we have done before. The job of this attribute is going to be to add other PostSharp aspects to our classes. The other aspects will then be picked up and used by PostSharp.Core. The ProvideAspects method looks like this:
1 public override void ProvideAspects(
object element,
LaosReflectionAspectCollection collection)
2 {
3 var targetType = (Type) element;
4 foreach (var property in targetType.UnderlyingSystemType
2 {
3 var targetType = (Type) element;
4 foreach (var property in targetType.UnderlyingSystemType
.GetProperties()
.Where(info => info.DeclaredIn(targetType) &&
info.IsOneWayAssociation()))
5 {
6 var fieldName = GetGeneratedFieldName(property);
7 collection.AddAspect(
property.GetCollectionTargetType(),
new OtherEndFieldSubAspect(
targetType.FullName,
fieldName,
5 {
6 var fieldName = GetGeneratedFieldName(property);
7 collection.AddAspect(
property.GetCollectionTargetType(),
new OtherEndFieldSubAspect(
targetType.FullName,
fieldName,
fieldName));
8
9 collection.AddAspect(
8
9 collection.AddAspect(
targetType,
new AssociationAttributeSubAspect(
property.Name,
fieldName));
10 }
10 }
11 }
In line 4 we use a bit of LINQ to iterate through the properties of our target type that we are interested in enchancing and then in line 6 we call a helper method to get the name of the field we want to generate. In lines 7 and 9 we add the aspects. In line 7 we add an aspect to the type at the other end of our collection and pass it the name of the type of the field we want to add, along with a field name and a string to be passed to the association attribute that we will add later. Note that we are using an extension method helper (GetCollectionTargetType) to get the type at the other end, although this is nothing more than normal reflection code. Next, in line 9, we add another aspect to the type that is declaring the collection property, passing it the name of the property that will be enchaned and a string that we will pass to the attribute that we add to the property.
So, we have used PostSharp.Laos to add aspects to our classes. These aspects contain some information that we will use later. In fact, as you can see below, the aspects that we have added contain little else other than the information we want to use later.
public class OtherEndFieldSubAspect : ILaosTypeLevelAspect
{
public OtherEndFieldSubAspect(
{
public OtherEndFieldSubAspect(
string fieldTypeName,
string fieldName,
string associationName)
{
FieldTypeName = fieldTypeName;
FieldName = fieldName;
AssociationName = associationName;
}
public void CompileTimeInitialize(Type type) { }
public bool CompileTimeValidate(Type type)
{
return true;
}
public void RuntimeInitialize(Type type) { }
public int AspectPriority
{
get { return int.MinValue; }
}
public string AssociationName { get; set; }
public string FieldName { get; set; }
public string FieldTypeName { get; set; }
}
public class AssociationAttributeSubAspect : ILaosTypeLevelAspect{
FieldTypeName = fieldTypeName;
FieldName = fieldName;
AssociationName = associationName;
}
public void CompileTimeInitialize(Type type) { }
public bool CompileTimeValidate(Type type)
{
return true;
}
public void RuntimeInitialize(Type type) { }
public int AspectPriority
{
get { return int.MinValue; }
}
public string AssociationName { get; set; }
public string FieldName { get; set; }
public string FieldTypeName { get; set; }
}
{
public AssociationAttributeSubAspect(
string propertyName, string associationName)
{
PropertyName = propertyName;
AssociationName = associationName;
}
public void CompileTimeInitialize(Type type) { }
public bool CompileTimeValidate(Type type)
{
return true;
}
public void RuntimeInitialize(Type type) { }
public int AspectPriority
{
get { return int.MinValue; }
}
public string AssociationName { get; set; }
public string FieldName { get; set; }
public string PropertyName { get; set; }
}
Note that these tasks implement ILaosTypeLevelAspect as we are applying the aspects to types.
Stage 2 - Create a Weaver Task
PostSharp's weaver is what we are going to use to create the new field and add the custom attributes. The public parts of PostSharp are LGPL and can therefore be freely distributed under any license you choose, whereas PostSharp.Core is licensed under the copyleft GPL, and so you cannot distribute PostSharp.Core in another GPL application. So, if we want to use this commercially, the weaver needs to be split out from the main distribution if the software is going to be used commercially. This is not a problem as the enhancements happen at compile time and the designer of PostSharp, Gael Fratieur has made this easy for us to seperate PostSharp.Core! The basis of this is to use PostSharp "tasks" to tell PostSharp.Core what to do at compile time. A task has a name and points to a weaver implementation that will handle the task. You configure the tasks in XML as below:
Implementation="Weaver.WeaverFactory, Weaver">
The task called WeaveTwoWayAssociations is handled by the type Weaver.WeaverFactory which can be found in the assembly called Weaver. This file and the rest of the weaver code sits in a seperate project. Other projects find this by adding the weaver to the search path and adding an assembly level attribute to the project where we defined our AssociationAttributeSubAspect and OtherEndFieldSubAspect aspects. We also add the weaver project's output folder to each referencing projects search path (inside project | properties). The assembly attribute looks like this:
[assembly: PostSharp.Extensibility.ReferencingAssembliesRequirePostSharp("WeaveTwoWayAssociations", "Weaver")]
[assembly: InternalsVisibleTo("Weaver")]
Note that the task name and the assembly in which it can be found are indicated. We also have to allow the weaver to reference our internals! Also, note that PostSharp has another, more portable, mechanism to tell reference the weaver files, as described in Ruurd's article. For whatever reason, I could not get this to work and so have just added the reference path. You will need to change this path on your machine or you will get compile errors about not being able to find a plugin:[assembly: InternalsVisibleTo("Weaver")]
Okay, so we've now got a task setup to let PostSharp's weaver loose on our aspects. We now just need to define what the weaver needs to do.
Stage 3 - Do the Weaving
In our configuration we said that the task called WeaveTwoWayAssociations was handled by the class Weaver.WeaverFactory. WeaverFactory implements ILaosAspectWeaverFactory and looks like this:
public class WeaverFactory : Task, ILaosAspectWeaverFactory
{
public LaosAspectWeaver CreateAspectWeaver(ILaosAspect aspect)
{
if (aspect is OtherEndFieldSubAspect)
{
var addField = (OtherEndFieldSubAspect)aspect;
return new AddFieldWeaver(
addField.FieldTypeName,
addField.FieldName,
addField.AssociationName);
}
if (aspect is AssociationAttributeSubAspect)
{
var addAttribute = (AssociationAttributeSubAspect)aspect;
return new PropertyAttributeWeaver(
addAttribute.PropertyName,
addAttribute.AssociationName);
}
return null;
}
}
Ttrue to its name, the factory is responsible for providing instances of types that can do some work for us. Note that the factory is looking for the aspects that we added in stage 1. When a OtherEndFieldSubAspect is encountered, an instance of AddFieldWeaver is created and returned to PostSharp. When a AssociationAttributeSubAspect is encountered, an instance of PropertyAttributeWeaver is created and returned to PostSharp.
So, as indicated above, the work of adding a field is done by the AddFieldWeaver class. You might be expecting this class to be pretty complex and have lots of IL in it. If you are, you're in for a surprise as it's actually really clean and straight forward:
public class AddFieldWeaver : TypeLevelAspectWeaver
{
public AddFieldWeaver(
{
public AddFieldWeaver(
string fieldTypeName, string fieldName,
string associationName)
{
FieldTypeName = fieldTypeName;
FieldName = fieldName;
AssociationName = associationName;
}
private ITypeSignature fieldType;
public override void Implement()
{
var newField =
{
FieldTypeName = fieldTypeName;
FieldName = fieldName;
AssociationName = associationName;
}
private ITypeSignature fieldType;
public override void Implement()
{
var newField =
new FieldDefDeclaration
{
Name = FieldName,
Attributes = FieldAttributes.Public,
FieldType = FieldType
};
((TypeDefDeclaration) TargetType).Fields.Add(newField);
var attribute = Utils.CreateAssociationAttribute(
{
Name = FieldName,
Attributes = FieldAttributes.Public,
FieldType = FieldType
};
((TypeDefDeclaration) TargetType).Fields.Add(newField);
var attribute = Utils.CreateAssociationAttribute(
Task.Project.Module, AssociationName);
newField.CustomAttributes.Add(attribute);
}
public string AssociationName { get; set; }
public ITypeSignature FieldType
{
get
{
if (fieldType == null)
{
fieldType =
newField.CustomAttributes.Add(attribute);
}
public string AssociationName { get; set; }
public ITypeSignature FieldType
{
get
{
if (fieldType == null)
{
fieldType =
Task.Project.Module.FindType(
FieldTypeName,
BindingOptions.Default);
}
return fieldType;
}
}
public string FieldTypeName { get; set; }
private string FieldName { get; set; }
}
}
return fieldType;
}
}
public string FieldTypeName { get; set; }
private string FieldName { get; set; }
}
The waever is an implementation of the PostSharp TypeLevelAspectWeaver and its Implement method is where the new field is created. This method simply creates a new FieldDefDeclaration that represents the new field, sets its name, defines its visibility (by setting the Attributes property) and sets it's type. The type is derived from a helper property called FieldType which uses PostSharp's built in Task.Project.Module to find the type of the name that was specified when we created the attribute in stage 1.
Then, the new field is simply added to the target type's fields collection. Next, a helper method is called to create an instance of the custom attribute we want to add and is added to teh new field's custom attributes collection. That's all there is to it!!!
PostSharp has been widely praised for having a very clean and well designed API. After doing this I can really see why it is so well thought of - we have been sheltered from all the horrid IL generation that is usually associated with this sort of task. Quite simply, PostSharp totally rocks and Gael Fraiteur has done an amazing job!
The Results
So, what do we have now? If you remeber, we started with types that looked like this:
[AllowXpoOneWayAssociations]
public class Customer : XPObject
{
public XPCollection
}
[AllowXpoOneWayAssociations]
public class Order : XPObject
{
}
If we now look at the assembly in Reflector, we can see that a public field along with an custom attribute has been added to Order and an attribute has been added to Customer:
Back to XPO
So, most of the above has been about PostSharp. Specific to XPO, the custom attribute I have added is the AssociationAttribute required by XPO to link the two sides of the association. So, I started with a one-way association and have used PostSharp to magically turn that into a two-way one-many association.
If you want the code, you can get it from here. Note that I've put up the XPO version and a version that doesn't require XPO.
Update: I've edited this to show that the XPCollection were the generic XPCollection
4 comments:
At the beginning of your article you write:
(start quote)
I want to be able to write this as valid XPO code:
public class Customer : XPObject
{
public XPCollection Orders { get; }
}
public class Order : XPObject
{
}
But XPO wants me to add the other end of the link to Order and to also add associated attributes
(end quote)
What happens if you do this?
If the code fails to run, how specifically does it fail?
If you don't include the Association attribute, there will be no "missing association" error.
Is it not possible to use XPCollection outside of an association?
Or does the code work, but DevExpress feels that you are not doing relations in the approved XPO way, and it may break in a future release of XPO?
Thanks in advance,
Adam Leffert
Hi Adam
An exception will be raised by XPO at runtime: "DevExpress.Xpo.Exceptions.RequiredAttributeMissingException: The 'Orders' property doesn't define the 'AssociationAttribute' attribute".
It is indeed possible to use an XPCollection outside of the context of an object. Not sure about XPCollection<T>. However, the exception is raised from the XPObject hierarchy, not the collection.
HTH
Sean
Is there a Solution for PostSharp 2.0, it does not work.
Thanks
PostSharp 2.0 does work differently. I have been meaning to post an update for this, but haven't yet had time. I'll keep trying and hope to get some time free.
Post a Comment