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.
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="MyDBEntities" connectionString="data source=(LocalDB)\MSSQLLocalDB;attachdbfilename=C:\TFS\PakkkoTFS\Blog\C#\BuildingEFGRepository\BuildingEFGRepository.DataBase\MyDB.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>
<connectionStrings>
<add name="MyDBEntities" connectionString="data source=(LocalDB)\MSSQLLocalDB;attachdbfilename=C:\YourSolutionPath\BuildingEFGRepository\BuildingEFGRepository.DataBase\MyDB.mdf;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>
Ficheros
Config a cambiar:
- BuildingEFGRepository.DAL.Tests\App.Config
- BuildingEFGRepository.MVC\Web.Config
- BuildingEFGRepository.WPF_DesCon\App.Config
¡¡¡EXCELENTE!!!
ResponderEliminarExcelente material se agradece.. una consulta la clas ViewModelBase nunca la encontre
ResponderEliminarMuy buen post, gracias !!
ResponderEliminar