This is the sixth of a series about how to go about using postcompilation in your solutions. You can read it as a tutorial on how to use PostSharp. I am very much new to that framework, but the power it provides could seriously change how you build your applications. While working on the EF contrib project, I had to dive into PostSharp, and I hope to share some of the things I learned along the way.
Part VI, the EdmScalarWeaver
Recap
We wish to create an attribute that can be placed on top of our ordinary Poco class, that will magically transform it into a class that implements the 3 IPoco interfaces. These are needed by the Entity Framework to do it's work. We will use PostSharp to do this.
Our previous post talked about the compound attribute and how it goes about implementing interfaces on classes for you.
We want to put custom attributes on our type that EF needs (the EDM scalar attributes on top of properties and the EDM type attribute that connects your type to a EDM type). Laos does not seem to have a ready-to-use aspect that provides that functionality, so we are going to need to hook into the weaver ourselves! How exciting!
Thankfully, we can derive from TypeLevelAspectWeaver to make life easy enough.
In the previous post, we hooked up the weaver, this post we are actually going to do stuff.
The Implement method
Our weaver derives from TypeLevelAspectWeaver, and thus can override the Implement() method. I have to do some stuff to get to your config file, using PostSharp to get the Path to the original App.Config. When I have that, I load it, and look at the connectionstring that matches the containername you have supplied the attribute. Then, I use the EntityConnectionBuilder to create a connection string and finally load in the metadata workspace from EDM. With the metadata in hand, I can start looking at the transformation I have to do.
Setting EDMScalarAttributes
I recently chatted with Gael (creator of PostSharp) and he assured me that there would be a highlevel method to add attributes to code. In this version of PostSharp, that is not directly possible (hence, the weaver we are using). So, we will do it ourselves.
First, let's loop through all the properties defined on our supplied businessEntity:
1 foreach (PropertyDeclaration prop in typeDef.Properties)
2 { 3 EdmProperty memberProperty;
4
5 // find it as a member
6 memberProperty = entityType.Members.SingleOrDefault(edmprop => edmprop.Name.Equals(prop.Name)) as EdmProperty;
7
8 // it can easily be something else than an edm property
9 if (memberProperty != null)
10 { 11 // it might be a key property. I have not yet found a better way to determine if it is a keymember or not. This seems wastefull
12 prop.CustomAttributes.Add(
13 CreatePropertyAttribute(memberProperty,
14 (entityType.KeyMembers.SingleOrDefault(edmprop => edmprop.Name.Equals(prop.Name)) != null)));
15
16 continue;
17 }
18 }
I use a bit of Linq to check if this a propety is a key, and call my CreatePropertyAttribute method:
1 CustomAttributeDeclaration CreatePropertyAttribute(EdmProperty edmProperty, bool IsKeyProperty)
2 { 3 CustomAttributeDeclaration attr = new CustomAttributeDeclaration(edmScalarPropertyAttribute);
4
5 // nullable
6 attr.NamedArguments.Add(
7 new MemberValuePair(MemberKind.Property,
8 0,
9 "IsNullable",
10 new SerializedValue(
11 SerializationType.GetSerializationType(this.module.FindType(typeof(bool), BindingOptions.Default)),
12 edmProperty.Nullable)
13 ));
14
15 // since we need to set the ordinal, take care to set this property last!
16 if (IsKeyProperty)
17 { 18 attr.NamedArguments.Add(
19 new MemberValuePair(MemberKind.Property,
20 1,
21 "EntityKeyProperty",
22 new SerializedValue(
23 SerializationType.GetSerializationType(this.module.FindType(typeof(bool), BindingOptions.Default)),
24 true)
25 ));
26 }
27
28 return attr;
29 }
As you can see, it get's a little bit more complicated. We need to add a custom attribute, but to get it, we need to have a constructor for the attribute. I already have it cached: at line 3 the cached IMethod is given to the PostSharp CustomAttributeDecaration class. I got to the ctor like this:
edmScalarPropertyAttribute = module.FindMethod(typeof(EdmScalarPropertyAttribute).GetConstructor(System.Type.EmptyTypes), BindingOptions.Default);
We use PostSharp to find the constructor in the module.
With the constructor, we can create a customAttributeDeclaration and from there we can add namedArguments. Note, that here again, we use PostSharp to find types for us. Kind of confusing, but it does provide a consistent way to do things. You could use it to call your own methods as well (!).
I do the same for the attribute that needs to be placed on the complete type, and we are ready!
Default values
In the EF designer, you have the ability to specify default values for properties. I needed to mimic this functionality for this project, so I got to work. It seemed quite simple, because I could get to the fields without a problem. However, fields are initialized in the ctor of your type (thank you reflector). So more work was needed.
First, I wanted to reuse this weaver, and wanted the weaver to add IL methods in the constructor. To do that, I implemented the ITypeLevelAdvice interface and added this line to the end of the implement():
// make sure this class is called to weave
this.Task.TypeLevelAdvices.Add(
this);
Implementing the ITypeLevelAdvice gives us the opportunity to supply some information about what we want to do exactly:
#region ITypeLevelAdvice Members
public JoinPointKinds JoinPointKinds { get { return JoinPointKinds.AfterInstanceInitialization; } }
public TypeDefDeclaration Type { get { return (TypeDefDeclaration)this.TargetElement; } }
#endregion
#region IAdvice Members
public int Priority
{
get { return 0; }
}
public bool RequiresWeave(PostSharp.CodeWeaver.WeavingContext context)
{
return true;
}
#endregion
As you can see, I want to use the AfterInstanceInitialization joinpoint. In other words, I want to be able to weave code, at that moment.
What to weave?? I know everything about my businessentity, but I only know which properties need default values. So I want to come up with some basic rules about which field belongs to a certain propertyname:
1 #region set default values. not yet emitting the instruction, but waiting for the Weave method
2 foreach (FieldDefDeclaration field in typeDef.Fields)
3 {
4 // we have to make concessions: we do not know how to find the field with the property exactly
5 EdmProperty memberProperty;
6
7 // find it as a member
8 // the rules: the field must match the ending of the propertyname. So underscore is okay
9 memberProperty = entityType.Members.SingleOrDefault(edmprop => field.Name.EndsWith(edmprop.Name, StringComparison.OrdinalIgnoreCase) ) as EdmProperty;
10
11 // in case that didn't match, try the autogenerated fieldname
12 if (memberProperty == null)
13 {
14 memberProperty = entityType.Members.SingleOrDefault(edmprop => (field.Name.IndexOf("<" + edmprop.Name + ">") == 0)) as EdmProperty;
15 }
16
17 // if this field belongs to a edm property, we can check for it's default value
18 if (memberProperty != null)
19 {
20 FieldsNeedingDefaultValue.Add(field, memberProperty.Default);
21 }
22
23 }
24 #endregion
I use two rules: if the field ends with the same name as the property, then this field belongs to that property. Another rule is, to look at the naming scheme that the compiler uses when it generates auto properties: <Propertyname>_k_backingfield;. Since that is how we will most likely use this whole project, I want to also support that.
I build up a default value dictionary that I use in a later stadium.
The weave method will be called when our joinpoint has been reached.
1 public void Weave(PostSharp.CodeWeaver.WeavingContext context, InstructionBlock block)
2 {
3 foreach (FieldDefDeclaration field in FieldsNeedingDefaultValue.Keys)
4 {
5 object value = FieldsNeedingDefaultValue[field];
6
7 if (value == null)
8 continue;
9
10 // the context is the ctor because we only use the joinpoint AfterinstanceInitialization
11 InstructionSequence sequence = context.Method.MethodBody.CreateInstructionSequence();
12 block.AddInstructionSequence(sequence, NodePosition.Before, null);
13 context.InstructionWriter.AttachInstructionSequence(sequence);
14 InstructionWriter writer = context.InstructionWriter;
15
16 if(value is int)
17 {
18 writer.EmitInstruction(OpCodeNumber.Nop);