sábado, 28 de diciembre de 2013

[ASP.NET MVC] Carga DropDownList dependientes - Usando un único action

 

Introducción


Este articulo propone una versión diferente a la planteada en el anterior

[ASP.NET MVC] Carga DropDownList dependientes

En este caso definiremos un único action para recibir la selección que realice el usuario.

La funcionalidad de la pantalla se conserva idéntico al articulo anterior, al igual que el modelo de datos.

 

Modelo de Vista


Para el modelo se define una clase que recibirá las propiedades con la selección del usuario, además de propiedades que permitirán cargar los controles de listas en la view

image

 

Definición de la View


La vista esta formada por un solo Form, el cual realiza el post al action Index() el cual se encuantra marcado con el atributo [HttpPost]

SNAGHTML19f9df10

 

Tanto si se selecciona un ítem combo, o se presiona el botón del submit (el cual selecciona los empleados) todas las acciones invocan al único action

A diferencia del articulo anterior ya no se requieren de hidden para conservar la selección de la acción anterior.

 

Definición del action


El action implementar bastante mas lógica que la usada en el articulo anterior, ya que deberá determinar que propiedades vienen con datos y cargar las listas en consecuencia.

 

[HttpPost]
public ActionResult Index(EmployeeModel model)
{
    //se carga la lista de regiones
    var regionList = regionRepository.GetAll();

    List<Territory> territoryList = null;
    List<Employee> employeeList = null;

    //si hay una region seleccionada 
    if (model.RegionId != null)
    {
        //se carga la lista de territorios
        territoryList = territoryRepository.Filter(x => x.RegionID == model.RegionId);

        //si hay un territorio seleccionado
        if (!string.IsNullOrEmpty(model.TerritoryId))
        {
            //se recupera el territorio, para poder obtener los empleados asociados
            employeeList = territoryRepository.GetEmployees(model.TerritoryId);
        }

    }


    model.RegionList = new SelectList(regionList, "RegionID", "Description");

    if (territoryList != null && territoryList.Count > 0)
    {
        model.TerritoryList = new SelectList(territoryList, "TerritoryID", "Description");
    }

    if (employeeList != null && employeeList.Count > 0)
    {
        model.EmployeeList = new SelectList(employeeList, "EmployeeID", "FullName");
    }


    return View("Index", model);

}

 

Si se selecciona una región se recibirá el valor en el parámetro del modelo

SNAGHTML1a04b6aa

 

Si luego se selecciona un territorio

SNAGHTML1a08bf80

 

Y al marcar empleados sobre el listbox y presionar el botón de submit se carga la lista

SNAGHTML1a0b84e0

 

En cada invocación al acción será necesario volver a cargar las listas del model utilizadas para definir los ítems de los controles la la view, ya que estas no participan en el proceso de binding donde son asignados los valores que se envían en al post a las propiedades del parámetro del action.

 

Model Binding


Para entender como se produce la asignación de la selección de los controles con las propiedades del modelo es necesario evaluar el html

Si las propiedades “name” de los controles que se renderizan en el html coinciden con los nombres de las propiedades de la clase definida como modelo la asignación del valor es automáticamente.

 

SNAGHTML1a315f77

si algún control no llegara a tener esta coincidencia se puede redefinir el name utilizando

image

 

Código


 

[C#]
 

viernes, 27 de diciembre de 2013

[ASP.NET MVC] Carga DropDownList dependientes (cascada o secuencial)

 

Introducción


Es muy común en las aplicaciones necesitar de controles que dependan unos de otros para cargar los ítems que serán desplegados al usuario.

En este articulo explicare como llevar a cabo esta tarea implementando la carga en cascada de combos y listas, además poder tomar en cada post la selección que realiza el usuario.

El ejemplo permitirá seleccionar una región la cual será utilizada como filtro para cargar los territorios, este ultimo cargar una lista de empleados, el usuario seleccionara uno o mas de la lista.

 

image

 

Modelo de datos


Haremos uso del siguiente modelo de datos el cual define las clases que serán mapeadas a las tablas utilizando Entity Framework

Vemos como interactúan las entidades para permitir la asociaciones que las vinculan.

image

 

Modelo de Vista


Para poder interactuar con al view de mvc creamos una clase que actuara de modelo, esta nos permitirá definir los ítems de las listas que desplegaremos al usuario.

En el modelo tendremos propiedades simples que contendrán la selección del usuario y también lista que nos permitirán cargar los dropdownlist y listbox que presentaremos al usuario.

 

image

Las propiedades de lista se definen del tipo SelectList, de esta forma desde el controlador indicamos cuales serán las propiedades que usara la lista como value y display en el dropdownlist

 

Lista de Regiones


Al iniciar la aplicación el primer controlador que se ejecuta es el Index(), en este invocamos al repositorio para obtener la lista de regiones

image

 

Creamos una instancia de la clase Model que el view requiere, a esta le asignamos la lista de regiones que mostrara en el combo

En la view se carga el dropdownlist y se controla con la ayuda de jquery cuando el usuario cambia la selección, lo cual produce el post del form.

En la imagen podemos ver como se relacionan los id del dropdownlist y del form con el evento change() de jquery para generar el submit al action del controlador

 

SNAGHTML146b09bb

 

Lista de Territorios


Al seleccionar una región se invoca el action definido en el BeginForm()

SNAGHTML14776cd1 

Si hacemos coincidir el name del control dropdownlist con el del parámetro del action este recibirá el valor. Usaremos la selección para filtrar la lista de territorios.

 

Lista de Empleados


Al seleccionar un ítem del combo de territorios se dispara el submit del forma que invoca el action del controlador, en este caso se pasaran como argumento el id de la región y del territorio para poder filtrar los datos y cargar las listas

 

SNAGHTML14cd3b57

Seguramente se habrá notado la definición de controles Hidden que permiten enviar en cada submit del form los datos seleccionados en una acción anterior

 

Selección de empleados


Cuando se selecciona los empleados de la lista y se presiona el botón se realiza el submit enviando la la selección del listbox al action

SNAGHTML14d57876

En cada invocación a los action se vuelve a crear una instancia del model asignando la selección y cargando las listas que requiere el view

 

Código


Se utilizo Visual Studio 2012, la base de datos de encuentra en la carpeta App_Data, se utilizo Sql Server Express 2012

[C#]
 

domingo, 3 de noviembre de 2013

[ASP.NET MVC] WebGrid Filtrado por DropDownList

 

Introducción


En el articulo aprenderemos :

  • como capturar la selección de un DropDownList
  • filtrar una query usando Entity Framework
  • desplegar los datos en un WebGrid

La idea es listas en un dropdownlist los clientes (customers) y al seleccionar alguno, listar las ordenes de compra realizadas.

image

 

Modelo de Dominio


Empezaremos analizando el dominio de entidades que usaremos en el ejemplo.

image

La entidad Orden tiene relación con el cliente (customer) y un empleado (employee), usaremos el primero para filtrar mientras que el segundo será información adicional que se mostrara en una columna del grid

 

Definición de la persistencia


El acceso a los datos lo realizaremos con al ayuda de Entity Framework, por lo que deberemos definir el DbContext y las clases de mapping para cada entidad

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
        Database.SetInitializer<NorthWindContext>(null);

        this.Configuration.LazyLoadingEnabled = false;
        this.Configuration.ProxyCreationEnabled = false;
    }

    public DbSet<Customer> Employees { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerMap());
        modelBuilder.Configurations.Add(new EmployeeMap());
        modelBuilder.Configurations.Add(new OrderMap());

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

        base.OnModelCreating(modelBuilder);
    }

}


public class CustomerMap : EntityTypeConfiguration<Customer>
{
    public CustomerMap()
    {
        HasKey(x => x.CustomerID);

        Property(x => x.CustomerID)
                .HasColumnType("nchar")
                .HasMaxLength(5);


        Property(x => x.CompanyName)
                .HasMaxLength(40)
                .IsRequired();

        Property(x => x.ContactName)
                .HasMaxLength(30);

        Property(x => x.ContactTitle)
                .HasMaxLength(30);

    }
}

public class EmployeeMap : EntityTypeConfiguration<Employee>
{
    public EmployeeMap()
    {
        HasKey(x => x.EmployeeID);

        Property(x => x.LastName)
                .HasMaxLength(20)
                .IsRequired(); ;

        Property(x => x.FirstName)
                .HasMaxLength(10)
                .IsRequired();

        Ignore(x => x.FullName);

    }
}

public class OrderMap : EntityTypeConfiguration<Order>
{
    public OrderMap()
    {
        HasKey(x => x.OrderID);

        Property(x => x.CustomerID)
            .HasColumnType("nchar")
            .HasMaxLength(5)
            .IsOptional();

        HasOptional(x => x.Customer)
                .WithMany()
                .HasForeignKey(x => x.CustomerID);

        HasOptional(x => x.Employee)
                .WithMany()
                .HasForeignKey(x => x.EmployeeID);

        Property(x => x.ShipName)
                .HasMaxLength(40);

        Property(x => x.ShipAddress)
                .HasMaxLength(60);

    }
}


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);
    }
}

 

En este caso estamos utilizando una db existente por lo tanto importante definir la línea:

Database.SetInitializer<NorthWindContext>(null);

de esta forma indicamos que no se debe crear la db cuando ejecutemos la aplicacion

 

Definir datos del dropdownlist


Para la vista definiremos un modelo que nos permita especificar los datos que mostraremos al usuario.

public class CustomerOrdersModel
{

    public SelectList CustomerList { get; set; }

    public List<Order> OrderList { get; set; }

}

 

Es por eso que encontraremos dos listas, una para cargar los ítems del combo y la segunda para las ordenes del grid.

Al ingresar al action Index(), el cual responde a los pedidos GET, del controlador debemos cargar la lista del combo, nos ayudaremos con el repositorio para esta tarea.

public ActionResult Index()
{

    CustomerOrdersModel model = new CustomerOrdersModel()
    {
        CustomerList = new SelectList(repository.GetAllWithoutSelection(), "CustomerID", "CompanyName")
    };

    return View(model);
}

 

la vista hará uso de las propiedades del modelo para cargar el dropdownlist

SNAGHTML978badb8

 

Capturar selección dropdownlist


La captura de la selección puede realizarse de varias formas, pero no debe perderse de vista que es la propiedad “name” de los controles html los que participan en el post que se realiza al servidor, no hay que confundirse con la propiedad “id” esta solo es útil para manipular el control en el cliente usando jquery.

Para capturar la acción del usuario al cambiar la selección en el dropdownlist nos ayudamos de jquery, para poder definir el selector definimos un id en el tag <form>

SNAGHTML97918cef

Es en ese momento cuando forzamos un submit para invocar a un action concreto

image

en el controlador encontraremos

[HttpPost]
public ActionResult Index(string CustomerList)
{
    //
    // usamos el metodo del repositorio el cual nos permite recuperar las odenes de compra del cliente
    // se incluye en esta la informacion del cliente y el empleaod que realizo la venta
    //
    var orders = orderRepository.Filter(x => x.CustomerID == CustomerList,
                                            new List<Expression<Func<Entities.Order,object>>>() { x=>x.Employee, x=>x.Customer });

    CustomerOrdersModel model = new CustomerOrdersModel()
    {
        CustomerList = new SelectList(repository.GetAllWithoutSelection(), "CustomerID", "CompanyName"),
        OrderList = orders
    };


    return View(model);
}

 

El nombre del parámetro debe coincidir con la propiedad “name” del control para poder tomar el dato, en este caso hemos dejado el valor por defecto que crear asp.net mvc, pero podríamos redefinirlo.

SNAGHTML97af81c9

Si en la vista cambiamos la propiedad “name” debemos también hacerlo en el parámetro del action.

SNAGHTML97a1f0d0

 

 

Definición del WebGrid


El post al action devolverá un nuevo modelo con la lista de ordenes cargada

image

En la definición del grid es cuando indicamos que propiedad del modelo define los datos

var grid = new WebGrid(Model.OrderList, canPage: false, canSort: false);

luego se define el lugar donde ira el grid y que columnas se mostrara

 

@if (Model.OrderList != null)
{
    @grid.GetHtml(
                tableStyle: "webgrid-table",
                headerStyle: "webgrid-head",
                alternatingRowStyle: "webgrid-alternating-row",
                selectedRowStyle: "webgrid-selected-row",
                
                columns: grid.Columns(
                    grid.Column(columnName: "Employee.FullName", header: "Empleado", style: "webgrid-column-employee"),
                    grid.Column(columnName: "OrderDate", header: "Fecha Pedido", format: x=>x.OrderDate.ToString("dd/MM/yyyy")),
                    grid.Column(columnName: "RequiredDate", header: "Fecha Requisito", format: x=>x.RequiredDate.ToString("dd/MM/yyyy")),
                    grid.Column(columnName: "ShippedDate", header: "Fecha Envio", format: x=>x.ShippedDate != null ? x.ShippedDate.ToString("dd/MM/yyyy") : ""),
                    grid.Column(columnName: "ShipName", header: "Nombre Envio"),
                    grid.Column(columnName: "ShipAddress", header: "Direccion Envio") 
                
                ))
}
else
{
    <p>No hay datos</p>
}

se podría resaltar como se especifica una columna cuando la entidad esta relacionada con otra

grid.Column(columnName: "Employee.FullName", header: "Empleado", style: "webgrid-column-employee"),

al necesitar el nombre del empleado se realiza por medio de la relación

También se definió formato para las columnas fecha

grid.Column(columnName: "ShippedDate", header: "Fecha Envio", format: x=>x.ShippedDate != null ? x.ShippedDate.ToString("dd/MM/yyyy") : ""),

no solo indicando la representación de la fecha sino también validando cuando esta pueda tener un valor nulo

 

WebGrid referencia en asp.net mvc


Según el tipo de proyecto que crees en el Visual Studio puede que no tengas acceso al WebGrid, si este es el caso deberá agregar la referencia a la librería, para esta tarea NuGet nos dará una mano

Solo es cuestión de buscar: microsoft-web-helper

en la herramienta de nuget y proceder a instalarlo

SNAGHTML988cc7c2

 

Código


Para el ejemplo hemos utilizado Visual Studio 2012, la base de datos se encuentra en la carpeta App_Data del proyecto web

[C#]
 

martes, 1 de octubre de 2013

MVP C# 2013

 

Simplemente quería agradecer por ser reconocido por cuarto año.

Esto marca que el camino es el correcto, así que seguiré ayudando y aportando mi granito de arena.

 

Leandro Tuttini - mvp2013

lunes, 9 de septiembre de 2013

[Entity Framework][Code First] Dividir Tabla (Table Splitting)

 

Introducción


Cuando se definen entidades estas pueden contener atributos que no siempre se quiere recuperar, ya que de hacerlo podría afectar la performance. Un ejemplo se observa en aquellos atributos donde se persisten datos de un volumen importante, como ser campos del tipo image, varbinary o ntext, los cuales por lo general no son necesarios recuperar cuando se carga un listado de la entidad en un grid, pero si son necesarios cuando se edita una entidad individual.

Por supuesto no solo esta limitado a este uso, se podría dividir simplemente porque un conjunto de propiedades representan la información clave de la entidad la cual es frecuentemente utilizada, separándola de aquellos atributos donde, salvo en algún uso puntual, no siempre se requieren.

Esta división permite mapear una tabla simple en múltiples entidades.

 

Complex Type Vs Table Splitting

Seguramente se estén preguntando, porque simplemente no se usa un Tipo Complejo? definiendo en este las propiedades que se consideran especiales.

El tema pasa porque los Tipo Complejos no permiten la carga retardada (lazy load), mientras que la división de la tabla en varias entidades si lo permite.

Al recuperar una entidad los tipo complejo serán siempre devueltos, no hay control sobre esta entidad, mientras que una tabla dividida en entidades se le puede indicar cuando se necesita recuperar este bloque de datos.

 

Definición del modelo


El objetivo es permitir separar un conjunto de propiedades en una entidad distinta

image

a pesar de esta dividido en varias entidades todas son mapeadas a la misma tabla, por lo que resulta

image

 

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 virtual Localization Localization { get; set; }

    public virtual EmployeeExtended EmployeeExt { get; set; }

}

public class EmployeeExtended
{
    public int EmployeeID { get; set; }

    public byte[] Photo { get; set; }
    public string PhotoPath { get; set; }

    public string Notes { get; set; }
    
}

La entidad principal define una propiedad que referencia a la entidad extendida, a la vez que ambas entidades definen la misma propiedad como key, ya que por medio de esta se relacionan.

 

Definición del Mapping


Al definir el mapping ambas entidades persisten en la misma tabla, por eso el uso del ToTable(). También hacen uso de la misma key.

El HasRequired() permite indicar cual será la propiedad que representaran los datos extendidos en la entidad principal.

 

public class EmployeeMap : EntityTypeConfiguration<Employee>
{
    public EmployeeMap()
    {
        ToTable("Employees");
        HasKey(x => x.EmployeeID);

        Property(x => x.LastName)
            .HasMaxLength(20)
            .IsRequired();

        Property(x => x.FirstName)
            .HasMaxLength(10)
            .IsRequired();

        HasRequired(x => x.EmployeeExt)
                    .WithRequiredPrincipal();
    }
}

public class EmployeeExtendedMap : EntityTypeConfiguration<EmployeeExtended>
{
    public EmployeeExtendedMap()
    {
        ToTable("Employees");
        HasKey(x => x.EmployeeID);

        Property(x => x.Notes)
            .HasColumnType("ntext")
            .IsOptional();

        Property(x => x.Photo)
            .HasColumnType("image")
            .IsOptional();

        Property(x => x.PhotoPath)
            .HasColumnType("nvarchar")
            .HasMaxLength(255)
            .IsOptional();

    }
}

Definición del Repositorio


Hay cierta funcionalidad que requiere un tratamiento especial para estos casos es que se crea funcionalidad en el repositorio de le entidad empleado.

Se define esta en la interfaz, para luego implementarla en la clase concreta.

 

public interface IEmployeeRepository : IRepository<Employee>
{
    EmployeeExtended GetExtendedById(int id);

    void DeleteIncludeExtended(Employee entity);
}

 

public class EmployeeRepository : BaseRepository<Employee>, IEmployeeRepository
{
    /// <summary>
    /// Recupera solo la entidad Extendida
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public EmployeeExtended GetExtendedById(int id)
    {
        using (NorthWindContext context = new NorthWindContext())
        {
            return context.EmployeeExtendeds.FirstOrDefault(x => x.EmployeeID == id);
        }
    }

    /// <summary>
    /// Elimina la entidad incluyendo la informacion extendida 
    /// en caso de tenerla
    /// </summary>
    /// <param name="entity"></param>
    public void DeleteIncludeExtended(Employee entity)
    {
        using (NorthWindContext context = new NorthWindContext())
        {

            if (entity.EmployeeExt == null)
                entity.EmployeeExt = new EmployeeExtended() { EmployeeID = entity.EmployeeID };

            context.Employees.Attach(entity);
            context.Employees.Remove(entity);

            context.SaveChanges();

        }
    }

}

Estos método serán utilizados en los Test, donde se probara su funcionalidad

 

Configuración del proyecto de Test


Para probar la funcionalidad se requiere que un archivo que representa la foto que será asignada a la propiedad de la entidad. Este archivo forma parte del proyecto de Test, pero debe ser copiado a la carpeta de output.

Sera necesario definir un archivo de TestSetting el cual no ayudara a configurar el entorno de prueba, para ello agregamos este archivo usando la opción

image

 

SNAGHTML4800a637

En este configuramos las opciones de deploy para que copie el archivo de la imagen a la carpeta que defina el test cuando ejecute

SNAGHTML480139af

Asignamos cual será el archivo usado en la ejecución de los test

image

 

SNAGHTML47f6aaaa

 

Test – Eliminar empleado con y sin información extendida


Si eliminamos una entidad haciendo uso del método Delete() del repositorio base obtendremos un error

image

Se produce porque se requiere no solo marcar para eliminar la entidad principal sino también la extendida, por esta razón en el repositorio de la entidad empleado se creo un método especifico para este caso.

El método DeleteIncludeExtended() elimina la entidad y también define la entidad extendida a pesar que no se envíe como dato en la entidad que se asigna como parámetro.

 

[TestMethod]
public void Delete_Employee()
{
    EmployeeRepository repoEmployee = new EmployeeRepository();

    //
    //se crea un empleado con info extendida
    //
    CreateEmployeeWithPhoto();

    //elimina el empleado y su informacion extendida
    repoEmployee.DeleteIncludeExtended(new Employee() { EmployeeID = employeeNew.EmployeeID } );

    //se recupera el empleado que fue eliminado en el paso anterior
    Employee employeeSel = repoEmployee.Single(x => x.EmployeeID == employeeNew.EmployeeID);

    Assert.IsNull(employeeSel);


    //
    //se crean el empleados sin informacion extendida
    //
    Employee employee = new Employee()
    {
        FirstName = "name 1",
        LastName = "lastname 1",
        EmployeeExt = new EmployeeExtended()
    };
    repoEmployee.Create(employee);

    repoEmployee.DeleteIncludeExtended(employee);

    //se recupera el empleado que fue eliminado en el paso anterior
    employeeSel = repoEmployee.Single(x => x.EmployeeID == employee.EmployeeID);

    Assert.IsNull(employeeSel);

}

El test tiene dos partes, la primera crea un empleado con funcionalidad extendida y lo elimina usando el método que hemos creado en el repositorio para este propósito, la segunda parte crea una entidad sin información extendida y también elimina esta entidad, demostrando que sin importar la información con la cual se cree la entidad puede ser eliminada.

Como se vera para ambos casos es necesario definir el id de la entidad extendida para que sea marcada tanto la entidad principal como la extendida para ser eliminadas.

 

Test – Obtener todos los empleados


Se crean dos empleado, uno con información extendida y el otro sin ella.

En el test recupera la lista de empleados, pudiéndose analizarse como la información extendida no se obtiene, mientas que la propiedad que representa el complex type si es devuelta al recuperar la entidad.

Este caso es el típico que uno usaría para cargar un listado o grid con la información base de la entidad.

 

[TestMethod]
public void GetAll_Employee()
{
    string foto = Path.Combine(this.TestContext.DeploymentDirectory, "foto.jpg");

    EmployeeRepository repoEmployee = new EmployeeRepository();

    //se eliminan las entidades que pudieran quedar de la ejecucion de test anteriores
    repoEmployee.GetAll().ForEach(x => repoEmployee.DeleteIncludeExtended(x));


    //se crean dos empleados
    CreateEmployeeWithPhoto();

    Employee employee2 = new Employee()
    {
        FirstName = "name 2",
        LastName = "lastname 2",
        EmployeeExt = new EmployeeExtended()
    };
    repoEmployee.Create(employee2);


    List<Employee> list = repoEmployee.GetAll();

    Assert.AreEqual(list.Count, 2);

    Assert.IsNull(list[0].EmployeeExt);
    Assert.IsNotNull(list[0].Localization);
    Assert.IsTrue(list[0].Localization.HasValue);

    Assert.IsNull(list[1].EmployeeExt);
    Assert.IsNotNull(list[1].Localization);
}

Test – Obtener entidad SIN información extendida


Aunque se haya creado un empleado con información adicional al recuperar la entidad esta no forma parte de los datos.

 

[TestMethod]
public void GetById_Employee()
{
    //se crea el empleado con informacion extendida
    CreateEmployeeWithPhoto();


    //Se recupera la entidad
    Employee employeeSel = repoEmployee.Single(x => x.EmployeeID == employeeNew.EmployeeID);


    Assert.AreEqual(employeeSel.EmployeeID, employeeNew.EmployeeID);
    Assert.IsTrue(employeeSel.Localization.HasValue);
    Assert.AreEqual(employeeSel.Localization.Address, "Address 1");
    Assert.AreEqual(employeeSel.Localization.City, "City 1");
    Assert.AreEqual(employeeSel.Localization.Country, "Country 1");

    Assert.IsNull(employeeSel.EmployeeExt);

}

La consulta recupera el empleado y deja evidencia de la inclusión de los campos que define el complex type pero no se incluyen los de la entidad extendida.

image

 

Test – Obtener entidad CON información extendida


En este caso el test define un “include” de la propiedad extendida, permitiendo recuperar la foto asignada al empleado

 

[TestMethod]
public void GetById_EmployeeExt()
{
    //se crea el empleado con informacion extendida
    CreateEmployeeWithPhoto();

    //se recupera la entidad y sus propiedad extendida
    Employee employeeSel = repoEmployee.Single(x => x.EmployeeID == employeeNew.EmployeeID,
                                                    new List<Expression<Func<Employee, object>>>() 
                                                    { 
                                                        x=>x.EmployeeExt 
                                                    });
                        


    Assert.AreEqual(employeeSel.EmployeeID, employeeNew.EmployeeID);
    Assert.IsTrue(employeeSel.Localization.HasValue);
    Assert.AreEqual(employeeSel.Localization.Address, "Address 1");
    Assert.AreEqual(employeeSel.Localization.City, "City 1");
    Assert.AreEqual(employeeSel.Localization.Country, "Country 1");

    Assert.IsNotNull(employeeSel.EmployeeExt);
    Assert.IsNotNull(employeeSel.EmployeeExt.Photo);
    Assert.AreEqual(employeeSel.EmployeeExt.PhotoPath, employeeNew.EmployeeExt.PhotoPath);

}

la consulta resultante es bastante mas compleja ya que debe unir los campos de la entidad extendida

image

Lo importante a destacar es que uno controla cuando es necesario recuperar los datos extendido para así mejorar la performance de la aplicación.

 

Test – Recuperar SOLO la información extendida


Si ya se dispone de la entidad con los datos básicos y se requiere solo los datos extendidos se podría recuperar únicamente estos.

 

[TestMethod]
public void GetById_Employee_RecoverOnlyExtended()
{

    CreateEmployeeWithPhoto();


    EmployeeExtended employeeSel = repoEmployee.GetExtendedById(employeeNew.EmployeeID);


    Assert.IsNotNull(employeeSel);
    Assert.AreEqual(employeeSel.EmployeeID, employeeNew.EmployeeID);
    Assert.IsNotNull(employeeSel.Photo);
    Assert.AreEqual(employeeSel.PhotoPath, employeeNew.EmployeeExt.PhotoPath);

}

El método GetExtendedById() hace uso de la propiedad del contexto que accede a esta entidad extendida de forma directa.

image

Es la misma consulta que recupera al empleado pero solo define los campos de la entidad extendida.

 

Documentación de referencia


 Associations in EF Code First: Part 4 – Table Splitting

 

Código


Se utilizo Visual Studio 2012, no se adjunta ninguna base de datos, esta será creada al ejecutar los test utilizando el modelo definido en el contexto de EF

 

[C#]
 

viernes, 30 de agosto de 2013

[Entity Framework][Code First] Herencia - Tabla por tipo concreto - Table per Concrete Type (TPC)

 

Introducción


Se continua analizando las diferentes implementaciones de herencia que Entity Framework nos permite modelar.

En este caso cada tipo mapeara a una tabla la cual define todos los campos, ya sean los particulares que este defina, así como los declarados en la clase base.

Este artículo utilizara el mismo modelo de los ejemplos anteriores:

[Entity Framework][Code First] Herencia - Tabla por tipo - Table per Type (TPT)

[Entity Framework][Code First] Herencia - Tabla por jerarquía - Table per Hierarchy (TPH)

 

Definición del modelo


Partiremos de un modelo de clases ya conocido en los artículos anteriores.

image

y obtendremos las tablas

image

La definición de cada clase derivada creara una tabla para si misma, llevando no solo las propiedades que esta defina como columnas sino que también lo hará con las de la clase base.

Un punto que se debe remarcar es que no hay relación entre las tablas.

En este ejemplo la clase Employee se define como “abstract” lo cual impide crear instancias de esta, es por eso que solo se crean dos tablas, si la base no fuera “abstract” se crearía una tercer tabla para soportar los datos de la entidad base.

 

Definición del Mapping


Para cada entidad derivada se define la clase de mapping donde se especifica el nombre de la tabla.

 

public class EmployeeInternalMap : EntityTypeConfiguration<EmployeeInternal>
{
    public EmployeeInternalMap()
    {

        Map(x =>
        {
            x.MapInheritedProperties();
            x.ToTable("InternalEmployee");
        });

      
    }
}

public class EmployeeExternalMap : EntityTypeConfiguration<EmployeeExternal>
{
    public EmployeeExternalMap()
    {

        Map(x =>
        {
            x.MapInheritedProperties();
            x.ToTable("ExternalEmployee");
        });

        Property(x => x.ConsultantName).IsRequired()
                                       .HasColumnType("varchar")
                                       .HasMaxLength(100);
    }
}

Se destaca del mapping la invocación al método MapInheritedProperties(), lo cual aplica un re-mapping las propiedades de la clase base.

image

 

Problema con la key de la tabla


Si los test los diseñamos en base a los artículos anteriores, al ejecutarlos obtendremos un error con la clave primaria.

SNAGHTML23be7cf7

La causa se debe a que este forma de mapear la herencia no define columnas identity para las claves, por medio de código se tendrá que asegurar el id secuencial que se asigna a la entidad cuando esta se persiste.

Para solucionar este problema se definió en la repositorio un método que nos ayudara a obtener el ultimo id. El método GetLastId() recupera el ultimo utilizado en la combinación de las tablas que definen la herencia.

Se lo declara primeramente en la interfaz.

public interface IEmployeeRepository : IRepository<Employee>
{
    List<EmployeeInternal> GetAllInternalType();

    List<Employee> GetAllExternalType();

    int GetLastId();

}

Y luego en la clase que implementa el repositorio.

 

  public class EmployeeRepository : BaseRepository<Employee>, IEmployeeRepository
  {

      public int GetLastId()
      {
          using (NorthWindContext context = new NorthWindContext())
          {
              int? lastId = context.Employees.Max(x => (int?)x.EmployeeID);

              return lastId.HasValue ? lastId.Value : 0;
          }
      }

  }

 

La ejecución del mismo implica un UNION ALL entre las tablas que deriven de la misma base para luego aplicar el MAX() del campo definido como key

image 

 

Test – Inicializar datos


La inicialización requiere que por cada entidad creada se realice una consulta previa para recuperar el ultimo id

image

En la imagen se observa como a pesar de ser diferentes tablas se mantiene el consecutivo en la key.

 

Test – Obtener todos los empleados


La query generada para recuperar todos los empleados requiere unir las tablas de cada clase derivada.

image

 

Test – Recuperar todos los empleados del tipo interno


Ya sea usando un repositorio especial para el tipo concreto “GetAllInternal_UsingSpecificRepository_Employee()” o definiendo un método en el repositorio base el cual podria utilizar el OfType<>  “GetAllInternal_UsingGenericRepository_Employee()”, el resultado es idéntico, solo realiza la query sobre una tabla en particular donde se define el tipo.

 

image

 

Test – Obtener todos los empleados Externos


Al igual que los test anteriores, hacer uso de un repositorios especifico “GetAllExternal_Employee()” para recuperar los empleados externos o definir un método en el repositorio base que haga uso del “is” para definir que tipo de entidad cargar “GetAllExternal_UsandoIs_Employee()”, resulta en la mismo SELECT.

 

image

 

Resumen


Si bien en algunos casos este tipo de persistencia puede resultar útil, el hecho de no poder definir un campo clave como secuencial puede dificultar su implementación.

La creación de diferentes tablas generan redundancia de campos al duplicarlos, lo cual no lo hace un modelo de persistencia aconsejable, aunque puede que esta sea una característica buscada cuando no se requiere un modelo normalizado.

En resumen, analizar previamente los modelos de herencia TPT y TPH, dejando como ultima alternativa el TPC.

El modelo TPH hace uso de un discriminador para definir los tipos concretos. El TPT, lo realiza mediante la relación entre las tablas. En TPC en cambio la tabla completa define el tipo.

 

Documentación de referencia


Inheritance with EF Code First: Part 3 – Table per Concrete Type (TPC)

 

Código


Se utilizo Visual Studio 2012, no se adjunta ninguna base de datos, esta será creada al ejecutar los test utilizando el modelo definido en el contexto de EF

[C#]
 

miércoles, 28 de agosto de 2013

[Entity Framework][Code First] Herencia - Tabla por tipo - Table per Type (TPT)

 

Introducción


Analizaremos un modelo de herencia el cual permite definir una tabla para cada tipo concreto. En este caso la relación entre las tablas define el tipo de instancia de la entidad.

El modelo utilizado en este artículo es idéntico al anterior:

[Entity Framework][Code First] Herencia - Tabla por jerarquía - Table per Hierarchy (TPH)

 

Definición del modelo


Modelaremos la entidad empleado y sus derivados que representan al personal externo y el contratado por la compañía.

Partiremos de un modelo de clases como el siguiente:

image

y obtendremos un modelo de tablas

image

en donde cada la clase base estará en una tabla mientras que los derivados, con sus atributos particulares, en otras distintas.

En este caso la relación entre las tablas define el tipo, a diferencia del modelo TPH (Tabla por jerarquía) en donde un campo era quien lo indicaba.

 

Definición del Mapping


Se va a necesitar definir en que tabla se persiste cada tipo especifico, es por ello que se requiere una clase de mapping para cada derivado que herede de la base.

 

public class NorthWindContext : DbContext
{

    public NorthWindContext()
        : base("NorthwindDb")
    {
        this.Configuration.LazyLoadingEnabled = false;
        this.Configuration.ProxyCreationEnabled = false;
    }

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

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

        modelBuilder.Configurations.Add(new EmployeeInternalMap());
        modelBuilder.Configurations.Add(new EmployeeExternalMap());

        base.OnModelCreating(modelBuilder);
    }

}

public class EmployeeMap : EntityTypeConfiguration<Employee>
{
    public EmployeeMap()
    {
        HasKey(x => x.EmployeeID);
        Property(x => x.LastName).HasMaxLength(20).IsRequired();
        Property(x => x.FirstName).HasMaxLength(10).IsRequired();

        Property(x => x.Address).HasMaxLength(60);
        Property(x => x.City).HasMaxLength(15);
        Property(x => x.Region).HasMaxLength(15);
        Property(x => x.PostalCode).HasMaxLength(10);
        Property(x => x.Country).HasMaxLength(15);

    }
}

public class EmployeeInternalMap : EntityTypeConfiguration<EmployeeInternal>
{
    public EmployeeInternalMap()
    {
        ToTable("EmployeeInternal");

      
    }
}

public class EmployeeExternalMap : EntityTypeConfiguration<EmployeeExternal>
{
    public EmployeeExternalMap()
    {
        ToTable("EmployeeExternal");

        Property(x => x.ConsultantName).IsRequired()
                                       .HasColumnType("varchar")
                                       .HasMaxLength(100);
    }
}

El principal detalle esta en la definición de los nombre de la tablas

image

 

Definición Repository


Este no sufrió cambios con respecto al articulo anterior donde tratamos TPH.

Nota: se agrego un método adicional a la interfaz del repositorio el cual implementa una query linq que utiliza el “is”, pero no varia la forma de definir esta funcionalidad con respecto al articulo anterior. Se analizara este punto más adelante cuando tratemos el Test que utiliza esta implementacion.

 

Test – Inicializar datos


La inicialización de datos es idéntica al artículo anterior donde explique TPH.

Pero el resultado de los “insert” que define EF son bastante diferentes, ya que por cada entidad creada requiere realiza dos operaciones, uno en la tabla base y otro en cada especialización, esto se aprecia claramente en el trace tomado desde el sql profiler.

image

 

Test – Obtener todos los empleados


Recuperar todos los empleados requiere implementar la unión de dos tablas, la de empleados internos y externos,  al ejecutar el test se puede observar claramente si analizamos el select definido por EF

image

La unión de las dos tablas que representan las entidades hijas se unen mediante un INNER JOIN con la tabla padre de empleados

Aquí nuevamente aparecen los “0X0X” y “0X1X” los cuales son usados por EF para determinar que tipo de instancia debe crear

 

Test – Recuperar todos los empleados de tipo interno, usando un repositorio especifico


Utilizamos el repositorio definido para un tipo concreto, en este caso se recuperan los empleados internos a la empresa

SNAGHTML31ecfb93

Esta acción unirá mediante un INNER JOIN la tabla que representa estos empleados con la tabla base, del cruce se obtendrán solo los registros de un único tipo.

 

image

 

Test – Recuperar todos los empleados del tipo interno, usando funcionalidad del repositorio genérico


Utilizar el método OfType<> definido en un método en al repositorio general

SNAGHTML31f473ce

dará el mismo resultado que utilizar el repositorio especifico del test anterior

image

 

Test – Obtener todos los empleados Externos


Se obtienes todos los empleados de contratación externa.

SNAGHTML32077f62

El join se realiza utilizando la tabla que representa los empleados externos a la compañía.

image

 

Test – Obtener todo los empleados externos, usando el “is”


Aplicaremos una variación en la técnica utilizada al definir el tipo que queremos filtrar, para ello nos ayudaremos con el “is” en el where de la query linq.

SNAGHTML321b1bf0

como veremos EF detecta el “is” como la acción de recuperar un tipo especifico, por eso el sql que define nuevamente hace uso del JOIN entre las tablas

image

 

Documentación de referencia


Inheritance with EF Code First: Part 2 – Table per Type (TPT)

 

Código


Se utilizo Visual Studio 2012, no se adjunta ninguna base de datos, esta será creada al ejecutar los test utilizando el modelo definido en el contexto de EF

[C#]