Mejor Patrón de Repositorio para ASP.NET MVC


Recientemente aprendí ASP.NET MVC (Me encanta). Estoy trabajando con una empresa que utiliza la inyección de dependencias para cargar una instancia de repositorio en cada solicitud, y estoy familiarizado con el uso de ese repositorio.

Pero ahora estoy escribiendo un par de aplicaciones MVC por mi cuenta. No entiendo completamente cómo y por qué del repositorio que utiliza mi empresa, y estoy tratando de decidir el mejor enfoque para implementar el acceso a los datos.

Estoy usando C # y Entity Framework (con todas las últimas versiones).

Veo tres enfoques generales para manejar el acceso a los datos.

  1. Contexto de base de datos normal dentro de una instrucción using cada vez que tengo acceso a datos. Esto es simple y funciona bien. Sin embargo, si dos ubicaciones necesitan leer los mismos datos dentro de una solicitud, los datos deben leerse dos veces. (Con un único repositorio por solicitud, se usaría la misma instancia en ambos lugares y entiendo que la segunda lectura simplemente devolvería los datos de la primera lectura.)

  2. A típico patrón de repositorio. Por razones que no entiendo, este patrón típico implica crear una clase wrapper para cada tabla utilizada desde la base de datos. Eso me parece mal. De hecho, ya que se implementan también como interfaces, técnicamente estaría creando dos clases de envoltura para cada tabla. EF crea tablas para mí. No creo que este enfoque tenga sentido.

  3. También hay un patrón genérico de repositorio donde se crea una sola clase de repositorio para servir a todos los objetos de entidad. Esto tiene mucho más sentido para mí. Pero ¿tiene sentido para los demás? ¿Es el enlace anterior el mejor enfoque?

Me encantaría obtener información de otros sobre este tema. Estás escribiendo tu propio repositorio, usando uno de los anteriores, o haciendo algo completamente diferente. Por favor, comparte.

Author: Jonathan Wood, 2012-06-07

4 answers

He utilizado una mezcla de #2 y #3, pero prefiero un repositorio genérico estricto si es posible (más estricto de lo que incluso se sugiere en el enlace para #3). #1 no es bueno porque juega mal con las pruebas unitarias.

Si tiene un dominio más pequeño o necesita restringir qué entidades que su dominio permite consultar, supongo que tiene sentido #2 - o #3 que define interfaces de repositorio específicas de entidad que implementan un repositorio genérico. Sin embargo, me parece agotador y innecesario escribir una interfaz y una implementación concreta para cada entidad que quiero consultar. ¿De qué sirve public interface IFooRepository : IRepository<Foo> (de nuevo, a menos que necesite restringir a los desarrolladores a un conjunto de raíces agregadas permitidas)?

Acabo de definir mi interfaz de repositorio genérica, con Add, Remove, Get, GetDeferred, Count, y Find métodos (Find devuelve una interfaz IQueryable que permite LINQ), crear una implementación genérica concreta, y llamarlo un día. Confío mucho en Find y por lo tanto en LINQ. Si necesito usar un consulta específica más de una vez, uso métodos de extensión y escribo la consulta usando LINQ.

Esto cubre el 95% de mis necesidades de persistencia. Si necesito realizar algún tipo de acción de persistencia que no se puede hacer genéricamente, utilizo una API ICommand casera. Por ejemplo, digamos que estoy trabajando con NHibernate y necesito realizar una consulta compleja como parte de mi dominio, o tal vez necesito hacer un comando masivo. La API se ve más o menos así:

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

Ahora puedo crear una interfaz para representar una orden específica.

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

Puedo crear una implementación concreta y usar SQL crudo, NHibernate HQL, lo que sea, y registrarlo con mi localizador de servicios.

Ahora en mi lógica de negocios puedo hacer algo como esto: {[19]]}

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

var overdueAccounts = query.Execute();

También puede usar un patrón de especificación con IQuery para construir consultas significativas basadas en la entrada del usuario, en lugar de tener una interfaz con millones de propiedades confusas, pero eso asume que no encuentra el patrón de especificación confuso en su propio derecho ;).

Una última pieza del rompecabezas es cuando su repositorio necesita hacer una operación específica antes y después del repositorio. Ahora, puede crear muy fácilmente una implementación de su repositorio genérico para una entidad específica, luego anular los métodos relevantes y hacer lo que necesita hacer, y actualizar su registro de IoC o service locator y terminar con él.

Sin embargo, a veces esta lógica es transversal y difícil de implementar al reemplazar un método de repositorio. Así que creado IRepositoryBehavior, que es básicamente un sumidero de eventos. (A continuación es solo una definición aproximada de la parte superior de mi cabeza)

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

Ahora, estos comportamientos pueden ser cualquier cosa. Auditoría, comprobación de seguridad, eliminación suave, aplicación de restricciones de dominio, validación, etc. Creo un comportamiento, lo registro con el IoC o localizador de servicios, y modifico mi repositorio genérico para tomar una colección de IRepositoryBehaviors registrados, y compruebo cada comportamiento con el tipo de repositorio actual y envuelvo la operación en el controladores pre / post para cada comportamiento aplicable.

Aquí hay un ejemplo de comportamiento de soft-delete (soft-delete significa que cuando alguien pide eliminar una entidad, simplemente la marcamos como eliminada para que no pueda ser devuelta de nuevo, pero nunca se elimine físicamente).

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

Sí, esto es básicamente una implementación simplificada y abstracta de los oyentes de eventos de NHibernate, pero es por eso que me gusta. A) Puedo probar unitariamente un comportamiento sin incluir a NHibernate en la imagen B) Puedo usar estos comportamientos fuera de NHibernate (digamos que el repositorio es una implementación de cliente que envuelve las llamadas al servicio REST) C) los oyentes de eventos de NH pueden ser un verdadero dolor en el culo;)

 34
Author: HackedByChinese,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-07 05:36:36

Recomendaría el número 1, con algunas advertencias. El número 2 es lo que parece ser más común, pero en mi experiencia, el repositorio simplemente termina siendo un terreno de descarga desordenado para las consultas. Si usas un repositorio genérico (2) es solo una envoltura delgada alrededor del DbContext, un poco inútil realmente a menos que estés planeando cambiar el repository (mala idea).

Pero cuando acceso directamente a DbContext prefiero usar un patrón de Tuberías y Filtros para que pueda reutilizar la lógica común, algo como

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

El ByPhoneNumber y By Organisation son solo métodos de extensión.

 12
Author: Craig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-07 03:37:30

Aquí vamos para el Mejor Patrón de Repositorio en Asp.Net MVC:

El patrón de repositorio agrega una capa de separación entre las capas de datos y de dominio de una aplicación. También hace que las partes de acceso a datos de una aplicación sean más probables.

Fábrica de bases de datos (IDatabaseFactory.cs):

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

Implementaciones de Fábrica de Bases de Datos (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();
    }
}

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

Clase Abstracta (Repositorio.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>();
    }
}

Cómo acceder a este patrón de repositorio en el controlador:

1. Tienes el Modelo de Usuario:

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

2. Ahora tienes que crear la Clase Repository de tu 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. Ahora tienes que crear la interfaz UserService (IUserService.cs) con todos los métodos CRUD:

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

4. Ahora tienes que crear la interfaz UserService (UserService.cs) con todos los métodos 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. Ahora está todo configurado para su Patrón de repositorio y puede acceder a todos los datos en el Controlador de Usuario:

//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();
    }
}
 1
Author: Laxman Gite,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-13 14:42:31

Existe una solución lista para usar en URF - Unit of Work & (extensible/generic) Repositories Framework. Te ahorrará mucho tiempo. Implementaron un repositorio genérico (también hay un repositorio asincrónico). Para extender cualquier repositorio han utilizado extensiones como esta:

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

Algunas clases como QueryObject pueden ser un exceso de trabajo dependiendo de su proyecto, pero en general es una buena solución para ayudarlo a ponerse en marcha.

 0
Author: Arvand,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-16 11:00:10