domingo, 26 de noviembre de 2017

Construyendo un Entity Framewor Generic Repository Desc





El patrón Repository, separa la lógica de devolución de información de la base de datos, de la capa de la lógica de negocio. Cuando optamos por este tipo de patrón, creamos un Repository por cada una de nuestras entidades de negocio, que comúnmente suelen ser cada una de las tablas de nuestra base de datos.









Los Repositorios se suelen caracterizar por contener un número común de métodos para todos ellos como:
  • All
  • GetData
  • Find
  • Add
  • Remove
  • Update
  • Etc.

Esto significa que tenemos que escribir mucho código prácticamente repetido, tanto para la implementación como para sus tests.
La inclusión de Repositorios Genéricos, puede ahorrarnos mucho tiempo y código.

Estas son sus mayores ventajas:
  • Reducción del código.
  • Reducen el nº de tests. (Solo tendrás que testear Custom Repositories).
  • Aumenta la cobertura de código.
  • Baja el tiempo de desarrollo.
  • Mejora el mantenimiento.



Índice

  • Tipos de Repositorios Genéricos
  • El método Set<TEntity> de DbContext
  • Clases de ejemplo
  • Entity Framework Generic Repositories Disconnected
  • Construyendo Entity Framework Generic Repositories Disconnected
    • All / AllAsync
    • Find / FindAsync
    • GetData / GetDataAsync
    • Add / AddAsync
    • Remove / RemoveAsync
    • Update / UpdateAsync
  • Extrayendo la Interface
  • Ejemplo de MVC
  • Ejemplo WPF
  • Extendiendo DisconGeneriRepository<TEntitiy>
  • Projecto de Prueba



Tipos de Repositorios genéricos

Los repositorios genéricos que vamos a tratar en este artículo, están enfocados en la tecnología más moderna de acceso a datos de Microsoft, su ORM Entity Framework. Según el uso que vayamos a hacer de ellos estos se dividen en 2 clases, conectados y desconectados.






























Con el fin de no alargar demasiado este artículo y no hacerlo demasiado pesado, en él vamos a tratar el tipo desconectado, dejando el tipo conectado para futuras entregas.


El método Set<TEntity> de DbContext

Dentro de la construcción de repositorios genéricos para Entity Framework este es uno de los métodos más importantes. Este método nos devuelve la referencia a el DbSet de nuestro tipo genérico incluido dentro del DbContext.

En otras palabras, el método Set<TEntity> nos da acceso a el DbSet de la entidad del tipo desde la clase base DbContext sin conocer el nombre de la propiedad del DbSet ni el tipo específico del DbContext.

Más información aquí.

Trataremos de explicarlo con código.

Tenemos un simple DbContext GeneralEntities con un simple DbSet Customers de Tipo Customer:

public partial class  GeneralEntities :  DbContext
{
    public GeneralEntities() : base("name=GeneralEntities") {  }
 
 
    public  DbSet<Customer>  Customers  { get; set; }
 
 
    /// Mas codigo
 
} 

Podríamos crear un método muy sencillo que accede a nuestro DbSet Customers:

public void Do(GeneralEntities context)
{
    /// Conocemos el tipo expecifico DbContext (GeneralEntities).
    /// El tipo DbSet es estatico.
    /// Conocemos el nombre del  DbSet --> Customers.
            
 
    DbSet<Customer> myDbSet = context.Customers;
}

El caso es extremadamente simple, algo que no tiene mucho sentido, pero que hace hincapié en que conocemos tanto el tipo del DbContext en este caso GeneralEntities y conocemos el nombre del DbSet y podemos acceder como propiedad de la instancia GeneralEntities.

El siguiente caso es un caso de instanciación dinámica del DbSet sin conocer el tipo específico de DbContext:

public void Do(DbContext context)
{
    /// No conocemos el tipo DbContext, solo conocemos su clase base.
    /// El tipo del DbSet es estatico (Customer)
    /// No conocemos el nombre del DbSet --> Customers.
 
    DbSet<Customer> myDbSet = context.Set<Customer>();
}
 
public void Do<TEntity>(DbContext context) where TEntity : class
{
    /// No conocemos el tipo DbContext, solo conocemos su clase base.
    /// El tipo del DbSet es generico (TEntity)
    /// No conocemos el nombre del DbSet --> Customers.
 
    DbSet<TEntity> myDbSet = context.Set<TEntity>();
}

Los parámetros de los métodos son de tipo DbContext (clase base) y no tienen acceso directo a la propiedad Customers de tipo DbSet<Customer>.


Gráfico de comparación:
























Ejemplo de clases

Estas son las clases de Ejemplo:

public partial class MyDBEntities : DbContext
{
    public MyDBEntities()
        : base("name=MyDBEntities")
    {
    }
 
    public virtual DbSet<City> Cities { get; set; }
    public virtual DbSet<FootballClub> FootballClubs { get; set; }
 
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<City>()
            .Property(e => e.Name)
            .IsUnicode(false);
 
        modelBuilder.Entity<FootballClub>()
            .Property(e => e.Name)
            .IsUnicode(false);
 
        modelBuilder.Entity<FootballClub>()
            .Property(e => e.Members)
            .HasPrecision(18, 0);
    }
}

public partial class City
{
    public int Id { get; set; }
 
    [Required]
    [StringLength(50)]
    public string Name { get; set; }
 
    [Column(TypeName = "numeric")]
    public decimal? People { get; set; }
 
    [Column(TypeName = "numeric")]
    public decimal? Surface { get; set; }
 
    public ICollection<FootballClub> FootballClubs { get; set; }
}

public partial class FootballClub
{
    public int Id { get; set; }
 
    public int CityId { get; set; }
 
    [Required]
    [StringLength(50)]
    public string Name { get; set; }
 
    [Column(TypeName = "numeric")]
    public decimal Members { get; set; }
 
    [Required]
    [StringLength(50)]
    public string Stadium { get; set; }
 
    [Column(TypeName = "date")]
    public DateTime? FundationDate { get; set; }
 
    public string Logo { get; set; }
}



Entity Framework Generic Repositories Disconnected

El Repositorio Genérico Desconectado de Entity Framework, es usado en procesos o aplicaciones sin estado tales como Asp.Net MVC, WebAPI, WPF/Forms. Éstas tienen un enfoque desconectado, son procesos batch, etc.

Estos repositorios hacen los cambios de 1 en 1 y normalmente trabajan con popups o pantallas de edición.

Sus principales características son:
  • Deben de recibir inyectado en el contructor un creador de contextos de tipo Func<DbContext> para poder crear un contexto con cada una de la ejecución de sus métodos.
  • No necesita almacenar una propiedad de tipo DbContext ni implementar la interfaz IDisposable, ya que crea y destruye los contextos por cada una de las ejecuciones de sus métodos.
  • No tiene necesidad de contener una propiedad de tipo ObservableCollection<TEntity>, ya que interactúa directamente con el DbSet.
  • No tiene método Save o SaveChanges, ya que en cada llamada realiza los cambios pertinentes en base de datos.
  • En caso de tener muchos clientes abiertos, no sobresatura la base de datos, ya que solo consume conexión en el momento de realizar un cambio, y por un espacio de tiempo ínfimo.



Construyendo un Entity Framework Generic Repositories Disconnected

El primer paso será crear la clase:

public class DisconGenericRepository<TEntity> where TEntity : class
{
    protected readonly Func<DbContext> _dbContextCreator;
 
    public DesconGenericRepository(Func<DbContext> dbContextCreator)
    {
        if (dbContextCreator == null) throw new ArgumentNullException(nameof(dbContextCreator), $"The parameter dbContextCreator can not be null");
 
        _dbContextCreator = dbContextCreator;
    }
}


La clase, como comentamos en las características, tiene un constructor con un parámetro de tipo Func<DbContext> inyectado por dependencia con su correspondiente campo de solo lectura.

La clase contiene una restricción genérica para tipos por referencia.

Vamos a ver sus métodos:


All / AllAsync

El método All/AllAsync devuelve todos los datos de la tabla.

public IEnumerable<TEntity> All()
{
    var result = Enumerable.Empty<TEntity>();
 
    using(var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        result = dbSet.ToList();
    }
 
    return result;
}
 
public Task<IEnumerable<TEntity>> AllAsync()
{
    return Task.Run(() =>
    {
        return All();
    });
}


Como podemos observar, en la sentencia using creamos una instancia de DbContext, mediante el nuestro campo de tipo Func<DbContext>. Esto será una constante en todos nuestros métodos dentro del Generic Repository Disconnected. El siguiente paso será recuperar la instancia del DbSet y llamar al método de LinQ To Entities, ToList(), para que realice la consulta y genere los datos de resultado.

En uso:

[TestMethod]
public void All_OK()
{
    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    IEnumerable<FootballClub> result = instance.All();
 
    Assert.IsNotNull(result);
    Assert.IsTrue(result.Count() > 0);
}



Find / FindAsync

El método Find/FindAsync, es muy similar al All/AllAsync, con la diferencia de que el método Find/FindAsync siempre realizará la búsqueda por la PK de la tabla, con lo que siempre retornará un único registro.


public TEntity Find(params object[] pks)
{
    if (pks == null) throw new ArgumentNullException(nameof(pks), $"The parameter pks can not be null");
 
    TEntity result = null;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        result = dbSet.Find(pks);
    }
 
    return result;
}
 
public Task<TEntity> FindAsync(params object[] pks)
{
    return Task.Run(() =>
    {
        return Find(pks);
    });
}

El parámetro PK, es de tipo params, ya que puede aceptar varios objetos separados por comas para PKs compuestas.

En uso para una PK simple:

[TestMethod]
public void Find_OK2()
{

    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    FootballClub result = instance.Find(1);
 
    Assert.AreEqual(result.Id, 1);
}

En uso para una PK compuesta.

Definición de tabla:
















[TestMethod]
public void Find_OK2()
{
    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);
 
    string   propertyPk1 = "pk1";
    int      propertyPk2 = 15;
    DateTime propertyPk3 = DateTime.Today;
 
    FootballClub result = instance.Find(propertyPk1, propertyPk2, propertyPk3);
 
    Assert.AreEqual(result.Id, 1);
}



GetData / GetDataAsync

Como con Find/FindAsync, el método GetData/GetDataAsync es muy similar a All/AllAsync a diferencia de que GetData/GetDataAsync contiene un parámetro de tipo Expression<Func<TEntity, bool>> para filtrar los datos a consultar.

All/AllAsync unlike, GetData has an Expression<Func<TEntity,bool>> parameter for filter the query.

public IEnumerable<TEntity> GetData(Expression<Func<TEntity, bool>> filter)
{
    if (filter == null) throw new ArgumentNullException(nameof(filter), $"The parameter filter can not be null");
 
    var result = Enumerable.Empty<TEntity>();
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        result = dbSet.Where(filter).ToList();
    }
 
    return result;
}
 
public Task<IEnumerable<TEntity>> GetDataAsync(Expression<Func<TEntity, bool>> filter)
{
    return Task.Run(() =>
    {
        return GetData(filter);
    });
}


En uso:

[TestMethod]
public void GetData_OK()
{
    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    Expression<Func<FootballClub, bool>> filter = a => a.Name == "Real Madrid C. F.";
 
    IEnumerable<FootballClub> result = instance.GetData(filter);
 
    Assert.IsNotNull(result);
    Assert.IsTrue(result.Count() == 1);
}



Add / AddAsync

Add/AddAsync como la traducción de su propio nombre sugiere, realiza inserciones en la tabla.

public int Add(TEntity newEntity)
{
      if (newEntity == null) throw new ArgumentNullException(nameof(newEntity), $"The parameter newEntity can not be null");

    var result = 0;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        dbSet.Add(newEntity);
 
        result = context.SaveChanges();
    }
 
    return result;
}
 
public Task<int> AddAsync(TEntity newEntity)
{
    return Task.Run(() =>
    {
        return Add(newEntity);
    });
}
 
public int Add(IEnumerable<TEntity> newEntities)
{
if (newEntities == null) throw new ArgumentNullException(nameof(newEntities), $"The parameter newEntities can not be null");

    var result = 0;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        dbSet.AddRange(newEntities);
 
        result = context.SaveChanges();
    }
 
    return result;
}
 
public Task<int> AddAsync(IEnumerable<TEntity> newEntities)
{
    return Task.Run(() =>
    {
        return Add(newEntities);
    });
}


Posee dos sobrecargas, una para insertar una sencilla entidad y otra para insertar un grupo de entidades en la base de datos.

En uso:

[TestMethod]
public void Add_SimpleItem_OK()
{
    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    FootballClub newEntity = new FootballClub
    {
        IdCity        = 1,
        Name          = "New Team",
        Members       = 0,
        Stadium       = "New Stadium",
        FundationDate = DateTime.Today
    };
 
    int result = instance.Add(newEntity);
    int expected = 1;
 
    Assert.AreEqual(expected, result);
}
 
[TestMethod]
public void Add_MultiItems_OK()
{
    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    IEnumerable<FootballClub> newEntities = new List<FootballClub>
    {
        new FootballClub
        {
            IdCity        = 1,
            Name          = "New Team",
            Members       = 0,
            Stadium       = "New Stadium",
            FundationDate = DateTime.Today
        },
            new FootballClub
            {
                IdCity        = 1,
                Name          = "New Team 2",
                Members       = 0,
                Stadium       = "New Stadium 2",
                FundationDate = DateTime.Today
            }
    };
 
    int result = instance.Add(newEntities);
    int expected = 2;
 
    Assert.AreEqual(expected, result);
}



Remove / RemoveAsync

Estos métodos poseen más sobrecargas y está divididos en dos grupos:
  • Remove por Entity.
  • Remove por PKs.


/// Por Object (TEntity)

public int Remove(TEntity removeEntity)
{
    if (removeEntity == null) throw new ArgumentNullException(nameof(removeEntity), $"The parameter removeEntity can not be null");
 
    var result = 0;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        dbSet.Attach(removeEntity);
 
        context.Entry(removeEntity).State = EntityState.Deleted;
 
        result = context.SaveChanges();
    }
 
    return result;
}
 
public Task<int> RemoveAsync(TEntity removeEntity)
{
    return Task.Run(() =>
    {
        return Remove(removeEntity);
    });
}
 
public int Remove(IEnumerable<TEntity> removeEntities)
{
    if (removeEntities == null) throw new ArgumentNullException(nameof(removeEntities), $"The parameter removeEntities can not be null");
 
    var result = 0;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        foreach (var removeEntity in removeEntities)
        {
            dbSet.Attach(removeEntity);
 
            context.Entry(removeEntity).State = EntityState.Deleted;
        }
 
        dbSet.RemoveRange(removeEntities);
 
        result = context.SaveChanges();
    }
 
    return result;
}
 
public Task<int> RemoveAsync(IEnumerable<TEntity> removeEntities)
{
    return Task.Run(() =>
    {
        return Remove(removeEntities);
    });
}

/// Por PKs

public int Remove(params object[] pks)
{
    if (pks == null) throw new ArgumentNullException(nameof(pks), $"The parameter removeEntity can not be null");
 
    var result = 0;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        var entity = Find(pks);
 
        dbSet.Attach(entity);
 
        context.Entry(entity).State = EntityState.Deleted;
 
        result = context.SaveChanges();
    }
 
    return result;
}
 
public Task<int> RemoveAsync(params object[] pks)
{
    return Task.Run(() =>
    {
        return Remove(pks);
    });
}



Para los métodos de borrado, hemos empleado 2 importantes métodos de Entity Framework:
  • DbSet.Attach .- Este método de la clase DbSet, añade una entidad dentro de la propiedad del DbSet, para que puedan empezar a auditarse sus cambios. Esto es necesario, porque si llamáramos directamente al método DbSet.Remove, lanzaría una excepción, ya que no encontraría la entidad a borrar.
  • DbContext.Entry(obj).State .- Consulta la propiedad ChangeTracker DbContext y modifica su estado a deleted.


En uso:


[TestMethod]
public void Remove_SimpleItem_forEntity_OK()
{
    /// cambiar pk para tests
 
    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    var removeEntity = instance.Find(99);
 
    int result = instance.Remove(removeEntity);
    int expected = 0;
 
    Assert.AreEqual(expected, result);
}

[TestMethod]
public void Remove_MultiItems_forEntity_OK()
{
    /// cambiar pk para tests

    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    IEnumerable<FootballClub> removeEntities = new List<FootballClub>
    {
        new FootballClub
        {
            Id            = 9999,
            CityId        = 1,
            Name          = "New Team",
            Members       = 0,
            Stadium       = "New Stadium",
            FundationDate = DateTime.Today
        },
            new FootballClub
            {
                Id            = 100,
                CityId        = 1,
                Name          = "New Team 2",
                Members       = 0,
                Stadium       = "New Stadium 2",
                FundationDate = DateTime.Today
            }
    };
 
    int result = instance.Remove(removeEntities);
    int expected = 0;
 
    Assert.AreEqual(expected, result);
}

[TestMethod]
public void Remove_SimpleItem_forPK_OK()
{
    /// cambiar pk para tests

    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);

    int result = instance.Remove(pks: 9999);
    int expected = 0;
 
    Assert.AreEqual(expected, result);
}




Update / UpdateAsync

Actualiza los valores en la base de datos, Es muy similar al método Remove, pero un poco más simple porque no tiene que actualizar por PKs o por colecciones.

public int Update(TEntity updateEntity)
{
    if (updateEntity == null) throw new ArgumentNullException(nameof(updateEntity), $"The parameter updateEntity can not be null");
 
    var result = 0;
 
    using (var context = _dbContextCreator())
    {
        var dbSet = context.Set<TEntity>();
 
        dbSet.Attach(updateEntity);
 
        context.Entry(updateEntity).State = EntityState.Modified;
 
        result = context.SaveChanges();
    }
 
    return result;
}
 
public Task<int> UpdateAsync(TEntity updateEntity)
{
    return Task.Run(() =>
    {
        return Update(updateEntity);
    });
}

En uso:

[TestMethod]
public void Update_OK()
{
    /// changed values for tests

    Func<DbContext> contextCreator = () => new MyDBEntities() as DbContext;
 
    instance = new DisconGenericRepository<FootballClub>(dbContextCreator: contextCreator);
 
    FootballClub updateEntity = new FootballClub
    {
        Id            = 9999,
        CityId        = 1,
        Name          = "New Team 3",
        Members       = 10,
        Stadium       = "New Stadium 3",
        FundationDate = DateTime.Today
    };
 
    int result = instance.Update(updateEntity);
    int expected = 0;
 
    Assert.AreEqual(expected, result);
}




Extrayendo la Interface

Una vez que tenemos realizados todos los métodos, procederemos a extraer la interfaz, para tener una parte visible común y reutilizable.










Resultado:

public interface IDisconGenericRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> All();
    Task<IEnumerable<TEntity>> AllAsync();
    TEntity Find(params object[] pks);
    Task<TEntity> FindAsync(params object[] pks);
    IEnumerable<TEntity> GetData(Expression<Func<TEntity, bool>> filter);
    Task<IEnumerable<TEntity>> GetDataAsync(Expression<Func<TEntity, bool>> filter);
    int Add(TEntity newEntity);
    Task<int> AddAsync(TEntity newEntity);
    int Add(IEnumerable<TEntity> newEntities);
    Task<int> AddAsync(IEnumerable<TEntity> newEntities);
    int Remove(TEntity removeEntity);
    Task<int> RemoveAsync(TEntity removeEntity);
    int Remove(IEnumerable<TEntity> removeEntities);
    Task<int> RemoveAsync(IEnumerable<TEntity> removeEntities);
    int Remove(params object[] pks);
    Task<int> RemoveAsync(params object[] pks);
    int Update(TEntity updateEntity);
    Task<int> UpdateAsync(TEntity updateEntity);
}



Ejemplo MVC

Vamos a intentar de usar nuestro repositorio genérico dentro de una ‘aplicación real’, en este caso una aplicación web Asp.Net MVC.

Añadimos un proyecto web MVC a nuestra solución:




Instalaremos Autofac.MVC para realizar las inyecciones de dependencias.

 Para quien necesite más información sobre Autofac.MVC, dejamos este link.




























Para realizer las inyecciones de dependencias realizaremos una serie de cambios dentro de la clase Globalasax.cs. Crearemos el método RegisterAutofac() y lo llamaremos en la primera línea de su método principal Application_Start().

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        /// Add call
        RegisterAutofac();
 
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
 
 
    private void RegisterAutofac()
    {
        var builder = new ContainerBuilder();
        builder.RegisterControllers(Assembly.GetExecutingAssembly());
        builder.RegisterSource(new ViewRegistrationSource());
 
        // manual registration of types;
        IDisconGenericRepository<FootballClub> footbalRepository = new DisconGenericRepository<FootballClub>(() => new MyDBEntities());
        builder.Register<IDisconGenericRepository<FootballClub>>(a => footbalRepository);
 
 
        var container = builder.Build();
 
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}



Añadiremos un nuevo Controlador: FootballClubsController, y generaremos todas las acciones con sus vistas.

Vamos a ver el resultado de la clase Controller:

public class FootballClubsController : Controller
{
    private readonly IDisconGenericRepository<FootballClub> _repository;
 
 
    public FootballClubsController(IDisconGenericRepository<FootballClub> repository)
    {
        _repository = repository;
    }
 }


Esta es la inyección de dependencia para el controlador.

Vamos con las acciones de base de datos:

// GET: FootballClubs
public ActionResult Index()
{
    var model = _repository.All();
 
    return View(model);
}

Para la acción Index, emplearemos el método All de nuestro repositorio.

// GET: FootballClubs/Details/5
public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    FootballClub footballClub = _repository.Find(id);
    if (footballClub == null)
    {
        return HttpNotFound();
    }
    return View(footballClub);
}

Para la acción de Details, emplearemos el método Find de nuestro repositorio, para seleccionar el registro por Id.

// POST: FootballClubs/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,CityId,Name,Members,Stadium,FundationDate,Logo")] FootballClub footballClub)
{
    if (ModelState.IsValid)
    {
        _repository.Add(footballClub);
        return RedirectToAction("Index");
    }
 
    return View(footballClub);


Para la acción de Create, emplearemos el método Add de nuestro repositorio, con el fin de insertar el registro dentro de la base de datos.

// POST: FootballClubs/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,CityId,Name,Members,Stadium,FundationDate,Logo")] FootballClub footballClub)
{
    if (ModelState.IsValid)
    {
        _repository.Update(footballClub);
 
        return RedirectToAction("Index");
    }
    return View(footballClub);
}


Para la acción post Edit, emplearemos el método Update de nuestro repositorio, para actualizar el registro en base de datos.

// POST: FootballClubs/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
    _repository.Remove(id);
 
    return RedirectToAction("Index");
}

Para la acción post DeletedConfirmed, emplearemos el método Remove de nuestro repositorio y borraremos el registro en base de datos por Id. Recuerda que nuestro repositorio también tenía la opción de borrar por un objeto completo, mediante una sobrecarga.

Para más información de la clase FootballClubController, mirar en el proyecto de tests.
















































Ejemplo WPF

Aunque WPF soporta aplicaciones con estado (conectadas), también podemos utilizar tecnología desconectada, con el fin de liberar recursos y hacerlas un poquito menos pesadas, aunque algo más trabajosas.

Recordar que la aplicación solo conecta con base de datos en el momento de realizar alguna acción, y que no mantiene N conexiones durante su ciclo de vida.

Para realizar la aplicación de ejemplo, hemos utilizado el patrón MVVM y nos hemos ayudado para ello de la fantástica librería MVVM Light y la no menos increíble Mahapps para mejorar el entorno gráfico. Aquí os dejo enlaces con más información para cada una de ellas:
























Vamos ir mostrando las clases, en este caso ViewModels, donde se realiza uso de los Repositorios Genéricos, que ya que ese es el objetivo de este artículo.

InsertViewModel:

public class InsertViewModel : ViewModelBase
{
 
    private FootballClub _model;
    public FootballClub Model
    {
        get { return _model; }
        set { Set(nameof(Model), ref _model, value); }
    }    
 
    private readonly IDisconGenericRepository<FootballClub> _repository;
 
 
    public InsertViewModel(FootballClub model, IDisconGenericRepository<FootballClub> repository)
    {
        Model = model;
        _repository = repository;
    }
 
 
    public RelayCommand InsertCommand => new RelayCommand(InsertExecute);
    private void InsertExecute()
    {
        _repository.Add(Model);

        Messenger.Default.Send(new NotificationMessage("Inserted"));
    }
     
     // ... mas codigo

}


Llamamos al método Add dentro del método InsertExecute del RelayCommand InsertCommand.


EditViewModel:

public class EditViewModel : ViewModelBase
{
 
    private FootballClub _model;
    public FootballClub Model
    {
        get { return _model; }
        set { Set(nameof(Model), ref _model, value); }
    }
 

    private readonly IDisconGenericRepository<FootballClub> _repository;
 
 
    public EditViewModel(FootballClub model, IDisconGenericRepository<FootballClub> repository)
    {
        Model = model;
        _repository = repository;
    }
 
 
    public RelayCommand AceptChangesCommand => new RelayCommand(AceptChangesExecute);
    private void AceptChangesExecute()
    {
        _repository.Update(Model);

        Messenger.Default.Send(new NotificationMessage("Updated"));
    }
 
    public RelayCommand CancelCommand => new RelayCommand(CancelExecute);
    private void CancelExecute()
    {
        Messenger.Default.Send(new NotificationMessage("Cancel"));
    }
 
    // ... mas codigo
 
}


Llamaremos al método Update del repositorio dentro del método UpdateExecute del RelayCommand UpdateCommand.


MainViewModel:

public class MainViewModel : ViewModelBase
{
    private readonly IDisconGenericRepository<FootballClub> _repository;
 
 
    public ObservableCollection<FootballClub> Data { get; set; }
 
 
    private FootballClub _selectedItem;
    public FootballClub SelectedItem
    {
        get { return _selectedItem; }
        set { Set(nameof(SelectedItem), ref _selectedItem, value); }
    }
 
 
 
 
    public MainViewModel(IDisconGenericRepository<FootballClub> repository)
    {
        _repository = repository;
 
        Data = new ObservableCollection<FootballClub>(_repository.All());
    }
 

     // ... mas codigo 


    public RelayCommand DeleteCommand => new RelayCommand(DeleteExecute, () => SelectedItem != null);
 
    private void DeleteExecute()
    {
        _repository.Remove(SelectedItem);
 
        Data.Remove(SelectedItem);
    }
 
    // ... mas codigo
}


Dentro de este ViewModel realizaremos las acciones de borrado, por lo que llamaremos al método Remove de nuestro repositorio dentro del método DeleteExecute del RelayCommand DeleteCommand.


















Extendiendo DisconGenericRepository<TEntity>

La clase DisconGenericRepository tiene unos pocos métodos interesantes, pero podríamos necesitar expandir su funcionalidad con unos cuantos nuevos métodos.

La mejor forma de hacerlo sería heredando de la clase DisconGenericRepository.

public class FootballClubRepository : DisconGenericRepository<FootballClub>, IFootballClubRepository
{
    public FootballClubRepository(Func<DbContext> dbContextCreator) : base(dbContextCreator) { }
 
 
 
    public int UpdateRangeLow(IEnumerable<FootballClub> entities)
    {
        int result = 0;
 
        foreach (var entity in entities)
        {
            /// Es mas lento, porque crea una conexion por cada iteracion
            /// hemos usado este caso por razones didacticas
            result += base.Update(entity);
        }
 
        return result;
    }
 
    public int UpdateRangeFast(IEnumerable<FootballClub> entities)
    {
        int result = 0;
 
        using(var context = base._dbContextCreator())
        {
            entities.ToList().ForEach(e => UpdateEntity(e, context));
 
            result = context.SaveChanges();
        }
 
        return result;
    }
 
 
 
    private void UpdateEntity(FootballClub entity, DbContext context)
    {
        var dbSet = context.Set<FootballClub>();
 
        dbSet.Attach(entity);
 
        context.Entry(entity).State = EntityState.Modified;
    }
 
}


No es muy común hacer actualizaciones en bloque de esta forma, podría ser de utilidad.

Por razones didácticas, hemos insertado dos métodos de actualización. El primero es muy lento y no es recomendable de hacer, pero nos da mucha utilidad para demostrar el uso de los métodos de la clase base. El segundo método tiene mejor rendimiento y ejecuta las sentencias de update dentro del mismo contexto de la base de datos. Existe un tercer método Update privado, que existe solo por causas de refactorización.



Projecto de pruebas

El proyecto está disponible para descargar desde github en el siguiente link.

El proyecto de pruebas contiene 5 projectos:










  • BuildingEFGRepository.DAL .- Contiene la lógica de los repositories genéricos.
  • BuildingEFGRepository.DataBase .- Contiene las clases de Entity Framework, las clases POCO de base de datos y los repositorios personalizados.
  • BuildingEFGRepository.DataBase.Tests .- Contienen los tests de BuildingEFGRepository.DataBase.
  • BuildingEFGRepository.MVC .- Contiene la aplicación web ASP.Net MVC.
  • BuildingEFGRepository.WPF_DesCon .- Contiene la aplicación WPF.



Necesitamos cambiar 3 connection string para que el proyecto funcione correctamente.

Cambiaremos el path que aparece en el connectionstring por el path de nuestra máquina donde hemos descargado el proyecto.

Example:

C:\TFS\PakkkoTFS\Blog\C#\BuildingEFGRepository\BuildingEFGRepository.DataBase\MyDb.mdf

por

C:\YourSolutionPath\BuildingEFGRepository\BuildingEFGRepository.DataBase\MyDb.mdf


  <connectionStrings>
    <add name="MyDBEntitiesconnectionString="data source=(LocalDB)\MSSQLLocalDB;attachdbfilename=C:\TFS\PakkkoTFS\Blog\C#\BuildingEFGRepository\BuildingEFGRepository.DataBase\MyDB.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFrameworkproviderName="System.Data.SqlClient" />

  </connectionStrings>

  <connectionStrings>
    <add name="MyDBEntitiesconnectionString="data source=(LocalDB)\MSSQLLocalDB;attachdbfilename=C:\YourSolutionPath\BuildingEFGRepository\BuildingEFGRepository.DataBase\MyDB.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFrameworkproviderName="System.Data.SqlClient" />


  </connectionStrings>

Ficheros Config a cambiar:
  1. BuildingEFGRepository.DAL.Tests\App.Config
  2. BuildingEFGRepository.MVC\Web.Config
  3. BuildingEFGRepository.WPF_DesCon\App.Config





3 comentarios :