Entity Framework 6 Code First - ¿Es Buena la Implementación del Repositorio?


Estoy a punto de implementar un diseño de Entity Framework 6 con un repositorio y una unidad de trabajo.

Hay tantos artículos alrededor y no estoy seguro de cuál es el mejor consejo: Por ejemplo, realmente me gusta el patrón implementado aquí: por las razones sugeridas en el artículo aquí

Sin embargo, Tom Dykstra (Senior Programming Writer on Microsoft's Web Platform & Tools Content Team) sugiere que debe hacerse en otro artículo: aquí

Me suscribo a Pluralsight, y se implementa de una manera ligeramente diferente casi cada vez que se se utiliza en un curso por lo que elegir un diseño es difícil.

Algunas personas parecen sugerir que la unidad de trabajo ya está implementada por DbContext como en este post , por lo que no deberíamos tener que implementarla en absoluto.

Me doy cuenta de que este tipo de pregunta se ha hecho antes y esto puede ser subjetivo, pero mi pregunta es directa:

Me gusta el enfoque en el primer artículo (Código Fizzle) y quería saber si es quizás más mantenible y tan fácilmente comprobable como otros ¿se acerca y es seguro seguir adelante?

Cualquier otro punto de vista es más que bienvenido.

Author: davy, 2014-01-25

7 answers

@Chris Hardie es correcto, EF implementa UoW fuera de la caja. Sin embargo, muchas personas pasan por alto el hecho de que EF también implementa un patrón de repositorio genérico fuera de la caja también:

var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();

...y esta es una implementación de repositorio genérico bastante buena que está integrada en la propia herramienta.

¿Por qué tomar la molestia de crear un montón de otras interfaces y propiedades, cuando DbContext le da todo lo que necesita? Si desea abstraer el DbContext detrás del nivel de aplicación interfaces, y desea aplicar la segregación de consultas de comandos, podría hacer algo tan simple como esto:

public interface IReadEntities
{
    IQueryable<TEntity> Query<TEntity>();
}

public interface IWriteEntities : IReadEntities, IUnitOfWork
{
    IQueryable<TEntity> Load<TEntity>();
    void Create<TEntity>(TEntity entity);
    void Update<TEntity>(TEntity entity);
    void Delete<TEntity>(TEntity entity);
}

public interface IUnitOfWork
{
    int SaveChanges();
}

Puede usar estas 3 interfaces para todo el acceso de su entidad, y no tiene que preocuparse por inyectar 3 o más repositorios diferentes en el código de negocio que funciona con 3 o más conjuntos de entidades. Por supuesto, aún usaría IoC para asegurarse de que solo hay 1 instancia DbContext por solicitud web, pero las 3 interfaces están implementadas por la misma clase, lo que lo hace sencillo.

public class MyDbContext : DbContext, IWriteEntities
{
    public IQueryable<TEntity> Query<TEntity>()
    {
        return Set<TEntity>().AsNoTracking(); // detach results from context
    }

    public IQueryable<TEntity> Load<TEntity>()
    {
        return Set<TEntity>();
    }

    public void Create<TEntity>(TEntity entity)
    {
        if (Entry(entity).State == EntityState.Detached)
            Set<TEntity>().Add(entity);
    }

    ...etc
}

Ahora solo necesita inyectar una única interfaz en su dependencia, independientemente de cuántas entidades diferentes necesita trabajar con:

// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW's answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
    private readonly IWriteEntities _entities;

    //Using Dependency Injection 
    public RecipeController(IWriteEntities entities)
    {
        _entities = entities;
    }

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
            .ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _entities.Create(recipe);
        foreach(Tag t in model.Tags) {
            _entities.Create(tag);
        }
        _entities.SaveChanges();
        return RedirectToAction("CreateRecipeSuccess");
    }
}

Una de mis cosas favoritas de este diseño es que minimiza las dependencias de almacenamiento de la entidad en el consumidor . En este ejemplo el RecipeController es el consumidor, pero en una aplicación real el consumidor sería un controlador de comandos. (Para un manejador de consultas, normalmente consumiría IReadEntities solo porque solo quiero devolver datos, no mutar ningún estado.) Pero para este ejemplo, usemos RecipeController como el consumidor para examinar las implicaciones de dependencia:

Digamos que tiene un conjunto de pruebas unitarias escritas para la acción anterior. En cada una de estas pruebas unitarias, se actualiza el Controlador, pasando un simulacro al constructor. Luego, supongamos que su cliente decide permitir que las personas creen un nuevo libro de cocina o lo agreguen a uno existente al crear una nueva receta.

Con un repositorio por entidad o patrón de interfaz repositorio-por-agregado, tendría que inyectar una nueva instancia de repositorio IRepository<Cookbook> en su constructor de controlador (o usando la respuesta de @Chris Hardie, escribir código para adjuntar otro repositorio a la instancia de UoW). Esto inmediatamente haría que todas sus otras pruebas unitarias se rompieran, y tendría que volver a modificar el código de construcción en todas ellas, pasando otra instancia simulada y ampliando su matriz de dependencias. Sin embargo con el antedicho, toda su otra unidad los tests al menos se compilarán. Todo lo que tiene que hacer es escribir pruebas adicionales para cubrir la nueva funcionalidad del libro de cocina.

 45
Author: danludwig,
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
2014-07-10 15:35:50

Lamento (no) decir que el codefizzle, el artículo de Dyksta y las respuestas anteriores son incorrectos. Por el simple hecho de que utilizan las entidades EF como objetos de dominio (negocio), que es un gran WTF.

Actualización : Para una explicación menos técnica (en palabras simples) lea Patrón de repositorio para Dummies

En pocas palabras, cualquier interfaz de repositorio no debe acoplarse a NINGÚN detalle de persistencia (persistence). La interfaz repo solo trata con objetos que tiene sentido para el resto de la aplicación (dominio, tal vez interfaz de usuario como en la presentación). Muchas personas (con MS liderando el paquete, con intención sospecho) cometen el error de creer que pueden reutilizar sus entidades EF o que pueden ser objeto de negocios encima de ellas.

Mientras que puede suceder, es bastante raro. En la práctica, tendrá muchos objetos de dominio 'diseñados' después de las reglas de la base de datos, es decir, un mal modelado. El propósito del repositorio es desacoplar el resto de la aplicación (principalmente el negocio capa) de su forma de persistencia.

¿Cómo desacoplarlo cuando su repositorio trata con entidades EF (detalle de persistencia) o sus métodos devuelven IQueryable, una abstracción filtrada con semántica incorrecta para este propósito (IQueryable le permite construir una consulta, lo que implica que necesita conocer los detalles de persistencia, negando así el propósito y la funcionalidad del repositorio)?

Un objeto domin nunca debe saber sobre persistencia, EF, joins, etc. No debería saber qué motor de base de datos eres usando o si estás usando uno. Lo mismo con el resto de la aplicación, si desea que sea desacoplado de los detalles de persistencia.

La interfaz del repositorio solo sabe lo que sabe la capa superior. Esto significa, que una interfaz de repositorio de dominio genérico se ve así

public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
   void Save(TDomainObject entity);
   TDomainObject Get(Guid id);
   void Delete(Guid id);
 }

La implementación residirá en el DAL y utilizará EF para trabajar con la BD. Sin embargo, la implementación se ve así

public class UsersRepository:IStore<User>
 {
   public UsersRepository(DbContext db) {}


    public void Save(User entity)
    {
       //map entity to one or more ORM entities
       //use EF to save it
    }
           //.. other methods implementation ...

 }

Usted no tiene realmente un repositorio genérico concreto . El único uso de un repositorio genérico concreto es cuando cualquier objeto de dominio se almacena en forma serializada en una tabla tipo clave-valor. No es el caso de un OR.

¿Qué hay de las consultas?

 public interface IQueryUsers
 {
       PagedResult<UserData> GetAll(int skip, int take);
       //or
       PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take); 
 }

El UserData es el modelo de lectura/vista adecuado para el uso del contexto de consulta.

Puede usar directamente EF para realizar consultas en un manejador de consultas si no le importa que su DAL conozca los modelos de vista y, en ese caso, no será necesidad de cualquier repo de consulta.

Conclusión

  • Su objeto de negocio no debería saber acerca de las entidades EF.
  • El repositorio utilizará un repository , pero nunca expone el OR al resto de la aplicación, por lo que la interfaz repo utilizará solo objetos de dominio o modelos de vista (o cualquier otro objeto de contexto de la aplicación que no sea un detalle de persistencia)
  • No le dices al repo cómo hacer su trabajo, es decir, NUNCA uses IQueryable con un repo interfaz
  • Si solo desea usar la base de datos de una manera más fácil/fresca y está tratando con una simple aplicación CRUD donde no necesita (asegúrese de ello) para mantener la separación de preocupaciones, entonces omita el repositorio todos juntos, use directamente EF para todos los datos. La aplicación estará estrechamente acoplada a EF, pero al menos cortarás al intermediario y será a propósito, no por error.

Tenga en cuenta que usar el repositorio de manera incorrecta, invalidará su uso y su la aplicación seguirá estando estrechamente acoplada a la persistencia (persistence).

En caso de que creas que el OR está ahí para almacenar mágicamente tus objetos de dominio, no lo está. El propósito de OR es simular un almacenamiento OOP sobre tablas relacionales. Tiene todo que ver con la persistencia y nada que ver con el dominio, así que no use el persistence outside persistence.

 40
Author: MikeSW,
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
2014-06-05 14:51:20

DbContext de hecho, se construye con la Unidad de patrón de trabajo. Permite que todas sus entidades compartan el mismo contexto mientras trabajamos con ellas. Esta implementación es interna a la DbContext.

Sin embargo, debe tenerse en cuenta que si instancias dos objetos DbContext, ninguno de ellos verá las entidades del otro que están rastreando. Están aislados unos de otros, lo que puede ser problemático.

Cuando compilo una aplicación MVC, quiero asegurarme de que durante el durante la solicitud, todo mi código de acceso a datos funciona a partir de un único DbContext. Para lograr eso, aplico la Unidad de Trabajo como un patrón externo a DbContext.

Aquí está mi unidad de objeto de trabajo de una aplicación de receta de barbacoa que estoy construyendo:

public class UnitOfWork : IUnitOfWork
{
    private BarbecurianContext _context = new BarbecurianContext();
    private IRepository<Recipe> _recipeRepository;
    private IRepository<Category> _categoryRepository;
    private IRepository<Tag> _tagRepository;

    public IRepository<Recipe> RecipeRepository
    {
        get
        {
            if (_recipeRepository == null)
            {
                _recipeRepository = new RecipeRepository(_context);
            }
            return _recipeRepository;
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }
    **SNIP**

Adjunto todos mis repositorios, que están inyectados con el mismo DbContext, a mi Unidad de objeto de Trabajo. Siempre y cuando los repositorios se soliciten desde la Unidad de objeto de trabajo, podemos estar seguros de que todos nuestros códigos de acceso a datos se gestionarán con la misma DbContext - salsa impresionante!

Si tuviera que usar esto en una aplicación MVC, me aseguraría de que la Unidad de Trabajo se use durante toda la solicitud instanciándola en el controlador y usándola durante todas sus acciones:

public class RecipeController : Controller
{
    private IUnitOfWork _unitOfWork;
    private IRepository<Recipe> _recipeService;
    private IRepository<Category> _categoryService;
    private IRepository<Tag> _tagService;

    //Using Dependency Injection 
    public RecipeController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = _unitOfWork.CategoryRepository;
        _recipeRepository = _unitOfWork.RecipeRepository;
        _tagRepository = _unitOfWork.TagRepository;
    }

Ahora en nuestra acción, podemos estar seguros de que todos nuestros códigos de acceso a datos usarán el mismo DbContext:

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _recipeRepository.Create(recipe);
        foreach(Tag t in model.Tags){
             _tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
        }
        _unitOfWork.Save();
 4
Author: Mister Epic,
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
2014-01-25 14:12:39

Buscando en Internet encontré esto http://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/ es un artículo de 2 partes sobre la utilidad del patrón de repositorio por Jon Smith. La segunda parte se centra en una solución. Espero que ayude!

 3
Author: Martin Blaustein,
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
2015-04-08 22:11:58

Repositorio con unidad de implementación de patrón de trabajo es una mala respuesta a su pregunta.

El DbContext de entity framework es implementado por Microsoft de acuerdo con la unidad de patrón de trabajo. Eso significa el contexto.SaveChanges guarda transaccionalmente tus cambios de una sola vez.

El DbSet es también una implementación del patrón del repositorio. No construyas repositorios que solo puedes hacer:

void Add(Customer c)
{
   _context.Customers.Add(c);
}

Crear un método de una sola línea para lo que puede hacer dentro de la servicio de todos modos ???

No hay beneficio y nadie está cambiando EFM a otro E hoy en día...

No necesitas esa libertad...

Chris Hardie está argumentando que podría haber varios objetos de contexto instanciados, pero ya haciendo esto lo haces mal...

Simplemente use una herramienta de IOC que le guste y configure el MyContext por solicitud Http y estará bien.

Tome ninject por ejemplo:

kernel.Bind<ITeststepService>().To<TeststepService>().InRequestScope().WithConstructorArgument("context", c => new ITMSContext());

El servicio que ejecuta la lógica de negocio obtiene contexto inyectado.

Simplemente mantenlo simple estúpido: -)

 2
Author: Pascal,
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
2014-06-18 17:44:34

Debe considerar "objetos de comando / consulta" como una alternativa, puede encontrar un montón de artículos interesantes alrededor de esta área, pero aquí hay uno bueno:

Https://rob.conery.io/2014/03/03/repositories-and-unitofwork-are-not-a-good-idea /

Cuando necesite una transacción sobre varios objetos de la base de datos, utilice un objeto de comando por comando para evitar la complejidad del patrón UOW.

Un objeto de consulta por consulta es probablemente innecesario para la mayoría de los proyectos. En su lugar podría elija comenzar con un objeto 'FooQueries' } con lo que quiero decir que puede comenzar con un patrón de Repositorio para LECTURAS, pero nombrarlo como "Consultas" para ser explícito de que no hace y no debe hacer ningún insert/updates.

Más tarde, podría encontrar que vale la pena dividir objetos de consulta individuales si desea agregar cosas como autorización y registro, podría alimentar un objeto de consulta en una canalización.

 1
Author: Darren,
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-01-12 01:42:06

Siempre uso UoW con el código EF primero. Me parece más eficiente y más fácil de manejar sus contextos, para evitar fugas de memoria y tal. Puedes encontrar un ejemplo de mi solución alternativa en mi github: http://www.github.com/stefchri en el proyecto RADAR.

Si tiene alguna pregunta al respecto, siéntase libre de preguntársela.

 0
Author: Gecko IT,
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
2014-01-25 12:31:59