El patrón de repositorio es uno de los elementos principales de la arquitectura que estaremos utilizando en esta serie y por eso comenzamos por ahí.
Este artículo es un poco largo porque hace falta implementar una parte importante de la infraestructura básica. También se incluye en la aplicación de consola lo mínimo necesario para verificar que todo esté funcionando bien, sin meternos todavía en pruebas de integración, las cuales trataremos en el próximo artículo.
Al terminar el artículo deberíamos tener una buena visión general de la arquitectura y conocer algunos detalles de los elementos principales.
También tendremos un esbozo de los beneficios y aportes del enfoque MDA en el desarrollo de aplicaciones, por ejemplo, en este artículo el 60% de las líneas de programa fueron generadas con Domion.
Para desarrollar aplicaciones de tamaño significativo es importante tener una forma de dividir el trabajo, de forma que varios equipos puedan trabajar en paralelo.
El primer paso para esto es la separación por capas, típicamente Modelo, Datos y Servicios y en segundo lugar la separación en áreas funcionales o módulos.
En este artículo vamos a explorar principalmente el primero de estos pasos, en especial con la implementación del patrón de repositorio y también vamos a explorar algo de la separación por áreas funcionales, aunque nuestro primer módulo sólo va a tener por ahora una clase en el modelo de dominio.
Durante el trabajo diario de desarrollo me ha resultado muy conveniente usar una base de datos local con SQL Server Developer Edition y el SQL Server Management Studio, en vez de las herramientas integradas en Visual Studio.
Siento que es mucho más fácil cambiar el contexto de trabajo a otra aplicación con un simple [Ctrl]+[Alt] que tener que buscar el servidor de SQL Server o LocalDb con el explorador de servidores en Visual Studio usando el ratón.
A - Paso a paso - Patrón de repositorio
Importante
El patrón de repositorio nos ofrece una abstracción del DbContext de EF Core, donde podemos agregar procesamiento y validaciones adicionales. También puede facilitar la realización de pruebas sin tener que involucrar al DbContext.
Esta implementación del patrón de repositorio se apoya en la funcionalidad del DbContext de EF Core, que mantiene en memoria una colección de las entidades que han sido modificadas (en el ChangeTracker), antes de enviar los cambios a la base de datos para salvarlos.
A-1 - Repositorio genérico
Esta implementación del patrón de repositorio se realiza con dos interfaces y un repositorio genérico, que luego se complementan con una interfaz y una clase específica identificada como “Manager” y que llamaremos en forma genérica como EntityManager.
Todo el acceso al DbContext se realiza a través del EntityManager específico, para que se pueda adaptar fácilmente a las necesidades particulares de cada situación, ocultando o implementando los métodos necesarios.
Las interfaces están en el proyecto Domion.Core y el repositorio genérico en Domion.Lib.
A-1.1 - IQueryManager - Interfaz genérica para queries
Esta interfaz define lo mínimo que debe implementar un EntityManager, ya que es lo que permite realizar consultas generales.
También es posible, aunque menos frecuente, implementar sólo consultas específicas y en ese caso bastaría con que el EntityManager no implemente la interfaz genérica.
Esta interfaz genérica nos permite implementar Extension Methods de uso común, aplicables a todos los “Managers”, sin necesidad de implementarlos para cada EntityManager.
usingSystem;usingSystem.Linq;usingSystem.Linq.Expressions;namespaceDomion.Core.Services{/// <summary>
/// Query Interface for the EntityManager.
/// </summary>
publicinterfaceIQueryManager<T>whereT:class{/// <summary>
/// Returns an query expression that, when enumerated, will retrieve all objects from the database.
/// </summary>
IQueryable<T>Query();/// <summary>
/// Returns an query expression that, when enumerated, will retrieve all objects from the database that satisfy the where condition.
/// </summary>
IQueryable<T>Query(Expression<Func<T,bool>>where);}}
A-1.2 - IEntityManager - Interfaz genérica para DbContext.Find()
La implementación de esta interfaz nos permite acceder al método Find de los DbContext, si decidimos exponerlo a través del EntityManager.
namespaceDomion.Core.Services{/// <summary>
/// DbContext Find Interface
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TKey">Key value(s)</typeparam>
publicinterfaceIEntityManager<T,TKey>whereT:class{/// <summary>
/// Finds an entity with the given primary key values.
/// If an entity with the given primary key values is being tracked by the context,
/// then it is returned immediately without making a request to the database.
/// Otherwise, a query is made to the database for an entity with the given primary key values and this entity,
/// if found, is attached to the context and returned.
/// If no entity is found, then null is returned. (From de official docs)
/// </summary>
TFind(TKeykey);}}
A-1.3 - BaseRepository - Repositorio genérico
Esta es la implementación base del repositorio genérico, está declarada como una clase abstracta, así que cada EntityManager específico debe heredar de ésta y entonces decidir que métodos cambiar u ocultar o, incluso, eliminando alguna de las interfaces de la declaración.
usingDomion.Core.Services;usingMicrosoft.EntityFrameworkCore;usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel.DataAnnotations;usingSystem.Linq;usingSystem.Linq.Expressions;namespaceDomion.Lib.Data{/// <summary>
/// Generic repository implementation.
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
/// <typeparam name="TKey">Key property type</typeparam>
publicabstractclassBaseRepository<TEntity,TKey>:IQueryManager<TEntity>,IEntityManager<TEntity,TKey>whereTEntity:class{privatereadonlyDbContext_dbContext;privatereadonlyDbSet<TEntity>_dbSet;/// <summary>
/// Creates the generic repository instance.
/// </summary>
/// <param name="dbContext">The DbContext to get the Entity Type from.</param>
publicBaseRepository(DbContextdbContext){_dbContext=dbContext;_dbSet=_dbContext.Set<TEntity>();}protectedvirtualDbContextDbContext=>_dbContext;/// <summary>
/// Detaches the entity from the DbContext's change tracker.
/// </summary>
publicvoidDetach(TEntityentity){DbContext.Entry(entity).State=EntityState.Detached;}/// <summary>
/// Finds an entity with the given primary key values.
/// If an entity with the given primary key values is being tracked by the context,
/// then it is returned immediately without making a request to the database.
/// Otherwise, a query is made to the database for an entity with the given primary key values and this entity,
/// if found, is attached to the context and returned.
/// If no entity is found, then null is returned. (From de official docs)
/// </summary>
publicvirtualTEntityFind(TKeykey){return_dbContext.Find<TEntity>(key);}/// <summary>
/// Returns an entity object with the original values when it was last read from the database.
/// Does not include any navigation properties, not even collections.
/// </summary>
publicvirtualTEntityGetOriginalEntity(TEntityentity){varentry=DbContext.Entry(entity);if(entry==null){returnnull;}returnentry.OriginalValues.ToObject()asTEntity;}/// <summary>
/// Returns an query expression that, when enumerated, will retrieve all objects.
/// </summary>
publicvirtualIQueryable<TEntity>Query(){returnQuery(null);}/// <summary>
/// Returns an query expression that, when enumerated, will retrieve only the objects that satisfy the where condition.
/// </summary>
publicvirtualIQueryable<TEntity>Query(Expression<Func<TEntity,bool>>where){IQueryable<TEntity>query=_dbSet;if(where!=null){query=query.Where(where);}returnquery;}/// <summary>
/// Saves changes from the DbContext's change tracker to the database.
/// </summary>
publicvirtualvoidSaveChanges(){_dbContext.SaveChanges();}/// <summary>
/// Marks an entity for deletion in the DbContext's change tracker if it passes the ValidateDelete method.
/// </summary>
protectedvirtualIEnumerable<ValidationResult>TryDelete(TEntityentity){vardeleteErrors=ValidateDelete(entity);if(deleteErrors.Any()){returndeleteErrors;}_dbSet.Remove(entity);returnEnumerable.Empty<ValidationResult>();}/// <summary>
/// Adds an entity for insertion in the DbContext's change tracker if it passes the ValidateSave method.
/// </summary>
protectedvirtualIEnumerable<ValidationResult>TryInsert(TEntityentity){varsaveErrors=ValidateSave(entity);if(saveErrors.Any()){returnsaveErrors;}_dbSet.Add(entity);returnEnumerable.Empty<ValidationResult>();}/// <summary>
/// Marks an entity for update in the DbContext's change tracker if it passes the ValidateSave method.
/// </summary>
protectedvirtualIEnumerable<ValidationResult>TryUpdate(TEntityentity){varsaveErrors=ValidateSave(entity);if(saveErrors.Any()){returnsaveErrors;}_dbSet.Update(entity);returnEnumerable.Empty<ValidationResult>();}/// <summary>
/// Validates if it's ok to delete the entity from the database.
/// </summary>
protectedvirtualIEnumerable<ValidationResult>ValidateDelete(TEntitymodel){yieldbreak;}/// <summary>
/// Validates if it's ok to save the new or updated entity to the database.
/// </summary>
protectedvirtualIEnumerable<ValidationResult>ValidateSave(TEntitymodel){yieldbreak;}}}
A-1.4 - IQueryManagerExtensions - Extensiones para el IQueryManager
Estos extension methods agregan funcionalidad de uso común con los IQueryable, directamente al IQueryManager, sin necesidad de implementarlos en cada EntityManager.
usingDomion.Core.Services;usingMicrosoft.EntityFrameworkCore;usingMicrosoft.EntityFrameworkCore.Query;usingSystem;usingSystem.Linq;usingSystem.Linq.Expressions;namespaceDomion.Lib.Extensions{/// <summary>
/// Extensions for generic IQueryManager < T >.
/// </summary>
publicstaticclassIQueryManagerExtensions{/// <summary>
/// Returns the first object that satisfies the condition or raises InvalidaOperationException if none.
/// </summary>
publicstaticTFirst<T>(thisIQueryManager<T>manager,Expression<Func<T,bool>>where=null)whereT:class{returnmanager.Query(where).First();}/// <summary>
/// Returns the first object that satisfies the condition or null if none.
/// </summary>
publicstaticTFirstOrDefault<T>(thisIQueryManager<T>manager,Expression<Func<T,bool>>where=null)whereT:class{returnmanager.Query(where).FirstOrDefault<T>();}/// <summary>
/// <para>
/// Especifies the related objects to include in the query result and returns and IIncludableQueryable that allows chaining of other IQueryable methods.
/// </para>
/// <para>
/// For more information and examples visit:
/// https://docs.microsoft.com/en-us/ef/core/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions#Microsoft_EntityFrameworkCore_EntityFrameworkQueryableExtensions_Include__2_System_Linq_IQueryable___0__System_Linq_Expressions_Expression_System_Func___0___1___
/// </para>
/// </summary>
/// <param name="includeExpression">The navigation property to include</param>
publicstaticIIncludableQueryable<T,TProperty>Include<T,TProperty>(thisIQueryManager<T>manager,Expression<Func<T,TProperty>>includeExpression)whereT:class{returnmanager.Query().Include(includeExpression);}/// <summary>
/// Returns the single object that satisfies the condition or raises InvalidaOperationException if none or more than one.
/// </summary>
publicstaticTSingle<T>(thisIQueryManager<T>manager,Expression<Func<T,bool>>where=null)whereT:class{returnmanager.Query(where).Single<T>();}/// <summary>
/// Returns the single object that satisfies the condition or null if none or raises InvalidaOperationException if more than one.
/// </summary>
publicstaticTSingleOrDefault<T>(thisIQueryManager<T>manager,Expression<Func<T,bool>>where=null)whereT:class{returnmanager.Query(where).SingleOrDefault<T>();}}}
A-2 - Extensiones para configuración de los modelos en EF Core
Importante
Las extensiones que se muestran a continuación facilitan la configuración de modelos grandes en EF Core.
Actualmente (EF Core 1.1.2), para configurar los modelos usando el Fluent API, es necesario hacerlo en un override del método OnModelCreation del DbContext, pero esto resulta poco práctico para aplicaciones de cualquier tamaño significativo, ya que el DbContext se puede extender más allá de lo razonable.
Estas clases se encuentran en el proyecto Domion.Lib
A-3 - Incluir referencias y compilar
Incluir las siguientes dependencias en Domion.Lib:
Domion.Core (Referencia)
Microsoft.EntityFrameworkCore - 1.1.2 (Nuget)
Estos son los componentes básicos de la infraestructura y en este momento se debería poder compilar la solución sin errores.
B - Paso a paso - MDA - Componentes básicos de la aplicación
Importante
El enfoque de diseño MDA - Model Driven Architecture permite generar cantidades importantes de código a partir de modelos de alto nivel y esto contribuye tanto con la productividad del equipo de desarrollo como con la calidad y facilidad de mantenimiento de los productos.
Vamos a aclarar que el tamaño de esta aplicación modelo no justifica, por sí mismo, el uso de la estructura de la solución que estamos desarrollando.
Sin embargo, como lo que buscamos es lograr una estructura flexible, que facilite el desarrollo de aplicaciones grandes y la división del trabajo entre varios equipos, entonces nos enfocamos en una aplicación muy sencilla, para poder dedicar el esfuerzo cognitivo en la estructura y no en el contenido.
A continuación, mostramos los modelos desarrollados con este enfoque y, como se puede observar, no son los modelos típicos de UML, con la excepción del modelo de dominio, sino que forman parte del DSL - Domain Specific Language diseñado específicamente para facilitar el desarrollo de aplicaciones con Domion.
B-1.1 - Modelo de Dominio
Este es el “modelo de dominio” de la aplicación por ahora.
Estamos de acuerdo que “Modelo de Dominio” le queda grande a esto, pero ya lo iremos ampliando durante el desarrollo de la serie.
B-1.2 - Modelo de “Datos”
Este no es modelo de base de datos que podríamos esperar, sino más bien un modelo de la capa de datos, que indica que la clase BudgetDbContext, con estereotipo dbcontext, debe tener una propiedad tipo DbSet.
Para esta serie vamos a trabajar con la opción “Code First” de Entity Framework, que se encarga de crear la base de datos a partir de las clases del modelo y las configuraciones, a través de las migraciones, por lo que no tenemos que preocuparnos directamente de la base de datos.
Veremos las migraciones más adelante en este mismo artículo.
B-1.3 - Modelo de “Servicios”
Este modelo indica que la clase BudgetClassManager, con estereotipo entity-manager, depende de las clases BudgetClass con estereotipo entity-model y de BudgetDbContext con estereotipo dbcontext.
Esta es la forma de especificar en Domion que el BudgetClassManager utiliza el BudgetDbContext para gestionar el acceso a los objetos BudgetClass.
B-1.4 - Generación de código
A partir de esos modelos, usando las plantillas de transformación y generación de Domion, generamos los componentes que se muestran a continuación.
Este proceso no se detalla en el artículo, sólo se muestra el resultado final.
¿Qué tanto código se genera a partir de los modelos?
Para este ejemplo, el 60% fue generado por Domion, el 14% por las EF Core Tools (migraciones) y sólo el 26% se produjo a mano.
Para efectos de estas métricas, contamos las líneas no vacías (incluyendo comentarios y documentación) de todos los archivos (*.cs), de la carpeta “samples”, menos los AssemblyInfo.cs, usando la herramienta SourceMonitor.
Aun cuando este número es interesante y, desde luego, este es un ejemplo muy sencillo, lo más importante es saber que ese código inicial se ha producido de una forma estandarizada y uniforme, con todos los beneficios que eso aporta.
B-2 - Componentes en DFlow.Budget.Core
B-2.1 - BudgetClass - Clasificación de conceptos del presupuesto [Generado 100%]
Esta es la clase “principal” (la única por ahora) del modelo de dominio.
Los atributos [Required] y [MaxLength(100)], así como el comentario // Key data —, son el resultado de especificaciones particulares que se hacen en el modelo en Enterprise Architect. El comentario Key data nos indica que los valores de esa propiedad se deben manejar como valores únicos en la base de datos.
Aunque los atributos indicados realmente pertenecen a la capa de datos y no a la capa del modelo de dominio, donde estamos, me parece que es útil tenerlos aquí como referencia al implementar las pantallas.
//------------------------------------------------------------------------------
// TransactionType.cs
//
// Implementation of: TransactionType (Enumeration) <<enumeration>>
// Generated by Domion-MDA - http://www.coderepo.blog/domion
//
// Created on : 02-jun-2017 10:49:11
// Original author: Miguel
//------------------------------------------------------------------------------
namespaceDFlow.Budget.Core.Model{publicenumTransactionType{Income,Expense,Loan,Savings,Investment,Tax}}
B-2.3 - IBudgetClassManager - Interfaz del EntityManager para BudgetClass [Generado 100%]
Esta es la interfaz específica para el BudgetClassManager, está declarada en el proyecto .Core para facilitar su uso desde las clases de dominio si hace falta.
En caso de ser necesario utilizar los managers desde la capa del modelo de dominio, se utilizará el patrón Service Locator con la clase System.Web.Mvc.DependencyResolver, para resolver las implementaciones de los IEntityManager necesarios, a través de un contenedor de inyección de dependencias, configurado convenientemente.
Esta interfaz se puede modificar tanto como sea necesario, por ejemplo, se podría eliminar la referencia a IQueryManager y ocultar en el BudgetClassManager los métodos del repositorio base, para entonces implementar métodos de consulta específicos.
//------------------------------------------------------------------------------
// IBudgetClassManager.cs
//
// Implementation of: IBudgetClassManager (Interface) <<entity-manager>>
// Generated by Domion-MDA - http://www.coderepo.blog/domion
//
// Created on : 02-jun-2017 10:49:09
// Original author: Miguel
//------------------------------------------------------------------------------
usingDFlow.Budget.Core.Model;usingDomion.Core.Services;usingSystem.Collections.Generic;usingSystem.ComponentModel.DataAnnotations;namespaceDFlow.Budget.Core.Services{publicinterfaceIBudgetClassManager:IQueryManager<BudgetClass>,IEntityManager<BudgetClass,int>{/// <summary>
/// <para>
/// Refreshes the entity in the DbContext's change tracker, requerying the database.
/// </para>
/// <para>
/// Important, this only refreshes the passed entity. It does not refresh the related entities
/// (navigation or collection properties). If needed yo have to modify this method and call the
/// method on each one.
/// </para>
/// </summary>
BudgetClassRefresh(BudgetClassentity);/// <summary>
/// Saves changes from the DbContext's change tracker to the database.
/// </summary>
voidSaveChanges();/// <summary>
/// Marks an entity for deletion in the DbContext's change tracker if no errors are found in the ValidateDelete method.
/// </summary>
IEnumerable<ValidationResult>TryDelete(BudgetClassentity);/// <summary>
/// Adds an entity for insertion in the DbContext's change tracker if no errors are found in the ValidateSave method.
/// This method also checks that the concurrency token (RowVersion) is EMPTY.
/// </summary>
IEnumerable<ValidationResult>TryInsert(BudgetClassentity);/// <summary>
/// Marks an entity for update in the DbContext's change tracker if no errors are found in the ValidateSave method.
/// This method also checks that the concurrency token (RowVersion) is NOT EMPTY.
/// </summary>
IEnumerable<ValidationResult>TryUpdate(BudgetClassentity);/// <summary>
/// Calls TryInsert or TryUpdate accordingly, based on the value of the Id property;
/// </summary>
IEnumerable<ValidationResult>TryUpsert(BudgetClassentity);}}
B-3 - Componentes en DFlow.Budget.Lib
B-3.1 - BudgetClassConfiguration - Configuración del modelo para EF Core [Generado 100%]
Esta es la clase de configuración del modelo de datos para BudgetClass. Aquí se pueden apreciar claramente los elementos relacionados con la base de datos.
En esta clase no están los elementos relativos al tamaño de los campos o si son requeridos o no, porque ya están incluidos como atributos en la clase del modelo, como se mostró anteriormente.
Vamos a destacar un elemento importante de la configuración, como uso de un schema de base de datos asociado a cada DbContext, como un modo de separar las áreas funcionales en la base de datos. Esto, además, nos facilitará el desarrollo de aplicaciones grandes, a la hora de distribuir el trabajo entre varios equipos y compartir el acceso a una base de datos desde varios DbContext.
También lo podemos ver como una ventana a la base de datos (con una interfaz de objetos) que nos facilita el acceso a los objetos necesarios.
Importante
Es un error común manejar en un único DbContext todo el modelo de dominio de una aplicación, lo ideal es dividir el modelo en un DbContext por módulo o por área funcional.
En un artículo próximo veremos cómo manejar múltiples DbContext compartiendo la misma base de datos.
//------------------------------------------------------------------------------
// BudgetDbContext.cs
//
// Implementation of: BudgetDbContext (Class) <<dbcontext>>
// Generated by Domion-MDA - http://www.coderepo.blog/domion
//
// Created on : 02-jun-2017 10:49:08
// Original author: Miguel
//------------------------------------------------------------------------------
usingDFlow.Budget.Core.Model;usingDomion.Lib.Data;usingMicrosoft.EntityFrameworkCore;usingNLog;usingSystem;namespaceDFlow.Budget.Lib.Data{publicclassBudgetDbContext:DbContext{privatestaticLoggerlogger=LogManager.GetCurrentClassLogger();publicBudgetDbContext():base(){}publicBudgetDbContext(DbContextOptions<BudgetDbContext>options):base(options){}publicvirtualDbSet<BudgetClass>BudgetClasses{get;set;}publicoverrideintSaveChanges(){try{returnbase.SaveChanges();}catch(Exceptionex){logger.Error(ex);throw;}}///
/// <param name="modelBuilder"></param>
protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){ConfigureLocalModel(modelBuilder);ConfigureExternalModel(modelBuilder);}///
/// <param name="modelBuilder"></param>
privatevoidConfigureExternalModel(ModelBuildermodelBuilder){}///
/// <param name="modelBuilder"></param>
privatevoidConfigureLocalModel(ModelBuildermodelBuilder){// Database schema is "Budget"
modelBuilder.AddConfiguration(newBudgetClassConfiguration());}}}
B-3.3 - BudgetClassManager - EntityManager para las clasificaciones [Generado 100%]
Esta es la implementación del EntityManager para BudgetClass. Es la responsable de gestionar el acceso al DbContext, en especial en cuanto a las validaciones relacionadas con incluir o eliminar objetos en el repositorio, por ejemplo, evitar elementos duplicados, o eliminación de objetos referenciados por otros.
Independientemente de que esas validaciones estén reforzadas a nivel de la base de datos, por ejemplo, usando Foreign Keys o índices únicos, esta clase permite detectar estos casos antes de que se levante una excepción por una validación de la base de datos y mostrar un mensaje controlado al usuario.
Observe cómo está implementada la validación contra nombre duplicados, con los métodos FindDuplicateByName y ValidateSave.
//------------------------------------------------------------------------------
// BudgetClassManager.cs
//
// Implementation of: BudgetClassManager (Class) <<entity-manager>>
// Generated by Domion-MDA - http://www.coderepo.blog/domion
//
// Created on : 02-jun-2017 10:49:07
// Original author: Miguel
//------------------------------------------------------------------------------
usingDFlow.Budget.Core.Model;usingDFlow.Budget.Core.Services;usingDFlow.Budget.Lib.Data;usingDomion.Core.Services;usingDomion.Lib.Data;usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel.DataAnnotations;usingSystem.Linq;usingSystem.Linq.Expressions;namespaceDFlow.Budget.Lib.Services{publicclassBudgetClassManager:BaseRepository<BudgetClass,int>,IQueryManager<BudgetClass>,IEntityManager<BudgetClass,int>,IBudgetClassManager{publicstaticstringduplicateByNameError=@"There's another BudgetClass with Name ""{0}"", can't duplicate! (Id={1})";/// <summary>
/// Entity manager for BudgetClass
/// </summary>
publicBudgetClassManager(BudgetDbContextdbContext):base(dbContext){}/// <summary>
/// Returns another BudgetClass with the same Name if it exists or null if doesn't.
/// </summary>
publicBudgetClassFindDuplicateByName(BudgetClassentity){if(entity.Id==0){returnQuery(bc=>bc.Name==entity.Name.Trim()).SingleOrDefault();}else{returnQuery(bc=>bc.Name==entity.Name.Trim()&&bc.Id!=entity.Id).SingleOrDefault();}}/// <summary>
/// Returns an IQueryable that, when enumerated, will retrieve the objects that satisfy the where condition
/// or all of them if where condition is null.
/// </summary>
publicoverrideIQueryable<BudgetClass>Query(Expression<Func<BudgetClass,bool>>where=null){returnbase.Query(where);}/// <summary>
/// <para>
/// Refreshes the entity in the DbContext's change tracker, requerying the database.
/// </para>
/// <para>
/// Important, this only refreshes the passed entity. It does not refresh the related entities
/// (navigation or collection properties). If needed yo have to modify this method and call the
/// method on each one.
/// </para>
/// </summary>
publicvirtualBudgetClassRefresh(BudgetClassentity){base.Detach(entity);returnFind(entity.Id);}/// <summary>
/// Marks an entity for deletion in the DbContext's change tracker if no errors are found in the ValidateDelete method.
/// </summary>
publicnewvirtualIEnumerable<ValidationResult>TryDelete(BudgetClassentity){returnbase.TryDelete(entity);}/// <summary>
/// Adds an entity for insertion in the DbContext's change tracker if no errors are found in the ValidateSave method.
/// This method also checks that the concurrency token (RowVersion) is EMPTY.
/// </summary>
publicnewvirtualIEnumerable<ValidationResult>TryInsert(BudgetClassentity){if(entity.RowVersion!=null&&entity.RowVersion.Length>0)thrownewInvalidOperationException("RowVersion not empty on Insert");CommonSaveOperations(entity);returnbase.TryInsert(entity);}/// <summary>
/// Marks an entity for update in the DbContext's change tracker if no errors are found in the ValidateSave method.
/// This method also checks that the concurrency token (RowVersion) is NOT EMPTY.
/// </summary>
publicnewvirtualIEnumerable<ValidationResult>TryUpdate(BudgetClassentity){if(entity.RowVersion==null||entity.RowVersion.Length==0)thrownewInvalidOperationException("RowVersion empty on Update");CommonSaveOperations(entity);returnbase.TryUpdate(entity);}/// <summary>
/// Calls TryInsert or TryUpdate accordingly, based on the value of the Id property;
/// </summary>
publicvirtualIEnumerable<ValidationResult>TryUpsert(BudgetClassentity){if(entity.Id==0){returnTryInsert(entity);}else{returnTryUpdate(entity);}}/// <summary>
/// Performs operations that have to be executed both on inserts and updates.
/// </summary>
internalvirtualvoidCommonSaveOperations(BudgetClassentity){TrimStrings(entity);}/// <summary>
/// Returns the validation results for conditions that prevent the entity to be removed.
/// </summary>
protectedoverrideIEnumerable<ValidationResult>ValidateDelete(BudgetClassentity){yieldbreak;}/// <summary>
/// Returns the validation results for conditions that prevent the entity to be added or updated.
/// </summary>
protectedoverrideIEnumerable<ValidationResult>ValidateSave(BudgetClassentity){BudgetClassduplicateByName=FindDuplicateByName(entity);if(duplicateByName!=null){yieldreturnnewValidationResult(string.Format(duplicateByNameError,duplicateByName.Name,duplicateByName.Id),new[]{"Name"});}yieldbreak;}privatevoidTrimStrings(BudgetClassentity){if(entity.Name!=null)entity.Name=entity.Name.Trim();}}}
B-4 - Incluir dependencias y compilar
Incluir las siguientes dependencias en DFlow.Budget.Core:
Domion.Core (Referencia)
Incluir las siguientes dependencias en DFlow.Budget.Core:
NLog - 5.0.0-beta7 (Nuget, con soporte para .NET Core)
Estos son los componentes básicos de la aplicación y en este momento se debería poder compilar la solución sin errores.
C - Paso a paso - Migraciones
Importante
Las migraciones generadas por Entity Framework cuando se trabaja con la modalidad “Code First”, permiten generar y actualizar la base de datos de forma automática y sin necesidad de dedicarle mucho tiempo.
En esta fase vamos a crear la migración inicial, con la que se creará la base de datos al ejecutar la aplicación.
C-1 - Crear proyecto de configuración
Vamos a crear un proyecto para manejar los temas de configuración del módulo, aunque por ahora sólo vamos a incluir lo referente a la configuración de la base de datos.
C-1.1 - Crear el proyecto “samples\DFlow.Budget.Setup”
Crear el proyecto tipo Class Library (.NET Core) en la carpeta “samples”
usingDFlow.Budget.Lib.Data;usingMicrosoft.EntityFrameworkCore;usingSystem;namespaceDFlow.Budget.Setup{publicclassBudgetDbSetupHelper{privatestring_connectionString;privateDbContextOptions<BudgetDbContext>_options;publicBudgetDbSetupHelper(stringconnectionString){_connectionString=connectionString;}/// <summary>
/// Returns the DbContext if the database has been set up.
/// </summary>
/// <returns></returns>
publicBudgetDbContextGetDbContext(){if(_options==null)thrownewInvalidOperationException($"Must run {nameof(BudgetDbSetupHelper)}.{nameof(SetupDatabase)} first!");returnnewBudgetDbContext(_options);}/// <summary>
/// Creates the database and applies pending migrations.
/// </summary>
publicvoidSetupDatabase(){varoptionBuilder=newDbContextOptionsBuilder<BudgetDbContext>();optionBuilder.UseSqlServer(_connectionString);_options=optionBuilder.Options;using(vardbContext=GetDbContext()){dbContext.Database.Migrate();}}}}
C-1.3 - Instalar dependencias
DFlow.Budget.Lib (Referencia)
Microsoft.EntityFrameworkCore - 1.1.2 (Nuget)
C-2 - Instalar “Tooling” de Entity Framework en DFlow.CLI
C-2.1 - Instalar paquete de tooling
El paquete de tooling es el encargado de crear las migraciones y aplicar actualizaciones en las bases de datos en el ambiente de desarrollo.
Para efectos de este artículo trabajaremos con la versión CLI (Command Line Interface) de las herramientas (Microsoft.EntityFrameworkCore.Tools.DotNet), para realizar las operaciones desde la línea de comandos. También se podría instalar la versión de PowerShell (Microsoft.EntityFrameworkCore.Tools) que se ejecuta desde la consola del Package Manager, es sólo un asunto de preferencias personales.
Para habilitar el tooling es necesario instalar el paquete Microsoft.EntityFrameworkCore.Tools.DotNet en DFlow.CLI, pero este es un tipo de paquete “DotNetCliTool”, que no se puede instalar como un NuGet cualquiera.
Entonces, siguiendo lo indicado en la página de la interfaz de comandos .NET EF Core (.NET Core EF CLI), hay que editar el archivo .csproj del proyecto (Solution Explorer, sobre DFlow.CLI: [Botón derecho > Edit DFlow.CLI.csproj]) y agregar las líneas siguientes:
Al salvar el archivo se debe instalar el paquete automáticamente. En caso contrario utilice el comando dotnet restore desde la interfaz de comandos en el proyecto DFlow.CLI.
Para verificar si el tooling quedó instalado correctamente, basta con abrir una ventana de comandos sobre el proyecto DFlow.CLI y ejecutar el comando dotnet ef. Si todo está bien se debe ver la siguiente pantalla.
C-3 - Crear migración inicial
C-3.1 - Ejecutar script de migración
Abrir una ventana de comandos en la carpeta “scripts” de la solución (con [Alt]+[Shift]+[,] sobre el archivo del script, si están instaladas las Productivity Power Tools 2017)
Ejecutar el script add-migration
Indicar los siguientes parámetros:
DFlow.Budget.Lib
BudgetDbContext
Create
Si todo está bien, se debe obtener una pantalla como esta:
Y se debe obtener un archivo como este en la carpeta Migrations de DFlow.Budget.Lib.
Originalmente había pensado incluir el proyecto de pruebas de integración e inyección de dependencias con Autofac, pero como el artículo ya está demasiado largo, sólo vamos a hacer una corrida rápida desde la aplicación de consola en DFlow.CLI.
D-1 - Preparar DFlow.CLI para ejecutar la aplicación
usingDFlow.Budget.Core.Model;usingDFlow.Budget.Lib.Services;usingDFlow.Budget.Setup;usingDomion.Lib.Extensions;usingSystem;usingSystem.Linq;usingSystem.Text;namespaceDFlow.CLI{internalclassProgram{privatestaticBudgetClass[]_dataSet=newBudgetClass[]{newBudgetClass{Name="Income",Order=1,TransactionType=TransactionType.Income},newBudgetClass{Name="Expenses",Order=2,TransactionType=TransactionType.Expense},newBudgetClass{Name="Investments",Order=3,TransactionType=TransactionType.Investment},};privatestaticBudgetDbSetupHelper_dbHelper;privatestaticvoidLoadSeedData(){Console.WriteLine("Seeding data...\n");using(vardbContext=_dbHelper.GetDbContext()){varmanager=newBudgetClassManager(dbContext);foreach(varitemin_dataSet){varentity=manager.SingleOrDefault(bc=>bc.Name.StartsWith(item.Name));if(entity==null){manager.TryInsert(item);}else{vartokens=entity.Name.Split('-');if(tokens.Length==1){entity.Name+=" - 1";}else{entity.Name=tokens[0].Trim()+$" - {int.Parse(tokens[1]) + 1}";}}}manager.SaveChanges();}}privatestaticvoidMain(string[]args){Console.OutputEncoding=Encoding.UTF8;Console.WriteLine("EF Core App\n");SetupDb();LoadSeedData();PrintDb();Console.WriteLine("Press any key to continue...");Console.ReadKey();}privatestaticvoidPrintDb(){using(vardbContext=_dbHelper.GetDbContext()){Console.WriteLine("Printing data...\n");Console.WriteLine("Budget Classes");Console.WriteLine("--------------");intnameLength=_dataSet.Select(c=>c.Name.Length).Max()+5;inttypeLength=_dataSet.Select(c=>c.TransactionType.ToString().Length).Max();foreach(varitemindbContext.BudgetClasses){Console.WriteLine($"| {item.Name.PadRight(nameLength)} | {item.Order} | {item.TransactionType.ToString().PadRight(typeLength)} |");}Console.WriteLine();}}privatestaticvoidSetupDb(){stringconnectionString="Data Source=localhost;Initial Catalog=DFlow.CLI;Integrated Security=SSPI;MultipleActiveResultSets=true";Console.WriteLine($"Setting up database\n ({connectionString})...\n");_dbHelper=newBudgetDbSetupHelper(connectionString);_dbHelper.SetupDatabase();}}}
D-1.2 - Activar DFlow.CLI como Startup project
Sobre el proyecto DFlow.Cli: [Botón derecho > Set as StartUp Project]
D-1.3 - Ejecutar la aplicación
Al ejecutar la aplicación por primera vez debe obtener una pantalla como esta:
Y esta otra al ejecutarla por segunda vez:
Y de esta forma verificamos que la aplicación está funcionando y terminamos el artículo, ¡finalmente!
Resumen
En este artículo exploramos una implementación del patrón de repositorio y la utilizamos para poner a funcionar el backend del primer módulo de la aplicación de presupuesto personal.
También tuvimos una visión general de los resultados de usar el enfoque MDA - Model Driven Architecture y como, gracias al enfoque Code First de Entity Framework, pasamos a tener una base de datos completamente funcional, con muy poco esfuerzo y casi sin tener que pensar en ello.
De hecho, en este ejemplo 60% de las líneas de programa se generaron con Domion y 14% con las EF Core Tools, sólo fue necesario escribir el 26%, sin contar las clases de las librerías base, en la carpeta “src “.
Espero que este artículo le haya resultado útil y le invito a darme su opinión en la sección de comentarios.