Best Repository Pattern for ASP.NET MVC

2019-01-15 23:59发布

I recently learned ASP.NET MVC (I love it). I'm working with a company that uses dependency injection to load a Repository instance in each request, and I'm familiar with using that repository.

But now I'm writing a couple of MVC applications of my own. I don't fully understand the hows and whys of the repository my company uses, and I'm trying to decide the best approach to implement data access.

I am using C# and Entity Framework (with all the latest versions).

I see three general approaches for handling data access.

  1. Regular DB context within a using statement each time I access data. This is simple and it works okay. However, if two locations need to read the same data within one request, the data must be read twice. (With a single repository per request, the same instance would be used in both places and I understand the second read would simply return the data from the first read.)

  2. A typical repository pattern. For reasons I don't understand, this typical pattern involves creating a wrapper class for every table used from the database. That seems wrong to me. In fact, since they are implemented also as interfaces, I'd technically be creating two wrapper classes for each table. EF creates tables for me. I don't believe this approach makes sense.

  3. There is also a generic repository pattern where a single repository class is created to serve all entity objects. This makes much more sense to me. But does it make sense to others? Is the link above the best approach?

I'd love to get some input from others on this topic. Are you writing your own repository, using one of those above, or doing something different altogether. Please share.

4条回答
Juvenile、少年°
2楼-- · 2019-01-16 00:33

I have used a blend of #2 and #3, but I prefer a strict generic repository if possible (stricter than even suggested in the link for #3). #1 is no good because it plays poorly with unit testing.

If you have a smaller domain or need to constrict which entities that your domain allows to be queried, I suppose #2- or #3 that defines entity specific repository interfaces that themselves implement a generic repository- makes sense. However, I find it to be exhausting and unnecessary to write an interface and a concrete implementation for every entity I want to query. What good is public interface IFooRepository : IRepository<Foo> (again, unless I need to constrain developers to a set of allowed aggregate roots)?

I just define my generic repository interface, with Add, Remove, Get, GetDeferred, Count, and Find methods (Find returns an IQueryable interface allowing LINQ), create a concrete generic implementation, and call it a day. I rely heavily on Find and thus LINQ. If I need to use a specific query more than once, I use extension methods and write the query using LINQ.

This covers 95% of my persistence needs. If I need to perform some sort of persistence action that can't be done generically, I use a home-grown ICommand API. For example, say I'm working with NHibernate and I need to perform a complex query as part of my domain, or perhaps I need to do a bulk command. The API looks roughly like this:

// 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;
}

Now I can create an interface to represent a specific command.

public interface IAccountsWithBalanceQuery : IQuery<AccountWithBalance>
{
    Decimal MinimumBalance { get; set; }
}

I can create a concrete implementation and use raw SQL, NHibernate HQL, whatever, and register it with my service locator.

Now in my business logic I can do something like this:

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

var overdueAccounts = query.Execute();

You can also use a Specification pattern with IQuery to build meaningful, user-input-driven queries, rather than having an interface with million confusing properties, but that assumes you don't find the specification pattern confusing in its own right ;).

One last piece of the puzzle is when your repository needs to do specific pre- and -post repository operation. Now, you can very easily create an implementation of your generic repository for a specific entity, then override the relevant method(s) and do what you need to do, and update your IoC or service locator registration and be done with it.

However, sometimes this logic is cross-cutting and awkward to implement by overriding a repository method. So I created IRepositoryBehavior, which is basically an event sink. (Below is just a rough definition off the top of my head)

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);
}

Now, these behaviors can be anything. Auditing, security checking, soft-delete, enforcing domain constraints, validation, etc. I create a behavior, register it with the IoC or service locator, and modify my generic repository to take in a collection of registered IRepositoryBehaviors, and check each behavior against the current repository type and wrap the operation in the pre/post handlers for each applicable behavior.

Here's an example soft-delete behavior (soft-delete means that when someone asks to delete an entity, we just mark it as deleted so it can't be returned again, but is never actually physically removed).

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.
   }
}

Yes, this is basically a simplified and abstracted implementation of NHibernate's event listeners, but that's why I like it. A) I can unit test a behavior without bringing NHibernate into the picture B) I can use these behaviors outside of NHibernate (say the repository is client implementation that wraps REST service calls) C) NH's event listeners can be a real pain in the ass ;)

查看更多
姐就是有狂的资本
3楼-- · 2019-01-16 00:42

Here we go for Best Repository Pattern in Asp.Net MVC:

The Repository pattern adds a separation layer between the data and domain layers of an application. It also makes the data access parts of an application better testable.

Database Factory (IDatabaseFactory.cs):

public interface IDatabaseFactory : IDisposable
{
    Database_DBEntities Get();
}

Database Factory Implementations (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();
    }
}

Base Interface (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();
}

Abstract Class (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>();
    }
}

How to access this repository pattern in controller:

1. You have User Model :

public partial class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

2. Now you have to create Repository Class of your 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. Now you have to create UserService Interface (IUserService.cs) with all CRUD Methods:

public interface IUserService
{
    #region User Details 
    List<User> GetAllUsers();
    int SaveUserDetails(User Usermodel);
    int UpdateUserDetails(User Usermodel);
    int DeleteUserDetails(int Id);
    #endregion
}

4. Now you have to create UserService Interface (UserService.cs) with all CRUD Methods:

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. Now you All set for your Repository Pattern and you can access all data in User Controller:

//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();
    }
}
查看更多
Animai°情兽
4楼-- · 2019-01-16 00:46

There is a ready to use solution at URF - Unit of Work & (extensible/generic) Repositories Framework. It will save you a lot of time. They implemented a generic repository (also there is an async repository). For extending any repository they have used extensions like this:

     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();
    }

Some classes like QueryObject may be an overwork depending on your project but overally it is good solution to help you get up and running.

查看更多
ゆ 、 Hurt°
5楼-- · 2019-01-16 00:51

I would recommend number 1, with some caveats. Number 2 is what seems to be most common but in my experience the repository just ends up a messy dumping ground for queries. If you use a generic repository (2) it is just a thin wrapper around the DBContext, a bit pointless really unless you are planning on changing ORM's (bad idea).

But when I access DBContext directly I prefer to use a Pipes and Filters pattern so you can reuse common logic, something like

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

The ByPhoneNumber and By Organisation are just extension methods.

查看更多
登录 后发表回答