(X) Hide this
    • Login
    • Join
      • Say No Bots Generate New Image
        By clicking 'Register' you accept the terms of use .

WCF Data Services Advanced Topics - Custom proxy based on T4 templates

(75 votes)
Alexey Zakharov
>
Alexey Zakharov
Joined Sep 25, 2008
Articles:   8
Comments:   5
More Articles
6 comments   /   posted on Feb 26, 2009

This article is compatible with the latest version of Silverlight.


1. Introduction

This is the first article of my series about deep dive into  WCF Data Services. In this article I'm going to show you how to implement your own WCF Data Services proxy with T4 templates.

Source code and database backup

2. Content

2.1 Problem

WCF Data Services is a very powerful toy, but as many other cool Microsoft technologies it needs some workarounds to become usable in the real world applications.

Most of the problems are connected with the auto generated proxy which is created after the addition of a service reference. Here are the most popular:

1. Data Contract objects do not support INotifyPropertyChanged which is needed for two way data binding.There are partial methods for detecting changes to specific properties which makes implementing the INotifyPropertyChanged interface possible, but not quick.

2. Add some common interface for all generated entites. (e.g. IIdentifyable)

3. Add custom attributes to properties of generated entities.

4. And many many others, which is caused by absence of control over the code generation process.

Uncontrolled code generation is a problem of all Microsoft ORM products, that is why community have created similar projects for LINQ to SQL (Damien Guard) and Entity Framework (Danny Simmons, WCF Data Services team).

2.2 Solution

Solution to this problem is using of T4 templates for generation of WCF Data Service proxy. In this case you will have absolute control over code generation process, so such task as implementing INotifyPropertyChanged interface will become trivial.

Mostly I was inspired by Damien Guard project T4 template for generating LINQ to SQL Data Context. It was my first experience with T4 templates, that's why template helpers code may be a bit dirty.

2.3 How it works

To generate WCF Data Service entities and service context class we need some metadata, which describes them. You can find metadata file (services.edmx) in WCF Data Service reference. But this is not a good place to look for it =) The best way to get metadata file is to add the keyword $metadata to the end of the data service url (http://MySite.com/MyDataService.svc/$metadata).

The language used to describe WCF Data Services is the conceptual schema definition language (CSDL) defined by the ADO.NET Entity Data Model.

With the help of Linq to XML I will parse this metadata and wrap it into single object which will be used inside T4 template.

2.4  Implementation

First of all copy MetadataHelper.tt file to your project. It includes all metadata parsing logic and provides you single object with data needed for proxy generation. I'm not going to describe its implementation in the article, but its source is open so you are free to explore and modify it.

Next step is to add your T4 template, which will generate entities and data service context. There is no visual studio item template for T4 template, that is why you should add text file and change its extension to .tt. After changing of extension Visual Studio will automatically recognize it as T4 template and will add code behind .cs file where generated entities and data service context will appear.

After you have successfully added T4 template item you should perform little setup:

1. Include MetadataHelper.tt

2. Initialize options anonymous object with metadataUri and namespace of generated proxy. This is the only piece of information that will be normally changed while reusing of WCF Data Services proxy t4 template.

3. Create instance of Data class by providing metadata Uri to its constructor. Created instance of Data class that has all information which we need to generate entities and data service context object.

 <#@ template language="C#v3.5" hostspecific="True" debug="True"  #>
 <#@ include file="MetadataHelper.tt" #>
 <# 
 var options = new {
     MetadataUri = "http://localhost:51002/DataService.svc/$metadata",
     Namespace = "DataAccess"
 };
  
 var data = new Data(options.MetadataUri); 
 #> 

Now we are ready to  create the entities template. Let us assume that we need to implement INotifyPropertyChanged for each property.

Information about all entities provided by Data Service is held in Data class Entities property. So first of all we create cycle through all entities.

 <# 
 foreach (var entity in data.Entities)
 {
 #>
     Entity template is here!
 <#
 }
 #>

Each entity has name and collections of properties (info about properties, which has standard simple type such as string, int, double, DateTime) and navigation properties (info about properties which represent relationships between entities).

Entity template:

 public class <#= entity.Name #> : INotifyPropertyChanged
 {
 <# 
 foreach (var property in entity.Properties)
 {
 #>
     Property template is here!
 <#
 }
 foreach (var property in entity.NavigationProperties)
 {
 #>
     Navigation property template is here!
 <#
 }   
 #>
     public event PropertyChangedEventHandler PropertyChanged;
  
     private void OnPropertyChanged(string propertyName)
     {
         PropertyChangedEventHandler handler = PropertyChanged;
         if (handler != null)
         {
             handler(this, new PropertyChangedEventArgs(propertyName));
         }
     }
 }

Each entity property has name, type and flag that shows if property is nullable.

Property template:

 private <#= property.Type #><#= property.IsNullable ? "?" : ""  #> <#= property.Name.ToLower() #>;
 public <#= property.Type #><#= property.IsNullable ? "?" : ""  #> <#= property.Name #>
 {
     get { return <#= property.Name.ToLower() #>; }
     set
     {
         if (<#= property.Name.ToLower() #> != value)
         {
             <#= property.Name.ToLower() #> = value;
             OnPropertyChanged("<#= property.Name #>");
         }
     }            
 }

Each entity navigation property has name, type and flag that shows if property is collection.

Navigation property template:

 <#
 if (property.IsCollection) {  
 #>
 public Collection<<#= property.Type #>> <#= property.Name #> {get;set;}
 <#  
 } else { 
 #>
 public <#= property.Type #> <#= property.Name #> {get;set;}
 <# 
 }
 #>

Whole entity generation template will look like this.

 namespace <#= options.Namespace #>
 { 
 <# 
 foreach (var entity in data.Entities)
 {
 #>
     public class <#= entity.Name #> : INotifyPropertyChanged
     {
 <# 
      foreach (var property in entity.Properties)
      {
 #>
          private <#= property.Type #><#= property.IsNullable ? "?" : ""  #> <#= property.Name.ToLower() #>;
          public <#= property.Type #><#= property.IsNullable ? "?" : ""  #> <#= property.Name #>
          {
              get { return <#= property.Name.ToLower() #>; }
              set
              {
                  if (<#= property.Name.ToLower() #> != value)
                  {
                      <#= property.Name.ToLower() #> = value;
                      OnPropertyChanged("<#= property.Name #>");
                  }
              }            
          }
  <#
  }
      foreach (var property in entity.NavigationProperties)
      {
          if (property.IsCollection) {  
  #>
          public Collection<<#= property.Type #>> <#= property.Name #> {get;set;}
  <#  
          } else { 
  #>
          public <#= property.Type #> <#= property.Name #> {get;set;}
  <# 
          } 
      }
  #>
          public event PropertyChangedEventHandler PropertyChanged;
   
          private void OnPropertyChanged(string propertyName)
          {
              PropertyChangedEventHandler handler = PropertyChanged;
              if (handler != null)
              {
                  handler(this, new PropertyChangedEventArgs(propertyName));
              }
          }
      }
  <#
  }
  #>

And here is an example of generated entities:

 public class Client : INotifyPropertyChanged
 {
     private int id;
     public int ID
     {
         get { return id; }
         set
         {
             if (id != value)
              {
                  id = value;
                  OnPropertyChanged("ID");
              }
         }            
      }
      private string name;
      public string Name
      {
          get { return name; }
          set
          {
              if (name != value)
              {
                  name = value;
                  OnPropertyChanged("Name");
              }
          }            
      }
      private string address;
      public string Address
      {
          get { return address; }
          set
          {
              if (address != value)
              {
                  address = value;
                  OnPropertyChanged("Address");
              }
          }            
      }
      public Collection<Order> Orders {get;set;}
      public event PropertyChangedEventHandler PropertyChanged;
   
      private void OnPropertyChanged(string propertyName)
      {
          PropertyChangedEventHandler handler = PropertyChanged;
          if (handler != null)
          {
              handler(this, new PropertyChangedEventArgs(propertyName));
          }
      }
  }
  public class Order : INotifyPropertyChanged
  {
      private int id;
      public int ID
      {
          get { return id; }
          set
          {
              if (id != value)
              {
                  id = value;
                  OnPropertyChanged("ID");
              }
          }            
      }
      private int amount;
     public int Amount
      {
          get { return amount; }
          set
          {
              if (amount != value)
             {
                 amount = value;
                  OnPropertyChanged("Amount");
              }
          }            
      }
      public Client Client {get;set;}
      public Product Product {get;set;}
      public event PropertyChangedEventHandler PropertyChanged;
   
      private void OnPropertyChanged(string propertyName)
      {
          PropertyChangedEventHandler handler = PropertyChanged;
          if (handler != null)
          {
              handler(this, new PropertyChangedEventArgs(propertyName));
         }
      }
  }
  public class Product : INotifyPropertyChanged
  {
      private int id;
      public int ID
      {
          get { return id; }
          set
          {
              if (id != value)
              {
                  id = value;
                  OnPropertyChanged("ID");
              }
          }            
      }
      private string name;
      public string Name
      {
          get { return name; }
          set
          {
              if (name != value)
              {
                  name = value;
                  OnPropertyChanged("Name");
              }
          }            
      }
      private int price;
      public int Price
      {
          get { return price; }
          set
          {
              if (price != value)
              {
                  price = value;
                  OnPropertyChanged("Price");
              }
          }            
      }
      private double? weight;
      public double? Weight
      {
          get { return weight; }
          set
          {
              if (weight != value)
              {
                  weight = value;
                  OnPropertyChanged("Weight");
              }
          }            
      }
      public Collection<Order> Orders {get;set;}
      public event PropertyChangedEventHandler PropertyChanged;
   
      private void OnPropertyChanged(string propertyName)
      {
         PropertyChangedEventHandler handler = PropertyChanged;
          if (handler != null)
          {
              handler(this, new PropertyChangedEventArgs(propertyName));
          }
      }
  }

Now we should generate data service context. To create your own data service context you should inherit from DataServiceContext class and implement to methods which resolve difference between client and server entity namespaces. To generate these methods Data class provides list of server namespaces (SeverNamspaces property).

Also we need to generate a number of properties for each entity set of data service. To generate these properties Data class provide dictionary, which Key is name of entity set and Value is entity type (EntitySets property).

DataContext template:

  
     public class AstoriaServiceContext : DataServiceContext
     {
 <# 
  foreach (var es in data.EntitySets)
  {
 #>        
         public DataServiceQuery<<#= es.Value #>> <#= es.Key #>
         {
              get
              {
                  return CreateQuery<<#= es.Value #>>("<#= es.Key #>");
              }
          }
  <#
  }
  #>
          public AstoriaServiceContext(Uri serviceRoot) : base(serviceRoot)
          {
              ResolveName = ResolveNameFromType;
              ResolveType = ResolveTypeFromName;
          }
   
          protected Type ResolveTypeFromName(string typeName)
          {
  <# 
  foreach (var ns in data.ServerNamespaces)
  {
  #>        
              if (typeName.StartsWith("<#= ns #>", StringComparison.Ordinal))
              {
                  return this.GetType().Assembly.GetType(string.Concat("<#= options.Namespace #>", typeName.Substring(<#= ns.Length #>)), true);
              }
  <#
  }
  #>                       
              return null;
          }
   
          protected string ResolveNameFromType(Type clientType)
          {
  <# 
  foreach (var ns in data.ServerNamespaces)
  {
  #>            
              if (clientType.Namespace.Equals("<#= options.Namespace #>", StringComparison.Ordinal))
              {
                  return string.Format("{0}.{1}","<#= ns #>", clientType.Name);
              }
  <#
  }
  #>            
              return null;
          }
      }

3. Summary

I think T4 template technique described in this article has a great potential, because it give us absolute control over code generation process. Unfortunately, T4 template has rather poor integration with visual studio that's why debugging of it may be annoying.

If you find this T4 WCF Data Services proxy useful, please write a comment or send me email with your suggestions and thoughts.

4. FAQ

What are T4 templates?

T4 templates is Visual Studio built-in code generator. However, it's not deep-deep built in, because there's no item templates in File | New Item and there's no intellisense or syntax highlighting. But guys from Clarius Consulting created a product T4 Editor, which supports coloring and intellisense (C# code coloring and intellisense supported only in commercial version).

5. Links

1. EDM Specifications - specifications you need to know to perform WCF Data Services metadata parsing.

2. http://www.olegsych.com/ - great resource about T4 templates.

3. T4 template for generating LINQ to SQL Data Context (by Damien Guard)

4. T4 template for generating Entity Framework classes (by Danny Simmons, ADO.NET team)

PS

Current version of MetadataHelper.tt is not well tested, so use it on your own risk.


Subscribe

Comments

  • -_-

    RE: ADO.NET Data Services Advanced Topics - Custom proxy based on T4 templates


    posted by krokodilov on Feb 26, 2009 09:55

    Useful article! It's exactly what I need!

  • -_-

    RE: ADO.NET Data Services Advanced Topics - Custom proxy based on T4 templates


    posted by Kalle on Mar 03, 2009 16:56

    Heh, *hugs and kisses*...

    Finishing a presentation ADO.NET DS with T4 customed LINQ to SQL model for fine-grained control over user-specific data; was just looking if someone had made the proxy creation T4 and here I stumbled.

    Excellent timing ;-) I was testing out serve ADS => ADS to be able to filter the data from the proxy and serve it on. Will spend some time with this later on to see if the user-specific/role specific  (field) data filtering is achievable while passing queries through ADS...

     

  • -_-

    RE: ADO.NET DS Custom Classes with change tracking and conflict resolution


    posted by Tim on Apr 29, 2009 07:18
    Hi there,

    in case you look also for change tracking and conflict resolution i recommend the following how-do-i video:

    http://t4-editor.tangible-engineering.com/How-Do-I-With-T4-Editor-Text-Templates.htm 

    Regards

     

    Tim


  • -_-

    RE: ADO.NET Data Services Advanced Topics - Custom proxy based on T4 templates


    posted by crystal on Sep 03, 2009 02:13

    Hi

     

    it failed at private static readonly Dictionary<string, string> edmTypes = new Dictionary<string, string>

    Error 1 Compiling transformation: A new expression requires () or [] after type c:\Documents and Settings\cxue\projects\APODataService\TestSilverlightApplication\Service References\MetadataHelper.tt 26 65 TestSilverlightApplication

    Can you give any help?

     

    Thanks,

     

    Crystal

     

     

    {

  • -_-

    RE: ADO.NET Data Services Advanced Topics - Custom proxy based on T4 templates


    posted by Aquilax on Sep 22, 2009 10:42
    @Crystal

     From the Metadata.tt file delete the generated cs files or in the file properties windows under advanced section clear the CustomTool property (it should contains the name "TextTemplatingFileGenerator").

  • -_-

    RE: ADO.NET Data Services Advanced Topics - Custom proxy based on T4 templates


    posted by Rob on Nov 01, 2009 04:50
    Btw there's a typo in the MetaDataHelper which causes an exception if you have a Boolean type in the metadata.  Line 28 should be Edm.Boolean and not Edm.Booleand

Add Comment

Login to comment:
  *      *