Подтвердить что ты не робот

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

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

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

Я использую С# и Entity Framework (со всеми последними версиями).

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Я просто определяю свой общий интерфейс репозитория с помощью методов Add, Remove, Get, GetDeferred, Count и Find (Find возвращает интерфейс IQueryable, позволяющий LINQ), создавать конкретную общую реализацию, и назовите это днем. Я сильно полагаюсь на Find и, следовательно, на LINQ. Если мне нужно использовать конкретный запрос более одного раза, я использую методы расширения и записываю запрос с помощью LINQ.

Это покрывает 95% моих потребностей в персистентности. Если мне нужно выполнить какое-то действие настойчивости, которое невозможно сделать в общем случае, я использую самодельный API ICommand. Например, скажем, что я работаю с NHibernate, и мне нужно выполнить сложный запрос как часть моего домена, или, возможно, мне нужно выполнить команду bulk. 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 для создания значимых запросов, управляемых пользователем, вместо того, чтобы иметь интерфейс с миллионом запутывающих свойств, но предполагается, что вы не обнаружите, что шаблон спецификации запутан в своей собственной right;).

Одна из последних частей головоломки заключается в том, что вашему репозиторию необходимо выполнить конкретную операцию репозитория pre-and-post. Теперь вы можете очень легко создать реализацию своего общего репозитория для определенного объекта, затем переопределить соответствующий метод и выполнить то, что вам нужно сделать, и обновить регистрацию 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);
}

Теперь это поведение может быть чем угодно. Аудит, проверка безопасности, soft-delete, принудительное использование ограничений домена, проверка и т.д. Я создаю поведение, регистрирую его с помощью IoC или локатора службы и изменяю свой общий репозиторий, чтобы взять коллекцию зарегистрированных IRepositoryBehavior s и проверить каждое поведение против текущего типа репозитория и завершение операции в обработчиках pre/post для каждого применимого поведения.

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

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

Ответ 2

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

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

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

ByPhoneNumber и By Organization - это просто методы расширения.

Ответ 3

Здесь мы рассмотрим шаблон Best Repository в Asp.Net MVC:

Шаблон репозитория добавляет разделительный слой между слоями данных и домена приложения. Это также делает доступным для доступа к данным части приложения более надежным.

База данных Factory (IDatabaseFactory.cs):

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

База данных Factory Реализации (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. Теперь вам нужно создать интерфейс 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 Interface (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. Теперь вы все настроены для своего шаблона репозитория, и вы можете получить доступ ко всем данным в 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();

    }
}

Приветствия!

Ответ 4

Существует готовое к использованию решение в URF - Единица работы и (расширяемая/общая) структура хранилищ. Это сэкономит вам много времени. Они реализовали общий репозиторий (также есть репозиторий async). Для расширения любого репозитория они использовали расширения вроде этого:

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