Holding a Reference to the EF MetadataWorkspace for WCF Services

The title lends itself to a bit thought.  Why would the developer feel the need hold a static reference to the MetadataWorkspace of an Entity Framework (EF) model from within their WCF service-oriented applications?  The premise being that the WCF services are using EF to serve up data from a backend data store.  The answer is twofold.  Firstly, loading the EF metadata data into memory is expensive and secondly and most importantly is that EF has a depreciation policy for the metadata cache.  The deprecation policy kicks in after the predetermined threshold has been reach (typically 20 minutes of idle time) and removes the metadata from cache.  This means that the expense of loading EF metadata into memory is incurred after the idle time has been reached and the deprecation policy has been completed.  In laymen terms, the first WCF client call making use of an EF query is subject to a warm-up delay while EF loads the metadata cache.  The larger the model, the longer that ‘first’ user of the service must wait for their results.  To maintain consistent service response rates, one can hold a reference to the MetadataWorkspace object to ensure that EF doesn’t remove it from its cache.

In short, the solution submitted in this blog pertains to those applications exhibiting lengthy EF startup durations, periodic idle time with their services and a client base that expects predictable service response times.

Background

When an EntityConnection is created, the Entity Framework will load the conceptual, storage and mapping between the conceptual model and the storage model.  You can see this if you examine the CSpace (conceptual), SSpace (storage), and CSSpace (conceptual/storage mapping) item collection properties of the MetadataCache instance once the EDM has been loaded.  For example, the following code opens an EntityConnection and a QuickWatch of the MetadataWorkspace property of the connection reveals the CSpace, CSSpace, OCSpace, OSpace and SSpace collection properties.  With this model we have 274 items in the conceptual model, 314 items in the storage model and 1 item in the conceptual to storage mapping.

Note: the model was created from the AdventureWorksLT2008R2 which is included with the SQL Server 2008R2 product sample databases on codeplex.

Code Snippet

  1. using (EntityConnection connection = new EntityConnection("Name=AdventureWorksLT2008R2Entities"))
  2. {
  3.     MetadataWorkspace workspace = connection.GetMetadataWorkspace();
  4.     CountMetaDataItems(workspace);
  5. }

 

Model Count      

 CSpace  274
OSpace  0
SSpace  314
CSSpace 1
OCSpace 0

Note that object model and mapping between the object and conceptual model is not loaded when an EntityConnection is created, the associated item collections are empty.  The CountMetaDataItems helper function was created to count the items in each collection.

 

Code Snippet

  1. public static void CountMetaDataItems(MetadataWorkspace workspace)
  2. {
  3.     ItemCollection itemCollection = null;
  4.  
  5.     // CSpace
  6.     if (workspace.TryGetItemCollection(DataSpace.CSpace, out itemCollection))
  7.         Console.WriteLine("{0}  {1}", DataSpace.CSpace.ToString(), itemCollection.Count);
  8.     else
  9.         Console.WriteLine("{0}  {1}", DataSpace.CSpace.ToString(), 0);
  10.  
  11.     // OSpace
  12.     if (workspace.TryGetItemCollection(DataSpace.OSpace, out itemCollection))
  13.         Console.WriteLine("{0}  {1}", DataSpace.OSpace.ToString(), itemCollection.Count);
  14.     else
  15.         Console.WriteLine("{0}  {1}", DataSpace.OSpace.ToString(), 0);
  16.  
  17.     // SSpace
  18.     if (workspace.TryGetItemCollection(DataSpace.SSpace, out itemCollection))
  19.         Console.WriteLine("{0}  {1}", DataSpace.SSpace.ToString(), itemCollection.Count);
  20.     else
  21.         Console.WriteLine("{0}  {1}", DataSpace.SSpace.ToString(), 0);
  22.  
  23.     // CSSpace
  24.     if (workspace.TryGetItemCollection(DataSpace.CSSpace, out itemCollection))
  25.         Console.WriteLine("{0} {1}", DataSpace.CSSpace.ToString(), itemCollection.Count);
  26.     else
  27.         Console.WriteLine("{0} {1}", DataSpace.CSSpace.ToString(), 0); ;
  28.  
  29.     // OCSpace
  30.     if (workspace.TryGetItemCollection(DataSpace.OCSpace, out itemCollection))
  31.         Console.WriteLine("{0} {1}", DataSpace.OCSpace.ToString(), itemCollection.Count);
  32.     else
  33.         Console.WriteLine("{0} {1}", DataSpace.OCSpace.ToString(), 0);
  34. }

So how do we load the object model?  The MetadataWorkspace object constructor sound promising.  It can load all the .csdl, .msl and ssdl embedded resources from the available assemblies.  Let’s give it a try.  The following code assumes that model resources are embedded into the executing assembly.  If not, the assembly fully qualified name would have to be used together with a known type to return the assembly for the model. An example of which is commented out in the code snippet below.  If your model is created in a separate assembly, then you must load the assembly using the assembly qualified name of a type within the model as shown in the lines 1-4 in the code snippet below.

Code Snippet

  1. //string assemblyQualifiedName = @"AdventureWorksModel.AdventureWorksLT2008R2Entities," +
  2. //                   @"AdventureWorksModel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
  3. //Type anyModelType = Type.GetType(assemblyQualifiedName);
  4. //Assembly modelAssembly = Assembly.GetAssembly(anyModelType);
  5.  
  6. Assembly modelAssembly = Assembly.GetExecutingAssembly();
  7. Assembly[] assemblys = { modelAssembly };
  8. MetadataWorkspace workspace = new MetadataWorkspace(new string[] { @"res://*/"},
  9.     new Assembly[] { Assembly.GetExecutingAssembly() });
  10. CountMetaDataItems(workspace);

 

Model Count      

 CSpace  274
OSpace  0
SSpace  314
CSSpace 1
OCSpace 0

As we can see from the output above, the object model is not loaded.  This is because the object model is loaded during view generation or during the first query referencing an entity set.  The generated views are then stored inside the object model.  So what this means is that we can load the conceptual, storage and conceptual to storage mapping into a MetadataWorkspace instance and then instantiate a new EntityConnection using this MetadataWorkspace instance.  The EntityConnection instance can then be used to in the ObjectContext constructor.  The only thing left would be to build a LINQ query which becomes converted into a command tree upon which the object model is loaded into cache.  A code sample is shown below.

Code Snippet

  1. string constr = ConfigurationManager.ConnectionStrings["AdventureWorksDB"].ConnectionString;
  2. System.Data.Common.DbConnection dbConn = new SqlConnection(constr);
  3. EntityConnection entConn = new EntityConnection(workspace, dbConn);
  4.  
  5. using (AdventureWorksLT2008R2Entities dc = new AdventureWorksLT2008R2Entities(entConn))
  6. {
  7.     var custString = from c in dc.Customers
  8.                      select c;
  9.     CountMetaDataItems(dc.MetadataWorkspace);
  10. }

Model Count      

 CSpace  274
OSpace  39
SSpace  314
CSSpace 1
OCSpace 37

 

The output above shows that each of the models has been loaded into the workspace simply because the application reused a reference to the metadata workspace when constructing the EntityConnection.  To ensure that the Entity Framework deprecation policies do not remove the metadata from cache after a period of inactivity, hold a static reference to the workspace.  I will show this below.

WCF Services Initialization

What’s left is to initialize the EF MetadataWorkspace during startup of the WCF service and hold a static reference which can be used for subsequent EF queries.  Fortunately WCF offers a number of trivial options to perform ‘hook’ into the service startup.  Below is a list of a number of options, you can read this blog entry for a more descriptive read.  I will show a straight forward sample of initializing the MetadataWorkspace by creating custom ServiceHostFactory and ServiceHost classes and implementing the ServiceHostBase.InitializeRunTime() method.

  • Global.asmx file

    • Provides that the WCF service is in ASP.NET compatibility mode
    • Called during startup of WCF service
  • Static AppInitialize() method

    • Must be place into the App_Code folder
  • Implementing ServiceHostFactory and a custom ServiceHost

    • override the InitializeRuntime() method
  • WCF ServiceHostEvents

WCF Sample Implementation

Create a class to hold a global static reference to the MetadataWorkspace which can be globally referenced. 

Code Snippet

  1. public class GlobalWorkspace
  2. {
  3.     private static object locker = new object();
  4.     private static MetadataWorkspace workSpace = null;
  5.  
  6.     public static MetadataWorkspace WorkSpace
  7.     {
  8.         get
  9.         {
  10.             lock (locker)
  11.             {
  12.                 if (workSpace == null)
  13.                 {
  14.                     string assemblyQualifiedName = @"AdventureWorksModel.AdventureWorksLT2008R2Entities," +
  15.                                        @"AdventureWorksModel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
  16.                     Type anyModelType = Type.GetType(assemblyQualifiedName);
  17.                     Assembly modelAssembly = Assembly.GetAssembly(anyModelType);
  18.  
  19.                     Assembly[] assemblys = { modelAssembly };
  20.                     workSpace = new MetadataWorkspace(new string[] { @"res://*/" }, assemblys);
  21.  
  22.                     string constr = ConfigurationManager.ConnectionStrings["AdventureWorksDB"].ConnectionString;
  23.                     System.Data.Common.DbConnection dbConn = new SqlConnection(constr);
  24.                     EntityConnection entConn = new EntityConnection(workSpace, dbConn);
  25.  
  26.                     using (AdventureWorksLT2008R2Entities dc = new AdventureWorksLT2008R2Entities(entConn))
  27.                     {
  28.                         var custString = from c in dc.Customers
  29.                                          select c;
  30.                         workSpace = dc.MetadataWorkspace;
  31.                     }
  32.                 }
  33.             }
  34.             return workSpace;
  35.         }
  36.         private set {}
  37.     }
  38. }

Create a WCF Service called AWSales.  Modify the interface to include a operational contract, for example GetCustomers(string firstName). 

Code Snippet

  1. [ServiceContract]
  2. public interface IAWSales
  3. {
  4.     [OperationContract]
  5.     IList<Customer> GetCustomers(string firstName);
  6. }

Create a custom service host by deriving from ServiceHost. 

Code Snippet

  1. public class AWSalesServiceHost : ServiceHost
  2. {
  3.     public AWSalesServiceHost(Type serviceType, params Uri[] baseAddresses)
  4.     : base(serviceType, baseAddresses) { }
  5.  
  6.     protected override void InitializeRuntime()
  7.     {
  8.         MetadataWorkspace workspace = GlobalWorkspace.WorkSpace;
  9.         base.InitializeRuntime();
  10.     }
  11. }

Create a custom ServiceHostFactory to instantiate the derived ServiceHost.

Code Snippet

  1. public class AWSalesServiceHostFactory : ServiceHostFactory
  2. {
  3.     protected override ServiceHost CreateServiceHost(Type t, Uri[] baseAddresses)
  4.     {
  5.         return new AWSalesServiceHost(typeof(AWSales), baseAddresses);
  6.     }
  7.  
  8.     public override ServiceHostBase CreateServiceHost(string service,
  9.         Uri[] baseAddresses)
  10.     {
  11.         return new AWSalesServiceHost(typeof(AWSales), baseAddresses);
  12.     }
  13. }

Implement the service contract and utilize the static reference to MetadataWorkspace when creating your EntityConnections.

Code Snippet

  1. public class AWSales : IAWSales
  2. {
  3.     public IList<Customer> GetCustomers(string firstName)
  4.     {
  5.         IList<Customer> customers = null;
  6.         string constr = ConfigurationManager.ConnectionStrings["AdventureWorksDB"].ConnectionString;
  7.         System.Data.Common.DbConnection dbConn = new SqlConnection(constr);
  8.         EntityConnection entConn = new EntityConnection(GlobalWorkspace.WorkSpace, dbConn);
  9.  
  10.         using (AdventureWorksLT2008R2Entities dc = new AdventureWorksLT2008R2Entities(entConn))
  11.         {
  12.             customers = (from c in dc.Customers
  13.                          where c.FirstName == firstName
  14.                          select c).ToList<Customer>();
  15.         }
  16.         return customers;
  17.     }
  18. }

Set the ‘Factory’ attribute in the .svc file.

Code Snippet

  1. <% @ServiceHost Debug="true" Factory="WcfService.AWSalesServiceHostFactory" %>

Conclusion

The practice suggested in this blog does not pertain to all WCF services.  For those smaller or medium size EF models it doesn’t really matter if the Entity Framework deprecates the metadata cache after periods of inactivity, the model will load quite quickly and in the end, the user will never know this occurred.  But for larger models, where loading time is much longer and the experience of the end user could be impacted, the technique outlined in this blog may be worth exploring.