共用方式為


Developing Testable Silverlight Applications: Part One – Abstracting Data Persistence

At Microsoft PDC 2009 I gave a talk entitled “Developing Testable Silverlight Applications”. We looked at how to use the Inversion of Control principle with the Model-View-ViewModel design pattern to isolate the dependencies within a Silverlight 4 application for easier testing. We also saw how to improve testability by taking advantage of the natural boundaries in Silverlight applications utilizing WCF RIA Services to query for data and persist units of work. There was so much material to cover during my assigned lunch hour (or rather, lunch 45-minute) session that I am going use a series of blog posts to provide more in-depth detail on the techniques I talked about.

First, I feel that I should provide a short description of my application-design philosophy.

  • Good design is a long-term investment – it is usually expensive and not necessarily cost effective for short term gains
  • Good design has many benefits – for example, improved modularity, extensibility, and testability
  • Average design and even poor design have places in the programming world but should not be used by default
  • The “right” design should be chosen through cost-benefit analysis

Abstracting the DAL’s Persistence Logic

This first entry in the series will actually be about the last demonstration in the talk since my brief explanation did not do it justice. First, let’s take a look at the problem we’re trying to solve. The following diagram shows how the WCF RIA Services components (colored blue) fit into an overall n-tier Silverlight application.

ntier

In this post we’ll be focusing on how to test the domain model without reading/writing data to/from the data store. One way you can handle this is by implementing the repository pattern to completely abstract domain services and other business logic from the Data Access Layer (DAL). The WCF RIA Services team has a sample available for that approach.

One of the benefits of that approach is that you can change your DAL mechanism if you need to. On the other hand, it is fairly expensive to implement since you have to wrap each of your collections of entities and maintain their lifetimes yourself. It looks simple enough if you have one entity type (e.g. Products), but it doesn’t scale out well for real-world domain problems (e.g. Products, Orders, OrderDetails and more).

There is definitely value in that flexibility especially if you’ll be maintaining an application over several years as technology changes around you. Looking at your past experiences however, how often have you had to change your DAL? Many applications don’t need that flexibility but they still want to decouple the business logic from the persistence of those changes for the sake of testability. To help with that, the ADO.NET Entity Framework features in Microsoft .NET v4.0 has made some huge strides. Particularly helpful for our problem, they introduced an IObjectSet<T> interface and opened up their T4 code generation mechanism for customization. The rest of this post is going to explore how to use those features with RIA Services Silverlight applications to decouple persistence in as few steps as possible.

The quickest way to get started is to download the source code for the demo app and tests I used in my talk.

Steps to use this support with your own projects

  1. Download the source files.
  2. Install Unity 1.2 and Unity 1.2 for Silverlight.
  3. Install the November 2009 release of the  Microsoft Silverlight Toolkit (for the Silverlight Unit Test Framework and Shiny Red theme that the demo uses).
  4. Include the PersistenceStrategy project in your own solution and add references to it in your Web project and Web test project.
  5. Open your edmx file so that the data model designer is showing and in the properties window change the "Code Generation Strategy" property to "None".
  6. Include the DataModel.tt file in your Web project and change the 'SourceCsdlPath = @"DataModel.edmx"' line to point to your own edmx file.
  7. In your Web.config for the Web project add a unity section like what is shown in the attached example solution (or in the explanations below).
  8. In your domain service override CreateObjectContext() to create an instance of your object context passing in the Unity-resolved IPersistenceStrategy instance. See FantasyPresentingDomainService.cs in the example solution.
  9. Update the wizard-generated code in your domain service to use IObjectSet<T> friendly API's. See the Making the domain service IObjectSet<TEntity> friendly section at the bottom of this post for details.

Continue reading for an in-depth explanation of how I abstracted data persistence.

Customizing Entity Framework code generation to use IObjectSet<T>

The IObjectSet<T> interface essentially exposes the repository pattern within the Entity Framework as the class diagram suggests below. Also, in the class diagram you can see that the Entity Framework supplies a default implementation, ObjectSet<T>, which inherits from ObjectQuery<T>.

diagram

The code generated by Entity Framework uses ObjectSet<T> by default. We would like it to use IObjectSet<T> so that any callers won’t take a dependency on a specific implementation. Entity Framework uses T4 to generate the entity and object context code for your data model and in Visual Studio 2010 Beta you can override the code generation with your own template.

  1. Right click the Entity Data Model Designer
  2. Select “Add Code GenerationItem…”
  3. In the Add New Item dialog, select “ADO.NET EntityObject Generator” and click OK

The Entity Framework Design blog has step-by-step instructions and screenshots for doing this – please note that the menu item has text has changed from “Add New Artifact Generation Item…” since that post was written.

Now, let’s edit the .tt file contents so that it generates code using IObjectSet<T> instead of ObjectSet<T>. To do so, look for the comment with the text “Write EntityContainer and ObjectContext ObjectSet properties.” Replace, the two instances of ObjectSet with IObjectSet as shown below. Note: a full copy of a .tt file that you can drop into your own project is available in the source code zip file at the beginning of this post.

    1: <#
    2:         ////////
    3:         //////// Write EntityContainer and ObjectContext ObjectSet properties. 
    4:         ////////
    5:         region.Begin(GetResourceString("Template_RegionObjectSetProperties"));
    6:         foreach (EntitySet set in container.BaseEntitySets.OfType<EntitySet>())
    7:         {
    8:             VerifyEntityTypeAndSetAccessibilityCompatability(set);
    9: #>
   10:  
   11:     /// <summary>
   12:     /// <#=SummaryComment(set)#>
   13:     /// </summary><#=LongDescriptionCommentElement(set, region.CurrentIndentLevel)#>
   14:     <#=code.SpaceAfter(NewModifier(set))#><#=Accessibility.ForReadOnlyProperty(set)#> ObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>> <#=code.Escape(set)#>
           <#=code.SpaceAfter(NewModifier(set))#><#=Accessibility.ForReadOnlyProperty(set)#> IObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>> <#=code.Escape(set)#>
   16:     {
   17:         get
   18:         {
   19:             if ((<#=code.FieldName(set)#> == null))
   20:             {
   21:                 <#=code.FieldName(set)#> = base.CreateObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>>("<#=set.Name#>");
   22:             }
   23:             return <#=code.FieldName(set)#>;
   24:         }
   25:     }
   26:     private ObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>> <#=code.FieldName(set)#>;
           private IObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>> <#=code.FieldName(set)#>;

After you save your changes, look in the generated .cs file (it should be shown as a childe of the .tt file in the solution explorer) and you should see that IObjectSet<T> is now used everywhere ObjectSet<T> was before. Great. But we still have to insert some logic somewhere to tell the program which implementation of IObjectSet<T> to use. Enter the IPersistenceStrategy interface, dependency injection, and a handful of other helper interfaces and classes.

Building out a strategy pattern for runtime-swapping of IObjectSet<T> implementations

Unfortunately, plugging in a new implementation to abstract DB access isn’t quite as simple just providing something like an IObjectSet factory class. There are other pieces of the Entity Framework workflow for querying and submitting changes that also expect a DB connection. What we need is a collection of patterns which need to be implemented as a whole to provide support for a non-DB persistence model. That sounds like a strategy pattern to me so I pulled them together in to one strategy pattern interface, IPersistenceStrategy. Note: Full doc-comments are provided in the downloadable source at the end of this post.

    1: /// <summary>
    2: /// Defines a set of API's for abstracting the persistence of changes to a datastore. The
    3: /// interface is intended to be consumed by an Entity Framework-generated class deriving
    4: /// from ObjectContext.
    5: /// </summary>
    6: public interface IPersistenceStrategy
    7: {
    8:     /// <summary>
    9:     /// Factory method for creating an IObjectSet<TEntity> for the strategy implementation.
   10:     /// </summary>
   11:     IObjectSet<TEntity> CreateObjectSet<TEntity>(ObjectContext objectContext, string entitySetName) where TEntity : class;
   12:  
   13:     /// <summary>
   14:     /// Factory method for creating an EntityKey for the strategy implementation
   15:     /// </summary>
   16:     EntityKey CreateEntityKey(ObjectContext objectContext, string entitySetName, object entity);
   17:  
   18:     /// <summary>
   19:     /// Method for including results from additional tables as part of the query.
   20:     /// </summary>
   21:     IQueryable<TEntity> Include<TEntity>(IQueryable<TEntity> queryable, string path);
   22:     
   23:     /// <summary>
   24:     /// Method to enable change tracking for an existing original entity and modify it with values from the current entity.
   25:     /// </summary>
   26:     void AttachAsModified<TEntity>(IObjectSet<TEntity> objectSet, TEntity current, TEntity original) where TEntity : class, EntityObject;
   27:     
   28:     /// <summary>
   29:     /// Method to persist the changes in the ObjectContext.
   30:     /// </summary>
   31:     int SaveChanges(ObjectContext objectContext, SaveOptions options);
   32: }

Here’s a brief description for why we need all of these methods. In all cases the arguments in the signatures are predetermined based on what the underlying real DB persistence implementation needs, but more on that later.

  • CreateObjectSet – a basic factory method to create and return instances of whichever IObjectSet<T> implementation the strategy implementation chooses.
  • CreateEntityKey – a basic factory method to create and return an EntityKey instance for a given entity. Entity keys are used by Entity Framework to verify the uniqueness of a given entity and to improve the performance of queries. It is required on all entities and creating one using the standard Entity Framework implementation requires a connection to the DB.
  • Include – a helper method to abstract the ObjectQuery.Include method implementation. Many RIA Services domain service use the Include method to load multiple entity types with one query. It takes and returns an IQueryable<TEntity> because the original Include method returns an ObjectQuery<T> which does not implement IObjectSet<T>. Plus this method needs to be a fluent API like the original.
  • AttachAsModified – a helper method to adapt the RIA Services ObjectContext.AttachAsModified extension method to work against IObjectSet<T>. The RIA Services implementation makes calls to the ObjectContext which requires a DB connection so we also need to be able to provide an alternate implementation.
  • SaveChanges – a helper method to abstract the persistence of the data.

Here is the IPersistenceStrategy implementation that uses the actual ObjectContext to work with the real DB datastore. You’ll notice that in a few places I make the assumption that the passed in IQueryable<TEntity> or IObjectSet<TEntity> is actually an ObjectQuery<TEntity>. This is a reasonable expectation since the CreateObjectSet implementation returns an ObjectSet<TEntity> which derives from ObjectQuery<TEntity>.

    1: /// <summary>
    2: /// IPersistenceStrategy implementation for persisting data changes to the backing datastore with Entity Framework
    3: /// </summary>
    4: public class DatastorePersistenceStrategy : IPersistenceStrategy
    5: {
    6:     public IObjectSet<TEntity> CreateObjectSet<TEntity>(ObjectContext objectContext, string entitySetName) where TEntity : class
    7:     {
    8:         return objectContext.CreateObjectSet<TEntity>(entitySetName);
    9:     }
   10:  
   11:     public EntityKey CreateEntityKey(ObjectContext objectContext, string entitySetName, object entity)
   12:     {
   13:         return objectContext.CreateEntityKey(entitySetName, entity);
   14:     }
   15:  
   16:     public IQueryable<TEntity> Include<TEntity>(IQueryable<TEntity> queryable, string path)
   17:     {
   18:         ObjectQuery<TEntity> asObjectQuery = queryable as ObjectQuery<TEntity>;
   19:         if (asObjectQuery != null)
   20:         {
   21:             return asObjectQuery.Include(path);
   22:         }
   23:         else
   24:         {
   25:             throw new InvalidOperationException("This implementation only handles queryables which derive from ObjectQuery<T>.");
   26:         }
   27:     }
   28:  
   29:     public void AttachAsModified<TEntity>(IObjectSet<TEntity> objectSet, TEntity current, TEntity original) where TEntity : EntityObject
   30:     {
   31:         ObjectQuery<TEntity> asObjectQuery = objectSet as ObjectQuery<TEntity>;
   32:         if (asObjectQuery != null)
   33:         {
   34:             asObjectQuery.Context.AttachAsModified(current, original);
   35:         }
   36:         else
   37:         {
   38:             throw new InvalidOperationException("This implementation only handles object sets deriving from ObjectQuery<T>.");
   39:         }
   40:     }
   41:  
   42:     public int SaveChanges(ObjectContext objectContext, SaveOptions options)
   43:     {
   44:         return objectContext.SaveChanges(options);
   45:     }
   46: }

Before showing the alternate test implementations let’s first look at how to pull the pieces together.

Consuming the IPersistenceStrategy interface

Our next step is to get the Entity Framework-generated object context code to use the IPersistenceStrategy methods instead of the default ones. We could extend the existing generated code with a partial class but since we are already fooling with the T4 template, our custom .tt file seemed like the more appropriate place to put this support. There are three places that we need to update in our custom .tt file.

1. Add System.Data and PersistenceStrategy namespace imports.

    1: using System;
    2: using System.Data; 
    3: using System.Data.Objects;
    4: using System.Data.Objects.DataClasses;
    5: using System.Data.EntityClient;
    6: using System.ComponentModel;
    7: using System.Xml.Serialization;
    8: using System.Runtime.Serialization;
    9: using PersistenceStrategy; 

2. Add a private member variable to hold the IPersistenceStrategy instance, a constructor to set the member variable, a protected read-only accessor property for the member variable, and shadow the CreateEntityKey and SaveChanges methods from the parent ObjectContext type to use the implementations provided by the IPersistenceStrategy implementation. Note that I add this code in its own region just after the region containing the OnContextCreated partial method definition.

    1: #region Persistence Strategy
    2:  
    3: private IPersistenceStrategy _persistenceStrategy;
    4:  
    5: public <#=code.Escape(container)#>(IPersistenceStrategy persistenceStrategy)
    6:     : this()
    7: {
    8:     this._persistenceStrategy = persistenceStrategy;
    9: }
   10:  
   11: protected IPersistenceStrategy PersistenceStrategy
   12: {
   13:     get
   14:     {
   15:         return this._persistenceStrategy;
   16:     }
   17: }
   18:  
   19: public new EntityKey CreateEntityKey(string entitySetName, object entity)
   20: {
   21:     return this.PersistenceStrategy.CreateEntityKey(this, entitySetName, entity);
   22: }
   23:  
   24: public new int SaveChanges(SaveOptions options)
   25: {
   26:     return this.PersistenceStrategy.SaveChanges(this, options);
   27: }
   28:  
   29: #endregion
   30:  

3. Change the line of code that calls the parent ObjectContext’s CreateObjectSet method to instead call IPersistenceStrategy’s CreateObjectSet method. This is in the same block of template code that we changed earlier to support IObjectSet<T>.

    1: /// <summary>
    2: /// <#=SummaryComment(set)#>
    3: /// </summary><#=LongDescriptionCommentElement(set, region.CurrentIndentLevel)#>
    4: <#=code.SpaceAfter(NewModifier(set))#><#=Accessibility.ForReadOnlyProperty(set)#> IObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>> <#=code.Escape(set)#>
    5: {
    6:     get
    7:     {
    8:         if ((<#=code.FieldName(set)#> == null))
    9:         {
   10:             <#=code.FieldName(set)#> = base.CreateObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>>("<#=set.Name#>");
                   <#=code.FieldName(set)#> = this.PersistenceStrategy.CreateObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>>(this, "<#=set.Name#>");
   12:         }
   13:         return <#=code.FieldName(set)#>;
   14:     }
   15: }
   16: private IObjectSet<<#=MultiSchemaEscape(set.ElementType, code)#>> <#=code.FieldName(set)#>;

Finally, we need to tell our domain service class to use the constructor we just added for the generated object context. To do that we override the CreateObjectContext method of the parent LinqToEntitiesDomainService<T> in our domain service.

    1: /// <summary>
    2: /// Overridden method to create FantasyPresentingEntities with
    3: /// a specific IPersistenceStrategy implementation
    4: /// </summary>
    5: protected override FantasyPresentingEntities CreateObjectContext()
    6: {
    7:     return new FantasyPresentingEntities(new DatastorePersistenceStrategy());
    8: }

One of the themes of my PDC talk was to improve testability we want to de-couple and hard coding CreateObjectContext to pass an instance of DatastorePersistenceStrategy into our constructor violates that. To fix the problem, I used a dependency injection framework to register/resolve the desired implementation of IPersistenceStrategy at runtime. This will pay huge dividends when it comes time to testing our service.

Using Unity to hook up the dependencies

Unity is a dependency injection framework created by the Microsoft Patterns and Practices team. My persistence strategy solution uses Unity’s ability to load the dependency mapping (or registration) from the default configuration file which makes it easy to switch mappings at runtime which helps in our quest for testability as we’ll see later. Here is the <unity> section from our web project’s Web.config file.

    1: <unity>
    2:   <typeAliases>
    3:     <!-- Lifetime manager types -->
    4:     <typeAlias alias="singleton" type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager, Microsoft.Practices.Unity" />
    5:     <!-- User-defined type aliases -->
    6:     <typeAlias alias="IPersistenceStrategy" type="PersistenceStrategy.IPersistenceStrategy, PersistenceStrategy" />
    7:     <typeAlias alias="DatastorePersistenceStrategy" type="PersistenceStrategy.DatastorePersistenceStrategy, PersistenceStrategy" />
    8:   </typeAliases>
    9:   <containers>
   10:     <container>
   11:       <types>
   12:         <!-- Type mapping using aliases defined above -->
   13:         <!-- Use the DatastorePersistenceStrategy class where IPersistenceStrategy instances are required.-->
   14:         <type type="IPersistenceStrategy" mapTo="DatastorePersistenceStrategy">
   15:           <lifetime type="singleton"/>
   16:         </type>
   17:       </types>
   18:     </container>
   19:   </containers>
   20: </unity>

There are a few things to note. First, the classes are defined in the namespace PersistenceStrategy in an assembly named PersistenceStrategy. Second, the mapping is set up as a singleton. Third, there is also an additional section at the the top of the Web.config file with the following xml. You can check out this MSDN article for more details on using configuration files with Unity.

    1: <configuration>
    2:   <configSections>
    3:     <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    4:   </configSections>
    5:   ...

The second half of the dependency injection process is resolving the dependencies at runtime – when I need an IPersistenceStrategy instance, give me an instance of the implementation specified in my configuration file. To do this I need an instance of a UnityContainer. I chose to hold on to a singleton instance of a UnityContainer class for the lifetime of the AppDomain. While many Unity samples add the instance as a static member of the application (or possibly part of an ASP.NET application’s property bag), our instance needs to live outside the application so I followed the standard singleton class pattern.

    1: /// <summary>
    2: /// Singleton class for a configured unity container read from the application's default
    3: /// configuration file.
    4: /// </summary>
    5: public class ConfiguredUnityContainer
    6: {
    7:     private readonly static object _padlock = new object();
    8:     private static IUnityContainer _instance;
    9:  
   10:     private ConfiguredUnityContainer()
   11:     {
   12:     }
   13:  
   14:     public static IUnityContainer Instance
   15:     {
   16:         get
   17:         {
   18:             lock (ConfiguredUnityContainer._padlock)
   19:             {
   20:                 if (ConfiguredUnityContainer._instance == null)
   21:                 {
   22:                     // Read the configuration data from the default configuration file
   23:                     // (e.g. web.config for ASP.NET apps or app.config for .NET apps including test apps).
   24:                     ConfiguredUnityContainer._instance = new UnityContainer();
   25:                     UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
   26:                     section.Containers.Default.Configure(ConfiguredUnityContainer._instance);
   27:                 }
   28:             }
   29:             return ConfiguredUnityContainer._instance;
   30:         }
   31:     }
   32: }

Now we can change our CreateObjectContext overridden method in the domain service to be loosely coupled.

    1: /// <summary>
    2: /// Overridden method to use Unity to resolve the IPersistenceStrategy
    3: /// implementation at runtime.
    4: /// </summary>
    5: protected override FantasyPresentingEntities CreateObjectContext()
    6: {
    7:     return new FantasyPresentingEntities(new DatastorePersistenceStrategy() );
           return new FantasyPresentingEntities(ConfiguredUnityContainer.Instance.Resolve<IPersistenceStrategy>() );
    9: }

Not only is the solution loosely-coupled, but the IPersistenceStrategy implementation can be shared by multiple consumers. Even better is that since we set the mapping up as a singleton in the configuration file, the same instance of DatastorePersistenceStrategy will be returned each time Resolve is called for the IPersistenceStrategy generic type. This is especially helpful to plug a couple of holes (albeit a bit awkwardly).

First, ever since we’ve changed the generated object context to return IObjectSet<T> instances, we’ve had compilation errors wherever we’ve used the Include extension method in our domain service. This is because Include is an extension method for the ObjectQuery type. So we need to provide an Include extension method for IObjectSet<T>. In the implementation below you’ll notice that we delegate the call to IPersistenceStrategy.Include on an instance we get from the ConfiguredUnityContainer singleton.

    1: /// <summary>
    2: /// Method for including results from additional tables as part of the query.
    3: /// </summary>
    4: public static IQueryable<T> Include<T>(this IQueryable<T> queryable, string path) where T : class
    5: {
    6:     IPersistenceStrategy persistenceStrategy =
    7:         ConfiguredUnityContainer.Instance.Resolve<IPersistenceStrategy>();
    8:     return persistenceStrategy.Include<T>(queryable, path);
    9: }

Second, we need redirect all usages of the AttachAsModified extension method for the ObjectContext type to use the AttachAsModified implementation we provide through the IPersistenceStrategy implementation. To do this, we can take a similar approach and add a new AttachAsModified extension method for the IObjectSet<T> type.

    1: /// <summary>
    2: /// Method to enable change tracking for an existing original entity and modify it with values from the current entity.
    3: /// </summary>
    4: public static void AttachAsModified<T>(this IObjectSet<T> objectSet, T current, T original) where T : EntityObject
    5: {
    6:     IPersistenceStrategy persistenceStrategy =
    7:         ConfiguredUnityContainer.Instance.Resolve<IPersistenceStrategy>();
    8:     persistenceStrategy.AttachAsModified(objectSet, current, original);
    9: }

The domain service code will automatically pick up the new Include extension method, but to get the domain service to use the new AttachAsModified extension method we need to do the following replacement in all of the Update methods (using a Team entity type as an example).

Old New
this.ObjectContext.AttachAsModified(currentTeam, this.ChangeSet.GetOriginal(currentTeam)); this.ObjectContext.Teams. AttachAsModified(currentTeam, this.ChangeSet.GetOriginal(currentTeam));

That should be it, but unfortunately the Domain Service wizard in the RIA Services Preview for VS 2010 Beta 2 does not generate IObjectSet<T> friendly code. I am following up with the RIA Services team to get them to fix this but in the meantime we’ll need to do one more step. Follow the steps detailed in the Making the domain service IObjectSet<TEntity> friendly section under Caveats/Things to Note at the bottom of the post.

Now we can build and run our application with its new loosely-couple backend.

Leveraging the Abstracted Persistence Logic for Tests

Everything we’ve done to this point has essentially been refactoring to achieve the end goal of testability. So we can just plug in a couple of new “test” implementations for IObjectSet<T> and IPersistenceStrategy and write our tests.

Implementing IObjectSet<T> and IPersistenceStrategy for testing

What we’re going to do is provide an alternative implementation of IObjectSet<T>  which manipulates in-memory data for the purpose of testing. The implementation I’ve used is taken nearly verbatim from this MSDN article – the only change is that I named the class InMemoryObjectSet. It is a very basic implementation using a HashSet to store and operate on the in-memory entities so I won’t include the code here. Take a look at the source files downloadable at the beginning of this post or follow the link to the MSDN article.

Now let’s have a look at a test implementation of IPersistenceStrategy.

    1: /// <summary>
    2: /// IPersistenceStrategy implementation for working with in-memory data without saving the changes to the backing datastore.
    3: /// </summary>
    4: public class InMemoryPersistenceStrategy : IPersistenceStrategy
    5: {
    6:     private IInMemoryDatastore _datastore;
    7:  
    8:     public InMemoryPersistenceStrategy(IInMemoryDatastore datastore)
    9:     {
   10:         this._datastore = datastore;
   11:     }
   12:  
   13:     public IObjectSet<TEntity> CreateObjectSet<TEntity>(ObjectContext objectContext, string entitySetName) where TEntity : class
   14:     {
   15:         return this._datastore.GetObjectSet<TEntity>();
   16:     }
   17:  
   18:     public EntityKey CreateEntityKey(ObjectContext objectContext, string entitySetName, object entity)
   19:     {
   20:         return new EntityKey();
   21:     }
   22:  
   23:     public IQueryable<TEntity> Include<TEntity>(IQueryable<TEntity> queryable, string path)
   24:     {
   25:         // Include is not applicable for in-memory data sets
   26:         return queryable;
   27:     }
   28:  
   29:     public void AttachAsModified<TEntity>(IObjectSet<TEntity> objectSet, TEntity current, TEntity original) where TEntity : EntityObject
   30:     {
   31:         objectSet.Attach(original);
   32:  
   33:         // Use reflection to look for DataMember attributes which adequately identify the members whose values need to be copied
   34:         var propertiesToCopy = from p in typeof(TEntity).GetProperties()
   35:                                where p.GetCustomAttributes(typeof(DataMemberAttribute), false).FirstOrDefault() != null
   36:                                select p;
   37:         foreach (PropertyInfo property in propertiesToCopy)
   38:         {
   39:             property.SetValue(original, property.GetValue(current, new object[] { }), new object[] { });
   40:         }
   41:     }
   42:  
   43:     public int SaveChanges(ObjectContext objectContext, SaveOptions options)
   44:     {
   45:         // TODO: It would be great to return something other than zero here
   46:         return 0;
   47:     }
   48: }

You’ll notice a couple of things. First, it introduces yet another interface IInMemoryDatastore. This interface allows you to decouple this implementation from the actual test data which allows you to re-use this test implementation for any project with any test data.

    1: /// <summary>
    2: /// Defines an API for retrieving an IObjectSet<TEntity> instance for given entity type.
    3: /// </summary>
    4: public interface IInMemoryDatastore
    5: {
    6:     /// <summary>
    7:     /// Method to retrieve an IObjectSet<TEntity> instance.
    8:     /// </summary>
    9:     IObjectSet<TEntity> GetObjectSet<TEntity>() where TEntity : class;
   10: }

Before I show the implementation for that interface lets look a little closer at our InMemoryPersistenceStrategy implementation.

  • CreateObjectSet delegates the IObjectSet<TEntity> creation to our IInMemoryDatstore implementation.
  • CreateEntityKey creates an empty default EntityKey instance. This seems to be enough satisfy the requirements of my testing so far.
  • Include just returns the passed in IQuerable<TEntity> since all of the entities are already loaded into memory. This is one of the three places where I feel a little squeamish about the persistence abstraction strategy (the others being the CreateEntityKey and SaveChanges implementations above) since it doesn’t “act” on the IQueryable<TEntity> like the real Entity Framework Include method does. This means that the returned IQueryable<TEntity> could behave differently. Diego Vega briefly talks about this in this post. I’d be happy to hear any input from anybody else on what might be a better answer here.
  • AttachAsModified attaches the original object to the IObjectSet<TEntity> instance and uses reflection to copy the values of the properties marked with [DataMember] from the altered entity to the original entity.
  • SaveChanges returns zero since we aren’t actually saving any changes. That seems logical, but again our test implementation will return a different result than the real Entity Framework implementation so it could invalidate your testing (depending on your viewpoint).

Providing the test data

We’re getting closer to writing tests but first we need the test data. By the way, all of the remaining code I’ll be discussing should be in a different project/assembly than the persistence strategy guts we’ve talked about to this point. In the downloadable solution file at the beginning of this post I include this stuff in the FantasyPresenting.Web.Test project.

Here is an excerpt of the simple test data for the application I used in my demos at PDC.

    1: using System.Collections.Generic;
    2: using FantasyPresenting.Web;
    3:  
    4: namespace FantasyPresenting.Test
    5: {
    6:     /// <summary>
    7:     /// Sample data for test purposes
    8:     /// </summary>
    9:     public class TestData
   10:     {
   11:         public readonly List<Team> Teams = new List<Team>()
   12:         {
   13:              new Team { TeamId=1, TeamName="Team1", UserId=1234, LeagueId=1},
   14:              new Team { TeamId=2, TeamName="Team 2", UserId=2345, LeagueId=1}
   15:         };
   16:         ...
   17:     }
   18: }

I’m a big fan of reusing test data because it takes so much effort to generate. One of the nice side effects of using RIA Services for your n-tier development is that the client code generation outputs the client entity code in the same namespace as it is on the mid-tier (in this case FantasyPresenting.Web). This way I can use my sample data for my testing my client code as well as the domain service code on the mid-tier.

The piece that connects the test data to our InMemoryPersistenceStrategy implementation is through an implementation of the IInMemoryDatastore interface. Remember how the InMemoryPersistenceStrategy constructor takes an IInMemoryDatastore instance as a parameter? Below is the TestDatastore class which creates InMemoryObjectSet instances for the lists of entities in our test data.

    1: /// <summary>
    2: /// IInMemoryDatastore implementation which uses InMemoryObjectSet<TEntity>
    3: /// as a test implementation of IObjectSet<TEntity>.
    4: /// </summary>
    5: public class TestDatastore : IInMemoryDatastore
    6: {
    7:     private Dictionary<Type, object> _objectSets = new Dictionary<Type, object>();
    8:  
    9:     public TestDatastore()
   10:     {
   11:         FantasyPresenting.Test.TestData testData = new FantasyPresenting.Test.TestData();
   12:         this._objectSets.Add(typeof(Team), new InMemoryObjectSet<Team>(testData.Teams));
   13:         this._objectSets.Add(typeof(League), new InMemoryObjectSet<League>(testData.Leagues));
   14:         this._objectSets.Add(typeof(Presenter), new InMemoryObjectSet<Presenter>(testData.Presenters));
   15:         this._objectSets.Add(typeof(Event), new InMemoryObjectSet<Event>(testData.Events));
   16:     }
   17:  
   18:     public IObjectSet<TEntity> GetObjectSet<TEntity>() where TEntity : class
   19:     {
   20:         return this._objectSets[typeof(TEntity)] as IObjectSet<TEntity>;
   21:     }
   22: }

Next we’ll use Unity to glue all of the pieces together in the test project. Our tests will be instantiating the domain service and other classes through a compiled Visual Studio unit test assembly so it won’t be spinning up the domain service in the context of an ASP.NET app. Therefore, the unity configuration will need to be defined in the test project’s App.config. Here’s what it would look like.

    1: <?xml version="1.0" encoding="utf-8" ?>
    2: <configuration>
    3:   <configSections>
    4:     <section name="unity"
    5:               type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
    6:                  Microsoft.Practices.Unity.Configuration, Version=1.2.0.0,
    7:                  Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    8:   </configSections>
    9:   <connectionStrings>
   10:     <add name="FantasyPresentingEntities" connectionString="metadata=res://*/DataModel.csdl|res://*/DataModel.ssdl|res://*/DataModel.msl;provider=System.Data.SqlClient;" providerName="System.Data.EntityClient" />
   11:   </connectionStrings>
   12:   <unity>
   13:     <typeAliases>
   14:       <!-- User-defined type aliases -->
   15:       <typeAlias alias="IPersistenceStrategy"
   16:            type="PersistenceStrategy.IPersistenceStrategy, PersistenceStrategy" />
   17:       <typeAlias alias="InMemoryPersistenceStrategy"
   18:            type="PersistenceStrategy.InMemoryPersistenceStrategy, PersistenceStrategy" />
   19:       <typeAlias alias="IInMemoryDatastore"
   20:            type="PersistenceStrategy.IInMemoryDatastore, PersistenceStrategy"/>
   21:       <typeAlias alias="TestDatastore"
   22:            type="FantasyPresenting.Web.Test.TestDatastore, FantasyPresenting.Web.Test"/>
   23:     </typeAliases>
   24:     <containers>
   25:       <container>
   26:         <types>
   27:           <!-- Type mapping using aliases defined above -->
   28:           <!-- Use the InMemoryPersistenceStrategy class where IPersistenceStrategy instances are required.-->
   29:           <type type="IPersistenceStrategy" mapTo="InMemoryPersistenceStrategy"/>
   30:           <!-- Use the TestDatastore class where IInMemoryDatastore instances are required.-->
   31:           <type type="IInMemoryDatastore" mapTo="TestDatastore"/>
   32:         </types>
   33:       </container>
   34:     </containers>
   35:   </unity>
   36: </configuration>

This information gets read by the ConfiguredUnityContainer singleton instance when it gets created so that requests for IPersistenceStrategy resolve to the InMemoryPersistenceStrategy class and IInMemoryDatastore resolve to the TestDatastore class.

You may also note that I’ve included a “FantasyPresentingEntities” connection string. This information corresponds to a connection string by the same name in the mid-tier assembly’s Web.config file. While we don’t need to provide full connection string data, Entity Framework requires at least the metadata part of the connection string entry when our domain service creates an instance of our generated object context (i.e. FantasyPresentingEntities).

Writing the tests

Finally, we have everything we need to write some tests. Here is an example test class for the FantasyPresentingDomainService. Before a domain service instance can be used it needs to have its Initialize method called. Unfortunately that method takes an instance of a DomainServiceContext class whose constructor requires an IServiceProvider. From my research I couldn’t find an appropriate class anywhere in the .NET framework so I cobbled together a FakeServiceProvider nested class for testing.

    1: /// <summary>
    2: /// Sample set of unit tests for the FantasyPresentingDomainService
    3: /// </summary>
    4: [TestClass]
    5: public class DomainServiceTests
    6: {
    7:     private TestContext testContextInstance;
    8:  
    9:     /// <summary>
   10:     ///Gets or sets the test context which provides
   11:     ///information about and functionality for the current test run.
   12:     ///</summary>
   13:     public TestContext TestContext
   14:     {
   15:         get
   16:         {
   17:             return testContextInstance;
   18:         }
   19:         set
   20:         {
   21:             testContextInstance = value;
   22:         }
   23:     }
   24:  
   25:     [TestMethod]
   26:     public void QueryTest()
   27:     {
   28:         FantasyPresentingDomainService domainService = InstantiateDomainService(DomainOperationType.Query);
   29:         IQueryable<Team> teams = domainService.GetTeamsForUser(2345);
   30:         Assert.AreEqual(1, teams.Count(), "Only one team should have been returned.");
   31:         Assert.AreEqual(teams.Single().TeamId, 2, "Team with ID 2 should have been returned.");
   32:     }
   33:  
   34:     [TestMethod]
   35:     public void SubmitChangesTest()
   36:     {
   37:         FantasyPresentingDomainService domainService = InstantiateDomainService(DomainOperationType.Submit);
   38:         bool result = domainService.Submit(new ChangeSet(new ChangeSetEntry[] { }));
   39:         Assert.IsTrue(result, "Submit should have succeeded.");
   40:     }
   41:  
   42:     /// <summary>
   43:     /// Helper method to create a FantasyPresentingDomainService instance for a given operation
   44:     /// </summary>
   45:     private FantasyPresentingDomainService InstantiateDomainService(DomainOperationType operation)
   46:     {
   47:         FantasyPresentingDomainService domainService = new FantasyPresentingDomainService();
   48:         domainService.Initialize(new DomainServiceContext(new FakeServiceProvider(), operation));
   49:         return domainService;
   50:     }
   51:  
   52:     /// <summary>
   53:     /// Fake class for the purpose initializing a new FantasyPresentingDomainService
   54:     /// </summary>
   55:     private class FakeServiceProvider : IServiceProvider
   56:     {
   57:         public object GetService(Type serviceType)
   58:         {
   59:             return null;
   60:         }
   61:     }
   62: }

 

Caveats/Things to Note

Just a starting point

This support is intended as a starting point and hopefully it gets you going in the right direction. You might need to expand the IPersistenceStrategy interface and the classes which implement it to support richer test cases. Even more helpful would be that RIA Services will continue to improve the testability experience to reduce the workarounds we’ve had to implement in this post.

Making the domain service IObjectSet<TEntity> friendly

Remember that the default code generated by the Domain Services class wizard will not take advantage of the changes in the custom .tt file to use IObjectSet<TEntity> which bypasses all the abstraction work that we did. I am following up with the RIA Services team to make sure they change the Domain Services wizard code spit, but you'll have to hand edit your own Domain Service code.

Here are the patterns you will need to change (using a Team entity type as an example). Your Get (or Select) methods will not need any fix-ups.

Operation Old New
Insert this.ObjectContext.AddToTeams(team); this.ObjectContext.Teams. AddObject(team);
Update this.ObjectContext.AttachAsModified(currentTeam, this.ChangeSet.GetOriginal(currentTeam)); this.ObjectContext.Teams. AttachAsModified(currentTeam, this.ChangeSet.GetOriginal(currentTeam));
Delete this.ObjectContext.Attach(team); this.ObjectContext.Teams. Attach(team);
Delete this.ObjectContext.DeleteObject(team); this.ObjectContext.Teams. DeleteObject(team);

Testing update methods

Testing the Update methods is tricky since you have to call this.ChangeSet.GetOriginal(...) to get the original entity to pass into AttachAsModified. However the ChangeSet property returns null in mocked scenarios like we’re doing and even if it weren’t null the list of entries is read-only. My goal was to be able to demonstrate that you don't have change the way you write your domain service to be able to test it, but it falls apart here. I'm following up with the RIA Services team to make sure they improve this experience. In the meantime you can abstract the call to GetOriginal like I did in the downloadable source code for my demo. Note: No, you didn’t miss something from the PDC talk. I included this support in the demo solution afterwards.

FantasyPresenting.zip

Comments

  • Anonymous
    January 12, 2010
    Your blog post corresponded very well with my schedual. We just had a break in development and I am putting in the persistence strategy. I am creating a sample app to get the feel for the strategy before I implement it. When I got to the part about the overriding CreateObjectContext() in the domain service I realized I hadn't created a domain service yet. When I create it now I get an error (Identifier expected) because the code gen is broken. It just puts: this.ObjectContext.(group); when it should put: this.ObjectContext.AddToGroup(group) I wonder if RIA needs something that was touched in the EF code gen?? For now I am going to back up and create the domain service first.

  • Anonymous
    January 13, 2010
    Thanks for the heads up. I'll look into it from my end. In the meantime you can temporarily re-enable the default Entity Framework code gen by opening the entity model designer and changing the Code Generation Strategy property in the property grid from "None" to "Default". You'll also want to temporarily exclude the .tt file from the project. Then once you add your Domain Service, just include the .tt file and change the Code Generation Strategy property back to "None".

  • Anonymous
    January 13, 2010
    No Problem. I hadn't read the last point of your blog so it is moot since I have to go change those lines of code anyway. My test app was a success, I am really looking forward to your blogs about testing client side.

  • Anonymous
    July 23, 2010
    :( it looks like the souce code for your the demo app disappears => 404 - File or directory not found.

  • Anonymous
    June 21, 2011
    I have scenarios like where I had opted to generate the metadata classes along with the generation of the domainservice using the VS wizzard. I wanted to add some custom methods to my classes, so I created partial classes for the entities that I wanted to add methods to. But I have scenarios, where in I am calling the Load method for child entity of an entity. e.g I have Sprint and a Sprint has many Stories. So, the method that require Stories to be loaded in my partial Sprint class, I had to call Stories.Load(). Now the navigation propeties happen to be an instance of the EntityCollection<T>, so my Stories is actually an instance of EntityCollection<Story> which is a navigation property in the Sprint class. In order to test the method call that has, Stories.Load() call, I have to mock it so that it does actually hit the database. But the problem is that no opensource mocking library support the mocking of the sealed class (EntityCollection happens to be a sealed class). What do you suggest in this case I should do?

  • Anonymous
    June 21, 2011
    One thing to note here.... I see that in your example solution, the following method from the DatastorePersistenceStrategy class does not compile: public void AttachAsModified<TEntity>(IObjectSet<TEntity> objectSet, TEntity current, TEntity original) where TEntity : EntityObject        {            var asObjectQuery = objectSet as ObjectQuery<TEntity>;            if (asObjectQuery != null)            {                asObjectQuery.Context.AttachAsModified(current, original);            }            else            {                throw new InvalidOperationException("This implementation only handles object sets deriving from ObjectQuery<T>.");            }        } It can't find the AttachAsModified method on following line: asObjectQuery.Context.AttachAsModified(current, original); Am I missing something here??