Using the Generic repository/Unit of work pattern in large projects

前端 未结 2 1154
悲&欢浪女
悲&欢浪女 2021-02-04 21:52

I\'m working on a quite large application. The domain has about 20-30 types, implemented as ORM classes (for example EF Code First or XPO, doesn\'t matter for the question). I\'

相关标签:
2条回答
  • 2021-02-04 22:31

    Building on my comments above and on top of the answer here.

    With a slightly modified unit of work abstraction

    public interface IMyUnitOfWork
    {
        void CommitChanges();
        void DropChanges();
    
        IRepository<T> Repository<T>();
    }
    

    You can expose named repositories and specific repository methods with extension methods

    public static class MyRepositories
    {
        public static IRepository<User> Users(this IMyUnitOfWork uow)
        {
            return uow.Repository<User>();
        }
    
        public static IRepository<Product> Products(this IMyUnitOfWork uow)
        {
            return uow.Repository<Product>();
        }
    
        public static IEnumerable<User> GetUsersInRole(
            this IRepository<User> users, string role)
        {
            return users.AsQueryable().Where(x => true).ToList();
        }
    
        public static IEnumerable<Product> GetInCategories(
            this IRepository<Product> products, params string[] categories)
        {
            return products.AsQueryable().Where(x => true).ToList();
        }
    }
    

    That provide access the data as required

    using(var uow = new MyUnitOfWork())
    {
        var allowedUsers = uow.Users().GetUsersInRole("myRole");
    
        var result = uow.Products().GetInCategories("scarf", "hat", "trousers");
    }
    
    0 讨论(0)
  • 2021-02-04 22:48

    The way I tend to approach this is to move the type constraint from the repository class to the methods inside it. That means that instead of this:

    public interface IMyUnitOfWork : IDisposable
    {
        IRepository<Customer> Customers { get; }
        IRepository<Product> Products { get; }
        IRepository<Orders> Orders { get; }
        ...
    }
    

    I have something like this:

    public interface IMyUnitOfWork : IDisposable
    {
        Get<T>(/* some kind of filter expression in T */);
        Add<T>(T);
        Update<T>(T);
        Delete<T>(/* some kind of filter expression in T */);
        ...
    }
    

    The main benefit of this is that you only need one data access object on your unit of work. The downside is that you don't have type-specific methods like Products.GetInCategories() any more. This can be problematic, so my solution to this is usually one of two things.

    Separation of concerns

    First, you can rethink where the separation between "data access" and "business logic" lies, so that you have a logic-layer class ProductService that has a method GetInCategory() that can do this:

    using (var uow = new MyUnitOfWork())
    {
        var productsInCategory = GetAll<Product>(p => ["scarf", "hat", "trousers"].Contains(u.Category));
    }
    

    Your data access and business logic code is still separate.

    Encapsulation of queries

    Alternatively, you can implement a specification pattern, so you can have a namespace MyProject.Specifications in which there is a base class Specification<T> that has a filter expression somewhere internally, so that you can pass it to the unit of work object and that UoW can use the filter expression. This lets you have derived specifications, which you can pass around, and now you can write this:

    using (var uow = new MyUnitOfWork())
    {
        var searchCategories = new Specifications.Products.GetInCategories("scarf", "hat", "trousers");
        var productsInCategories = GetAll<Product>(searchCategories);
    }
    

    If you want a central place to keep commonly-used logic like "get user by role" or "get products in category", then instead of keeping it in your repository (which should be pure data access, strictly speaking) then you could have those extension methods on the objects themselves instead. For example, Product could have a method or an extension method InCategory(string) that returns a Specification<Product> or even just a filter such as Expression<Func<Product, bool>>, allowing you to write the query like this:

    using (var uow = new MyUnitOfWork())
    {
        var productsInCategory = GetAll(Product.InCategories("scarf", "hat", "trousers");
    }
    

    (Note that this is still a generic method, but type inference will take care of it for you.)

    This keeps all the query logic on the object being queried (or on an extensions class for that object), which still keeps your data and logic code nicely separated by class and by file, whilst allowing you to share it as you have been sharing your IRepository<T> extensions previously.

    Example

    To give a more specific example, I'm using this pattern with EF. I didn't bother with specifications; I just have service classes in the logic layer that use a single unit of work for each logical operation ("add a new user", "get a category of products", "save changes to a product" etc). The core of it looks like this (implementations omitted for brevity and because they're pretty trivial):

    public class EFUnitOfWork: IUnitOfWork
    {
        private DbContext _db;
    
        public EntityFrameworkSourceAdapter(DbContext context) {...}
    
        public void Add<T>(T item) where T : class, new() {...}
        public void AddAll<T>(IEnumerable<T> items) where T : class, new() {...}
    
        public T Get<T>(Expression<Func<T, bool>> filter) where T : class, new() {...}
        public IQueryable<T> GetAll<T>(Expression<Func<T, bool>> filter = null) where T : class, new() {...}
    
        public void Update<T>(T item) where T : class, new() {...}
    
        public void Remove<T>(Expression<Func<T, bool>> filter) where T : class, new() {...}
    
        public void Commit() {...}
    
        public void Dispose() {...}
    }
    

    Most of those methods use _db.Set<T>() to get the relevant DbSet, and then just query it with LINQ using the provided Expression<Func<T, bool>>.

    0 讨论(0)
提交回复
热议问题