So in this article I will describe how the code generation in RIA
services SP1 works and I will demonstrate some scenarios and ideas were
customizing the generated code is a nice technique to extend your own
framework. I will try to focus on the code generation aspects so views
and viewmodels, dynamic loading of modules, ... will not be handled in
these examples.
WCF RIA Services code generation
I'm using RIA Services since the first betas because it is a
powerful framework which generates a lot of Silverlight client code for
you. When you have implemented entities, DTO's and domain services on
the server side an MSBuild task will automatically generate client
proxies each time you build your project. RIA Services can also copy
validation or other logic from the service side to the client. For each
DomainService a client side DomainContext will be created and these
DomainContexts will load your entities and DTO's and they support
change tracking and submitting changes.
Setup of demo application
I created a simple
Silverlight Navigation Application with an ASP.NET project . At server side I created a folder called
Domain with an
Entity Framework Model that includes some entities of the
Northwind database.
In the folder
Services a RIA DomainService class was implemented for the
Product entity. This
ProductService looks like this:
using System.Data;
using System.Linq;
using System.ServiceModel.DomainServices.EntityFramework;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Services
{
[EnableClientAccess()]
public class ProductService : LinqToEntitiesDomainService<NorthwindEntities>
{
[Query(IsDefault = true)]
public IQueryable<Product> GetProducts()
{
var products = ObjectContext.Products;
return products;
}
public IQueryable<Product> GetProductsByName(string searchText)
{
var products = ObjectContext.Products.Where(p => p.ProductName.Contains(searchText));
return products;
}
public void InsertProduct(Product product)
{
if ((product.EntityState != EntityState.Detached))
{
ObjectContext.ObjectStateManager.ChangeObjectState(product, EntityState.Added);
}
else
{
ObjectContext.Products.AddObject(product);
}
}
public void UpdateProduct(Product currentProduct)
{
ObjectContext.Products.AttachAsModified(currentProduct, ChangeSet.GetOriginal(currentProduct));
}
public void DeleteProduct(Product product)
{
if ((product.EntityState != EntityState.Detached))
{
ObjectContext.ObjectStateManager.ChangeObjectState(product, EntityState.Deleted);
}
else
{
ObjectContext.Products.Attach(product);
ObjectContext.Products.DeleteObject(product);
}
}
}
}
The Silverlight project has a
WCF RIA Services link to the server side ASP.NET project which hosts the DomainServices.

When you build the solution the code geneneration will be executed. In the hidden folder
Generated_Code in the Silverlight project you will find following default generated client code.

Now you can easily access the your product data in Silverlight by calling
Load methods of the
ProductContext class:
var context = new ProductContext();
context.Load(context.GetProductsQuery());
context.Load(
context.GetProductsByNameQuery("ton"),
LoadBehavior.MergeIntoCurrent,
loadOperation =>
{
var products = context.Products;
var loadedEntities = loadOperation.Entities;
},
null);
For more information about RIA Services you can read the documentation at
MSDN.
Installation of Toolkit
Now we can start modifying this project. First of all we have to install the
WCF RIA Services Toolkit (April 2011). You can download the installation package from the
Microsoft website.
Another and easier way is to open the new package manager
NuGet which is available after installing
Visual Studio SP1. Choose
Get Library Package Reference and look for RiaServices in the online gallery. Then install the
RIAServices.T4 library.

Since the April update of the Toolkit you do not need to modify the
Silverlight project file anymore. Before this release it was required
to add the
<RiaClientCodeGeneratorName> tag in the project file.
T4 Code generation classes
The T4 Code Generator classes are included in the
Microsoft.ServiceModel.DomainServices.Tools and
Microsoft.ServiceModel.DomainServices.Tools.TextTemplate assemblies. The T4 Code Generator consists of 2 parts:
The
CSharpClientCodeGenerator class and the
DomainServiceClientCodeGenerator attribute are the main components that hook into the extensibility features of the WCF RIA Services code generation process.
Secondly there are 5 different code generators:
- CSharpDomainContextGenerator
- CSharpEntityGenerator
- CSharpComplexObjectGenerator
- CSharpEnumGenerator
- CSharpWebContextGenerator
Each of the 5 code generators have protected virtual methods which
you can override to change the code generation of a specific part of a
class (class declaration, constructor, property, ...). And finally
there is a
TransformText method which contains all the generated code.
So in my server project I added a
Generators folder with a
MyCodeGenerator class derived from
CSharpClientCodeGenerator and decorated with the
DomainServiceClientCodeGenerator attribute.
using Microsoft.ServiceModel.DomainServices.Tools;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
[DomainServiceClientCodeGenerator("MyCodeGenerator", "C#")]
public class MyCodeGenerator : CSharpClientCodeGenerator
{
protected override EntityGenerator EntityGenerator
{
get { return new MyEntityGenerator(); }
}
protected override DomainContextGenerator DomainContextGenerator
{
get { return new MyDomainContextGenerator(); }
}
protected override WebContextGenerator WebContextGenerator
{
get { return new MyWebContextGenerator(); }
}
protected override ComplexObjectGenerator ComplexObjectGenerator
{
get { return base.ComplexObjectGenerator; }
}
protected override EnumGenerator EnumGenerator
{
get { return base.EnumGenerator; }
}
}
}
I also created 3 other generator classes for the entity, domain context and web context.
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
public override string TransformText()
{
return base.TransformText();
}
protected override void GenerateClassDeclaration()
{
base.GenerateClassDeclaration();
}
protected override void GenerateProperty(PropertyDescriptor propertyDescriptor)
{
base.GenerateProperty(propertyDescriptor);
}
}
}
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyDomainContextGenerator : CSharpDomainContextGenerator
{
public override string TransformText()
{
return base.TransformText();
}
protected override void GenerateClassDeclaration()
{
base.GenerateClassDeclaration();
}
}
}
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyWebContextGenerator : CSharpWebContextGenerator
{
public override string TransformText()
{
return base.TransformText();
}
protected override void GenerateClassDeclaration()
{
base.GenerateClassDeclaration();
}
}
}
If you build your project again you will see that the generated code
(ScipBe.Demo.RiaCodeGen.Web.g.cs) will be different from the default
code generation. The functionality is the same but the order of the
classes differs and comment headers are not generated.
Let us start manipulating the default code generation behaviour.
Adding interfaces to the generated entities
Wouldn't it be handy if you could use the same interfaces for your
entities at server and client side? With just a few lines of code we
can accomplish this. First I created an
IId interface with an int
Id
property. I renamed the key properties in my Northwind EDM and I added
the IId interface in the partial class of each entity.
namespace ScipBe.Demo.RiaCodeGen.Web.Domain
{
public interface IId
{
int Id { get; set; }
}
}
namespace ScipBe.Demo.RiaCodeGen.Web.Domain
{
public partial class Product : IId
{
}
}
Because the interface is also needed on client side I created a
shared link to this file in the
Domain folder in my Silverlight project.

In the
MyEntityGenerator class I implemented the
GenerateClassDeclaration method and there I added a
using to the namespace of the IId interface.
Finally in the
TransformText method I check if the entity is derived from
IId and then this interface is added in the generated code by replacing strings.
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Web.Domain;");
base.GenerateClassDeclaration();
}
public override string TransformText()
{
var generatedCode = base.TransformText();
if (typeof(IId).IsAssignableFrom(Type))
{
generatedCode = generatedCode
.Replace(
"System.ServiceModel.DomainServices.Client.Entity",
"System.ServiceModel.DomainServices.Client.Entity, IId");
}
return generatedCode;
}
}
}
The
generated code will look like this:
Adding attributes to the generated entities
The same approach can be used to decorate the generated client entities with attributes. I like to use the
DebuggerVisualizer attribute which makes it easier to inspect collection of entities in the
Local and
Watch windows of Visual Studio.
So on server side I decorated the
Product class with the
DebuggerDisplay attribute.
using System.Diagnostics;
namespace ScipBe.Demo.RiaCodeGen.Web.Domain
{
[DebuggerDisplay("{ProductName}")]
public partial class Product : IId
{
}
}
In the
TransformText method in the
MyEntityGenerator
class I added some code to check if the class is decorated with the
attribute and then I replace some of the generated code. Don't forget
to add the using of
System.Diagnostics.
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
protected override void GenerateClassDeclaration()
{
WriteLine("using System.Diagnostics;");
base.GenerateClassDeclaration();
}
public override string TransformText()
{
var generatedCode = base.TransformText();
var debuggerDisplayAttributes = Type.GetCustomAttributes(typeof(DebuggerDisplayAttribute), true)
.Cast<DebuggerDisplayAttribute>().FirstOrDefault();
if (debuggerDisplayAttributes != null)
{
var displayName = debuggerDisplayAttributes.Value;
generatedCode = generatedCode
.Replace("public sealed partial class",
"[DebuggerDisplay(\"" + displayName + "\")]\r\n public sealed partial class");
}
return generatedCode;
}
}
}
When debugging the Silverlight project you will be able to see the names of the products in Visual Studio
Locals,
Watches and tooltip windows.
Using derived data annotation attributes
The Entity Framework and RIA Services support several data annotation attributes like
Required, StringLength, RegularExpression, Range,
... You can add these attributes in your own metadata classes but RIA
Services also adds them automatically for constraints which are defined
on the database. By modifying the generated code you can implement your
own derived attributes.
using System.ComponentModel.DataAnnotations;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public class MyRequiredAttribute : RequiredAttribute
{
}
}
public override string TransformText()
{
var generatedCode = base.TransformText();
generatedCode = generatedCode
.Replace("[System.ComponentModel.DataAnnotations.RequiredAttribute()]", "[MyRequired]");
return generatedCode;
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
base.GenerateClassDeclaration();
}
Adding XML comments to the generated entities
I also found a nice example of an implementation of the T4 Code Generators at the blog of
Yavor Georgiev
who is a Program Manager at Microsoft. He describes how you can copy
the XML comment of the classes and properties of EDM entities.
I tried to improve his sources a little bit and moved the logic to a
XmlCommentsProvider class.
using System;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Diagnostics;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class XmlCommentsProvider
{
private XElement comments;
public XmlCommentsProvider(Type classType, string path)
{
var assemblyXmlFileName = path.Replace("dll", "xml");
comments = XElement.Load(new FileStream(assemblyXmlFileName, FileMode.Open));
}
private XElement FindCommentElementByName(string name)
{
return comments
.Descendants("member")
.Where(x => String.Compare(x.Attributes("name").FirstOrDefault().Value.ToString(), name, true) == 0)
.FirstOrDefault();
}
private string PrintInnerXml(XElement element)
{
string[] lines = element.Nodes()
.Aggregate("", (b, node) => b += node.ToString())
.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
var sb = new StringBuilder();
foreach (string line in lines)
{
sb.AppendLine(line.Trim().Insert(0, "///"));
}
return sb.ToString().Trim();
}
public string GetClassComments(Type classType)
{
var element = FindCommentElementByName("T:" + classType.FullName);
if (element != null)
{
return PrintInnerXml(element);
}
return null;
}
public string GetPropertyComments(PropertyDescriptor propertyDescriptor)
{
var element = FindCommentElementByName("P:" + propertyDescriptor.ComponentType.FullName + "." + propertyDescriptor.Name);
if (element != null)
{
return PrintInnerXml(element);
}
return null;
}
}
}
Now I can call the
XmlCommentsProvider. GetClassComments method in the
GenerateClassDeclaration method and
XmlCommentsProvider. GetPropertyComments in the
GenerateProperty method.
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
XmlCommentsProvider xmlCommentsProvider;
protected override void GenerateClassDeclaration()
{
// Add XML documentation for class
xmlCommentsProvider =new XmlCommentsProvider(this.Type, this.ClientCodeGenerator.Options.ClientProjectPath);
WriteLine(xmlCommentsProvider.GetClassComments(this.Type));
base.GenerateClassDeclaration();
}
protected override void GenerateProperty(PropertyDescriptor propertyDescriptor)
{
// Add XML documentation for property
if (xmlCommentsProvider != null)
{
var comments = xmlCommentsProvider.GetPropertyComments(propertyDescriptor);
if (!string.IsNullOrEmpty(comments))
{
WriteLine(comments);
}
}
base.GenerateProperty(propertyDescriptor);
}
}
}
If you add comments to the entity
then the final generated code will look like this:
Using derived classes for entities, domain contexts and the web context
I always prefer to create derived classes from the .NET classes in
my framework so that I have the freedom to add new or modify default
behaviour. On server side I always implement my own DomainService
class. Now we can also implement derived classes on the client for
Entity, DomainContext and WebContext.
So I added 3 classes
MyEntity, MyDomainContext and
MyWebContext in a
Framework folder in the Silverlight project.
using System.Runtime.Serialization;
using System.ServiceModel.DomainServices.Client;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
[DataContract]
public class MyEntity : Entity
{
}
}
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.DomainServices.Client;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
}
}
using System.ServiceModel.DomainServices.Client.ApplicationServices;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public class MyWebContext : WebContextBase
{
}
}
The next step is very easy and you just have to replace some text in the
TransformText method.
This is how my
MyEntityGenerator class will look with all the previous examples implemented.
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
using ScipBe.Demo.RiaCodeGen.Web.Domain;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyEntityGenerator : CSharpEntityGenerator
{
XmlCommentsProvider xmlCommentsProvider;
public override string TransformText()
{
var myEntity = "MyEntity";
if (typeof(IId).IsAssignableFrom(Type))
{
myEntity += ", IId";
}
var generatedCode = base.TransformText();
var debuggerDisplayAttributes = Type.GetCustomAttributes(typeof(DebuggerDisplayAttribute), true)
.Cast<DebuggerDisplayAttribute>().FirstOrDefault();
if (debuggerDisplayAttributes != null)
{
var displayName = debuggerDisplayAttributes.Value;
generatedCode = generatedCode
.Replace("public sealed partial class",
"[DebuggerDisplay(\"" + displayName + "\")]\r\n public sealed partial class");
}
generatedCode = generatedCode
.Replace("System.ServiceModel.DomainServices.Client.Entity", myEntity);
generatedCode = generatedCode
.Replace("[System.ComponentModel.DataAnnotations.RequiredAttribute()]", "[MyRequired]");
return generatedCode;
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
WriteLine("using ScipBe.Demo.RiaCodeGen.Web.Domain;");
WriteLine("using System.Diagnostics;");
// Add XML documentation for class
xmlCommentsProvider = new XmlCommentsProvider(this.Type);
WriteLine(xmlCommentsProvider.GetClassComments(this.Type));
base.GenerateClassDeclaration();
}
protected override void GenerateProperty(PropertyDescriptor propertyDescriptor)
{
// Add XML documentation for property
if (xmlCommentsProvider != null)
{
var comments = xmlCommentsProvider.GetPropertyComments(propertyDescriptor);
if (!string.IsNullOrEmpty(comments))
{
WriteLine(comments);
}
}
base.GenerateProperty(propertyDescriptor);
}
}
}
The
MyDomainContextGenerator and
MyWebContextGenerator classes are implemented in the same way.
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyDomainContextGenerator : CSharpDomainContextGenerator
{
public override string TransformText()
{
return base.TransformText()
.Replace(
"System.ServiceModel.DomainServices.Client.DomainContext",
"MyDomainContext");
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
base.GenerateClassDeclaration();
}
}
}
using Microsoft.ServiceModel.DomainServices.Tools.TextTemplate.CSharpGenerators;
namespace ScipBe.Demo.RiaCodeGen.Web.Generators
{
public class MyWebContextGenerator : CSharpWebContextGenerator
{
public override string TransformText()
{
return base.TransformText()
.Replace(
"System.ServiceModel.DomainServices.Client.ApplicationServices.WebContextBase",
"MyWebContext");
}
protected override void GenerateClassDeclaration()
{
WriteLine("using ScipBe.Demo.RiaCodeGen.Framework;");
base.GenerateClassDeclaration();
}
}
}
The generated code will have following changes:
Implementing your own logic in the derived classes
Now you can start to add your own logic in these framework classes. For example you could modify the
endpoint timeouts in the
MyDomainContext class. It is a lot easier to debug a Silverlight RIA Services application when the timeouts are increased.
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
public MyDomainContext(DomainClient domainClient) : base(domainClient)
{
var channelFactory = (ChannelFactory)DomainClient.GetType().GetProperty("ChannelFactory").GetValue(DomainClient, null);
var timeout = new TimeSpan(0, 5, 00);
if (Debugger.IsAttached)
{
timeout = new TimeSpan(0, 15, 00);
}
var endpointBinding = channelFactory.Endpoint.Binding;
endpointBinding.OpenTimeout = timeout;
endpointBinding.CloseTimeout = timeout;
endpointBinding.SendTimeout = timeout;
}
}
}
You could also create your own overloaded
Load methods.
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
public LoadOperation Load(EntityQuery query, Action<LoadOperation> callback)
{
return Load(query, LoadBehavior.RefreshCurrent, callback, null);
}
}
}
I don't know if you ever faced the problem of logging out with RIA
Services while domain contexts or still loading or submitting data. You
will get exceptions. Therefore it would be handy of you could check if
domain contexts somewhere in your viewmodels are still processing. In
the singleton
MyWebContext class I added a 2 static methods to increase and decrease a counter and in the
Load method of my
MyDomainContext I call this increase method and in the callback action the counter is decreased. Of course you should do the same in the
SubmitChanges and the two
InvokeOperation methods.
using System;
using System.Diagnostics;
using System.ServiceModel;
using System.ServiceModel.DomainServices.Client;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public abstract class MyDomainContext : DomainContext
{
public MyDomainContext(DomainClient domainClient) : base(domainClient)
{
var channelFactory = (ChannelFactory)DomainClient.GetType().GetProperty("ChannelFactory").GetValue(DomainClient, null);
var timeout = new TimeSpan(0, 5, 00);
if (Debugger.IsAttached)
{
timeout = new TimeSpan(0, 15, 00);
}
var endpointBinding = channelFactory.Endpoint.Binding;
endpointBinding.OpenTimeout = timeout;
endpointBinding.CloseTimeout = timeout;
endpointBinding.SendTimeout = timeout;
}
public LoadOperation Load(EntityQuery query, Action<LoadOperation> callback)
{
return Load(query, LoadBehavior.RefreshCurrent, callback, null);
}
public override LoadOperation Load(EntityQuery query, LoadBehavior loadBehavior, Action<LoadOperation> callback, object userState)
{
Action<LoadOperation> newCallback =
loadOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(loadOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.Load(query, loadBehavior, newCallback, userState);
}
public override SubmitOperation SubmitChanges(Action<SubmitOperation> callback, object userState)
{
Action<SubmitOperation> newCallback =
submitOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(submitOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.SubmitChanges(newCallback, userState);
}
public override InvokeOperation<TValue> InvokeOperation<TValue>(string operationName, Type returnType,
System.Collections.Generic.IDictionary<string, object> parameters, bool hasSideEffects,
Action<InvokeOperation<TValue>> callback, object userState)
{
Action<InvokeOperation<TValue>> newCallback =
invokeOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(invokeOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.InvokeOperation<TValue>(operationName, returnType, parameters, hasSideEffects, newCallback, userState);
}
public override InvokeOperation InvokeOperation(string operationName, Type returnType,
System.Collections.Generic.IDictionary<string, object> parameters, bool hasSideEffects,
Action<InvokeOperation> callback, object userState)
{
Action<InvokeOperation> newCallback =
invokeOperation =>
{
MyWebContext.DecreaseBusyCounter();
if (callback != null)
{
callback.Invoke(invokeOperation);
}
};
MyWebContext.DecreaseBusyCounter();
return base.InvokeOperation(operationName, returnType, parameters, hasSideEffects, newCallback, userState);
}
}
}
using System.ServiceModel.DomainServices.Client.ApplicationServices;
namespace ScipBe.Demo.RiaCodeGen.Framework
{
public class MyWebContext : WebContextBase
{
private static object lockObject = new object();
private static int busyCounter;
public static void IncreaseBusyCounter()
{
lock (lockObject)
{
busyCounter++;
}
}
public static void DecreaseBusyCounter()
{
lock (lockObject)
{
busyCounter--;
}
}
public bool IsBusy
{
get { return busyCounter > 0; }
}
}
}
Now you can easily check if domain contexts are busy loading or submitting data.
if (!WebContext.Current.IsBusy)
{
WebContext.Current.Authentication.Logout();
}
I hope that you like my T4 code generation examples for RIA Services
and that you can take advantage of it. It is a very powerful feature
and I'm quite sure these features will be extended in future versions.
If you have any other ideas, remarks or suggestions, please let me know.
Downloaden
Demo applicatie RIA Services code generatie |
 |
Inhoud |
Demo applicatie WCF RIA Services code generatie |
Versie |
(2011-06-12) |
Grootte |
195.9 Kb |
|
|
|