UPDATE: The RIA Services team released a December 2010 Toolkit, and that release fixes the build issues encountered below. But this post still has value as it shows some ways to modify the code generated by our T4 Code Generator. Be sure to check out Varun Puranik’s Blog for more details on the December 2010 update.
The RIA Services T4 Code Generation feature was checked in about 3 hours before we published our MSI for today’s release. Talk about publishing an experimental feature! So tonight, I decided to take the app that I’ve been working on for my RIA Services Validation series and flip it over to the T4 generator to see what the experience is like. Here is my uncensored experience.
Flipping the Switch
In order to use our T4 code generator, you first need to add an assembly reference:
- Microsoft.ServiceModel.DomainServices.Tools.TextTemplate (Our T4 code generator from the Toolkit)
Then you need to edit your Silverlight project file and add the following into the project properties:
<RiaClientCodeGeneratorName>
Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators.CSharpClientCodeGenerator,
Microsoft.ServiceModel.DomainServices.Tools.TextTemplate,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
</RiaClientCodeGeneratorName>
We would like your project to flip over to T4 just by adding the reference; we’ve filed a bug for that. Alternatively, it would be nice to only specify the class name here instead of the fully-qualified name. As you’ll see below, when you add your own code generator, it actually gets simpler though.
At this point, your project is switched over to use the T4 code generator. Compile and see how we did. If you don’t have any compile errors, then you should be in good shape to use the T4 generator. Some of you might experience build errors though, and if you do these are bugs and we need to hear about them.
Getting Build Errors
As a matter of fact, with my RudeValidation solution, after I flipped the switch, I got build errors. I have found a bug already. Hey, I told you these were experimental bits! Here’s what I saw after my first T4 compile.
As you can see, my custom ValidationAttributes accept parameters of enum types, and the enum values are not being qualified properly. Oh well, I guess this T4 stuff won’t work for me, huh? Wait! I have control over the generated code… maybe I can fix this!
Providing a ClientCodeGenerator
I want to introduce my own code generator to take control and try to fix this issue. I need to create a new class and derive from CSharpClientCodeGenerator, and decorate my class with a DomainServiceClientCodeGenerator attribute. Let’s start with that.
using Microsoft.ServiceModel.DomainServices.Tools;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace RudeValidation.Web.T4
{
// Needed to add a reference to Microsoft.ServiceModel.DomainServices.Tools
[DomainServiceClientCodeGenerator(typeof(RudeValidationClientCodeGenerator), "C#")]
public class RudeValidationClientCodeGenerator : CSharpClientCodeGenerator
{
}
}
You’ll notice that I needed to add a reference to Microsoft.ServiceModel.DomainServices.Tools for this to work. That assembly is in our framework (not the Toolkit) and it’s where the DomainServiceClientCodeGeneratorAttribute class is defined. Also, in order for this to compile, I needed to add a reference to System.ComponentModel.Composition (MEF) because that attribute class actually derives from ExportAttribute.
To switch from the default T4 code generator to our custom one, we’ll simply go back into the Silverlight project file and remove the <RiaClientCodeGeneratorName> tag completely. Alternatively, we could specify the class name to the desired generator (RudeValidation.Web.T4.RudeValidationClientCodeGenerator), but since we only have 1 generator defined in our project it will be picked up by default. After removing that tag and reloading the Silverlight project, our build is now using the custom code generator. We still have the build error though, since we haven’t done anything to address the issue.
Providing an EntityGenerator
At this point, we need to find the right hook in the code generation to override behavior. We can explore the virtual methods in CSharpClientCodeGenerator by typing “override” and letting IntelliSense lead us. In doing so, I found that there’s a virtual property for EntityGenerator—that sounds promising, so let’s override that. We can then derive from CSharpEntityGenerator and provide our own. This is where we are now:
using Microsoft.ServiceModel.DomainServices.Tools;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace RudeValidation.Web.T4
{
// Needed to add a reference to Microsoft.ServiceModel.DomainServices.Tools
[DomainServiceClientCodeGenerator(typeof(RudeValidationClientCodeGenerator), "C#")]
public class RudeValidationClientCodeGenerator : CSharpClientCodeGenerator
{
protected override EntityGenerator EntityGenerator
{
get
{
return new RudeValidationEntityGenerator();
}
}
public class RudeValidationEntityGenerator : CSharpEntityGenerator
{
}
}
}
We’re still not customizing behavior, but we’re getting close. We now have our own EntityGenerator and our custom DomainServiceClientCodeGenerator is set up to use it.
Customizing the Code
Let’s let IntelliSense guide us again. From inside the derived EntityGenerator class, I typed “override” and discovered GenerateAttributes is a virtual method that sounds promising. This method accepts a list of Attribute instances that have been instantiated based on what the server declares for every entity, and its job is to write out the client code to represent those attribute declarations. Using T4, this would be done in a TT file; in a C# class file, this can be done using this.Write/this.WriteLine.
To be honest, I’m kind of stuck here though. The logic required to transform server-side attribute instances into client-side attribute declarations is nontrivial at best. For those seeking to take full control over the attribute declarations, we have given them exactly what they need, full control. But those those like me who just need to tweak the current behavior ever so slightly, the barrier to entry is pretty high from here.
This is precisely the kind of feedback our team needs to hear. When you are trying to take over on code gen, where are you getting stuck? Technically, all of the hooks are in place for you to completely own the generated code, but what helpers can we provide to allow you to generate the code you desire?
Implementing a Hack
What, did you think I was going to give up? No way! I want my project to build using the T4 generator. Even if I don’t have the right tools to tweak this through a nifty API, I still own the code that is getting generated, so let’s fix it.
In the end, each generator has a TransformText() method that returns the generated code. We can override this method and massage the output as much as we want. While the following code is by no means elegant, it gets the job done.
using Microsoft.ServiceModel.DomainServices.Tools;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace RudeValidation.Web.T4
{
// Needed to add a reference to Microsoft.ServiceModel.DomainServices.Tools
[DomainServiceClientCodeGenerator(typeof(RudeValidationClientCodeGenerator), "C#")]
public class RudeValidationClientCodeGenerator : CSharpClientCodeGenerator
{
protected override EntityGenerator EntityGenerator
{
get
{
return new RudeValidationEntityGenerator();
}
}
public class RudeValidationEntityGenerator : CSharpEntityGenerator
{
public override string TransformText()
{
return base.TransformText()
.Replace("(GreaterThan", "(RudeValidation.Web.Validators.CompareOperator.GreaterThan")
.Replace("(LessThan", "(RudeValidation.Web.Validators.CompareOperator.LessThan")
.Replace("(Future", "(RudeValidation.Web.Validators.DateValidatorType.Future");
}
}
}
}
Yes, I just did that. I used 3 string replacements to fix a bug. And you know what, I love it. Why? Because there was a bug in the RIA Services T4 code generation and I was able to fix it with very little code in my own project. With this in place, my project now builds and works just as it did with the default CodeDom code generator. I call that a Win!
Reverting to CodeDom
I’m sure you’ll need to know, so I wanted to go ahead and call this out.
We no longer have anything specified in our Silverlight project to indicate which code generator to use, but our custom generator is being picked up by convention. There are a couple of ways of reverting back to the default CodeDom generator:
- Comment out the DomainServiceClientCodeGenerator attribute on the custom class; or
- Open the Silverlight project file back up and supply a RiaClientCodeGeneratorName property using the full type name of the CodeDomClientCodeGenerator class:
<RiaClientCodeGeneratorName>Microsoft.ServiceModel.DomainServices.Tools.CodeDomClientCodeGenerator</RiaClientCodeGeneratorName>
Notice that for this generator, only the class name is required and not the fully-qualified name. You’ll only need the fully-qualified name for our T4 CSharpClientCodeGenerator.
However, there is another bug here: I tried having a line breaks between the tags and the generator name, and it failed to strip those out. So be sure to specify this on one line.
Call to Action
If you are at all interested in controlling or modifying your generated code, please grab the new SP1 Beta and Toolkit release and flip the switch over to T4. Let us know what bugs you find and let us know what APIs you want to have for modifying the generated code. Any scenarios you can share with us for why and how you are seeking to modify the generated code will be greatly appreciated!
You can report issues and open discussions on our forums: http://forums.silverlight.net/forums/53.aspx