ASP.NET MVC 的最佳存储库模式

Posted

技术标签:

【中文标题】ASP.NET MVC 的最佳存储库模式【英文标题】:Best Repository Pattern for ASP.NET MVC 【发布时间】:2012-06-11 02:53:39 【问题描述】:

我最近学习了 ASP.NET MVC(我喜欢它)。我正在与一家公司合作,该公司使用依赖注入在每个请求中加载一个 Repository 实例,并且我熟悉使用该存储库。

但现在我正在编写自己的几个 MVC 应用程序。我不完全了解我的公司使用存储库的方式和原因,我正在尝试确定实现数据访问的最佳方法。

我正在使用 C# 和实体框架(所有最新版本)。

我看到了处理数据访问的三种通用方法。

    每次访问数据时,使用语句中的常规数据库上下文。这很简单,而且工作正常。但是,如果两个位置需要在一个请求中读取相同的数据,则必须读取两次数据。 (每个请求只有一个存储库,两个地方都会使用相同的实例,我知道第二次读取只会返回第一次读取的数据。)

    典型的repository pattern。由于我不明白的原因,这种典型的模式涉及为数据库中使用的每个表创建一个包装器类。这对我来说似乎是错误的。事实上,由于它们也是作为接口实现的,所以从技术上讲,我会为每个表创建两个包装类。 EF 为我创建表格。我认为这种方法没有意义。

    还有一个generic repository pattern,其中创建了一个存储库类来服务所有实体对象。这对我来说更有意义。但这对其他人有意义吗?上面的链接是最好的方法吗?

我很想从其他人那里得到一些关于这个主题的意见。您是在编写自己的存储库,使用上述其中之一,还是在做一些完全不同的事情。请分享。

【问题讨论】:

我会说 2 号中的链接不是典型的存储库模式。通常,DDD speak 中的每个聚合根都有一个存储库。这是一个很好的SO thread 关于这个主题。正如您所提到的,第 2 项中的示例似乎只是包装了一张桌子。看起来他们正在实施该模式只是为了实施该模式而没有真正的好处。所以我同意你的看法。 你可能是对的。但是,在搜索网络时,我发现的大多数示例都为每个实体创建了单独的包装器,包括我拥有的一些书中的那些。在这方面,我发布的链接中的代码似乎很典型。感谢您的链接。我去看看。 @JonathanWood 这是solution I like best(该死,我经常使用这个链接)。即,具有通用方法的非通用存储库接口。它仍然是DbContext 的一个相对较薄的包装器,但它可以更轻松地进行测试。 【参考方案1】:

我使用了#2 和#3 的混合,但如果可能的话,我更喜欢严格的通用存储库(甚至比#3 链接中建议的更严格)。 #1 不好,因为它在单元测试中表现不佳。

如果您有一个较小的域或需要限制您的域允许查询的实体,我认为定义实体特定存储库接口的 #2- 或 #3 本身实现通用存储库 - 是有意义的。但是,我发现为我要查询的每个实体编写接口和具体实现既累人又没有必要。 public interface IFooRepository : IRepository<Foo> 有什么好处(同样,除非我需要将开发人员限制在一组允许的聚合根中)?

我只定义了我的通用存储库接口,使用AddRemoveGetGetDeferredCountFind 方法(查找返回一个允许 LINQ 的IQueryable 接口),创建一个具体的通用实现,然后收工。我非常依赖Find,因此也依赖 LINQ。如果我需要多次使用特定查询,我会使用扩展方法并使用 LINQ 编写查询。

这涵盖了我 95% 的持久性需求。如果我需要执行一些一般情况下无法完成的持久性操作,我会使用自制的ICommand API。例如,假设我正在使用 NHibernate,并且我需要在我的域中执行一个复杂的查询,或者我可能需要执行一个批量命令。 API 大致如下所示:

// marker interface, mainly used as a generic constraint
public interface ICommand



// commands that return no result, or a non-query
public interface ICommandNoResult : ICommand

   void Execute();


// commands that return a result, either a scalar value or record set
public interface ICommandWithResult<TResult> : ICommand

   TResult Execute();


// a query command that executes a record set and returns the resulting entities as an enumeration.
public interface IQuery<TEntity> : ICommandWithResult<IEnumerable<TEntity>>

    int Count();


// used to create commands at runtime, looking up registered commands in an IoC container or service locator
public interface ICommandFactory

   TCommand Create<TCommand>() where TCommand : ICommand;

现在我可以创建一个接口来表示一个特定的命令。

public interface IAccountsWithBalanceQuery : IQuery<AccountWithBalance>

    Decimal MinimumBalance  get; set; 

我可以创建一个具体的实现并使用原始 SQL、NHibernate HQL 等,并将其注册到我的服务定位器。

现在在我的业务逻辑中,我可以这样做:

var query = factory.Create<IAccountsWithBalanceQuery>();
query.MinimumBalance = 100.0;

var overdueAccounts = query.Execute();

您还可以将规范模式与 IQuery 一起使用来构建有意义的、用户输入驱动的查询,而不是使用具有数百万个令人困惑的属性的界面,但这假设您不会发现规范模式本身就令人困惑对;)。

最后一个难题是您的存储库何时需要执行特定的存储库前后操作。现在,您可以非常轻松地为特定实体创建通用存储库的实现,然后覆盖相关方法并执行您需要执行的操作,并更新您的 IoC 或服务定位器注册并完成它。

但是,有时这种逻辑是跨领域的,并且难以通过覆盖存储库方法来实现。所以我创建了IRepositoryBehavior,它基本上是一个事件接收器。 (以下只是我脑海中的一个粗略定义)

public interface IRepositoryBehavior

    void OnAdding(CancellableBehaviorContext context);
    void OnAdd(BehaviorContext context);

    void OnGetting(CancellableBehaviorContext context);
    void OnGet(BehaviorContext context);

    void OnRemoving(CancellableBehaviorContext context);
    void OnRemove(BehaviorContext context);

    void OnFinding(CancellableBehaviorContext context);
    void OnFind(BehaviorContext context);

    bool AppliesToEntityType(Type entityType);

现在,这些行为可以是任何东西。审计、安全检查、软删除、强制域约束、验证等。我创建一个行为,向 IoC 或服务定位器注册它,并修改我的通用存储库以接收注册的IRepositoryBehaviors 的集合,然后检查针对当前存储库类型的每个行为,并将操作包装在每个适用行为的前/后处理程序中。

这是一个软删除行为示例(软删除意味着当有人要求删除一个实体时,我们只是将其标记为已删除,因此它不能再次返回,但实际上永远不会被物理删除)。

public SoftDeleteBehavior : IRepositoryBehavior

   // omitted

   public bool AppliesToEntityType(Type entityType)
   
       // check to see if type supports soft deleting
       return true;
   

   public void OnRemoving(CancellableBehaviorContext context)
   
        var entity = context.Entity as ISoftDeletable;
        entity.Deleted = true; // when the NHibernate session is flushed, the Deleted column will be updated

        context.Cancel = true; // set this to true to make sure the repository doesn't physically delete the entity.
   

是的,这基本上是 NHibernate 事件侦听器的简化和抽象实现,但这就是我喜欢它的原因。 A)我可以在不将 NHibernate 带入图片的情况下对行为进行单元测试 B)我可以在 NHibernate 之外使用这些行为(比如存储库是包装 REST 服务调用的客户端实现) C)NH 的事件侦听器可能是一个真正的麻烦;)

【讨论】:

感谢代码的 sn-ps。我会花一些时间更仔细地研究它。 我终于有更多时间花在这个上了。我对其中的一些代码感到有些惊讶。您似乎在说您喜欢一种非常通用的方法,但您似乎正在创建专门的接口,这甚至比我检查过的示例更具体。为什么需要这个? (顺便说一句,如果您想用源代码编写更完整的文章,我很乐意在我的blackbeltcoder.com 网站上发布类似的内容。)【参考方案2】:

我会推荐 1 号,但有一些注意事项。 2 号似乎是最常见的,但根据我的经验,存储库最终只是一个用于查询的杂乱垃圾场。如果您使用通用存储库 (2),它只是 DBContext 的一个薄包装器,除非您打算更改 ORM(坏主意),否则真的有点毫无意义。

但是当我直接访问 DBContext 时,我更喜欢使用管道和过滤器模式,这样您就可以重用通用逻辑,例如

items = DBContext.Clients
    .ByPhoneNumber('1234%')
    .ByOrganisation(134);

ByPhoneNumber 和 By Organization 只是扩展方法。

【讨论】:

谢谢,但是像我提出的那些可能的性能问题呢?如果每次需要时都新建一个 DBContext,则代码的不同部分可能会请求相同的数据,并且不会被缓存。 @Johnathan:使用依赖注入,这样任何需要 DBContext 的东西都会在每个请求的生命周期中接收相同的上下文。【参考方案3】:

我们来看看 Asp.Net MVC 中的最佳存储库模式:

存储库模式在应用程序的数据层和域层之间添加了一个分离层。它还使应用程序的数据访问部分具有更好的可测试性。

数据库工厂 (IDatabaseFactory.cs):

public interface IDatabaseFactory : IDisposable

    Database_DBEntities Get();

数据库工厂实现(DatabaseFactory.cs):

public class DatabaseFactory : Disposable, IDatabaseFactory

    private Database_DBEntities dataContext;
    public Database_DBEntities Get()
    
        return dataContext ?? (dataContext = new Database_DBEntities());
    

    protected override void DisposeCore()
    
        if (dataContext != null)
            dataContext.Dispose();
    

基础接口 (IRepository.cs):

public interface IRepository<T> where T : class

    void Add(T entity);
    void Update(T entity);
    void Detach(T entity);
    void Delete(T entity);
    T GetById(long Id);
    T GetById(string Id);
    T Get(Expression<Func<T, bool>> where);
    IEnumerable<T> GetAll();
    IEnumerable<T> GetMany(Expression<Func<T, bool>> where);
    void Commit();

抽象类(Repository.cs):

public abstract class Repository<T> : IRepository<T> where T : class

    private Database_DBEntities dataContext;
    private readonly IDbSet<T> dbset;
    protected Repository(IDatabaseFactory databaseFactory)
    
        DatabaseFactory = databaseFactory;
        dbset = DataContext.Set<T>();
    

    /// <summary>
    /// Property for the databasefactory instance
    /// </summary>
    protected IDatabaseFactory DatabaseFactory
    
        get;
        private set;
    

    /// <summary>
    /// Property for the datacontext instance
    /// </summary>
    protected Database_DBEntities DataContext
    
        get  return dataContext ?? (dataContext = DatabaseFactory.Get()); 
    

    /// <summary>
    /// For adding entity
    /// </summary>
    /// <param name="entity"></param>
    public virtual void Add(T entity)
    
        try
        
            dbset.Add(entity);
            //  dbset.Attach(entity);
            dataContext.Entry(entity).State = EntityState.Added;
            int iresult = dataContext.SaveChanges();
        
        catch (UpdateException ex)
        
        
        catch (DbUpdateException ex) //DbContext
        
        
        catch (Exception ex)
        
            throw ex;
        
    

    /// <summary>
    /// For updating entity
    /// </summary>
    /// <param name="entity"></param>
    public virtual void Update(T entity)
    
        try
        
            // dbset.Attach(entity);
            dbset.Add(entity);
            dataContext.Entry(entity).State = EntityState.Modified;
            int iresult = dataContext.SaveChanges();
        
        catch (UpdateException ex)
        
            throw new ApplicationException(Database_ResourceFile.DuplicateMessage, ex);
        
        catch (DbUpdateException ex) //DbContext
        
            throw new ApplicationException(Database_ResourceFile.DuplicateMessage, ex);
        
        catch (Exception ex) 
            throw ex;
        
    

    /// <summary>
    /// for deleting entity with class 
    /// </summary>
    /// <param name="entity"></param>
    public virtual void Delete(T entity)
    
        dbset.Remove(entity);
        int iresult = dataContext.SaveChanges();
    

    //To commit save changes
    public void Commit()
    
        //still needs modification accordingly
        DataContext.SaveChanges();
    

    /// <summary>
    /// Fetches values as per the int64 id value
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public virtual T GetById(long id)
    
        return dbset.Find(id);
    

    /// <summary>
    /// Fetches values as per the string id input
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public virtual T GetById(string id)
    
        return dbset.Find(id);
    

    /// <summary>
    /// fetches all the records 
    /// </summary>
    /// <returns></returns>
    public virtual IEnumerable<T> GetAll()
    
        return dbset.AsNoTracking().ToList();
    

    /// <summary>
    /// Fetches records as per the predicate condition
    /// </summary>
    /// <param name="where"></param>
    /// <returns></returns>
    public virtual IEnumerable<T> GetMany(Expression<Func<T, bool>> where)
    
        return dbset.Where(where).ToList();
    

    /// <summary>
    /// 
    /// </summary>
    /// <param name="entity"></param>
    public void Detach(T entity)
    
        dataContext.Entry(entity).State = EntityState.Detached;
    

    /// <summary>
    /// fetches single records as per the predicate condition
    /// </summary>
    /// <param name="where"></param>
    /// <returns></returns>
    public T Get(Expression<Func<T, bool>> where)
    
        return dbset.Where(where).FirstOrDefault<T>();
    

如何在控制器中访问此存储库模式:

1.你有用户模型:

public partial class User

    public int Id  get; set; 
    public string Name  get; set; 

2。现在您必须创建 UserModel 的存储库类

public class UserRepository : Repository<User>, IUserRepository

    private Database_DBEntities dataContext;

    protected IDatabaseFactory DatabaseFactory
    
        get;
        private set;
    

    public UserRepository(IDatabaseFactory databaseFactory)
        : base(databaseFactory)
    
        DatabaseFactory = databaseFactory;
    

    protected Database_DBEntities DataContext
    
        get  return dataContext ?? (dataContext = DatabaseFactory.Get()); 
    

    public interface IUserRepository : IRepository<User>
     
    

3.现在您必须使用所有 CRUD 方法创建用户服务接口 (IUserService.cs):

public interface IUserService

    #region User Details 
    List<User> GetAllUsers();
    int SaveUserDetails(User Usermodel);
    int UpdateUserDetails(User Usermodel);
    int DeleteUserDetails(int Id);
    #endregion

4.现在您必须使用所有 CRUD 方法创建用户服务接口 (UserService.cs):

public class UserService : IUserService

    IUserRepository _userRepository;
    public UserService()  
    public UserService(IUserRepository userRepository)
    
        this._userRepository = userRepository;
    

    public List<User> GetAllUsers()
    
        try
        
            IEnumerable<User> liUser = _userRepository.GetAll();
            return liUser.ToList();
        
        catch (Exception ex)
        
            throw ex;
        
    

    /// <summary>
    /// Saves the User details.
    /// </summary>
    /// <param name="User">The deptmodel.</param>
    /// <returns></returns>
    public int SaveUserDetails(User Usermodel)
    
        try
        
            if (Usermodel != null)
            
                _userRepository.Add(Usermodel);
                return 1;
            
            else
                return 0;
        
        catch
        
            throw;
        
   

   /// <summary>
   /// Updates the User details.
   /// </summary>
   /// <param name="User">The deptmodel.</param>
   /// <returns></returns>
   public int UpdateUserDetails(User Usermodel)
   
       try
       
           if (Usermodel != null)
           
               _userRepository.Update(Usermodel);
               return 1;
           
           else
               return 0;
       
       catch
       
           throw;
       
   

   /// <summary>
   /// Deletes the User details.
   /// </summary>
   /// <param name="Id">The code identifier.</param>
   /// <returns></returns>
   public int DeleteUserDetails(int Id)
   
       try
       
           User Usermodel = _userRepository.GetById(Id);
           if (Usermodel != null)
           
               _userRepository.Delete(Usermodel);
               return 1;
           
           else
               return 0;
       
       catch
       
           throw;
       
   

5.现在你已经为你的存储库模式设置好了,你可以访问用户控制器中的所有数据:

//Here is the User Controller 
public class UserProfileController : Controller

    IUserService _userservice;
    public CustomerProfileController(IUserService userservice)
    
        this._userservice = userservice;
    

    [HttpPost]
    public ActionResult GetAllUsers(int id)
    
        User objUser=new User();

        objUser = _userservice.GetAllUsers().Where(x => x.Id == id).FirstOrDefault();
    

【讨论】:

似乎很多代码已经用 DbContext 为你实现了。不确定这种方法是否有意义。【参考方案4】:

URF - Unit of Work & (extensible/generic) Repositories Framework 有一个现成的解决方案。它将为您节省大量时间。 他们实现了一个通用存储库(也有一个异步存储库)。为了扩展任何存储库,他们使用了这样的扩展:

     public static decimal GetCustomerOrderTotalByYear(this IRepository<Customer> repository, string customerId, int year)
    
        return repository
            .Queryable()
            .Where(c => c.CustomerID == customerId)
            .SelectMany(c => c.Orders.Where(o => o.OrderDate != null && o.OrderDate.Value.Year == year))
            .SelectMany(c => c.OrderDetails)
            .Select(c => c.Quantity*c.UnitPrice)
            .Sum();
    

某些类(如 QueryObject)可能会根据您的项目而过度工作,但总体而言,它是帮助您启动和运行的好解决方案。

【讨论】:

以上是关于ASP.NET MVC 的最佳存储库模式的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ASP.NET Core MVC 中使用依赖注入设计存储库模式?

在使用 IdentityDbContext 的情况下,如何在 ASP.NET Core MVC 中使用存储库模式?

在使用 ASP.NET MVC 和 ORM 解决方案时,我们是不是需要使用存储库模式?

ASP.NET Core ---- 系列文章

使用 Twitter Bootstrap 在 ASP.NET MVC 中调用模式对话框的最佳方法是啥?

新手轻松学会MVC