Лучший шаблон репозитория для ASP.NET MVC


Я недавно узнал ASP.NET MVC (я люблю его). Я работаю с компанией, которая использует инъекцию зависимостей для загрузки экземпляра репозитория в каждом запросе, и я знаком с использованием этого репозитория.

но теперь я пишу несколько собственных приложений MVC. Я не совсем понимаю, как и почему репозиторий, который использует моя компания, и я пытаюсь решить лучший подход для реализации доступа к данным.

Я использую C# и Entity Framework (со всеми новейшая версия.)

Я вижу три общих подхода для обработки доступа к данным.

  1. обычный контекст БД в инструкции using каждый раз, когда я обращаюсь к данным. Это просто и работает нормально. Однако если два местоположения должны считывать одни и те же данные в рамках одного запроса, данные должны быть прочитаны дважды. (С одним репозиторием на запрос один и тот же экземпляр будет использоваться в обоих местах, и я понимаю, что второе чтение просто вернет данные из первого читать.)

  2. типичный шаблон репозитория. По причинам, которые я не понимаю, этот типичный шаблон включает в себя создание класса-оболочки для каждой таблицы, используемой из базы данных. Мне это кажется неправильным. Фактически, поскольку они реализованы также как интерфейсы, я бы технически создавал два класса-оболочки для каждой таблицы. EF создает таблицы для меня. Я не верю, что такой подход имеет смысл.

  3. есть еще и generic шаблон репозитория где создается один класс репозитория для обслуживания всех объектов сущностей. Это имеет гораздо больше смысла для меня. Но имеет ли это смысл для других? Является ли ссылка выше лучшим подходом?

Я хотел бы получить некоторый вклад от других по этой теме. Вы пишете свой собственный репозиторий, используя один из вышеперечисленных, или делать что-то совсем другое. Пожалуйста, поделитесь.

4 61

4 ответа:

я использовал смесь #2 и #3, но я предпочитаю строгий универсальный репозиторий, если это возможно (строже, чем даже предложено в ссылке для #3). #1 не годится, потому что он плохо играет с модульным тестированием.

если у вас есть меньший домен или вам нужно сжать, какие сущности, которые ваш домен позволяет запрашивать, я полагаю, что #2 - или #3, который определяет интерфейсы репозитория для конкретных сущностей, которые сами реализуют общий репозиторий, имеет смысл. Тем не менее, я считаю, что это утомительно и ненужно писать интерфейс и конкретную реализацию для каждой сущности, которую я хочу запросить. Что толку public interface IFooRepository : IRepository<Foo> (опять же, если мне не нужно ограничивать разработчиков набором разрешенных агрегатных корней)?

я просто определяю свой общий интерфейс репозитория, с Add,Remove,Get,GetDeferred,Count и Find методы (Find возвращает IQueryable интерфейс, позволяющий LINQ), создать конкретную универсальную реализацию и назвать ее днем. Я сильно полагаюсь на 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; }
}

я могу создать конкретную реализацию и использовать raw SQL, NHibernate HQL, что угодно, и зарегистрировать его с помощью моего Service locator.

теперь в моей бизнес-логике я могу сделать что-то вроде этого:

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

var overdueAccounts = query.Execute();

вы также можете использовать шаблон спецификации с IQuery для создания значимых запросов, управляемых пользователем, а не имеющих интерфейса с миллион запутанных свойств, но это предполагает, что вы не найдете шаблон спецификации запутанным сам по себе ;).

последний кусочек головоломки-это когда вашему репозиторию нужно выполнить определенную операцию до и после репозитория. Теперь вы можете очень легко создать реализацию своего общего репозитория для определенного объекта, затем переопределить соответствующие методы и сделать то, что вам нужно сделать, а также обновить регистрацию IoC или service locator и сделать это оно.

однако иногда эта логика является сквозной и неудобной для реализации путем переопределения метода репозитория. Так что я создал 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 или Service locator и изменяю свой общий репозиторий принять в коллекцию зарегистрированных IRepositoryBehaviors, и проверьте каждое поведение относительно текущего типа репозитория и оберните операцию в обработчики pre/post для каждого применимого поведения.

вот пример поведения мягкого удаления (мягкое удаление означает, что когда кто-то просит удалить объект, мы просто помечаем его как удаленный, чтобы он не мог быть возвращен снова, но никогда не удаляется физически).

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, но именно поэтому мне это нравится. А) я могу модульно протестировать поведение без привлечения NHibernate в картину б) я могу использовать эти поведения за пределами NHibernate (скажем, репозиторий-это реализация клиента, которая обертывает вызовы службы REST) в) слушатели событий NH могут быть настоящей болью в заднице;)

Я бы рекомендовал номер 1, с некоторыми оговорками. Номер 2-это то, что кажется наиболее распространенным, но по моему опыту репозиторий просто заканчивается грязной свалкой для запросов. Если вы используете общий репозиторий (2), это просто тонкая оболочка вокруг DBContext, немного бессмысленная, если вы не планируете изменять ORM (плохая идея).

но когда я обращаюсь к DBContext напрямую, я предпочитаю использовать шаблон труб и фильтров, чтобы вы могли использовать общую логику, что-то как

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

ByPhoneNumber и по организации-это просто методы расширения.

здесь мы идем для лучшего шаблона репозитория в 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();
}

Абстрактный Класс (Репозитория.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. Теперь вы должны создать Интерфейс UserService (IUserService.cs) со всеми методами CRUD:

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

4. Теперь вам нужно создать интерфейс UserService (UserService.cs) со всеми методами CRUD:

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

есть готовое к использованию решение на 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 может быть переутомление в зависимости от вашего проекта, но в целом это хорошее решение, чтобы помочь вам встать и работать.