When updating a small RIA application which has been based on a previous beta of Visual Studio 2010 (and which worked perfectly back then), I noticed an interesting bug, which has in the meantime been confirmed at silverlight.net: if you are using 1:n relationships for which the n part uses a compound primary key consisting of at least one non-nullable type, you can not add items at runtime without explicitly specifying the primary key (and a few other things, like deletes, might also result in the same error:
ArgumentNullException occurred. An EntityKey value cannot be null
This exception happens in the client-side entity’s GetIdentity() method, which looks like the following:
- public override object GetIdentity()
- {
- return EntityKey.Create(this._invoiceNumber, this._pos);
- }
(In my case, the string _invoiceNumber and the int _pos determine the primary key).
Based on the post at silverlight.net, this is a bug in the RIA codegen, as the code should actually check whether _invoiceNumber is null, and should simply return null instead of trying to create an EntityKey. The poster also suggested that it would be possible to create a CodeProcessor which changes the generated code.
And that’s what he remainder of this post is about.
In RIA Services, the code generation is performed in a way which is very different from the regular “Add Service Reference …” way of generating a proxy. It is however also very easily extensible so that you can intercept the code generation with just a few lines of code in a class which derives from CodeProcessor.
Instead of directly working with the generated CodeDom elements, I decided to take the easy route and write a processor which simply lets me inject additional code at the beginning of any method. It does this, I first designed a custom attribute which allows me to specify the code to be injected:
- [AttributeUsage(AttributeTargets.Class)]
- public class InjectClientsideCodeAttribute : Attribute
- {
- public InjectClientsideCodeAttribute(string methodName, string code)
- {
- Code = code;
- MethodName = methodName;
- }
- public string Code;
- public string MethodName;
- }
This attribute is used on server side’s partial class implementation for each entity you want to modify. In my application, I simply wanted to prepend the method GetIdentity with the specified code fragment to check the nullness of _invoicenumber:
- #if !SILVERLIGHT
- [InjectClientsideCode("GetIdentity", "if (_invoiceNumber == null) return null;")]
- #endif
- public partial class InvoiceLine
- {
- // ...
- }
The codeprocessor which does the actual work is relatively simple as well. It looks for all types which have the [InjectClientsideCode] attribute set, and then iterates over all the methods which match the method name and inserts the code by using a CodeSnippetStatement (which also means that the code in your [InjectClientsideCode]-attribute has to be specified in the correct programming language you are using for your client-side project).
- public class MethodPatchingCodeProcessor : CodeProcessor
- {
- public MethodPatchingCodeProcessor(CodeDomProvider codeDomProvider) : base(codeDomProvider) { }
- public override void ProcessGeneratedCode(DomainServiceDescription domainServiceDescription,
- System.CodeDom.CodeCompileUnit codeCompileUnit,
- IDictionary<Type, CodeTypeDeclaration> typeMapping)
- {
- Dictionary<Type, CodeTypeDeclaration> typesToPatch = typeMapping.Where(tm => tm.Key.GetCustomAttributes(typeof(InjectClientsideCodeAttribute), false).Length > 0).ToDictionary(p => p.Key, p => p.Value);
- foreach (var typeToPatch in typesToPatch)
- {
- foreach (InjectClientsideCodeAttribute injectionAttribute in typeToPatch.Key.GetCustomAttributes(typeof(InjectClientsideCodeAttribute), false))
- {
- var methodsToPatch = typeToPatch.Value.Members.OfType<CodeMemberMethod>().ToList();
- methodsToPatch = methodsToPatch.Where(p => p.Name == injectionAttribute.MethodName).ToList();
- foreach (var methodToPatch in methodsToPatch)
- {
- methodToPatch.Statements.Insert(0, new CodeSnippetStatement(injectionAttribute.Code));
- }
- }
- }
- }
- }
To make this work, you can simply add the sourecode for InjectClientsideCodeAttribute and MethodPatchingCodeProcessor to your server-side project and apply the following attribute to your domain service to indicate that you want to customize the code generation:
- [DomainIdentifier("TTOverviewDomainService", CodeProcessor = typeof(MethodPatchingCodeProcessor))]
- public partial class ThinktectureDomainService : LinqToEntitiesDomainService<ThinktectureEntities>
- {
- // ...
- }
Et voilà. Instant code injection for your RIA services.
Comments