This project is read-only.

Repository Pattern with interface instead of concrete objects

Mar 22, 2009 at 9:24 PM
Edited Mar 24, 2009 at 8:57 PM
Hi, after a while reading all around internet about the subject I found this project to be a good starting point to implement it.

In a project i'm working on we relay heavilly on Linq2Sql and I wantted to encapsulate our datalayer code using the Repository Pattern. But after reading a few blogs and watching a few examples laying around I noticed that only a few of this examples were using interfaces instead of concrete objects (or POCOs). What was even worst, was that all examples which were using interfaces were at the same time missing any other feature or idea which I like from NCommon (things like UnitOfWork, Specifications, etc.). So, I ended up modifing NCommon to be able to handle interface based repositories.

Here I will try to explain my approach:
  1. A new "RepositoryBase<TInterface, TEntity>" has been created so it will allow defining a repository exposing aggregate root's interfaces and using concrete types nehind the scenes.
  2. In this new RepositoryBase implementation, all method inherited from IRepository are based on TInterface.
  3. The old "RepositoryBase<TEntity>" derives from this new interface based RepositoryBase now. This is because it is instantiated as "RepositoryBase<TEntity>" which translates as "RepositoryBase<TEntity, TEntity>".
  4. A new LinqToSql repository implementation deriving from RepositoryBase<TInterface, TEntity> has been created. (LinqToSqlRepository<TInterface, TEntity>)
  5. This new implementation has a new protected member called "GetTableFor(Type type)" which will handle interface-type to table-type mapping issues.
    1. Right now this mapping is based on looking up concrete implementations of passed interface-type. But in the future this can be extended by using additional technics like Attrbute based mapping, etc.
    2. Also, this can be used not only for interface based mapping, but for abstract types mapping.
  6. Just the same way, "RepositoryQuery" property has been modified so it will return an "IQueryable<TInterface>" by making use of the "Cast<>" linq extension method,
  7. At this point I created a copy of the Linq2Sql tests and modified them so they will work with interfaces instead of concrete types.
    1. To do so, I added a few interfaces: IOrder, IOrderLine, ICustomer, etc.
    2. Then I modified the provided L2S DataModel to make each objet derive from their corresponding interface type. To do so, I used partial classes which added the needed interface dependency and also any needed method or members.
  8. Having done all this I run the tests and found (surprisingly) that most tests were passed ok. The only failing ones were related to lazy and/or eager loading.
  9. After investigating the issue I found two different things:
    1. Test "Query_Allows_Eger_Loading_Using_With" was not failing, but it was due a bug in which Asserts were being done inside "UnitOfWork" scope. I modified it so asserts were done outside of UoW boundary and find it was failing as expected.
    2. Test "Repository_For_Uses_Registered_Fetching_Strategies" was failing as expected, something based on the fact that specifing loadOptions based on interface types was meaningless to Linq, as it is expecting a DataContext known type.
  10. After thinking a while on howto fix this issue I remembered about an "ExpressionVisitor" laying around NCommon's code, it was used to create/modify expression when performing queries based on Specification objects.
  11. This made me thougth that maybe I can make a new ExpressionVisitor which will rewrite expression objects passed to With<> and With() methods of our new LinqToSqlRepository.
  12. Finally, after a bit of reading about expressions, I made a new "TranslatorExpressionVisitor" which will relay on a delegate passed during construction for type/table translation purposes.
  13. Using this new TranslatorExpressionVisitor inside With<> method I was able to make all tests pass ok.
Hoping this code is usefull to othe people I will let here a link to my modified code: Here. Note that this is an unified diff patch file which applies to v0.3 which you can donwload from release page.

*** An updated version of the code (diff) which better supports lazy loading by using attributes to declare property load dependencies is located here

Greets.
Mar 22, 2009 at 10:08 PM
Edited Mar 23, 2009 at 12:31 AM
Hi again,

Thinking about Instance creation, I was going to add a new IExtendedRepository<> implemeting a new "TInterface CreateInstance()" method which will handle implementation details concerning instance creation.

But while implementing this method I found atleast two different ways of doing so:
  1. Force implementors to define a concrete object which provides a constructor w/o arguments. This way I can simply provide a virtual method at RepositoryBase which will call Activator.CreateInstance().
    1. This will allows us to modify IRepository instead of creating a new interface w/o breaking anything based on current RepositoryBase classes.
    2. Taking into account that I will declare this method as "virtual" will allow any implementor class to override this method and provide it's one one which doesnt require a parameterless constructor on concrete objects (aggregate roots).
  2. Go stright with the IExtendedRepository approach and assume "normal" IRepository objects cannot create objects (which has no sense in an interface based repository)
Also, on the specific LinqToSql implementation. Do you think we sould call "InsertOnSubmit()" passing the newly created object before returning the instance.. Or should the user call "IRepository.Add()" by themselves.

Again, What you opinion on this subject?
Mar 25, 2009 at 4:42 AM
With respect to using interface types rather than concrete types with Repositories, I think that can be done by IoC / Dependency inejection containers. I havn't tested it but the open generics support in most containers should be able to automatically resolve a IRepository<IOrder> to a Repository<Order> automatically.

I think I'll do some additional tests to see how viable this approach is. 

Thanks,
Ritesh
Mar 25, 2009 at 1:15 PM
Hi again,

Well,  there are a few caveats, at least with Linq2Sql, I will try to spote the most important ones:
  1. Linq2Sql s internal mapping mechanism are quite limited compared to the ones provided with EF or NHibernate. And this makes impossble to use things like "LoadWith" (lazy loading) without a bit of help.
    1. Since my initial post I've managed a better approach to fix this by using a new kind of Attribute which I can delare on entities classes to help during the lazy loading infering process. (I will try to post about this shortly).
    2. Then using a custom made ExpressionVisitor I build up a list of LoadWith dependencies to handle.
  2. You cannot "as is" get a table from l2s DataContext by specifing only an interface. And also, even if you do. you cannot return this table as the proposed interface instead of its concrete object without first calling l2s Cast<>() method.
    1. This is specially painfull if you think about methods like "Linq2SqlRepository.RepositoryQuery".
    2. To overcome this limitation I made up a simple GetTableFor virtual method which tries to lookup the appropiate table given an interface. And also made some minor modifications elsewhere to add the Cast<>() call.

Here are the most important bit's of code:
  • I created a new RepositoryBase<TInterface, TEntity>, and then made RepositoryBase<TEntity> just inherit from it clearing it's body.
Here is the complete class w/o comments to save space and easy reading:

+       public abstract class RepositoryBase<TInterface, TEntity> : IRepository<TInterface>
+               where TEntity : TInterface
+       {
+               protected abstract IQueryable<TInterface> RepositoryQuery { get; }

+               protected TUnitOfWork GetCurrentUnitOfWork<TUnitOfWork>() where TUnitOfWork : IUnitOfWork
+               {
+                       Guard.Against<InvalidOperationException>(!UnitOfWork.HasStarted,
+                                                                                                       "No compatible UnitOfWork instance w
as found. Please start a compatible unit of work " +
+                                                                                                       "before creating the repository or u
se the constructor overload to explicitly provide a ObjectContext.");
+                       Guard.TypeOf<TUnitOfWork>(UnitOfWork.Current,
+                                                                                         "The current UnitOfWork instance is not compatible
 with the repository. " +
+                                                                                         "Please start a compatible unit of work before usi
ng the repository.");
+                       return ((TUnitOfWork)UnitOfWork.Current);
+               }

+               public IEnumerator<TInterface> GetEnumerator()
+               {
+                       return RepositoryQuery.GetEnumerator();
+               }

+               IEnumerator IEnumerable.GetEnumerator()
+               {
+                       return RepositoryQuery.GetEnumerator();
+               }
+

+               public Expression Expression
+               {
+                       get { return RepositoryQuery.Expression; }
+               }
+

+               public Type ElementType
+               {
+                       get { return RepositoryQuery.ElementType; }
+               }

+               public IQueryProvider Provider
+               {
+                       get { return RepositoryQuery.Provider; }
+               }

+               public abstract void Add(TInterface entity);
+               public abstract void Save(TInterface entity);
+               public abstract void Delete(TInterface entity);
+               public abstract void Detach(TInterface entity);
+               public abstract void Attach(TInterface entity);
+               public abstract void Refresh(TInterface entity);
+               public abstract void With(Expression<Func<TInterface, object>> path);
+               public abstract void With<T>(Expression<Func<T, object>> path);

+               public IRepository<TInterface> For<TService>()
+               {
+                       var strategies = ServiceLocator.Current
+                                       .GetAllInstances<IFetchingStrategy<TInterface, TService>>();
+                       if (strategies != null && strategies.Count() > 0)
+                               strategies.ForEach(x => x.Define(this));
+                       return this;
+               }

+               public IEnumerable<TInterface> Query(ISpecification<TInterface> specification)
+               {
+                       return RepositoryQuery.Where(specification.Predicate).AsQueryable();
+               }

+}

  • As said, RepositoryBase<TEntity> changed to:
+    public abstract class RepositoryBase<TEntity> : RepositoryBase<TEntity, TEntity>, IRepository<TEntity>
  • The implementation of the new RepositoryBase:
+    public class LinqToSqlRepository<TInterface, TEntity> : RepositoryBase<TInterface, TEntity>
+               where TEntity : class, TInterface
+               where TInterface : class
+        private DataContext _privateDataContext;
+               private readonly IList<MemberInfo> _loadedMembers = new List<MemberInfo>();
+        private readonly DataLoadOptions _loadOptions = new DataLoadOptions();
+               private readonly Dictionary<Type, Type> _TableMaps = new Dictionary<Type, Type>();

+        public LinqToSqlRepository() {}

+        public LinqToSqlRepository(DataContext context)
+        {
+            if (context == null)
+                return; //ArgumentNullException is not thrown when context is null to allow a possible IoC injection.
+
+            this._privateDataContext = context;
+        }

+        protected DataContext DataContext
+        {
+            get
+            {
+                return _privateDataContext ?? GetCurrentUnitOfWork<LinqToSqlUnitOfWork>().Context;
+            }   
+        }

+        protected ITable Table
+        {
+            get
+            {
+                               return DataContext.GetTable(GetTableFor(typeof(TInterface)));
+            }
+        }

+               protected virtual Type GetTableFor(Type type)
+               {
+                       // If already looked up, overcome looking it up again..
+                       if (_TableMaps.ContainsKey(type))
+                               return _TableMaps[type];
+
+                       var tmp = DataContext.Mapping.GetTables().Single(e => type.IsAssignableFrom(e.RowType.Type));
+
+                       if (tmp != null)
+                       {
+                               _TableMaps.Add(type, tmp.RowType.Type);
+                               return tmp.RowType.Type;
+                       }
+
+                       // If not found and type is not instantiable.. throw an exception
+                       if (type.IsInterface || type.IsAbstract) {
+                               var str = String.Format("Unable to translate interface or abstract type {0} into it contrete counterpart!",
type);
+                               throw new InvalidOperationException(str);
+                       }
+
+                       return type;
+               }

+               private static MemberInfo GetLoadWithMemberInfo(LambdaExpression lambda)
+               {
+                       Expression body = lambda.Body;
+                       if ((body != null) && ((body.NodeType == ExpressionType.Convert) || (body.NodeType == ExpressionType.ConvertChecked)
))
+                       {
+                               body = ((UnaryExpression)body).Operand;
+                       }
+                       MemberExpression expression2 = body as MemberExpression;
+                       if ((expression2 == null) || (expression2.Expression.NodeType != ExpressionType.Parameter))
+                       {
+                               throw new ArgumentOutOfRangeException("lambda");
+                       }
+                       return expression2.Member;
+               }
+
+               protected void _AddLoadOption(LambdaExpression expr)
+               {
+                       var member = GetLoadWithMemberInfo(expr);
+
+                       if (!_loadedMembers.Contains(member))
+                       {
+                               _loadOptions.LoadWith(expr);
+                               _loadedMembers.Add(member);
+                       }
+               }

+        protected override IQueryable<TInterface> RepositoryQuery
+        {
+            get
+            {
+                DataContext.LoadOptions = _loadOptions;
+                return Table.Cast<TInterface>();
+            }
+        }
+

+        public override void Add(TInterface entity)
+        {
+            Table.InsertOnSubmit(entity);
+        }

+        public override void Save(TInterface entity)
+        {
+            //Don't do anything as Linq to SQL uses Change Tracking to figure out updated entities.
+        }
+

+        public override void Delete(TInterface entity)
+        {
+            Table.DeleteOnSubmit(entity);
+        }

+        public override void Detach(TInterface entity)
+        {
+            throw new NotSupportedException(); //Entities auto-detach when the Context is disposed off.
+        }

+        public override void Attach(TInterface entity)
+        {
+            Table.Attach(entity);
+        }

+        public override void Refresh(TInterface entity)
+        {
+            DataContext.Refresh(RefreshMode.OverwriteCurrentValues, entity);
+        }

+        public override void With(Expression<Func<TInterface, object>> path)
+        {
+            With<TInterface>(path);
+        }

+        public override void With<T>(Expression<Func<T, object>> path)
+        {
+            Guard.Against<ArgumentNullException>(path == null, "Expected a non null valid path expression.");
+                       if (typeof(T) == typeof(TEntity))
+                               _AddLoadOption(path as LambdaExpression);
+                       else
+                       {
+                               var visitor = new TranslatorExpressionVisitor(GetTableFor);
+                               visitor.Visit(path);
+                               foreach (var member in visitor.VisitedMembers)
+                               {
+                                       LambdaExpression expr =
+                                               Expression.Lambda(
+                                                       Expression.MakeMemberAccess(
+                                                               Expression.Parameter(member.ReflectedType, "x"),
+                                                               member
+                                                       ),
+                                                       Expression.Parameter(member.ReflectedType, "x")
+                                               );
+                                       _AddLoadOption(expr);
+                               }
+                       }
+        }

  • LinqToSqlRepository<TEntity> is now as:
+    public class LinqToSqlRepository<TEntity> : LinqToSqlRepository<TEntity, TEntity>
+               where TEntity : class
+ {
+ }

  • The LoadWithAttribute class:
+       [AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
+       public class LoadWithAttribute : Attribute
+       {
+               public readonly string LoadWithMember;
+               public readonly Type LoadWithType;
+
+               public LoadWithAttribute(string member)
+               {
+                       Guard.Against<ArgumentNullException>(member == null, "member");
+
+                       LoadWithMember = member;
+               }
+
+               public LoadWithAttribute(Type type, string member)
+               {
+                       Guard.Against<ArgumentNullException>(type == null, "type");
+                       Guard.Against<ArgumentNullException>(member == null, "member");
+                       Guard.Against<ArgumentOutOfRangeException>(
+                               type.GetMember(member).Count() == 0,
+                               String.Format("Type {0} has no member named {1}",
+                                       type, member
+                               )
+                       );
+
+                       LoadWithType = type;
+                       LoadWithMember = member;
+               }
+       }
  • And finally the Visitor:
+ public delegate Type GetConcreteType(Type @interface);

+    public class TranslatorExpressionVisitor : ExpressionVisitor
+    {
+               private GetConcreteType TranslateType = null;
+               private readonly List<MemberInfo> _VisitedMembers = new List<MemberInfo>();
+
+               public IList<MemberInfo> VisitedMembers { get { return _VisitedMembers.AsReadOnly(); } }
+
+               public TranslatorExpressionVisitor(GetConcreteType translator)
+               {
+                       TranslateType = translator;
+               }
+
+               protected void _AddVisitedMember(MemberInfo member)
+               {
+                       if (!_VisitedMembers.Contains(member))
+                               _VisitedMembers.Add(member);
+               }
+
+               protected MemberInfo _GetMember(Type basetype, Type realtype, MemberInfo member)
+               {
+                       MemberInfo ret = null;
+
+                       Guard.Against<ArgumentOutOfRangeException>(
+                               member.MemberType != MemberTypes.Field && member.MemberType != MemberTypes.Property,
+                               "Invalid MemberInfo.MemberType"
+                       );
+
+                       // First try as "explicit interface"
+                       if (basetype != null && basetype.IsInterface && member.MemberType == MemberTypes.Property)
+                       {
+                               ret = realtype.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)
+                                       .Where(x => x.GetGetMethod(true) != null)
+                                       .Where(x => x.GetGetMethod(true).IsFinal)
+                                       .Where(x => x.GetGetMethod(true).IsPrivate)
+                                       .Where(x => x.PropertyType == ((PropertyInfo)member).PropertyType)
+                                       .Where(x => x.Name == basetype.FullName + "." + member.Name)
+                                       .SingleOrDefault();
+                       }
+
+                       // Ok, try as a normal member..
+                       if (ret == null)
+                       {
+                               ret = realtype.GetProperties().SingleOrDefault(x => x.MemberType == member.MemberType && x.Name == member.Na
me);
+                       }
+
+                       return ret;
+               }

+        protected override Expression VisitMemberAccess(MemberExpression methodExp)
+        {
+                       Type membtype = TranslateType(methodExp.Member.ReflectedType);
+                       Type exprtype = TranslateType(methodExp.Expression.Type);
+
+            if (methodExp.Member.MemberType != MemberTypes.Field && methodExp.Member.MemberType != MemberTypes.Property)
+                               throw new NotSupportedException("TranslatorExpressionVisotor does not support a member access of type " +
+                                                methodExp.Member.MemberType.ToString());
+            
+                       // If translated types differ, then lookup LoadWith attributes..
+                       if (membtype != methodExp.Member.ReflectedType || exprtype != methodExp.Expression.Type)
+                       {
+                               var member = _GetMember(methodExp.Expression.Type, exprtype, methodExp.Member);
+
+                               Guard.Against<InvalidOperationException>(member == null, "member == false!");
+
+                               // Try to obtain LoadWithAttributes..
+                               var attrs = member.GetCustomAttributes(typeof(LoadWithAttribute), true) as LoadWithAttribute[];
+
+                               Guard.Against<InvalidOperationException>(attrs.Count() == 0,
+                                       String.Format(
+                                               "Unable to configure load options on {0}.{1}. Are you missing a LoadWithAttribute declaratio
n?",
+                                               methodExp.Expression.Type.Name, methodExp.Member.Name
+                                       )
+                               );
+
+                               // Visit each given expression..
+                               foreach (var attr in attrs)
+                                       _AddVisitedMember(exprtype.GetMember(attr.LoadWithMember).First());
+                       }
+                       else
+                       {
+                               _AddVisitedMember(methodExp.Member);
+                       }
+
+                       return base.VisitMemberAccess(methodExp);
+        }

+        protected override Expression VisitMethodCall(MethodCallExpression methodCallExp)
+        {
+            throw new NotSupportedException(
+                "TranslatorExpressionVisotor does not support method calls. Only MemberAccess expressions are allowed.");
+        }

  • And this is how you linq2sql partial classes should look like:
+    partial class OrderItem : NCommon.Data.LinqToSql.Tests.Domain.IOrderItem
     {
         public double TotalPrice
         {
             get { return Price*Quantity; }
         }
+
+               [LoadWith("Product")]
+               IProduct IOrderItem.Product { get { return this.Product; } }
+
+               [LoadWith("Order")]
+               IOrder IOrderItem.Order { get { return this.Order; } }
     }
+
+       partial class Product : NCommon.Data.LinqToSql.Tests.Domain.IProduct
+       {
+               [LoadWith("OrderItems")]
+               IEnumerable<IOrderItem> IProduct.OrderItems { get { return this.OrderItems.Cast<IOrderItem>(); } }
+       }
+
+       partial class Customer : ICustomer
+       {
+               [LoadWith("Orders")]
+               IEnumerable<IOrder> ICustomer.Orders { get { return this.Orders.Cast<IOrder>(); } }
+       }
 Greets
Pablo