domingo, 30 de junio de 2013

[Entity Framework][Code First] Complex Type

 

Introducción


Cuando se modelan entidades suele plantearse la necesidad de definir propiedades que se agrupen para definir un tipo de dato, ejemplo de estos podrían ser los datos de contacto, direcciones, etc

Estos tipos de dato no tienen una entidad por si mismos, o sea no tienen un id o código que los identifique, sino que son parte de otra entidad.

La idea es poder representar algo como lo siguiente

image

Las entidades de proveedores y empleados se asocian a otras que permiten agrupar propiedades bajo un mismo concepto.

Pero a nivel de persistencia la vista es bastante diferente

image

Los campos que se agrupan en entidades como ser Localidad o Contacto ahora son campos individuales en cada tabla.

Existen varias formas de definir este tipo de persistencia en Entity Framework según se la quiere utilizar para una sola entidad o compartir entre varias.

 

Definición de las entidades


Tanto la entidad empleado como el proveedor defienden un grupo de propiedades que representa la localización, pero el proveedor además define un grupo adicional denominado contacto

La definición del proveedor se realiza en Supplier.cs

public class Supplier
{
    public Supplier()
    {
        this.Localization = new Localization();
        this.Contact = new Contact();
    }

    public int Codigo { get; set; }
    public string CompanyName { get; set; }

    public Localization Localization { get; set; }
    public Contact Contact { get; set; }

}

public class Contact
{
    public string ContactName { get; set; }
    public string ContactTitle { get; set; }

    public string Phone { get; set; }
    public string Fax { get; set; }
    public string HomePage { get; set; }

    public bool HasValue
    {
        get
        {
            return this.ContactName != null
                || this.ContactTitle != null
                || this.Phone != null
                || this.Fax != null
                || this.HomePage != null;
        }
    }
}

La definición de empleado se realiza en Employee.cs

public class Employee
{
    public Employee()
    {
        this.Localization = new Localization();
    }

    public int EmployeeID { get; set; }

    public string LastName { get; set; }
    public string FirstName { get; set; }

    public Localization Localization { get; set; }

}

Y ambos hacen uso de la clase que define la localización, definida en LocationComplexType.cs 

public class Localization
{
    public string Address { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }

    public bool HasValue
    {
        get
        {
            return this.Address != null
                || this.City != null
                || this.Region != null
                || this.PostalCode != null
                || this.Country != null;
        }
    }

}

Toda esta separación se realiza para demostrar que hay varias formas forma de separar la clase, se puede realizar físicamente en un .cs o puede definirse junto a la clase que la utiliza.

 

Definición básica del modelo


Si solo definimos las entidades sin ningún otro tipo de especificación.

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
    }

    public DbSet<Supplier> Suppliers { get; set; }
    public DbSet<Employee> Employees { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {

        base.OnModelCreating(modelBuilder);
    }

}

creara los siguiente estructura de datos

image

Aplicara la definición por convención para los campos a los cuales les agregara un prefijo según el tipo complejo donde estén definida la propiedades.

Sin definir prácticamente nada ya obtenemos un modelo de datos en donde EF aplica las convenciones, pero puede que esto no nos guste por lo que a continuación vamos a ver como personalizar la persistencia de la entidad.

 

Definir ComplexType para una sola entidad


Analicemos la entidad Supplier, en la cual se puede especificar la definición del tipo complejo directamente en la entidad, es por eso que se utiliza:

Property(x => x.Contact.ContactName)…

Se accede a la propiedad que define la clase compleja y se mapea cada una de las propiedad.

 

public class SupplierMap : EntityTypeConfiguration<Supplier>
{
    public SupplierMap()
    {
        HasKey(x => x.SupplierID);
        Property(x => x.CompanyName).HasMaxLength(40).IsRequired();


        Property(x => x.Contact.ContactName).HasColumnName("ContactName").HasMaxLength(30);
        Property(x => x.Contact.ContactTitle).HasColumnName("ContactTitle").HasMaxLength(30);
        Property(x => x.Contact.Phone).HasColumnName("Phone").HasMaxLength(24);
        Property(x => x.Contact.Fax).HasColumnName("Fax").HasMaxLength(24);
        Property(x => x.Contact.HomePage).HasColumnName("HomePage").HasColumnType("ntext");

    }
}

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
    }

    public DbSet<Supplier> Suppliers { get; set; }
    public DbSet<Employee> Employees { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new SupplierMap());

        base.OnModelCreating(modelBuilder);
    }

}

 

la línea:

modelBuilder.Configurations.Add(new SupplierMap());

define la clase que tiene las especificaciones del mapping de la entidad

Estas modificaciones cambian el aspecto de la tabla generada en base al modelo

image

 

Definir ComplexType compartido entre entidades


Tanto la entidad Supplier como Employee  definen un tipo complejo en común, definido en la clase Localization.

El mapping de esta entidad puede realizarse de dos formas:

  • Se puede definir mediante ComplexTypeConfiguration<>  en el OnModelCreating()

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
    }

    public DbSet<Supplier> Suppliers { get; set; }
    public DbSet<Employee> Employees { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new SupplierMap());

        ComplexTypeConfiguration<Localization> complexLocalizacion = modelBuilder.ComplexType<Localization>();
        complexLocalizacion.Property(x => x.Address).HasColumnName("Address").HasMaxLength(60);
        complexLocalizacion.Property(x => x.City).HasColumnName("City").HasMaxLength(15);
        complexLocalizacion.Property(x => x.Region).HasColumnName("Region").HasMaxLength(15);
        complexLocalizacion.Property(x => x.PostalCode).HasColumnName("PostalCode").HasMaxLength(10);
        complexLocalizacion.Property(x => x.Country).HasColumnName("Country").HasMaxLength(15);

        modelBuilder.Configurations.Add(new EmployeeMap());

        base.OnModelCreating(modelBuilder);
    }

}
  • Definir una clase de mapping que especifique la configuración

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
    }

    public DbSet<Supplier> Suppliers { get; set; }
    public DbSet<Employee> Employees { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new SupplierMap());

        modelBuilder.Configurations.Add(new LocationComplexMap());

        modelBuilder.Configurations.Add(new EmployeeMap());

        base.OnModelCreating(modelBuilder);
    }

}
public class LocationComplexMap : EntityTypeConfiguration<Localization>
{
    public LocationComplexMap()
    {
        Property(x => x.Address).HasColumnName("Address").HasMaxLength(60);
        Property(x => x.City).HasColumnName("City").HasMaxLength(15);
        Property(x => x.Region).HasColumnName("Region").HasMaxLength(15);
        Property(x => x.PostalCode).HasColumnName("PostalCode").HasMaxLength(10);
        Property(x => x.Country).HasColumnName("Country").HasMaxLength(15);
    }
}

 

Por medio de la línea:

modelBuilder.Configurations.Add(new LocationComplexMap());

se asocia la clase de mapping con la definición del contexto

La ejecución del Test generara en la base de datos las tablas con al estructura y tipos de datos que buscamos:

image

 

Test - Recuperar todos los proveedores


Los diferentes test que confeccionemos nos ayudara a validar la definición del mapping de cada entidad

Recuperamos la lista de todos los proveedores

SNAGHTML52fcba0f

La ejecución del test inserta un nuevo proveedor

image

Y luego lo recupera

image

Se puede ver a simple fácilmente como se incluye en las queries los campos que forman las entidad complejas.

 

Test - Recuperar un solo empleado


En el test del empleado se crea una nueva entidad empleado, esto luego se recupera para validar que los campos son correctamente mapeados contra la tabla.

 

image

image

Al igual que en el test anterior la consulta filtra por el id de la entidad que se quiere recuperar incluyendo los campos que conforman la entidad mas los que definen los tipos complejos.

image

 

Código


Se utiliza Visual Studio 2012, la base de datos es creada por el mismo Entity Framework cuando se ejecutan los test

[C#]
 

sábado, 29 de junio de 2013

[Entity Framework][Code First] Crear entidad simple

 

Introducción


Este artículo sea el inicio de varios donde iremos analizando paso a paso las diferentes alternativas que Entity Framework nos brinda en su modalidad Code First.

Partiremos de ejemplos simples hasta analizar implementaciones mas complejas, evaluando como se generan las consultas que impactaran en la base de datos.

Haremos uso de la estructura planteada por la db NorthWind, agregándole algunas modificaciones para poder estudiar algunos casos no contemplados en el modelo original, como ser el caso de la herencia.

Comenzaremos armando la estructura del proyecto, el cual no contara con ninguna interfaz grafica ya que haremos uso de proyectos de Test para aplicar validar la lógica que permite recuperar los datos.

También armaremos el código haciendo uso del concepto de Repository dejando encapsulado la funcionalidad de Entity Framework a un solo proyecto.

 

Incluir librería Proyecto


La estructura esta formado por 3 proyecto:

  • Test, el cual permitirá probar la funcionalidad y evaluar el correcto desarrollado
  • DataAccess, donde se definen las clases repository, así como también el contexto que requiere Code First para mapear las entidades
  • Entities, define las clases que representan las entidades de negocio

image

en la capa de DataAccess es donde necesitaremos referenciar la librería de Entity Framework, para ello haremos uso de NuGet

SNAGHTML467f868

 

SNAGHTML65f603

Una vez instalada la librería podremos verla como referencia en el proyecto

SNAGHTML4690fdf

 

Definir conexión a la Base de Datos


Para poder usar EF debemos definir la cadena de conexión en un archivo App.config o Web.config según sea el tipo de proyecto que se este usando, en este caso al ejecutar desde un Test será un App.config con el siguiente contenido:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <connectionStrings>
  
    <add name="NorthwindDb"
          connectionString="Data Source=(local);Initial Catalog=Northwind;Integrated Security=SSPI;"
          providerName="System.Data.SqlClient"/>
  </connectionStrings>
  
</configuration>

 

El name de la key de la conexión se utilizara en la definición de la clase del contexto.

SNAGHTML341d896c

En este caso vamos a utilizar el servicio de Sql Server y no un attach dinámico porque dejaremos que el modelo definido en EF genere la base de datos, de esta forma se podrá analizar la estructura de tablas resultante.

Si la base de datos existe previamente (salvo que se lo indique lo contrario) usara esa db, sino la creara basándose en la estructura definida en el modelo.

 

Definición del contexto


El proyecto encargado de definir la persistencia será: NorthWind.DataAccess

Empezaremos creando la clase de contexto: NorthWindDataContext

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
    }

    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        

        base.OnModelCreating(modelBuilder);
    }

}

La clase hereda de DbContext, y define en el constructor la key del archivo de configuración que usara para tomar la conexión a la base de datos.

Por cada entidad que necesitemos mapear con una tabla se creara una propiedad cuyo tipo será DbSet<>

Dejamos preparado el contexto con la definición del OnModelCreating donde podremos personalizar el mapeo de la entidad, esto lo veremos mas adelante.

 

Definición del Repositorio Genérico


La clase del repositorio permitirá definir las acciones sobre la persistencia que serán comunes para todas las entidades.

Comenzaremos definiendo la interfaz del repositorio:

interface IRepository<T> where T:class
{
    List<T> GetAll();
    List<T> GetAll(List<Expression<Func<T, object>>> includes);

    T Single(Expression<Func<T, bool>> predicate);
    T Single(Expression<Func<T, bool>> predicate, List<Expression<Func<T, object>>> includes);

    List<T> Filter(Expression<Func<T, bool>> predicate);
    List<T> Filter(Expression<Func<T, bool>> predicate, List<Expression<Func<T, object>>> includes);

    void Create(T entity);
    void Update(T entity);

    void Delete(T entity);
    void Delete(Expression<Func<T, bool>> predicate);
}

La implementación base

public abstract class BaseRepository<T> : IRepository<T> where T:class
{

    public List<T> GetAll()
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return (List<T>)context.Set<T>().ToList();
        }
    }

    public List<T> GetAll(List<Expression<Func<T, object>>> includes)
    {
        List<string> includelist = new List<string>();

        foreach (var item in includes)
        {
            MemberExpression body = item.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("The body must be a member expression");

            includelist.Add(body.Member.Name);
        }

        using (NorthWindContext context = new NorthWindContext())
        {
            DbQuery<T> query = context.Set<T>();

            includelist.ForEach(x => query = query.Include(x));

            return (List<T>)query.ToList();
        }

    }


    public T Single(Expression<Func<T, bool>> predicate)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return context.Set<T>().FirstOrDefault(predicate);
        }
    }

    public T Single(Expression<Func<T, bool>> predicate, List<Expression<Func<T, object>>> includes)
    {
        List<string> includelist = new List<string>();

        foreach (var item in includes)
        {
            MemberExpression body = item.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("The body must be a member expression");

            includelist.Add(body.Member.Name);
        }

        using (NorthWindContext context = new NorthWindContext())
        {
            DbQuery<T> query = context.Set<T>();

            includelist.ForEach(x => query = query.Include(x));

            return query.FirstOrDefault(predicate);
        }
    }


    public List<T> Filter(Expression<Func<T, bool>> predicate)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return (List<T>)context.Set<T>().Where(predicate).ToList();
        }
    }

    public List<T> Filter(Expression<Func<T, bool>> predicate, List<Expression<Func<T, object>>> includes)
    {
        List<string> includelist = new List<string>();

        foreach (var item in includes)
        {
            MemberExpression body = item.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("The body must be a member expression");

            includelist.Add(body.Member.Name);
        }

        using (NorthWindContext context = new NorthWindContext())
        {
            DbQuery<T> query = context.Set<T>();

            includelist.ForEach(x => query = query.Include(x));

            return (List<T>)query.Where(predicate).ToList();
        }
    }
    

    public void Create(T entity)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            context.Set<T>().Add(entity);
            context.SaveChanges();
        }
    }

    public void Update(T entity)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            context.Entry(entity).State = EntityState.Modified;
            context.SaveChanges();
        }
    }

    public void Delete(T entity)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            context.Entry(entity).State = EntityState.Deleted;
            context.SaveChanges();
        }
    }

    public void Delete(Expression<Func<T, bool>> predicate)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            var entities = context.Set<T>().Where(predicate).ToList();
            entities.ForEach(x => context.Entry(x).State = EntityState.Deleted);
            context.SaveChanges();
        }
    }

}

 

Como se puede observar en este caso el repositorio define un contexto fijo para no complicar la implementación, pero se podría hacer uso de algún framework de IoC para inyectar el contexto ha utilizar.

 

Creación de una entidad simple


Para empezar vamos a hacer uso de una entidad muy simple, definiéndola en el proyecto: NorthWind.Entities

Se trata de una entidad que define categorías

image

public class Category
{
    public int CategoryID { get; set; }

    public string CategoryName { get; set; }
    public string Description { get; set; }

}

Definición del Repositorio especifico para al entidad


Es necesario implementar un repositorio para la entidad haciendo uso del base.

 

public interface ICategoryRepository
{
    Category GetById(int categoryID);
}

public class CategoryRepository : BaseRepository<Category>, ICategoryRepository
{

    public Category GetById(int categoryID)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return context.Set<Category>().FirstOrDefault(x => x.CategoryID == categoryID);
        }

    }

}

En esta entidad se puede especializar funcionalidad que se necesites puntualmente, en el ejemplo se implemento un método que devuelve la entidad según su id, el método es definido no solo en la clase concreta del repositorio, sino que se especifica una interfaz ICategoryRepository para permitir cambiar la implementación en caso de ser necesario (o si se utiliza algún IoC).

Por supuesto siempre existen alternativas, el método GetById() podría haberse omitido ya que el mismo dato podría haberse recuperado mediante la el método Single() que define el repositorio base, utilizando:

CategoryRepository repository = new CategoryRepository();

Category category = repository.Simple(x=>x.CategoryID == categoryId);

Ese método GetById() es solo un ejemplo que pretende demostrar como se puede extender la funcionalidad definida en el repositorio base, en este no se definieron los métodos como virtual para poder sobrescribirlos, pero podría hacerse sin inconveniente.

 

Test de categorías


Para validar el funcionamiento de lo codificado hasta el momento crearemos un test que nos ayude.

En el proyecto EF.Test definimos la clase CategoryTest

[TestMethod]
public void Get_All_Category()
{
    CategoryRepository repoCategory = new CategoryRepository();

    Category categoryNew = new Category()
    {
        CategoryName = "CatName1",
        Description = "Desc1"
    };
    repoCategory.Create(categoryNew);

    var categoryList = repoCategory.GetAll();

    Assert.AreEqual(categoryList.Count, 1);
}

Al ejecutar el Test si la base de dato no existe EF la creara por nosotros, en este caso la tabla de categorías define los campos:

image

EF define convenciones que pueden utilizarse para no tener que configurar nada de la entidad, por ejemplo si definimos una propiedad que lleve el nombre de la entidad mas ID, esta automáticamente será tomada como clave.

 

Cambiar estructura tabla


Cualquier diferencia con lo especificado en la conversión requiere definición, es aquí donde entra en juego el OnModelCreating.

En la siguiente código se agrega al contexto la especificación a las propiedades de la entidad

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
    }

    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CategoryMap());

        base.OnModelCreating(modelBuilder);
    }

}


public class CategoryMap : EntityTypeConfiguration<Category>
{
    public CategoryMap()
    {
        ToTable("Categories");

        HasKey(c => c.CategoryID);
        Property(c => c.CategoryID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        Property(c => c.CategoryName).IsRequired().HasMaxLength(15);
        Property(c => c.Description).HasColumnType("ntext");

    }
}

Al ejecutar nuevamente el Test la definición de la tabla habrá cambiado:

image

En este caso especificar la clave como identity es redundante, ya que por convención EF puede deducirlo, aunque es bueno conocer las opciones de mapping para los casos en donde no se ajuste a la convención.

 

Análisis de las consulta (Sql Profiler)


Para poder validar las consultas que Entity Framework genera contra la base de datos será necesario utilizar el Sql Profiler

Una vez ejecutada la aplicación el primer paso crea un nuevo Trace

image

En el cuadro de configuración podremos definir un nombre del trace, además de los filtros y columnas de información que necesitemos para realizar el seguimiento, en este el combo “Use the template” se selecciono un témplate que solo nos muestre los datos relativos a las consultas sql

SNAGHTML70825f7c

También se define el nombre de la base de datos que necesitamos analizar, esto lo conseguimos mediante la definición de un filtro.

SNAGHTML7074e5f9

El ultimo paso solo será inicia el trace con el botón “Run”

 

Obtener todas las entidades


Empezaremos definiendo un test que no permita validar el recuperar todas las entidades registradas.

[TestMethod]
public void Get_All_Category()
{
    CategoryRepository repoCategory = new CategoryRepository();

    Category categoryNew = new Category()
    {
        CategoryName = "CatName1",
        Description = "Desc1"
    };
    repoCategory.Create(categoryNew);

    var categoryList = repoCategory.GetAll();

    Assert.AreEqual(categoryList.Count, 1);
}

si analizamos en el Sql Profiler veremos que EF genera la query

image

En el ejemplo se instancia al repositorio directamente y se utilizan los métodos para crear o recuperar la entidad.

 

Obtener una entidad por el ID


Se hará uso del método Single() definido en el repository indicando el lambda que filtrara la entidad por el ID

 

[TestMethod]
public void Get_SingleById_Category()
{
    CategoryRepository repoCategory = new CategoryRepository();

    Category categoryNew = new Category()
    {
        CategoryName = "CatName1",
        Description = "Desc1"
    };
    repoCategory.Create(categoryNew);

    var categorySel = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID);

    Assert.IsNotNull(categorySel);
    Assert.AreEqual(categorySel.CategoryID, categoryNew.CategoryID);
    Assert.AreEqual(categorySel.CategoryName, categoryNew.CategoryName);
    Assert.AreEqual(categorySel.Description, categoryNew.Description);

}

La query que genera EF en el profiler será

image

Se puede apreciar claramente la definición del filtro en el WHERE al cual se le asigna el parámetro del Id

 

Eliminar una entidad


Para eliminare la entidad simplemente hace falta definir el id de la misma, no es obligatorio recuperar la entidad para poder eliminarla

En este caso como estamos en un mismo test method donde se crea y elimina en una misma secuencial para poder validar el código no tenga tanto sentido, pero es necesario entender que con solo crear una nueva entidad en donde solo se asigne el id alcanza para poder llevar a cabo la finalidad.

 

[TestMethod]
public void Delete_ById_Category()
{
    CategoryRepository repoCategory = new CategoryRepository();

    //
    //creamos una nueva categoria
    //
    Category categoryNew = new Category()
    {
        CategoryName = "CatName2",
        Description = "Desc2"
    };
    repoCategory.Create(categoryNew);

    //
    //la eliminamos
    //
    repoCategory.Delete(new Category() { CategoryID = categoryNew.CategoryID });

    //
    // se recupera para validar que no exista
    //
    var categorySel = repoCategory.GetById(categoryNew.CategoryID);

    Assert.IsNull(categorySel);

}

En la definición del test se crea una entidad nueva, pero solo se utiliza el id para crear una entidad nueva que se adjunta al contexto para poder asignar el estado

context.Entry(entity).State = EntityState.Deleted;

 

analizando con el profiler podremos ver como EF genera la instrucción sql para eliminar el registro

image

 

Código


El código fue confeccionado con Visual Studio 2012, utilizando Entity Framework 5

La base de datos es creada de forma automática por el mismo Entity Framework, solo es necesario definir el connectionstring al servicio de sql server

[C#]