Como mapear los Objetos-Valor de Dominio (DDD) en Sql Server con Entity Framework

miércoles, 22 de enero de 2014

Logo Entity Framework

Cuando diseñamos entidades siguiendo un modelo orientado al dominio (DDD) es frecuente que nos encontremos la circunstancia de crear Aggregate Roots como puede ser una entidad User que contiene otros tipos de objetos como pueden ser Entidades o ValueObjects, un ejemplo de Value Object bastante frecuente es el de un objeto dirección.

Normalmente un Object Value se persiste en base datos como campos en la tabla de su entidad padre.

Vamos a ver de que forma podemos hacer esto con Entity Framework para SQL Server.

Tenemos los siguientes objetivos con el ejemplo:

1 - Persistir dos objetos en relación padre e hijo como una sola tabla en base datos.
2 - Según un diseño orientado al dominio, una entidad no debería permitir modificar sus propiedades directamente, sino que debería tener métodos en sintonía con el Lenguaje Obicuo para modificar su estado.

Para el ejemplo vamos a tener User como Entidad y un Address como Value Object.


Para conseguir el punto 1, en Entity Framework tenemos lo que se llaman Complex Type que nos sirve perfectamente, podemos utilizar EF Code First que se basa en una serie de convecciones.

Para la detectar la clave primaria de la tabla User a crear, se basa  en la convección nombre de entidad + Id, de esta forma identifica UserId como clave primaria

Para identificar un Value Object como Complex Type se basa en la convección de que la clase debe de ser hija de una entidad, no tener ninguna referencia a su entidad padre, y que no tenga ninguna primary key. De esta forma identifica el objeto Address como Complex Type, creara sus propiedades como campos de la tabla User

Para conseguir el punto 2, en Entity Framework es necesario que existan propiedades para poder mapear una clase de modelo de dominio con una tabla de base de datos, y así poder crear un objeto en base a los valores de una fila de la base de datos, sin embargo, el accesor set no tiene porque ser público, podemos ponerlo como privado y EF será capaz de asignarle valor a sus propiedades cuando se recuperen de base de datos. Además tendremos que poner un constructor sin parámetros, pero este tampoco es necesario que sea publico, puede ser Internal o Private.

Nos creamos las clases User y Address en base a las convecciones que hemos mencionado antes:

    
public class Address 
    {
        public Address(string street1, string street2, string city, string region, string country, string postalCode)
        {
            Street1 = street1;
            Street2 = street2;
            City = city;
            Region = region;
            Country = country;
            PostalCode = postalCode;
        }

        internal Address()
        {
        }

        public string Street1 { get; private set; }
        public string Street2 { get; private set; }
        public string City { get; private set; }
        public string Region { get; private set; }
        public string Country { get; private set; }
        public string PostalCode { get; private set; }

        //mre
    }

    public class User
    {
        public User(string userName)
        {
            UserName = userName;
        }

        internal User()
        { 
        }

        public int UserId { get; private set; }

        public string UserName { get; private set; }

        public Address Address { get; private set; }

        public void ChangeUserName(string newUserName)
        {
            UserName = newUserName;
        }

        public void CreateNewAddress(string street1, string street2, string city, string region, string country, string postalCode)
        {
            Address = new Address(
              street1, street2,
              city, region,
              country, postalCode);
        }
    }

Ahora tenemos que crearnos el contexto de Entity Framework con un dbSet para User, de esta forma podremos acceder a los usuarios de base de datos desde la clase repositorio y también EF indentifica que tiene que realizar un mapeo de la Entidad User con la base de datos.

    
    public class Context:DbContext 
    {
        public Context(): base()
        {
            
        }
        public DbSet<User> Users { get; set; }
    }

Ahora nos creamos la clase repository.

    public class UserRepository:IUserRepository
    {
        public void Add(Domain.Model.User user)
        {
            using (var context = new Context())
            {
                try
                {
                    context.Users.Add(user);
                    context.SaveChanges();
                }
                catch (Exception ex)
                {                    
                    throw;
                }
            }
        }
    }

Y por último nos creamos el test, que simplemente crea un usuario, le añade una dirección y lo persiste en base datos mediante el repositorio. Creamos un método estático con el atributo ClassInitialize que se ejecuta una vez antes de lanzar el test y le indicamos a Entity Framework que elimine y cree la base de datos si el modelo cambia.

    [TestClass]
    public class UnitTest1
    {
        [ClassInitialize]
        public static void ClassInit(TestContext context)
        {
            System.Data.Entity.Database.SetInitializer(
                new System.Data.Entity.DropCreateDatabaseIfModelChanges<EFComplexTypeTest.Infrastructure.Data.Context>());
        }


        [TestMethod]
        public void AddUserJM()
        {
            User user = new User("EJ");
            user.CreateNewAddress("Calle ejemplo", "", "Madrid", "Madrid", "España", "28000");

            EFComplexTypeTest.Infrastructure.Data.UserRepository repository =
                new EFComplexTypeTest.Infrastructure.Data.UserRepository();
            repository.Add(user);
        }
    }

Al pasar el test, dbContext se encarga de crear la base de datos automáticamente por convección en la instancia LocalDB.

El nombre de la base de datos que va a crear esta basado en la clase contexto, en este ejemplo será EFComplexTypeTest.Infrastructure.Data.Context. Por defecto las columnas que va a crear en la tabla usuario correspondientes al Value Object Address serán Address_Street1,Address_Street2 etc..,
esta convección se puede saltar sobreescribiendo en el contexto el método OnModelCreating y utilizando DbModelBuilder que se pasa por parámetro podemos especificar que nombres de campos queremos que cree para cada propiedad del Complex Type.
    
public class Context:DbContext 
    {
        public Context(): base()
        {
            
        }
        public DbSet<User> Users { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.ComplexType<Address>
().Property(a => a.City).HasColumnName("City");
            modelBuilder.ComplexType<Address>
().Property(a => a.Country).HasColumnName("Country");
            modelBuilder.ComplexType<Address>
().Property(a => a.PostalCode).HasColumnName("PostalCode");
            modelBuilder.ComplexType<Address>
().Property(a => a.Region).HasColumnName("Region");
            modelBuilder.ComplexType<Address>
().Property(a => a.Street1).HasColumnName("Street1");
            modelBuilder.ComplexType<Address>
().Property(a => a.Street2).HasColumnName("Street2");
        }
    }

Libros Relacionados

Programming Entity Framework: DbContext

Programming Entity Framework: Code First

No hay comentarios:

Publicar un comentario