Skip to main content

Septiembre de 2017
Volumen 32, número 9

Puntos de datos: EF Core 2.0 para DDD

Por Julie Lerman | Septiembre de 2017 | Obtener el código: C#    VB

Julie LermanSi lleva tiempo siguiendo esta columna, habrá visto bastantes artículos sobre la implementación de Entity Framework (EF) al compilar soluciones basadas en los patrones y las instrucciones del diseño guiado por el dominio (DDD, por sus siglas en inglés). Aunque DDD se centra en el dominio y no en la persistencia de los datos, en algún momento se requiere la entrada y la salida de datos del software.

En iteraciones anteriores de EF, sus patrones (convencionales o personalizados) permitían a los usuarios asignar clases de dominio sin complicaciones directamente a la base de datos sin excesiva fricción. En general, considero que, si la capa de asignación de EF se encarga de introducir y extraer sus modelos de dominio bien diseñados en la base de datos sin tener que crear un modelo de datos adicional, es suficiente. No obstante, cuando se encuentra ajustando sus clases de dominio y su lógica para que funcionen mejor con Entity Framework, es señal de que ha llegado la hora de crear un modelo de persistencia de datos para asignarlo luego de las clases de modelo de dominio a las clases de modelo datos.

Me sorprendió un poco darme cuenta de cuánto tiempo hacía que se habían publicado esos artículos sobre las asignaciones de EF y DDD. Hace cuatro años que escribí la serie de tres partes denominada “Programación para un diseño guiado por el dominio: sugerencias para los desarrolladores enfocados en datos”, que se presentó en las ediciones de agosto, septiembre y octubre de 2013 de MSDN Magazine. Este es un vínculo a la primera parte, que incluye vínculos a toda la serie: msdn.com/magazine/dn342868.

Había dos columnas específicas que trataban los patrones de DDD y explicaban si EF se asignaba o no fácilmente entre las clases de dominio y la base de datos. A partir de EF6, uno de los principales problemas era el hecho de que no podía encapsular y, por tanto, proteger una colección "secundaria". El uso de patrones conocidos para proteger la colección (lo que a menudo significaba emplear un objeto IEnumerable) no se correspondía con los requisitos de EF, y EF ni tan solo reconocería que la navegación debe formar parte del modelo. Steve Smith y yo invertimos mucho tiempo pensando en esto cuando creamos nuestro curso de Pluralsight, Domain-Driven Design Fundamentals ( bit.ly/PS-DDD) y, finalmente, Steve proporcionó una excelente solución ( bit.ly/2ufw89D).

En la versión 1.1 de EF Core se resolvió definitivamente este problema con una nueva característica sobre la cual escribí en mi columna de enero de 2017 ( msdn.com/magazine/mt745093). Con EF Core 1.0 y 1.1 también se resolvieron algunas otras restricciones de DDD, aunque quedaron algunas brechas, especialmente la incapacidad de asignar objetos de valor de DDD usados en los tipos de dominio. La capacidad de hacerlo existía en EF en sus inicios, pero aún no se había incorporado a EF Core. No obstante, con la futura versión EF Core 2.0, esta limitación desaparece.

Lo que haré en este artículo es describir las características de EF Core 2.0 que tiene a su disposición y que se corresponden con muchos de los conceptos de DDD. EF Core 2.0 es mucho más práctico para los desarrolladores que aprovechan estos conceptos. Asimismo, es posible que se le presenten por primera vez cuando empiece a usarlo. Aunque no tenga previsto adoptar DDD, podrá aprovechar sus numerosos y geniales patrones. Además, con EF Core podrá hacerlo aún más.

Enfoque uno a uno ahora más inteligente

En su libro “Domain-Driven Design”, Eric Evans afirma que una asociación bidireccional significa que ambos objetos solo se pueden entender juntos. Asimismo, explica que, si los requisitos de la aplicación no exigen un recorrido en ambas direcciones, la adición de una dirección del recorrido reduce la interdependencia y simplifica el diseño. El seguimiento de estos consejos supuso la desaparición de efectos secundarios en mi código. EF siempre pudo controlar las relaciones unidireccionales uno a uno y uno a varios. De hecho, al escribir este artículo, aprendí que mi arraigada confusión sobre el hecho de que una relación uno a uno con ambos extremos necesarios forzaba una relación bidireccional no era correcta. No obstante, se debían configurar de forma explícita esas relaciones necesarias, lo que ya no es obligatorio en EF Core, excepto en casos extremos.

Un requisito desfavorable de EF6 para las relaciones uno a uno era que la propiedad de clave del tipo de dependencia tenía que actuar también como clave externa para la entidad principal. Esto le obligaba a diseñar clases de manera un tanto extraña, aunque estuviese acostumbrado a ello. Gracias a la compatibilidad con las claves externas únicas en EF Core, ahora puede tener una propiedad de clave externa explícita en el extremo dependiente de la relación uno a uno. Tener una clave externa explícita es más natural. En la mayoría de los casos, EF Core debería poder inferir correctamente el extremo dependiente de la relación en función de la existencia de esa propiedad de clave externa. Si no lo consigue debido a algún caso extremo, deberá agregar una configuración, que demostraré en breve cuando cambie el nombre de la propiedad de clave externa.

Para demostrar una relación uno a uno, usaré mi dominio de EF Core favorito: las clases de la película “Los siete samuráis”:

public class Samurai {
  public int Id { get; set; }
  public string Name { get; set; }
  public Entrance Entrance { get; set; }
}

public class Entrance {
  public int Id { get; set; }
  public string SceneName { get; set; }
  public int SamuraiId { get; set; }
}

Ahora, con EF Core, este par de clases, Samurai y Entrance (la primera aparición del personaje en la película), se identificarán correctamente como una relación uno a uno unidireccional, donde Entrance es el tipo dependiente. No tengo que incluir una propiedad de navegación en la entidad Entrance ni necesito ninguna asignación especial en la API fluida. La clave externa (SamuraiId) sigue la convención y, por tanto, EF Core puede reconocer la relación.

EF Core infiere en la base de datos que Entrance.SamuraiId es una clave externa única que apunta a Samurai. Tenga en cuenta algo contra lo que tuve que luchar, porque (como tengo que recordarme continuamente) EF Core no es EF6. De forma predeterminada, .NET y EF Core tratarán la propiedad Samurai.Entrance como opcional en tiempo de ejecución, salvo que tenga una lógica de dominio implementada que exija que se requiera Entrance. A partir de EF4.3, contaba con la ventaja de la API de validación, que respondería a una anotación [Required] en la clase o la asignación. No obstante, no existe (¿aún?) ninguna API de validación en EF Core para detectar ese problema concreto. Además, existen otros requisitos que están relacionados con la base de datos. Por ejemplo, Entrance.SamuraiId será una variable que no admite valores NULL. Si intenta insertar una entidad Entrance sin un valor SamuraiId rellenado, EF Core no capturará los datos no válidos, lo que también significa que el proveedor de InMemory no se queja actualmente. No obstante, su base de datos relacional debería generar un error para el conflicto de restricción.

Desde una perspectiva de DDD, sin embargo, esto no es realmente un problema, ya que no debería depender de la capa de persistencia para señalar errores en su lógica de dominio. Si la clase Samurai requiere un objeto Entrance, se trata de una regla de negocio. Si no puede tener objetos Entrance huérfanos, también es una regla de negocio. Por lo tanto, la validación debe formar parte de su lógica de dominio de todos modos.

Para los casos extremos que sugerí anteriormente, se incluye un ejemplo a continuación. Si la clave externa de la entidad dependiente (por ejemplo, Entrance) no sigue la convención, puede usar la API fluida para notificarlo a EF Core. Si Entrance.SamuraiId fuese, por ejemplo, Entrance.SamuraiFK, podría aclarar esa FK a través de:

modelBuilder.Entity<Samurai>().HasOne(s=>s.Entrance)
  .WithOne().HasForeignKey<Entrance>(e=>e.SamuraiFK);

Si la relación es necesaria en ambos extremos (es decir, Entrance debe tener un elemento Samurai), puede agregar IsRequired después de WithOne.

Las propiedades se pueden encapsular aún más

DDD le guía en la compilación de agregados (gráficos de objetos), donde la raíz agregada (el objeto principal del gráfico) controla todos los demás objetos del gráfico. Eso significa escribir código que impida que otro código use las reglas de manera indebida o abusiva. La encapsulación de las propiedades para que no puedan establecerse de manera aleatoria (y, con frecuencia, leerse de manera aleatoria) es un método clave para proteger un gráfico. En EF6 y versiones anteriores, siempre era posible hacer que las propiedades escalares y de navegación tuviesen establecedores privados y, aun así, EF las siguiera reconociendo al leer y actualizar datos, pero las propiedades no se podían convertir fácilmente en privadas. En una publicación de Rowan Miller se explica una manera de hacerlo en EF6 y se presentan vínculos a algunas alternativas anteriores ( bit.ly/2eHTm2t). No había ninguna manera real de proteger una colección de navegación en una relación uno a varios. Se ha escrito mucho sobre este último problema. Ahora, no solo puede conseguir fácilmente que EF Core funcione con propiedades privadas que tienen campos de respaldo (o campos de respaldo inferidos), sino que también puede encapsular realmente las propiedades de colección gracias a la compatibilidad con la asignación de IEnumerable<T>. Escribí sobre los campos de respaldo y la interfaz IEnumerable<T> en mi columna de enero de 2017, de modo que no repetiré los detalles aquí. No obstante, esto es muy importante para los patrones de DDD y, por tanto, es oportuno mencionarlo en este artículo.

Aunque puede ocultar propiedades escalares y colecciones, existe otro tipo de propiedad que probablemente querrá encapsular: las propiedades de navegación. Las colecciones de navegación cuentan con la compatibilidad de IEnumerable<T>, pero las propiedades de navegación que son privadas, como Samurai.Entrance, no se pueden incluir en el modelo. No obstante, existe una manera de configurar el modelo para que incluya una propiedad de navegación que está oculta en la raíz agregada.

Por ejemplo, en el código siguiente, declaré Entrance como una propiedad privada de Samurai (y no uso ningún campo de respaldo explícito, aunque podría hacerlo si fuese necesario). Puede crear una nueva entidad Entrance con el método CreateEntrance (que llama a Factory Method en Entrance) y solo puede leer una propiedad SceneName de Entrance. Observe que estoy usando el operador condicional nulo 6 de C# para evitar una excepción si aún no he cargado la entidad Entrance:

private Entrance Entrance {get;set;}
public void CreateEntrance (string sceneName) {
    Entrance = Entrance.Create (sceneName);
  }
public string EntranceScene => Entrance?.SceneName;

Por convención, EF Core no realizará una presunción sobre esta propiedad privada. Aunque tuviera el campo de respaldo, la entidad privada Entrance no se detectaría automáticamente y no podría usarla al interactuar con el almacén de datos. Este es un diseño de API intencionado que le ayuda a protegerse de posibles efectos secundarios. No obstante, puede configurarlo de forma explícita. Recuerde que, cuando la entidad Entrance es pública, EF Core puede incluir la relación uno a uno. No obstante, dado que es privada, primero debe asegurarse de que EF lo sepa.

En OnModelCreating, debe agregar la asignación fluida HasOne/WithOne para notificarlo a EF Core. Dado que la entidad Entrance es privada, no puede usar una expresión lambda como un parámetro de HasOne. En su lugar, debe describir la propiedad por el tipo y el nombre. WithOne suele tomar una expresión lambda para volver a especificar la propiedad de navegación en el otro extremo del emparejamiento. No obstante, la entidad Entrance no tiene ninguna propiedad de navegación Samurai, solo la clave externa. Eso está bien. Puede dejar el parámetro vacío, ya que EF Core ahora tiene información suficiente para descifrarlo:

modelBuilder.Entity<Samurai> ()
  .HasOne (typeof (Entrance), "Entrance").WithOne();

Qué sucede si usa una propiedad de respaldo, como _entrance en la clase Samurai, como se muestra en estos cambios:

private Entrance _entrance;
private Entrance Entrance { get{return _entrance;} }
public void CreateEntrance (string sceneName) {
    _entrance = _entrance.Create (sceneName);
  }
public string EntranceScene => _entrance?.SceneName;

EF Core descifrará que debe usar el campo de respaldo al materializar la propiedad Entrance. Como explicaba Arthur Vickers en la extensa conversación que mantuvimos sobre GitHub cuando estudiaba el tema, "si existe un campo de respaldo y no existe ningún establecedor, EF usa el campo de respaldo porque no puede usar nada más". Simplemente funciona.

Si ese campo de respaldo no sigue la convención, por ejemplo, si lo denominó _foo, necesitará una configuración de metadatos:

modelBuilder.Entity<Samurai> ()
  .Metadata
  .FindNavigation ("Entrance")
  .SetField("_foo");

Ahora, las actualizaciones de la base de datos y las consultas podrán resolver esa relación. Tenga en cuenta que, si quiere usar la carga diligente, deberá usar una cadena para la entidad Entrance, ya que no se puede detectar a través de la expresión lambda. Por ejemplo:

var samurai = context.Samurais.Include("Entrance").FirstOrDefault();

Puede usar la sintaxis estándar para interactuar con los campos de respaldo en elementos como filtros, tal como se muestra en la parte inferior de la página de documentación Backing Fields (Campos de respaldo) en bit.ly/2wJeHQ7.

Los objetos de valor ya se admiten

Los objetos de valor son un concepto importante para DDD, ya que permiten definir modelos de dominio como tipos de valor. Un objeto de valor no tiene su propia identidad y pasa a formar parte de la entidad que lo usa como una propiedad. Considere el tipo de valor de cadena, que está formado por una serie de caracteres. Dado que cambiar un solo carácter altera el significado de la palabra, las cadenas son inmutables. Para poder cambiar una cadena, debe reemplazar el objeto de cadena completo. DDD le orienta para que considere la posibilidad de usar objetos de valor en cualquier lugar donde identifique una relación uno a uno. Puede obtener más información sobre los objetos de valor en el curso DDD Fundamentals que mencioné anteriormente.

EF siempre admitió la posibilidad de incluir objetos de valor en su tipo ComplexType. Podía definir un tipo sin ninguna clave y usarlo como una propiedad de una entidad. Eso era suficiente para provocar que EF lo reconociese como ComplexType y asignase sus propiedades en la tabla en la que estaba asignada la entidad. A continuación, podía extender el tipo para que también tuviera las características necesarias de un objeto de valor, como la garantía de que el tipo es inmutable y un medio para evaluar cada propiedad al determinar la igualdad e invalidar la propiedad Hash. A menudo, derivo mis tipos de la clase base ValueObject de Jimmy Bogard para adoptar rápidamente estos atributos.

El nombre de una persona es un tipo que se usa habitualmente como un objeto de valor. Puede asegurarse de que siempre que alguien quiera tener el nombre de una persona en una entidad, siga un conjunto de reglas común. En la Figura 1 se muestra una clase PersonName simple que tiene las propiedades First y Last, ambas completamente encapsuladas, así como una propiedad para devolver un objeto FullName. La clase está diseñada para garantizar que siempre se proporcionen ambas partes del nombre.

Figura 1 El objeto de valor FullName
public class PersonName : ValueObject<PersonName> {
  public static PersonName Create (string first, string last) {
    return new PersonName (first, last);
  }
  private PersonName () { } 
  private PersonName (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;
}

Puedo usar PersonName como una propiedad en otros tipos y continuar desarrollando lógica adicional en la clase PersonName. Aquí, la gracia del objeto de valor en una relación uno a uno es que no tengo que mantener la relación durante la codificación. Esto es programación orientada a objetos estándar. Es simplemente otra propiedad. En la clase Samurai, he agregado una nueva propiedad de este tipo, convertido su establecedor en privado y proporcionado otro método denominado Identify para usarlo en lugar del establecedor:

 

public PersonName SecretIdentity{get;private set;}
public void Identify (string first, string last) {
  SecretIdentity = PersonName.Create (first, last);
}

Hasta EF Core 2.0, no había ninguna característica similar a ComplexTypes, por lo que no se podían usar fácilmente los objetos de valor sin agregar ningún modelo de datos aparte. En lugar de simplemente volver a implementar el elemento ComplexType en EF Core, el equipo de EF creó un concepto denominado “entidades de propiedad”, que aprovecha otra característica de EF Core, las propiedades reemplazadas. Ahora, las entidades de propiedad se reconocen como propiedades adicionales de los tipos a los que pertenecen. EF Core comprende cómo se resuelven en el esquema de la base de datos, y cómo se crean consultas y actualizaciones que respeten esos datos.

La convención de EF Core 2.0 no detectará automáticamente que esta nueva propiedad SecretIdentity es un tipo que se debe incorporar en los datos persistentes. Deberá indicar explícitamente a DbContext que la propiedad Samurai.SecretIdentity es una entidad de propiedad en DbContext.OnModelCreating mediante el método OwnsOne:

protected override void OnModelCreating (ModelBuilder modelBuilder) {
  modelBuilder.Entity<Samurai>().OwnsOne(s => s.SecretIdentity);
}

Esto hace que las propiedades de PersonName se resuelvan como propiedades de Samurai. Aunque su código usará el tipo Samurai.SecretIdentity y navegará a través de este hasta las propiedades First y Last, esas dos propiedades se resolverán como columnas en la tabla de base de datos Samurais. La convención de EF Core les asignará el nombre de la propiedad de Samurai (SecretIdentity) y el nombre de la propiedad de la entidad con propietario, como se muestra en la Figura 2.

Esquema de la tabla Samurais, incluidas las propiedades del valor
Figura 2 Esquema de la tabla Samurais, incluidas las propiedades del valor

Ahora puedo identificar un nombre secreto de Samurai y guardarlo con código similar al siguiente:

using (var context = new SamuraiContext()) {
  var samurai = new Samurai { Name = "HubbieSan" 
  samurai.Identify ("Late", "Todinner");
  context.Samurais.Add (samurai);
  context.SaveChanges ();
}

En el almacén de datos, "Late" se mantiene en el campo SecretIdentity_First y "Todinner", en el campo SecretIdentity_Last.

A continuación, puedo consultar simplemente una clase Samurai:

var samurai=context.Samurais .FirstOrDefaultAsync (s => s.Name == "HubbieSan")

EF Core garantizará que la propiedad SecretIdentity de Samurai resultante se rellene y, a continuación, podré ver la identidad mediante la siguiente solicitud:

samurai.SecretIdentity.FullName

EF Core requiere que las propiedades que son entidades de propiedad se rellenen. En la descarga de muestra, verá cómo diseñé el tipo PersonName para satisfacer este requisito.

Clases simples para lecciones simples

Lo que aquí le he mostrado son clases simples que aprovechan algunos de los principales conceptos de una implementación de DDD de la mínima manera para permitirle ver la respuesta de EF Core a esas construcciones. Ha observado que EF Core 2.0 puede comprender las relaciones uno a uno unidireccionales. Puede persistir datos de entidades donde las propiedades escalares, de navegación y de colección estén completamente encapsuladas. Además de permitirle usar los objetos de valor de su modelo de dominio, es capaz de mantenerlos.

Para este artículo, he mantenido las clases simples y carentes de la lógica adicional que restringe mejor las entidades y los objetos de valor mediante patrones de DDD. Esta simplicidad se refleja en la muestra de descarga, que también está disponible en GitHub en bit.ly/2tDRXwi. Allí encontrará tanto la versión simple como una rama avanzada, donde he reforzado este modelo de dominio y aplicado algunas prácticas de DDD adicionales a la raíz agregada (Samurai), su entidad relacionada (Entrance) y el objeto de valor (PersonName) para que pueda ver cómo EF Core 2.0 controla una expresión más realista de un agregado de DDD. En una columna futura, explicaré los patrones avanzados que se aplican a esta rama.

Tenga en cuenta que uso una versión de EF Core 2.0 poco antes de su lanzamiento definitivo. Aunque la mayoría de los comportamientos que he expuesto son sólidos, cabe la posibilidad de ajustes menores antes del lanzamiento de la versión 2.0.0.


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.