jueves, 4 de julio de 2013

[Entity Framework][Code First] Asociación uno a muchos (3/3)

 

Introducción


Analizaremos como implementar las acciones que permiten eliminar una entidad y sus relaciones, pudiendo además por medio de las opciones de EF cambiar el mapping de la entidad para lograr cambiar las opciones de eliminar en cascada entre las tablas.

Otro tema que veremos es como asociar las entidades a través de su instancia. Hasta el momento la relación de una entidad con otra existente se realiza asignando la propiedad ID, o sea si un producto tiene una relación con una categoría se asigna la propiedad CategoryID, cuando se asignaba la propiedad Category de la entidad Product se creaba una nueva categoría lo cual puede que no se quiere que suceda.

Vamos a analizar de que forma podemos modificar el código para indicar que entidad asociada no se debe volver a crear.

En resumen los temas que se trataran serán:

  1. Eliminar en cascada
  2. Asociar mediante la instancia de la entidad

Este artículo es la continuación de:

[Entity Framework][Code First] Asociación uno a mucho (1/3)

 

1- Eliminar en cascada


Al seguir las convenciones en la definición de relaciones EF determina las acción de eliminar en cascada al crear las relaciones entre las tablas.

Si definimos una relación como obligatoria marcara automáticamente para permitir eliminar en cascada, salvo que se indique lo contrario.

En cambio si definimos una relación como opcional mediante la definición de un tipo null (en la propiedad asignada para el Foreign Key) en este caso EF no especificara ninguna acción.

 

1.1- Asociación obligatoria – Eliminar en cascada


Si definimos las entidades usando

image

en este caso por seguir las convenciones podríamos o no definir la línea en el context

HasRequired(x => x.Category)
    .WithMany(x => x.Products)
    .HasForeignKey(x => x.CategoryID)

se crearan las tablas relacionándolas con al opción de cascada activa

image

si ejecutamos un test como ser

[TestMethod]
public void Delete_Cascade_Category()
{
    ProductRepository repoProduct = new ProductRepository();
    CategoryRepository repoCategory = new CategoryRepository();

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1"
    };
    repoCategory.Create(categoryNew);

    //se crea el producto relacionado con la categoria
    Product productNew1 = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID
    };
    repoProduct.Create(productNew1);

    Product productNew2 = new Product()
    {
        ProductName = "prod 2",
        UnitPrice = 12,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID
    };
    repoProduct.Create(productNew2);


    //elimina la categoria y sus productos asociados
    repoCategory.Delete(categoryNew);


    //se recupera la categoria para validar si se elimino
    var categorySelected = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID);

    Assert.IsNull(categorySelected);

    //se recupera el primer producto
    Product productSel1 = repoProduct.Single(x=>x.ProductID == productNew1.ProductID);
    Assert.IsNull(productSel1);

    //se recupera el primer producto
    Product productSel2 = repoProduct.Single(x=>x.ProductID == productNew2.ProductID);
    Assert.IsNull(productSel2);
}

 

se crea la categoría y sus dos productos relacionados

image

y la operación de elimina

image

pero al estar activa en la relación la opción de cascada se eliminan los productos, por eso si analizamos el test veremos que tanto la categoría como los productos ya no existen

 

SNAGHTML7ff1cd3d

 

1.2- Asociación obligatoria – Anular eliminar en cascada


Con el uso del método WillCascadeOnDelete() en la definición del contexto podremos cambiar la convención utilizada por EF cuando crea las relaciones entre las tablas.

image

al ejecutar el mismo test del caso anterior, obtendremos un error

SNAGHTML7ffbf289

por lo que ya no podremos eliminar la categoría si previamente no quitamos los productos asociados

 

1.3- Asociación opcional – Eliminar


Al definir la propiedad de relación como nullable EF interpretara que una asociación como opcional

image

por seguir las convenciones podríamos o no definir la línea en el context

HasOptional(x => x.Supplier)
    .WithMany(x => x.Products)
    .HasForeignKey(x => x.SupplierID);

se creara las relaciones entre las tablas en donde no se realiza ninguna acción al eliminar

image

Podemos validarlo por medio de un test

[TestMethod]
public void Delete_Supplier()
{
    ProductRepository repoProduct = new ProductRepository();
    SupplierRepository repoSupplier = new SupplierRepository();

    //se crea el proveedor
    Supplier supplierNew = new Supplier()
    {
         CompanyName = "supplier 1"
    };
    repoSupplier.Create(supplierNew);

    //se crea el producto relacionandolo con el proveedor
    Product productNew1 = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        Category =  new Category()
            {
                CategoryName = "category1",
                Description = "desc category 1"
            },
        SupplierID = supplierNew.SupplierID
    };
    repoProduct.Create(productNew1);

    //se crea otro producto relacionandolo con el proveedor
    Product productNew2 = new Product()
    {
        ProductName = "prod 2",
        UnitPrice = 12,
        Discontinued = false,
        Category =  new Category()
            {
                CategoryName = "category2",
                Description = "desc category 2"
            },
        SupplierID = supplierNew.SupplierID
    };
    repoProduct.Create(productNew2);


    //se recupera el proveedor para obtener los productos asociados
    var supplierSel = repoSupplier.Single(x => x.SupplierID == supplierNew.SupplierID,
                                        new List<Expression<Func<Supplier, object>>>() { x => x.Products });

    //se elimina la categoria y se desasocian los productos 
    repoSupplier.Delete(supplierSel);


    //se recupera la categoria para validar si se elimino
    var categorySelected = repoSupplier.Single(x => x.SupplierID == supplierNew.SupplierID);

    Assert.IsNull(categorySelected);

    //se recupera el primer producto
    Product productSel1 = repoProduct.Single(x => x.ProductID == productNew1.ProductID);
    Assert.IsNotNull(productSel1);

    //se recupera el primer producto
    Product productSel2 = repoProduct.Single(x => x.ProductID == productNew2.ProductID);
    Assert.IsNotNull(productSel2);
}

Durante la ejecución se puede ir verificando las acciones que se realizan en la db, se crea el proveedor y los productos

image

se recupera el proveedor y sus productos, esto es necesario ya que al realizar la eliminación los productos deben ser actualizados

image

ante de eliminar el proveedor se ejecuta el UPDATE que asigna null en el campo SupplierID para quitar la asociación, para luego si poder eliminar el registro del proveedor sin conflicto

image

la validación en el test confirma que el proveedor ya no existe, al recuperarlo se obtiene un null, no así con los productos que siguen existiendo

image

 

 

2 - Asociar mediante la instancia de la entidad


Si tenemos la instancia de una entidad y la asignamos a la propiedad de otro para relacionarla, al persistir no solo creara la entidad principal sino que creara una nuevo registro de la entidad que asignamos.

Si tenemos un código que hiciera esto:

ProductRepository repoProduct = new ProductRepository();
CategoryRepository repoCategory = new CategoryRepository();
SupplierRepository repoSupplier = new SupplierRepository();

//se crea la categoria
Category categoryNew = new Category()
{
    CategoryName = "category1",
    Description = "desc category 1"
};
repoCategory.Create(categoryNew);

//se crea un proveedor
Supplier supplierNew = new Supplier()
{
    CompanyName = "Company 1",
};
repoSupplier.Create(supplierNew);


//se crea el producto relacionado con la categoria
Product productNew = new Product()
{
    ProductName = "prod 1",
    UnitPrice = 10,
    Discontinued = false,
    Category = categoryNew,
    Supplier = supplierNew
};

repoProduct.Create(productNew);

Debe observarse particularmente como se crean las entidades satélite y luego se asignan estas a las propiedades Category y Supplier de la entidad Product, hacer esto resultara en las consultas siguientes:

image

como se observa en una primer acción se crean las entidades Category y Supplier, pero al crear el Product nuevamente se vuelven a insertar un nuevo Category y Supplier que termina relacionado

Esto sucede porque en el código del repositorio cuando adjuntamos la entidad Product al contexto indicamos que es una nueva entidad, pero las instancias de los objetos relacionados no le decimos nada, por lo que EF deduce que estas entidades también deben crearse.

La solución rápida a este problema se consigue haciendo uso de las propiedades que definen el Foreign Key y no utilizar las propiedades definidas como clase, o sea utilizar:

image

Pero vamos a ver que puede haber otra alternativa si adaptamos la clase del repositorio pudiendo indicar que propiedades deben cambiar de estado.

 

2.1- Marcar entidades relacionadas con Unchanged


Que sucede si aun así queremos asignar las instancias, existe un solución pero requiere que desde el repositorio asignemos el “State” del contexto como “Unchanged”.

Para lograrlo vamos a necesitar que Reflection nos ayude a recuperar el objeto que se asigna a la propiedad, desde el repositorio implementaremos.

public virtual void Create(T entity, List<Expression<Func<T, object>>> unchangeProp)
{
    // se obtiene la lista de propiedades que deben marcarse con el estado Unchanged 
    List<string> unchangelist = unchangeProp.Select(x => ((MemberExpression)x.Body).Member.Name).ToList();

    using (NorthWindContext context = new NorthWindContext())
    {
        context.Set<T>().Add(entity);

        if (unchangeProp != null)
        {
            // se toma la instancia del objeto que esta asignada a la propiedad 
            // y se asigna el estodo Unchanged
            foreach (string property in unchangelist)
            {
                PropertyInfo propertyInfo = typeof(T).GetProperty(property);
                var value = propertyInfo.GetValue(entity, null);

                context.Entry(value).State = EntityState.Unchanged;
            }
        }

        context.SaveChanges();
    }
}

El parámetro “unchangeProp” nos permite definir las propiedades asignadas y no queremos que la entidad sea creadas nuevamente.

Desde el código del test tendremos que definir que propiedades participaran

[TestMethod]
public void Create_Unchange_CategoryAndSupplier_Product()
{
    ProductRepository repoProduct = new ProductRepository();
    CategoryRepository repoCategory = new CategoryRepository();
    SupplierRepository repoSupplier = new SupplierRepository();

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1"
    };
    repoCategory.Create(categoryNew);

    //se crea un proveedor
    Supplier supplierNew = new Supplier()
    {
        CompanyName = "Company 1",
    };
    repoSupplier.Create(supplierNew);


    //se crea el producto relacionado con la categoria
    Product productNew = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        Category = categoryNew,
        Supplier = supplierNew
    };

    repoProduct.Create(productNew, 
                        new List<Expression<Func<Product, object>>>() { x => x.Category, x => x.Supplier });

    //se recupea el producto con la categoria y proveedor asociado
    var productSelected = repoProduct.Single(x => x.ProductID == productNew.ProductID,
                                            new List<Expression<Func<Product, object>>>() { x => x.Category, x => x.Supplier });

    Assert.IsNotNull(productSelected.Category);
    Assert.AreEqual(productSelected.Category.CategoryID, productNew.CategoryID);

    Assert.IsNotNull(productSelected.Supplier);
    Assert.AreEqual(productSelected.Supplier.SupplierID, productNew.SupplierID);
}

de todo el test el punto importante donde hay que enfocarse seria:

image

allí es donde se especifica las propiedades que deben asignarse como “Unchanged”

Al ejecutar el test veremos que solo se crea el INSERT del Product y no de las entidades que se se asignaron como sucedía anteriormente.

image

Ahora si podemos trabajas con las propiedades de instancia para crear las relaciones sin el problema que se vuelvan a crear nuevamente cuando las entidades ya existe.

 

Código


El código se encuentra en la primer parte del artículo.

[Entity Framework][Code First] Asociación uno a muchos (2/3)

 

Introducción


En esta segunda parte del artículo se ejecutaran varios Test que permitirán analizaran las consultas que Entity Framework generara contra la base de datos.

La primer parte del artículo define las entidades que aquí se validaran con los test.

[Entity Framework][Code First] Asociación uno a mucho (1/3)

 

Test – Crear Producto con categorías y proveedores existentes


Crearemos de forma individual la entidad de categorías y proveedores para luego asociarlas al producto.

[TestMethod]
public void GetSingle_WithCategoryAndSupplier_Product()
{
    ProductRepository repoProduct = new ProductRepository();
    CategoryRepository repoCategory = new CategoryRepository();
    SupplierRepository repoSupplier = new SupplierRepository();

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1"
    };
    repoCategory.Create(categoryNew);

    //se crea un proveedor
    Supplier supplierNew = new Supplier()
    {
        CompanyName = "Company 1",
    };
    repoSupplier.Create(supplierNew);

    //se crea el producto relacionado con la categoria
    Product prod = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID,
        SupplierID = supplierNew.SupplierID
    };
    repoProduct.Create(prod);

    //se recupea el producto con la categoria y proveedor asociado
    var productSelected = repoProduct.Single(x => x.ProductID == prod.ProductID,
                                            new List<Expression<Func<Product, object>>>() { x => x.Category, x=>x.Supplier });

    Assert.IsNotNull(productSelected.Category);
    Assert.AreEqual(productSelected.Category.CategoryID, categoryNew.CategoryID);
    
    Assert.IsNotNull(productSelected.Supplier);
    Assert.AreEqual(productSelected.Supplier.SupplierID, supplierNew.SupplierID);
}

 

Se instancias los repositorios para cada una de las entidades intervinientes y se crean las entidades de forma individual.

Por medio de la propiedad que representan la Foreing Key es que logramos la asociación entre las entidades. Cada operación genera en la db las siguientes operaciones de insert

image_thumb[31]

Al recuperar el producto y sus relaciones se debe especificar cuales se quiere traer en al definición del parámetro del include, esto genera la consulta:

image_thumb[21]

en una sola operación se recupera el producto, la categoría y el proveedor optimizando el acceso a la base de datos

 

Test – Crear Producto con categorías y proveedores NO existentes


Si se necesita en un único paso crear una entidad y sus relaciones se puede definir como el siguiente test

[TestMethod]
public void Create_WithCategoryAndSupplier_Product()
{
    ProductRepository repoProduct = new ProductRepository();

    //se crea el producto relacionado con la categoria
    Product productNew = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        Category = new Category()
                    {
                        CategoryName = "category1",
                        Description = "desc category 1"
                    },
        Supplier = new Supplier()
                    {
                        CompanyName = "Company 1",
                    }
    };
    repoProduct.Create(productNew);

    //se recupea el producto con la categoria y proveedor asociado
    var productSelected = repoProduct.Single(x => x.ProductID == productNew.ProductID,
                                            new List<Expression<Func<Product, object>>>() { x => x.Category, x => x.Supplier });

    Assert.IsNotNull(productSelected.Category);
    Assert.AreEqual(productSelected.Category.CategoryID, productNew.CategoryID);

    Assert.IsNotNull(productSelected.Supplier);
    Assert.AreEqual(productSelected.Supplier.SupplierID, productNew.SupplierID);
}

solo se utiliza un único repositorio creando las entidades asociadas directamente en la misma acción, al ejecutar en la db se registras las operaciones de insert por cada entidad

image_thumb[32]

a diferencia del test anterior aquí las operaciones se realizan una a continuación de otra, luego de la creación de las entidades estas tendrán asignado los Id que genera la db para todas las entidades, tanto la principal como para las relacionadas

el select que recupera el producto no sufre cambios comparado con el test anterior

image_thumb[33]

 

Test – Crear Categoría con Productos existentes


Crearemos el productos de forma independiente para después relacionar con la categoría.

[TestMethod]
public void GetSingle_AllProducts_Category()
{
    ProductRepository repoProduct = new ProductRepository();
    CategoryRepository repoCategory = new CategoryRepository();

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1"
    };
    repoCategory.Create(categoryNew);

    //se crea el producto relacionado con la categoria
    Product productNew1 = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID
    };
    repoProduct.Create(productNew1);

    Product productNew2 = new Product()
    {
        ProductName = "prod 2",
        UnitPrice = 12,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID
    };
    repoProduct.Create(productNew2);


    var categorySelected = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID, 
                                                    new List<Expression<Func<Category,object>>>() { x=>x.Products });

    Assert.IsNotNull(categorySelected);
    Assert.IsNotNull(categorySelected.Products);
    Assert.AreEqual(categorySelected.Products.Count, 2);

}

Sino se define el include al recuperar la categoría la propiedad “Products” devolverá null

image

pero si se define el “include” se podrán obtener los productos relacionados a la entidad

image

se ejecuta un único select para recuperar la categoría y sus productos

image

el “include” optimiza la consulta generada por EF para recuperar las entidades

 

Test – Crear Categoría con Productos NO existentes


Se puede crear las entidades y asociarlas en una misma acción, pero debe recordarse que se crearan registros nuevos en la tabla para la entidad principal como para las relaciones

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

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1",
        Products = new List<Product>(){
            new Product()
                {
                    ProductName = "prod 1",
                    UnitPrice = 10,
                    Discontinued = false
                },
            new Product()
                {
                    ProductName = "prod 2",
                    UnitPrice = 12,
                    Discontinued = false
                }
        }
    };
    repoCategory.Create(categoryNew);

    var categorySelected = repoCategory.Single(x => x.CategoryID == categoryNew.CategoryID,
                                                    new List<Expression<Func<Category, object>>>() { x => x.Products });

    Assert.IsNotNull(categorySelected);
    Assert.IsNotNull(categorySelected.Products);
    Assert.AreEqual(categorySelected.Products.Count, 2);

}

La creación de los registros ejecutara varias instrucciones insert, luego de la creación del registro devolverá los Id generados en cada instrucción

image

al recuperar al entidad principal y su relación con la colección optimizara el select para realizarlo en una única operación

image

 

Código


El código se encuentra en la primer parte del artículo.

miércoles, 3 de julio de 2013

[Entity Framework][Code First] Asociación uno a muchos (1/3)

 

Introducción


Al hacer uso de entidades rara vez son simples y están aisladas, lo normal es que una entidad interactué con otras relacionándose por medio de propiedades que permiten navegar sus ítems.

Durante este artículo y los siguiente veremos como Entity Framework nos ayudara en la tarea de recuperar las asociaciones a otras entidades.

Empezaremos por la configuración simple, para luego especializar la configuración, analizaremos como afecta las opciones de lazy load al definir el repositorio. En el siguiente artículo analizaremos mediante la ejecución de Test las distintas formas de asociar entidades y como repercuten en las consultas que EF generara contra la db. En el ultimo artículo veremos como eliminar cascada y asociar las entidades mediante la asignación de la instancia en las propiedades.

 

Definición de las entidades


Empezaremos definiendo las entidades que formaran nuestro dominio

image

 

public class Product
{

    public int ProductID { get; set; }

    public string ProductName { get; set; }
    public string QuantityPerUnit { get; set; }
    public decimal? UnitPrice { get; set; }

    public short? UnitsInStock { get; set; }
    public short? UnitsOnOrder { get; set; }
    public short? ReorderLevel { get; set; }

    public bool Discontinued { get; set; }

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

    public int? SupplierID { get; set; }
    public virtual Supplier Supplier { get; set; }
}

 

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

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

    public virtual ICollection<Product> Products { get; set; }
}

 

public class Supplier
{
    public int SupplierID { get; set; }
    public string CompanyName { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}

El entidad Product tiene asociación con Categoría (obligatoria) y Supervisor (opcional).

En este caso se estas siguiendo las convenciones, por lo que además de la propiedad que define la clase de navegación, se debe definir una propiedad adicional que identifique el campo que actuara como Foreign Key.

La obligatoriedad o no de una relación se define mediante la asignación de un tipo Nullable en la propiedad mencionada anteriormente (al permitir nulos la relación será opcional).

Las entidades Category y Supplier disponen de una propiedad de tipo colección que referencia al Product, pero esta propiedad puede ser opcional si es que no se desea recuperar una de las direcciones de la asociación.

 

Configuración estándar


Si solo definimos el contexto sin especificar ningún otro detalle.

public class NorthWindContext : DbContext
{

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

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Supplier> Suppliers { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
       

        base.OnModelCreating(modelBuilder);
    }

}

 

Al ejecutar los test estos generaran la base de datos con la estructura de tablas como la siguiente:

image

Como se puede visualizar con casi nada de especificación se consigue un modelo relacional aceptable, solo se siguieron algunas convenciones:

- Definir las propiedades de navegación entre las entidades y con una adicional que representa la Foreign Key

- Utilizando null en la propiedad Foreign Key para indicar si es opcional

 

Especificación con Fluent API


Si bien las convenciones nos dejan un modelo de persistencia bastante cercano a las necesidades, cuando la definiciones escapan a las normas se puede especializar mediante configuración, especialmente cuando se utiliza una base de datos existente.

 

public class NorthWindContext : DbContext
{

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

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Supplier> Suppliers { get; set; }

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

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

    }
}


public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Products");

        HasKey(x => x.ProductID);
        Property(x => x.ProductID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

        Property(x => x.ProductName).HasMaxLength(40);
        Property(x => x.QuantityPerUnit).HasMaxLength(20);
        Property(x => x.UnitPrice).HasColumnType("money").IsOptional();

        HasRequired(x => x.Category).WithMany(x => x.Products).HasForeignKey(x => x.CategoryID);
        //HasRequired(x => x.Category).WithMany().HasForeignKey(x => x.CategoryID);

        HasOptional(x => x.Supplier).WithMany(x => x.Products).HasForeignKey(x => x.SupplierID);
    }
}

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

    }
}

 

Al ejecutar alguno de los test se generar la estructura de la db:

image

Si bien a nivel de base de datos las relaciones no cambiaron desde código se puedo definir el código que detalla el modelo de persistencia que queremos.

Para definir la relación obligatoria con la entidad Categoría se utiliza la línea:

HasRequired(x => x.Category).WithMany(x => x.Products).HasForeignKey(x => x.CategoryID);

en este caso se define que propiedades intervienen en la relación, por supuesto si se hace uso de las convenciones esta de mas esta declaración, pero si las propiedades tienen nombres distintos definir esta línea es clave para que funcione la asociación.

La definición de la entidad proveedor como opcional se usa la línea:

HasOptional(x => x.Supplier).WithMany(x => x.Products).HasForeignKey(x => x.SupplierID);

solo cambia el uso de HasOptional(), aunque sigue siendo necesario definir la propiedad Foreign Key del tipo Nullable.

 

Asociación en una única dirección


Anteriormente comente que la propiedad que permite navegar la colección de productos no es obligatoria, pudiendo definirse de esta forma:

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

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

}

Nota: se quito la propiedad que define el ICollection<> hacia los Productos

Pero si se dejara las propiedades en la entidad Product que relacione con las entidades simples.

public class Product
{

    //resto de las propiedades

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

    public int? SupplierID { get; set; }
    public virtual Supplier Supplier { get; set; }
}

la configuración cambiaria a:

HasRequired(x => x.Category).WithMany().HasForeignKey(x => x.CategoryID);

solo se especifica la propiedad en una única dirección hacia la entidad simple y se quitan las colecciones.

 

Repository y Lazy Load


Un punto que encontré durante la definición del repositorio se relaciona con el lazy load de las propiedades que asocia las entidades.

Si ejecutamos un test como ser

[TestMethod]
public void GetSingle_IncludeCategory_Product()
{
    ProductRepository repoProduct = new ProductRepository();
    CategoryRepository repoCategory = new CategoryRepository();
    SupplierRepository repoSupplier = new SupplierRepository();

    //se crea la categoria
    Category categoryNew = new Category()
    {
        CategoryName = "category1",
        Description = "desc category 1"
    };
    repoCategory.Create(categoryNew);

    //se crea un proveedor
    Supplier supplierNew = new Supplier()
    {
        CompanyName = "Company 1",
    };
    repoSupplier.Create(supplierNew);

    //se crea el producto relacionado con la categoria
    Product productNew = new Product()
    {
        ProductName = "prod 1",
        UnitPrice = 10,
        Discontinued = false,
        CategoryID = categoryNew.CategoryID,
        SupplierID = supplierNew.SupplierID
    };
    repoProduct.Create(productNew);

    //se recupea el producto con la categoria asociada
    var productSelected = repoProduct.Single(x => x.ProductID == productNew.ProductID,
                                            new List<Expression<Func<Product, object>>>() { x => x.Category });

    Assert.IsNotNull(productSelected.Category);
    Assert.AreEqual(productSelected.Category.CategoryID, categoryNew.CategoryID);
    Assert.AreEqual(productSelected.CategoryID, categoryNew.CategoryID);

    Assert.AreEqual(productSelected.SupplierID, supplierNew.SupplierID);
    Assert.IsNull(productSelected.Supplier);
}

El cual crea un producto con categoría y proveedor relacionado, pero luego de crear la entidad se recuperar definiendo en el “include” solo la categoría, por estar estar habilitado el lazy load se obtendrá el siguiente mensaje:

 

SNAGHTML7b064e64

Este error se produce porque se crea un proxy que permite la relación de forma desatendida, pero al estar la entidad por fuera del contexto entonces falla.

Por esta razón se definieron de dos líneas en el constructor de la clase del contexto:

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

Al deshabilitando lazy load las propiedades relacionadas que no se especifican en el “include” no recuperan las instancias, por lo tanto estarán en null.

Ahora si se comporta como se espera

SNAGHTML7bae9e99

 

Documentación de referencia


Code First Conventions

Configuring Relationships with the Fluent API

Working with Proxies

 

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#]
 

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#]
 

viernes, 31 de mayo de 2013

[Dynamic CRM] Integrar Google Maps – PopUp Mapa Web (2/2)

 

Introducción


Se continua el artículo

[Dynamic CRM] Integrar Google Maps – PopUp Mapa Web (1/2)

 

Publicar Web Resources


El primer paso ser crear una nueva solución.

SNAGHTML39670615

En el cuadro de creación de la solución se define solo algunos campos

SNAGHTML3968906a

Editando la solución se irán agregando los js y html que requiere el mapa

SNAGHTML396a861d

Creamos un ítem por cada archivo

SNAGHTML397059c0

El listado final de archivos que conforman la solución seria:

SNAGHTML397ba532

Como paso final se publica

 

Edición de la Ribbon toolbar


Para agregar un nuevo botón en la toolbar del menú de CRM se hará uso de una utilidad de Codeplex

CRM 2011 Visual Ribbon Editor

El primer paso será configurar la conexión al CRM desde la herramienta

SNAGHTML398171b4

Una vez conectado usamos el botón “Open” para que se desplegué la selección de la entidad a la cual se quiere editar el ribbon

SNAGHTML3983c2df

Seleccionamos “account” y aceptamos. En el combo del tipo de ribbon disponibles para la entidad seleccionamos Homepage, la herramienta nos mostrara la representación visual de la toobar actual

SNAGHTML398551a6

Usaremos el botón “New Button” para crear nuestro botón que lanzara el mapa.

La definición del botón tiene asociada una imagen que previamente publicamos como recurso

SNAGHTML398a0e12

La solapa “Action” nos permitirá definir que función javascript será invocada al presionar el boton

Aquí se hará uso un archivo Mapa.js el cual define las funciones responsables de lanzar el popup de la ventana que contiene el mapa

 

function openMapStateProvince() 
{
    window.open('WebResources/new_Mapahtml?Data=grupo%3Dstateorprovince');
}

function openMapTerritory() 
{
    window.open('WebResources/new_Mapahtml?Data=grupo%3Dterritory');
}

Estas funciones javascript definen la propiedad por la cual se agruparan las cuentas y definen los colores de las marcas

SNAGHTML399220b7

 

El ultimo paso será grabar las modificaciones usando el botón “Save”, esto demorara un rato en aplicar los cambio.

 

Conclusión


Una vez concluida la publicación de los recursos y agregado el botón en la ribbon toolbar solo queda lanzar el mapa y disfrutar del resultado.

Recursos utilizados:

Versión 3 del API de JavaScript de Google Maps

Consejos prácticos para el InfoWindow en Google Maps

Superposiciones