Este es el segundo artículo de la serie Domion - Un sistema para desarrollar aplicaciones en .NET Core. En el artículo anterior creamos la solución para definir la estructura general, con los proyectos principales, aunque sin programas.

En esta oportunidad vamos a implementar el patrón de repositorio con Entity Framework Core (EF Core) en las librerías, para aplicarlo en el backend del primer módulo de la aplicación.

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.

Los puntos más importantes que cubriremos son:

Puntos Importantes
  1. Implementación del patrón de repositorio

  2. Facilidades para configurar modelos en Entity Framework Core

  3. Aplicación del proceso MDA - Model Driven Architecture

  4. Migraciones con Entity Framework Core “Code First”

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.

En el artículo siguiente trabajaremos con:

  1. Pruebas de integración con xUnit y FluentAssertions

Programas fuente

Artículo: Domion.Net-2.0.zip (release 2.0 del repositorio)
Repositorio: https://github.com/mvelosop/Domion.Net

Contexto

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.

Herramientas y plataforma

Paquetes NuGet utilizados

  • Microsoft.EntityFrameworkCore - 1.1.2
  • Microsoft.EntityFrameworkCore.Design - 1.1.2
  • Microsoft.EntityFrameworkCore.SqlServer - 1.1.2
  • NLog - 5.0.0-beta07
  • System.ComponentModel.Annotations - 4.3.0

Recomendación general

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.

IQueryManager.cs
[Domion.Net-2.0] src\Domion.Core\Services\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Linq;
using System.Linq.Expressions;

namespace Domion.Core.Services
{
    /// <summary>
    ///     Query Interface for the EntityManager.
    /// </summary>
    public interface IQueryManager<T> where T : 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.

IEntityManager.cs
[Domion.Net-2.0] src\Domion.Core\Services\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
namespace Domion.Core.Services
{
    /// <summary>
    ///     DbContext Find Interface
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TKey">Key value(s)</typeparam>
    public interface IEntityManager<T, TKey> where T : 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>
        T Find(TKey key);
    }
}

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.

BaseRepository.cs
[Domion.Net-2.0] src\Domion.Lib\Data\
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
using Domion.Core.Services;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;

namespace Domion.Lib.Data
{
    /// <summary>
    ///     Generic repository implementation.
    /// </summary>
    /// <typeparam name="TEntity">Entity type</typeparam>
    /// <typeparam name="TKey">Key property type</typeparam>
    public abstract class BaseRepository<TEntity, TKey> : IQueryManager<TEntity>, IEntityManager<TEntity, TKey> where TEntity : class
    {
        private readonly DbContext _dbContext;
        private readonly DbSet<TEntity> _dbSet;

        /// <summary>
        ///     Creates the generic repository instance.
        /// </summary>
        /// <param name="dbContext">The DbContext to get the Entity Type from.</param>
        public BaseRepository(DbContext dbContext)
        {
            _dbContext = dbContext;
            _dbSet = _dbContext.Set<TEntity>();
        }

        protected virtual DbContext DbContext => _dbContext;

        /// <summary>
        ///     Detaches the entity from the DbContext's change tracker.
        /// </summary>
        public void Detach(TEntity entity)
        {
            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>
        public virtual TEntity Find(TKey key)
        {
            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>
        public virtual TEntity GetOriginalEntity(TEntity entity)
        {
            var entry = DbContext.Entry(entity);

            if (entry == null)
            {
                return null;
            }

            return entry.OriginalValues.ToObject() as TEntity;
        }

        /// <summary>
        ///     Returns an query expression that, when enumerated, will retrieve all objects.
        /// </summary>
        public virtual IQueryable<TEntity> Query()
        {
            return Query(null);
        }

        /// <summary>
        ///     Returns an query expression that, when enumerated, will retrieve only the objects that satisfy the where condition.
        /// </summary>
        public virtual IQueryable<TEntity> Query(Expression<Func<TEntity, bool>> where)
        {
            IQueryable<TEntity> query = _dbSet;

            if (where != null)
            {
                query = query.Where(where);
            }

            return query;
        }

        /// <summary>
        ///     Saves changes from the DbContext's change tracker to the database.
        /// </summary>
        public virtual void SaveChanges()
        {
            _dbContext.SaveChanges();
        }

        /// <summary>
        ///     Marks an entity for deletion in the DbContext's change tracker if it passes the ValidateDelete method.
        /// </summary>
        protected virtual IEnumerable<ValidationResult> TryDelete(TEntity entity)
        {
            var deleteErrors = ValidateDelete(entity);

            if (deleteErrors.Any())
            {
                return deleteErrors;
            }

            _dbSet.Remove(entity);

            return Enumerable.Empty<ValidationResult>();
        }

        /// <summary>
        ///     Adds an entity for insertion in the DbContext's change tracker if it passes the ValidateSave method.
        /// </summary>
        protected virtual IEnumerable<ValidationResult> TryInsert(TEntity entity)
        {
            var saveErrors = ValidateSave(entity);

            if (saveErrors.Any())
            {
                return saveErrors;
            }

            _dbSet.Add(entity);

            return Enumerable.Empty<ValidationResult>();
        }

        /// <summary>
        ///     Marks an entity for update in the DbContext's change tracker if it passes the ValidateSave method.
        /// </summary>
        protected virtual IEnumerable<ValidationResult> TryUpdate(TEntity entity)
        {
            var saveErrors = ValidateSave(entity);

            if (saveErrors.Any())
            {
                return saveErrors;
            }

            _dbSet.Update(entity);

            return Enumerable.Empty<ValidationResult>();
        }

        /// <summary>
        ///     Validates if it's ok to delete the entity from the database.
        /// </summary>
        protected virtual IEnumerable<ValidationResult> ValidateDelete(TEntity model)
        {
            yield break;
        }

        /// <summary>
        ///     Validates if it's ok to save the new or updated entity to the database.
        /// </summary>
        protected virtual IEnumerable<ValidationResult> ValidateSave(TEntity model)
        {
            yield break;
        }
    }
}

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.

IQueryManagerExtensions.cs
[Domion.Net-2.0] src\Domion.Lib\Extensions\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using Domion.Core.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using System;
using System.Linq;
using System.Linq.Expressions;

namespace Domion.Lib.Extensions
{
    /// <summary>
    ///     Extensions for generic IQueryManager < T >.
    /// </summary>
    public static class IQueryManagerExtensions
    {
        /// <summary>
        ///     Returns the first object that satisfies the condition or raises InvalidaOperationException if none.
        /// </summary>
        public static T First<T>(this IQueryManager<T> manager, Expression<Func<T, bool>> where = null) where T : class
        {
            return manager.Query(where).First();
        }

        /// <summary>
        ///     Returns the first object that satisfies the condition or null if none.
        /// </summary>
        public static T FirstOrDefault<T>(this IQueryManager<T> manager, Expression<Func<T, bool>> where = null) where T : class
        {
            return manager.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>
        public static IIncludableQueryable<T, TProperty> Include<T, TProperty>(this IQueryManager<T> manager, Expression<Func<T, TProperty>> includeExpression) where T : class
        {
            return manager.Query().Include(includeExpression);
        }

        /// <summary>
        ///     Returns the single object that satisfies the condition or raises InvalidaOperationException if none or more than one.
        /// </summary>
        public static T Single<T>(this IQueryManager<T> manager, Expression<Func<T, bool>> where = null) where T : class
        {
            return manager.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>
        public static T SingleOrDefault<T>(this IQueryManager<T> manager, Expression<Func<T, bool>> where = null) where T : class
        {
            return manager.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.

Por eso, siguiendo una de las sugerencias en https://github.com/aspnet/EntityFramework/issues/2805, incluimos las siguientes clases:

A-2.1 - EntityTypeConfiguration - Configuración genérica por clase de dominio

El método Map de esta clase abstract recibe un EntityTypeBuilder con el que se puede refactorizar allí toda la configuración de una clase del modelo.

En el punto B-3.1 podemos ver cómo se utiliza esta clase para manejar la clase de configuración.

EntityTypeConfiguration.cs
[Domion.Net-2.0] src\Domion.Lib\Data\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Domion.Lib.Data
{
    // As suggested in: https://github.com/aspnet/EntityFramework/issues/2805

    public abstract class EntityTypeConfiguration<TEntity>
        where TEntity : class
    {
        public abstract void Map(EntityTypeBuilder<TEntity> builder);
    }
}

A-2.2 - ModelBuilderExtensions - Extensión de ModelBuilder

Este extension method permite invocar la configuración de una clase desde el DbContext.

ModelBuilderExtensions.cs
[Domion.Net-2.0] src\Domion.Lib\Data\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
using Microsoft.EntityFrameworkCore;

namespace Domion.Lib.Data
{
    // As suggested in: https://github.com/aspnet/EntityFramework/issues/2805

    public static class ModelBuilderExtensions
    {
        public static void AddConfiguration<TEntity>(this ModelBuilder modelBuilder, EntityTypeConfiguration<TEntity> configuration)
            where TEntity : class
        {
            configuration.Map(modelBuilder.Entity<TEntity>());
        }
    }
}

Con esto la configuración de cada clase del modelo queda reducida a una línea en el DbContext, por ejemplo:

    modelBuilder.AddConfiguration(new BudgetClassConfiguration());

Estas clases se encuentran en el proyecto Domion.Lib

A-3 - Incluir referencias y compilar

Incluir las siguientes dependencias en Domion.Lib:

  1. Domion.Core (Referencia)
  2. 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.

Como mencionamos en el artículo inicial, la aplicación de ejemplo será un “sistema” de flujo de caja personal y el “módulo” inicial va a tener, por ahora, una sola entidad: BudgetClass.

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.

B-1 - Modelos de la aplicación

También mecionamos en ese artículo que íbamos a aplicar el enfoque MDA - Model Driven Architecture usando Enterprise Architect.

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.

Patrón de repositorio con Entity Framework Core /posts/images/EA_2017-06-09_12-04-33.png

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.

Patrón de repositorio con Entity Framework Core /posts/images/EA_2017-06-09_12-40-09.png

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.

Patrón de repositorio con Entity Framework Core /posts/images/EA_2017-06-09_12-42-48.png

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.

BudgetClass.cs
[Domion.Net-2.0] samples\DFlow.Budget.Core\Model\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//------------------------------------------------------------------------------
//  BudgetClass.cs
//
//  Implementation of: BudgetClass (Class) <<ef-entity>>
//  Generated by Domion-MDA - http://www.coderepo.blog/domion
//
//  Created on     : 02-jun-2017 10:49:07
//  Original author: Miguel
//------------------------------------------------------------------------------

using System;
using System.ComponentModel.DataAnnotations;

namespace DFlow.Budget.Core.Model
{
    public class BudgetClass
    {
        public BudgetClass()
        {
            TransactionType = TransactionType.Income;
        }

        public int Id { get; set; }

        [Required]
        [MaxLength(100)]
        public virtual string Name { get; set; } // Key data ----------

        public virtual int Order { get; set; }

        public virtual Byte[] RowVersion { get; set; }

        public virtual TransactionType TransactionType { get; set; }
    }
}

B-2.2 - TransactionType - Tipo de transacción [Generado 100%]

Esto es simplemente un enum convencional.

TransactionType.cs
[Domion.Net-2.0] samples\DFlow.Budget.Core\Model\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//------------------------------------------------------------------------------
//  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
//------------------------------------------------------------------------------

namespace DFlow.Budget.Core.Model
{
    public enum TransactionType
    {
        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
[Domion.Net-2.0] samples\DFlow.Budget.Core\Services\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//------------------------------------------------------------------------------
//  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
//------------------------------------------------------------------------------

using DFlow.Budget.Core.Model;
using Domion.Core.Services;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace DFlow.Budget.Core.Services
{
    public interface IBudgetClassManager : 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>
        BudgetClass Refresh(BudgetClass entity);

        /// <summary>
        ///     Saves changes from the DbContext's change tracker to the database.
        /// </summary>
        void SaveChanges();

        /// <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(BudgetClass 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>
        IEnumerable<ValidationResult> TryInsert(BudgetClass 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>
        IEnumerable<ValidationResult> TryUpdate(BudgetClass entity);

        /// <summary>
        ///     Calls TryInsert or TryUpdate accordingly, based on the value of the Id property;
        /// </summary>
        IEnumerable<ValidationResult> TryUpsert(BudgetClass entity);
    }
}

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.

BudgetClassConfiguration.cs
[Domion.Net-2.0] samples\DFlow.Budget.Lib\Data\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//------------------------------------------------------------------------------
//  BudgetClassConfiguration.cs
//
//  Implementation of: BudgetClassConfiguration (Class) <<entity-configuration>>
//  Generated by Domion-MDA - http://www.coderepo.blog/domion
//
//  Created on     : 02-jun-2017 10:49:07
//  Original author: Miguel
//------------------------------------------------------------------------------

using DFlow.Budget.Core.Model;
using Domion.Lib.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace DFlow.Budget.Lib.Data
{
    public class BudgetClassConfiguration : EntityTypeConfiguration<BudgetClass>
    {
        public override void Map(EntityTypeBuilder<BudgetClass> builder)
        {
            builder.ToTable("BudgetClasses", schema: "Budget");

            builder.HasKey(bc => bc.Id);

            builder.Property(bc => bc.RowVersion)
                .IsRowVersion();

            builder.HasIndex(bc => bc.Name)
                .IsUnique();
        }
    }
}

B-3.2 - BudgetDbContext - DbContext para el módulo [Generado 100%]

El DbContext es el corazón de Entity Framework.

El DbContext es una implementación combinada de los patrones Unit of Work y Repository.

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.

Además, el DbContext también facilita la implementación de un Bounded Context, que es uno de los elementos pricipales del Domain Driven Design (DDD).

BudgetDbContext.cs
[Domion.Net-2.0] samples\DFlow.Budget.Lib\Data\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//------------------------------------------------------------------------------
//  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
//------------------------------------------------------------------------------

using DFlow.Budget.Core.Model;
using Domion.Lib.Data;
using Microsoft.EntityFrameworkCore;
using NLog;
using System;

namespace DFlow.Budget.Lib.Data
{
    public class BudgetDbContext : DbContext
    {
        private static Logger logger = LogManager.GetCurrentClassLogger();

        public BudgetDbContext()
            : base()
        {
        }

        public BudgetDbContext(DbContextOptions<BudgetDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<BudgetClass> BudgetClasses { get; set; }

        public override int SaveChanges()
        {
            try
            {
                return base.SaveChanges();
            }
            catch (Exception ex)
            {
                logger.Error(ex);

                throw;
            }
        }

        ///
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            ConfigureLocalModel(modelBuilder);

            ConfigureExternalModel(modelBuilder);
        }

        ///
        /// <param name="modelBuilder"></param>
        private void ConfigureExternalModel(ModelBuilder modelBuilder)
        {
        }

        ///
        /// <param name="modelBuilder"></param>
        private void ConfigureLocalModel(ModelBuilder modelBuilder)
        {
            // Database schema is "Budget"

            modelBuilder.AddConfiguration(new BudgetClassConfiguration());
        }
    }
}

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
[Domion.Net-2.0] samples\DFlow.Budget.Lib\Services\
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//------------------------------------------------------------------------------
//  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
//------------------------------------------------------------------------------

using DFlow.Budget.Core.Model;
using DFlow.Budget.Core.Services;
using DFlow.Budget.Lib.Data;
using Domion.Core.Services;
using Domion.Lib.Data;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;

namespace DFlow.Budget.Lib.Services
{
    public class BudgetClassManager : BaseRepository<BudgetClass, int>, IQueryManager<BudgetClass>, IEntityManager<BudgetClass, int>, IBudgetClassManager
    {
        public static string duplicateByNameError = @"There's another BudgetClass with Name ""{0}"", can't duplicate! (Id={1})";

        /// <summary>
        ///     Entity manager for BudgetClass
        /// </summary>
        public BudgetClassManager(BudgetDbContext dbContext)
            : base(dbContext)
        {
        }

        /// <summary>
        ///     Returns another BudgetClass with the same Name if it exists or null if doesn't.
        /// </summary>
        public BudgetClass FindDuplicateByName(BudgetClass entity)
        {
            if (entity.Id == 0)
            {
                return Query(bc => bc.Name == entity.Name.Trim()).SingleOrDefault();
            }
            else
            {
                return Query(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>
        public override IQueryable<BudgetClass> Query(Expression<Func<BudgetClass, bool>> where = null)
        {
            return base.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>
        public virtual BudgetClass Refresh(BudgetClass entity)
        {
            base.Detach(entity);

            return Find(entity.Id);
        }

        /// <summary>
        ///     Marks an entity for deletion in the DbContext's change tracker if no errors are found in the ValidateDelete method.
        /// </summary>
        public new virtual IEnumerable<ValidationResult> TryDelete(BudgetClass entity)
        {
            return base.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>
        public new virtual IEnumerable<ValidationResult> TryInsert(BudgetClass entity)
        {
            if (entity.RowVersion != null && entity.RowVersion.Length > 0) throw new InvalidOperationException("RowVersion not empty on Insert");

            CommonSaveOperations(entity);

            return base.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>
        public new virtual IEnumerable<ValidationResult> TryUpdate(BudgetClass entity)
        {
            if (entity.RowVersion == null || entity.RowVersion.Length == 0) throw new InvalidOperationException("RowVersion empty on Update");

            CommonSaveOperations(entity);

            return base.TryUpdate(entity);
        }

        /// <summary>
        ///     Calls TryInsert or TryUpdate accordingly, based on the value of the Id property;
        /// </summary>
        public virtual IEnumerable<ValidationResult> TryUpsert(BudgetClass entity)
        {
            if (entity.Id == 0)
            {
                return TryInsert(entity);
            }
            else
            {
                return TryUpdate(entity);
            }
        }

        /// <summary>
        ///     Performs operations that have to be executed both on inserts and updates.
        /// </summary>
        internal virtual void CommonSaveOperations(BudgetClass entity)
        {
            TrimStrings(entity);
        }

        /// <summary>
        ///     Returns the validation results for conditions that prevent the entity to be removed.
        /// </summary>
        protected override IEnumerable<ValidationResult> ValidateDelete(BudgetClass entity)
        {
            yield break;
        }

        /// <summary>
        ///     Returns the validation results for conditions that prevent the entity to be added or updated.
        /// </summary>
        protected override IEnumerable<ValidationResult> ValidateSave(BudgetClass entity)
        {
            BudgetClass duplicateByName = FindDuplicateByName(entity);

            if (duplicateByName != null)
            {
                yield return new ValidationResult(string.Format(duplicateByNameError, duplicateByName.Name, duplicateByName.Id), new[] { "Name" });
            }

            yield break;
        }

        private void TrimStrings(BudgetClass entity)
        {
            if (entity.Name != null) entity.Name = entity.Name.Trim();
        }
    }
}

B-4 - Incluir dependencias y compilar

Incluir las siguientes dependencias en DFlow.Budget.Core:

  1. Domion.Core (Referencia)

Incluir las siguientes dependencias en DFlow.Budget.Core:

  1. Domion.Lib (Referencia)
  2. Microsoft.EntityFrameworkCore.SqlServer - 1.1.2 (Nuget)
  3. 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”

  1. Crear el proyecto tipo Class Library (.NET Core) en la carpeta “samples”

C-1.2 - Agregar clase “samples\DFlow.Budget.Setup\BudgetDbSetupHelper.cs”

BudgetDbSetupHelper.cs
[Domion.Net-2.0] samples\DFlow.Budget.Setup\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using DFlow.Budget.Lib.Data;
using Microsoft.EntityFrameworkCore;
using System;

namespace DFlow.Budget.Setup
{
    public class BudgetDbSetupHelper
    {
        private string _connectionString;
        private DbContextOptions<BudgetDbContext> _options;

        public BudgetDbSetupHelper(string connectionString)
        {
            _connectionString = connectionString;
        }

        /// <summary>
        /// Returns the DbContext if the database has been set up.
        /// </summary>
        /// <returns></returns>
        public BudgetDbContext GetDbContext()
        {
            if (_options == null) throw new InvalidOperationException($"Must run {nameof(BudgetDbSetupHelper)}.{nameof(SetupDatabase)} first!");

            return new BudgetDbContext(_options);
        }

        /// <summary>
        /// Creates the database and applies pending migrations.
        /// </summary>
        public void SetupDatabase()
        {
            var optionBuilder = new DbContextOptionsBuilder<BudgetDbContext>();

            optionBuilder.UseSqlServer(_connectionString);

            _options = optionBuilder.Options;

            using (var dbContext = GetDbContext())
            {
                dbContext.Database.Migrate();
            }
        }
    }
}

C-1.3 - Instalar dependencias

  1. DFlow.Budget.Lib (Referencia)
  2. 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:

  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.1" />
  </ItemGroup>

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.

C-2.2 - Instalar dependencias

  1. DFlow.Budget.Lib (Referencia)
  2. Microsoft.EntityFrameworkCore.Core - 1.1.2. (NuGet)
  3. Microsoft.EntityFrameworkCore.Design - 1.1.2. (NuGet)
  4. Microsoft.EntityFrameworkCore.SqlServer - 1.1.2. (NuGet)

C-2.3 - Verificar instalación del tooling

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.

Patrón de repositorio con Entity Framework Core /posts/images/cmd_2017-06-09_19-40-25.png

C-3 - Crear migración inicial

C-3.1 - Ejecutar script de migración

  1. 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)
  2. Ejecutar el script add-migration
  3. Indicar los siguientes parámetros:
    • DFlow.Budget.Lib
    • BudgetDbContext
    • Create

Si todo está bien, se debe obtener una pantalla como esta:

Patrón de repositorio con Entity Framework Core /posts/images/cmd_2017-06-09_21-45-36.png

Y se debe obtener un archivo como este en la carpeta Migrations de DFlow.Budget.Lib.

20170609203746_CreateMigration_BudgetDbContext.cs
[Domion.Net-2.0] samples\DFlow.Budget.Lib\Migrations\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace DFlow.Budget.Lib.Migrations
{
    public partial class CreateMigration_BudgetDbContext : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.EnsureSchema(
                name: "Budget");

            migrationBuilder.CreateTable(
                name: "BudgetClasses",
                schema: "Budget",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    Name = table.Column<string>(maxLength: 100, nullable: false),
                    Order = table.Column<int>(nullable: false),
                    RowVersion = table.Column<byte[]>(rowVersion: true, nullable: true),
                    TransactionType = table.Column<int>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_BudgetClasses", x => x.Id);
                });

            migrationBuilder.CreateIndex(
                name: "IX_BudgetClasses_Name",
                schema: "Budget",
                table: "BudgetClasses",
                column: "Name",
                unique: true);
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "BudgetClasses",
                schema: "Budget");
        }
    }
}

D - Paso a Paso - Ejecutar la aplicación

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

D-1.1 - Modificar Program.cs

Program.cs
[Domion.Net-2.0] samples\DFlow.CLI\
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
using DFlow.Budget.Core.Model;
using DFlow.Budget.Lib.Services;
using DFlow.Budget.Setup;
using Domion.Lib.Extensions;
using System;
using System.Linq;
using System.Text;

namespace DFlow.CLI
{
    internal class Program
    {
        private static BudgetClass[] _dataSet = new BudgetClass[]
        {
            new BudgetClass { Name = "Income", Order = 1, TransactionType = TransactionType.Income },
            new BudgetClass { Name = "Expenses", Order = 2, TransactionType = TransactionType.Expense },
            new BudgetClass { Name = "Investments", Order = 3, TransactionType = TransactionType.Investment },
        };

        private static BudgetDbSetupHelper _dbHelper;

        private static void LoadSeedData()
        {
            Console.WriteLine("Seeding data...\n");

            using (var dbContext = _dbHelper.GetDbContext())
            {
                var manager = new BudgetClassManager(dbContext);

                foreach (var item in _dataSet)
                {
                    var entity = manager.SingleOrDefault(bc => bc.Name.StartsWith(item.Name));

                    if (entity == null)
                    {
                        manager.TryInsert(item);
                    }
                    else
                    {
                        var tokens = entity.Name.Split('-');

                        if (tokens.Length == 1)
                        {
                            entity.Name += " - 1";
                        }
                        else
                        {
                            entity.Name = tokens[0].Trim() + $" - {int.Parse(tokens[1]) + 1}";
                        }
                    }
                }

                manager.SaveChanges();
            }
        }

        private static void Main(string[] args)
        {
            Console.OutputEncoding = Encoding.UTF8;

            Console.WriteLine("EF Core App\n");

            SetupDb();

            LoadSeedData();

            PrintDb();

            Console.WriteLine("Press any key to continue...");

            Console.ReadKey();
        }

        private static void PrintDb()
        {
            using (var dbContext = _dbHelper.GetDbContext())
            {
                Console.WriteLine("Printing data...\n");

                Console.WriteLine("Budget Classes");
                Console.WriteLine("--------------");

                int nameLength = _dataSet.Select(c => c.Name.Length).Max() + 5;
                int typeLength = _dataSet.Select(c => c.TransactionType.ToString().Length).Max();

                foreach (var item in dbContext.BudgetClasses)
                {
                    Console.WriteLine($"| {item.Name.PadRight(nameLength)} | {item.Order} | {item.TransactionType.ToString().PadRight(typeLength)} |");
                }

                Console.WriteLine();
            }
        }

        private static void SetupDb()
        {
            string connectionString = "Data Source=localhost;Initial Catalog=DFlow.CLI;Integrated Security=SSPI;MultipleActiveResultSets=true";

            Console.WriteLine($"Setting up database\n ({connectionString})...\n");

            _dbHelper = new BudgetDbSetupHelper(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:

Patrón de repositorio con Entity Framework Core /posts/images/dotnet_2017-06-11_13-03-36.png

Y esta otra al ejecutarla por segunda vez:

Patrón de repositorio con Entity Framework Core /posts/images/dotnet_2017-06-11_12-59-30.png

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.

Gracias,

Miguel.

Enlaces relacionados

DbContext
https://docs.microsoft.com/en-us/ef/core/api/microsoft.entityframeworkcore.dbcontext

Domain Driven Design
https://domainlanguage.com/ddd/

DSL - Domain Specific Language
https://en.wikipedia.org/wiki/Domain-specific_language

Enterprise Architect
http://www.sparxsystems.com/products/ea/

Entity Framework Core
https://docs.microsoft.com/en-us/ef/core/index

Extension Methods
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods

MDA - Model Driven Architecture
https://en.wikipedia.org/wiki/Model-driven_architecture

Patrón Bounded Context
https://martinfowler.com/bliki/BoundedContext.html

Patrón de repositorio
https://martinfowler.com/eaaCatalog/repository.html

Patrón Service Locator
https://martinfowler.com/articles/injection.html#UsingAServiceLocator

Patrón Unit of Work
https://martinfowler.com/eaaCatalog/unitOfWork.html