Compartir a través de



Octubre de 2017

Volumen 32, número 10

Puntos de datos: EF Core 2.0 para DDD, parte 2

Por Julie Lerman

Julie LermanEn mi columna de septiembre (msdn.com/magazine/mt842503), expuse las muchas características de Entity Framework Core (EF Core) 2.0 que están bien alineadas con los principios del diseño basado en dominio (DDD). Además de proporcionar una guía y unos patrones fantásticos para el desarrollo de software, los principios de DDD también son fundamentales si diseña microservicios. En los ejemplos incluidos en el artículo, usé patrones simplistas para centrarme en la característica concreta de EF Core. Hacerlo de este modo supuso que el código no representó clases basadas en DDD bien diseñadas y prometí que, en una columna próxima, desarrollaría las clases para que se parecieran más a lo que podría escribir para una implementación real mediante DDD. Y eso es precisamente lo que voy a hacer en este artículo. Le guiaré por estas clases con una arquitectura mejorada y le mostraré que siguen funcionando bien cuando uso EF Core 2.0 para asignarlas a mi base de datos.

El modelo de dominio original

Empezaré con un repaso rápido de mi pequeño modelo de dominio. Dado que se trata de un ejemplo, el dominio no tiene los complejos problemas empresariales que generalmente le llevarían a usar DDD, pero, incluso sin esos problemas complicados, puedo aplicar los patrones para que los vea en acción y observe cómo responde a ellos EF Core 2.0.

El dominio comprende los personajes samuráis de la película “Los siete samuráis”, donde hago un seguimiento de su primera aparición en la película y sus identidades secretas.

En el artículo original, Samurai era la raíz del agregado y restringí el modelo para garantizar que Samurai era responsable de administrar sus entradas y su identidad secreta. Demostré algunas de estas restricciones como se indica a continuación:

Samurai y Entrance tienen una relación uno a uno. El campo Entrance de Samurai es privado. Entrance tiene un campo de clave externa, SamuraiId. Dado que el campo Samurai.Entrance es privado, necesité agregar una asignación de API fluida en la clase DbContext para asegurarme de que EF Core pudiera comprender la relación para recuperar los datos y conservarlos. Desarrollé la propiedad Entrance para vincularla a un campo de respaldo y, a continuación, modifiqué las asignaciones para hacérselo saber a EF Core.

PersonName_ValueObject (denominado así para facilitar la tarea del lector) es un tipo de objeto de valor sin su propia identidad. Se puede usar como propiedad en otros tipos. Samurai tiene una propiedad PersonName_ValueObject denominada SecretIdentity. Usé la nueva característica de entidad con propietario de EF Core para que SamuraiContext supiera tratar SecretIdentity de la misma manera en que versiones anteriores de EF controlaban ComplexType, es decir, almacenando las propiedades del objeto de valor en columnas de la misma tabla a la que se asignan los tipos de Samurai.

El modelo de dominio mejorado

A continuación se incluyen las clases más avanzadas del agregado, junto con la clase DbContext de EF Core 2.0 que estoy usando para realizar la asignación a la base de datos, que, en este caso, es SQLite. El diagrama de la Figura 1 muestra el agregado con sus detalles de clase. Las listas de código empezarán por las entidades que no son de raíz y acabarán en la raíz, Samurai, que controla las demás. Tenga en cuenta que he quitado las referencias al espacio de nombres, pero puede consultarlas en la descarga que acompaña a este artículo.

 

Diagrama del agregado avanzado

Figura 1 Diagrama del agregado avanzado

La Figura 2 muestra la clase Entrance evolucionada.

Figura 2 La clase Entrance diseñada según los patrones de DDD

public class Entrance {
  public Entrance (Guid samuraiGuidId,int movieMinute, string sceneName, string description) {
    MovieMinute = movieMinute;
    SceneName = sceneName;
    ActionDescription = description;
    SamuraiGuidId=samuraiGuidId;
  }
  private Entrance () { } // Needed by ORM
  public int Id { get; private set; }
  public int MovieMinute { get; private set; }
  public string SceneName { get; private set; }
  public string ActionDescription { get; private set; }

  private int SamuraiFk { get;  set; }
  public Guid SamuraiGuidId{get;private set;}
}

Gran parte del código DDD se centra en proteger el dominio de un mal uso o abuso accidentales. Se restringe el acceso a la lógica de las clases para garantizar que solo se puedan usar como usted quiera. Mi intención es que la clase Entrance (Figura 1) sea inmutable. Puede definir los valores de propiedad con el constructor sobrecargado, pasando los valores de todas sus propiedades, excepto SamuraiFk. Puede leer todas las propiedades, pero tenga en cuenta que todas tienen establecedores privados. El constructor es la única forma de afectar estos valores. Por lo tanto, si necesita modificarlo, tendrá que sustituirlo por una instancia de Entrance completamente nueva. Esta clase parece un candidato para un objeto de valor, especialmente porque es inmutable, pero quiero usarla para demostrar el comportamiento uno a uno en EF Core.

Con EF Core (y las iteraciones anteriores de EF), cuando realiza una consulta para obtener datos, EF puede materializar los resultados, incluso cuando las propiedades tienen establecedores privados, porque usa la reflexión. De este modo, EF Core puede trabajar con todas estas propiedades de Entrance que tienen establecedores privados.

Hay un constructor público con cuatro parámetros para completar las propiedades de Entrance. (En el ejemplo anterior, usé un método Factory Method que no aportaba ningún valor a esta clase, así que, en esta iteración, lo he quitado). En este dominio, una entidad Entrance sin alguna de esas propiedades no tendría sentido, así que he restringido su diseño para evitarlo. Después de ese constructor, hay un constructor privado sin parámetros. Dado que EF Core y EF usan la reflexión para materializar resultados, como otras API que crean instancias de objetos, como JSON.NET, se necesita un constructor sin parámetros disponible. El primer constructor reemplaza al constructor sin parámetros que proporciona la clase base (objeto) de la que derivan todas las clases. Por lo tanto, debe volver a agregarlo explícitamente. Este comportamiento no es nuevo para EF Core; es algo que ha tenido que hacer con EF durante mucho tiempo. Sin embargo, en el contexto de este artículo admite la repetición. Si nunca ha usado EF con esta versión, cabe destacar que, cuando se crea una entidad Entrance como resultado de una consulta, EF Core solo usará ese constructor sin parámetros para crear el objeto. El constructor público está disponible para crear nuevos objetos Entrance.

¿Qué pasa con los elementos Guid e int que apuntan a Samurai? El dominio usa Guid para conectar el samurái y la entrada de modo que la lógica de dominio no dependa del almacén de datos para sus id. SamuraiFk solo se usará para la persistencia. SamuraiFk es privado, pero EF Core puede inferir un campo de respaldo para este. Si se llamara SamuraiId, EF Core lo reconocería como clave externa, pero, dado que no sigue la convención, hay una asignación especial en el contexto para que EF Core sepa que es la clave externa. El motivo de que sea privada es que no es relevante para el dominio, pero es necesaria para que EF Core comprenda la relación a fin de almacenar y recuperar los datos correctamente. Esto es una concesión para evitar la lógica de persistencia en mi clase de dominio, pero, en mi opinión, es pequeña y no justifica el esfuerzo adicional de introducir y mantener un modelo de datos totalmente separado.

Hay una entidad nueva en el agregado: Quote. Se muestra en la Figura 3. En la película en que se basa este dominio de ejemplo, los distintos personajes tienen citas destacables de las que quiero hacer un seguimiento en este dominio. También me da la oportunidad de demostrar una relación uno a varios.

Figura 3 Tipo Quote diseñado según los patrones de DDD

public class Quote {
  public Quote (Guid samuraiGuidId,string text) {
    Text = text;
    SamuraiGuidId=samuraiGuidId;
  }
  private Quote () { } //ORM requires parameterless ctor
  public int Id { get; private set; }
  public string Text { get; private set; }
  private int SamuraiId { get; set; }
  public Guid SamuraiGuidId{get;private set;}
}

Tenga en cuenta que los patrones son los mismos que he explicado para la entidad Entrance: el constructor público sobrecargado y el constructor privado sin parámetros, los establecedores privados, la propiedad de clave externa privada para la persistencia y el GUID. La única diferencia es que SamuraiId, usado como CE de persistencia, sigue la convención de EF Core. Cuando llegue el momento de observar la clase DbContext, no habrá ninguna asignación especial para esta propiedad. He asignado nombres incoherentes a estas propiedades para que pueda ver la diferencia entre las asignaciones para nombres convencionales y no convencionales.

A continuación se presenta el tipo PersonFullName (antes, PersonName) y se muestra en la Figura 4. Es un objeto de valor. En el artículo anterior expliqué que EF Core 2.0 permite ahora la persistencia de un objeto de valor al asignarlo como entidad de propiedad de una entidad que lo posee, como la clase Samurai. Como objeto de valor, PersonFullName se usa como propiedad en otros tipos y entidades. Un objeto de valor no tiene ninguna identidad propia, es inmutable y no es una entidad. Además de en el artículo anterior, también he explicado los objetos de valor en detalle en otros artículos, así como en el curso de Pluralsight, Domain-Driven Design Fundamentals (Conceptos básicos del diseño basado en dominio), que creé con Steve Smith (bit.ly/PS-DDD). Un objeto de valor tiene otras facetas importantes y yo uso la clase base ValueObject que creó Jimmy Bogard (bit.ly/13SWd9h) para implementarlos.

Figura 4 El objeto de valor PersonFullName

public class PersonFullName : ValueObject<PersonFullName> {
  public static PersonFullName Create (string first, string last) {
    return new PersonFullName (first, last);
  }
  public static PersonFullName Empty () {
    return new PersonFullName (null, null);
  }
  private PersonFullName () { }

  public bool IsEmpty () {
    if (string.IsNullOrEmpty (First) && string.IsNullOrEmpty (Last)) {
      return true;
    } else {
      return false;
    }
  }
  private PersonFullName (string first, string last) {
    First = first;
    Last = last;
  }
  public string First { get; private set; }
  public string Last { get; private set; }
  public string FullName () => First + " " + Last;
}
}

Se usa PersonFullName para encapsular reglas comunes en el dominio para usar un nombre de persona en otra entidad o tipo. Esta clase tiene varias características destacables. Aunque no ha cambiado desde la versión anterior, en el artículo anterior no proporcioné la lista completa. Por lo tanto, es necesario explicar algunas cosas aquí, especialmente el método Factory Method Empty y el método IsEmpty. Debido a la manera de implementar la entidad de propiedad en EF Core, no puede tener el valor null en la clase propietaria. En mi dominio, se usa PersonFullName para almacenar la identidad secreta de un samurái, pero no hay ninguna regla que indique que se debe completar. Esto crea un conflicto entre mis reglas empresariales y las reglas de EF Core. De nuevo, tengo una solución bastante sencilla con la que no tengo la necesidad de crear y mantener otro modelo de datos, y con la que la forma de usar Samurai no se ve afectada. No quiero que nadie que use mi API de dominio tenga que recordar la regla de EF Core, así que he creado dos métodos Factory Method: Use Create si tiene los valores y Empty si no los tiene. Por su parte, el método IsEmpty puede determinar rápidamente el estado de PersonFullName. Las entidades que usan PersonFullName como propiedad deberán aprovechar esta lógica y las personas que usen esas entidades no tendrán que saber nada sobre la regla de EF Core.

Unirlo todo con la raíz de agregado

Finalmente, la clase Samurai se incluye en la Figura 5. Samurai es la raíz del agregado. Una raíz de agregado es el protector de todo el agregado: garantiza la validez de sus objetos internos y mantiene su coherencia. Como raíz del agregado, el tipo Samurai es responsable de cómo se crean y administran sus propiedades Entrance, Quotes y SecretIdentity.

Figura 5 Entidad Samurai (raíz del agregado)

public class Samurai {   
  public Samurai (string name): this() {
    Name = name;
    GuidId=Guid.NewGuid();
    IsDirty=true;
  }
  private Samurai () {
    _quotes = new List<Quote> ();
    SecretIdentity = PersonFullName.Empty ();
  }
  public int Id { get; private set; }
  public Guid GuidId{get;private set;}
  public string Name { get; private set; }
  public bool IsDirty { get; private set; }
 
  private readonly List<Quote> _quotes = new List<Quote> ();
  public IEnumerable<Quote> Quotes => _quotes.ToList ();
  public void AddQuote (string quoteText) {
     // TODO: Ensure this isn't a duplicate of an item already in Quotes collection
    _quotes.Add (Quote.Create(GuidId,quoteText));
     IsDirty=true;
  }

  private Entrance _entrance;
  private Entrance Entrance { get { return _entrance; } }
  public void CreateEntrance (int minute, string sceneName, string description) {
    _entrance = Entrance.Create (GuidId, minute, sceneName, description);
     IsDirty=true;
  }
  public string EntranceScene => _entrance?.SceneName;

  private PersonFullName SecretIdentity { get; set; }
  public string RevealSecretIdentity () {
    if (SecretIdentity.IsEmpty ()) {
      return "It's a secret";
    } else {
      return SecretIdentity.FullName ();
    }
  }
  public void Identify (string first, string last) {
    SecretIdentity = PersonFullName.Create (first, last);
    IsDirty=true;
  }
}

Como el resto de clases, Samurai tiene un constructor sobrecargado, que es la única forma de crear una instancia de un nuevo samurái. El único dato que se espera al crear un nuevo samurái es su nombre. El constructor define la propiedad Name y también genera un valor para la propiedad GuidId. La base de datos completa la propiedad SamuraiId. La propiedad GuidId garantiza que mi dominio no dependa del nivel de datos para tener una identidad única y es lo que se usa para conectar las entidades que no son raíz (Entrance y Quote) al samurái, incluso si el samurái aún no es persistente ni se le ha asignado ningún valor en el campo SamuraiId. El constructor anexa “: this()” para llamar al constructor sin parámetros en la cadena del constructor. El constructor sin parámetros (recordatorio: también lo usa EF Core al crear objetos a partir de resultados de consulta) garantiza que se cree SecretIdentity y una instancia de la colección Quotes. Aquí es donde uso el método Factory Method Empty. Incluso si alguien que escribe código con Samurai nunca proporciona valores para la propiedad SecretIdentity, EF Core estará satisfecho porque la propiedad no es null.

La encapsulación completa de Quotes en Samurai no es nueva. Aprovecho la compatibilidad con IEnumerable que describí en una columna anterior sobre EF Core 1.1 (msdn.com/magazine/mt745093).

La propiedad Entrance completamente encapsulada ha cambiado respecto al ejemplo anterior en dos pequeños detalles. En primer lugar, dado que quité el método Factory Method de Entrance, ahora creo sus instancias directamente. En segundo lugar, el constructor Entrance adquiere ahora otros valores, así que los paso aunque, en estos momentos, la clase Samurai no haga nada con estos valores adicionales.

Hay ciertas mejoras respecto a la propiedad SecretIdentity respecto al ejemplo anterior. En primer lugar, originalmente, la propiedad era pública, con un captador público y un establecedor privado. Esto permitía la persistencia de EF Core de la misma manera que en versiones anteriores de EF. Sin embargo, ahora, SecretIdentity se declara como propiedad privada, aunque no he definido ninguna propiedad de respaldo. Cuando se trata de persistencia, EF Core puede inferir una propiedad de respaldo para poder almacenar y recuperar estos datos sin ninguna otra asignación por mi parte. El método Identify, que permite especificar un nombre y apellido para la identidad secreta, estaba en el ejemplo anterior. Pero, en ese caso, si quería leer ese valor, podía acceder a él a través de la propiedad pública. Ahora que está oculto, he agregado un método nuevo, RevealSecretIdentity, que usará el método PersonFullName.IsEmpty para determinar si la propiedad está completada o no. Si lo está, devuelve el valor de FullName de SecretIdentity. Pero, si la identidad real de esta persona no se había identificado, el método devuelve la cadena: “It’s a secret”.

Samurai tiene una propiedad nueva, un elemento booleano denominado IsDirty. Cuando modifico las propiedades de Samurai, defino IsDirty como true. Puedo usar ese valor en cualquier otro lugar para determinar si debo llamar a SaveChanges en Samurai.

Así pues, en este agregado, no hay forma de sortear las reglas que creé en las entidades y la raíz, Samurai. La única forma de crear, modificar o leer Entrance, Quotes y SecretIdentity es a través de la lógica restringida incorporada a Samurai, que, como raíz de agregado, protege todo el agregado.

Asignación al almacén de datos con EF Core 2.0

El artículo anterior se centraba en cómo EF Core 2.0 puede persistir y recuperar datos asignados a estas clases restringidas. Con este modelo de dominio mejorado, EF Core puede entender la mayoría de las asignaciones, incluso con elementos fuertemente encapsulados en la clase Samurai. En algunos casos, tengo que ofrecer un poco de ayuda a DbContext para asegurarme de que comprende cómo se asignan las clases a la base de datos, como se muestra en la Figura 6.

Figura 6 Clase SamuraiContext DbContext

public class SamuraiContext : DbContext {
  public DbSet<Samurai> Samurais { get; set; }
  protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder) {
    optionsBuilder.UseSqlite ("Filename=DP0917Samurai.db");
  }
  protected override void OnModelCreating (ModelBuilder modelBuilder) {
    modelBuilder.Entity<Samurai> ()
      .HasOne (typeof (Entrance), "Entrance")
      .WithOne ().HasForeignKey(typeof (Entrance), "SamuraiFk");

    foreach (var entityType in modelBuilder.Model.GetEntityTypes ()) {
      modelBuilder.Entity (entityType.Name).Property<DateTime> 
        ("LastModified");
      modelBuilder.Entity (entityType.Name).Ignore ("IsDirty");
    }
    modelBuilder.Entity<Samurai> ().OwnsOne (typeof (PersonFullName), 
      "SecretIdentity");
  }

  public override int SaveChanges () {
    foreach (var entry in ChangeTracker.Entries ()
      .Where (e => e.State == EntityState.Added ||
        e.State == EntityState.Modified)) {
      if (!(entry.Entity is PersonFullName))
        entry.Property ("LastModified").CurrentValue = DateTime.Now;
    }
    return base.SaveChanges ();
  }
}

No han cambiado muchas cosas en SamuraiContext desde el primer ejemplo de mi primer artículo, pero cabe destacar algunas cosas a modo de recordatorio. Por ejemplo, la asignación OwnsOne permite que EF Core sepa que SecretIdentity es una entidad de propiedad y que sus propiedades deben ser persistentes *como si fueran propiedades individuales de Samurai. Para este ejemplo, he codificado el proveedor en el método OnConfiguring, en lugar de usar servicios de inversión de control (IoC) e inserción de dependencias. Como ya se mencionó en el primer artículo, EF Core puede comprender la relación uno o uno entre Samurai y Entrance, pero tengo que expresar la relación para acceder al método HasForeignKey para informar al contexto acerca de la propiedad de clave externa no convencional SamuraiFk. Al hacerlo, dado que Entrance es privada en Samurai, no puedo usar una expresión lambda y uso una sintaxis alternativa para los parámetros HasForeignKey.

LastModifed es una propiedad reemplazada (nueva en EF Core) que persistirá en la base de datos aunque no sea una propiedad en las entidades. La asignación Ignore sirve para garantizar que la propiedad IsDirty de Samurai no persista y sirve solo para la lógica relevante de dominio.

Y eso es todo. Debido a la cantidad de patrones de DDD que he aplicado en mis clases de dominio, queda poco que hacer en cuanto a las asignaciones especiales que debo agregar a la clase SamuraiContext para informar a EF Core 2.0 del aspecto de la base de datos, o cómo almacenar y recuperar datos de la base de datos. Eso me parece impresionante.

El ejemplo de DDD perfecto no existe

Este sigue siendo un ejemplo sencillo porque, aparte de devolver “It’s a secret” cuando no se ha asignado ningún valor a SecretIdentity, no soluciono ningún problema complejo de la lógica. El subtítulo del libro de DDD de Eric Evan es “Tackling Complexity in the Heart of Software” (Abordar la complejidad del corazón del software). Así pues, parte de la orientación respecto a DDD consiste en desglosar grandes problemas en problemas más pequeños que se puedan resolver. Los patrones de diseño de código son solo una parte del mecanismo. Todos tenemos problemas distintos que resolver en nuestros dominios y, a menudo, los lectores piden un ejemplo que se pueda usar como plantilla para su propio software. Pero los que compartimos nuestro código e ideas solo podemos proporcionar ejemplos como herramientas de aprendizaje. A continuación, puede extrapolar estas lecciones y aplicar parte del argumento y el proceso de toma de decisiones a sus propios problemas. Podría dedicar aún más tiempo a esta pequeña fracción de código y aplicar más lógica y patrones del arsenal de DDD, pero este ejemplo llega bastante lejos en su aprovechamiento de ideas de DDD para crear un enfoque más profundo sobre el comportamiento, en lugar de sobre las propiedades, y encapsular y proteger más el agregado.

Mi objetivo en estas dos columnas era mostrar que EF Core 2.0 resulta más útil para asignar el modelo de dominio basado en DDD a la base de datos. Además de haberlo demostrado, espero que los patrones de DDD que he incluido en estas clases le hayan servido de inspiración.


Julie Lerman es directora regional de Microsoft, MVP de Microsoft, mentora y consultora del equipo de software. Vive en las colinas de Vermont. Puede encontrarla haciendo presentaciones sobre el acceso a datos y otros temas en grupos de usuarios y en conferencias en todo el mundo. Su blog es thedatafarm.com/blog y es la autora de "Programming Entity Framework", así como de una edición de Code First y una edición de DbContext, de O’Reilly Media. Sígala en Twitter en @julielerman y vea sus cursos de Pluralsight en juliel.me/PS-Videos.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Cesar de la Torre


Discuta sobre este artículo en el foro de MSDN Magazine