Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Matthieu MEZIL

I love .Net

Abonnements

EF avec WPF

Beth Massi a publié 3 vidéos “How do I” dans lesquelles elle montre comment utiliser Entity Framework dans une application WPF.

How Do I: Get Started with Entity Framework in WPF Applications?

How Do I: Build a WPF Data Entry Form Using Entity Framework?

How Do I: Create a WPF Lookup Combobox using Entity Framework?

Posté le jeudi 2 juillet 2009 10:18 par Matthieu MEZIL | 1 commentaire(s)

Classé sous : , , ,

Windows 7 videos: How to…

De façon générale, j’adore les webcasts. J’en entasse d’ailleurs des 10 aines (voire 100 aines) de giga que je compte voir… quand j’aurai le temps.

Pour ceux qui se pose des questions sur les nouvelles fonctionnalités offertes par Windows 7, je vous conseille les petites videos de la série How To.

Posté le mercredi 1 juillet 2009 22:39 par Matthieu MEZIL | 0 commentaire(s)

Classé sous :

T4 et View Generation

L’EF team avait bloggué sur cela ici.

Le problème quand on utilise EF est le fait que la première requête peut être longue car EF va générer du code à partir des metadatas.

Afin de ne pas perdre ce temps lors de la première requête, il est possible de générer ce code et de l’inclure à la solution. Cela peut être fait avec le programme EdmGen.exe.

L’utilisation du template T4 est, à mon avis, plus naturel que l’utilisation de l’EdmGen.

De plus, contrairement à EdmGen, le template T4 gère les metadat artifacts embedded.

Par contre, si vous voulez utiliser le template fournit par MS, n’oubliez pas de changer les namespaces xml.

XNamespace edmxns = "http://schemas.microsoft.com/ado/2008/10/edmx";
XNamespace csdlns = "http://schemas.microsoft.com/ado/2008/09/edm";
XNamespace mslns = "http://schemas.microsoft.com/ado/2008/09/mapping/cs";
XNamespace ssdlns = "http://schemas.microsoft.com/ado/2009/02/edm/ssdl";

Cela nous donne donc :

<#@ template language="C#" hostspecific="true"#>
<#@ output extension=".cs" #>

<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="System.Data.Entity.Design" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>

<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.Entity.Design" #>
<#@ import namespace="System.Data.Metadata.Edm" #>
<#@ import namespace="System.Data.Mapping" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.Linq" #>

<#
    // Find EDMX file to process: Model1.Views.tt generates views for Model1.EDMX
    string edmxFileName = Path.GetFileNameWithoutExtension(this.Host.TemplateFile).ToLowerInvariant().Replace(".views", "") + ".edmx";
    string edmxFilePath = Path.Combine(Path.GetDirectoryName(this.Host.TemplateFile), edmxFileName);
    if (File.Exists(edmxFilePath))
    {
        // Call helper class to generate pre-compiled views and write to output
        this.WriteLine(GenerateViews(edmxFilePath));
    }
    else
    {
        this.Error(String.Format("No views were generated. Cannot find file {0}. Ensure the project has an EDMX file and the file name of the .tt file is of the form [edmx-file-name].Views.tt", edmxFilePath));
    }
    // All done!
#>

<#+
    private String GenerateViews(string edmxFilePath)
    {
        String generatedViews = String.Empty;
        try
        {
            using (StreamWriter writer = new StreamWriter(new MemoryStream()))
            {
                XmlReader csdlReader = null;
                XmlReader mslReader = null;
                XmlReader ssdlReader = null;

                // Crack open the EDMX file and get readers over the CSDL, MSL and SSDL portions
                GetConceptualMappingAndStorageReaders(edmxFilePath, out csdlReader, out mslReader, out ssdlReader);

                // Initialize item collections
                EdmItemCollection edmItems = new EdmItemCollection(new XmlReader[] { csdlReader });
                StoreItemCollection storeItems = new StoreItemCollection(new XmlReader[] { ssdlReader });
                StorageMappingItemCollection mappingItems = new StorageMappingItemCollection(edmItems, storeItems, new XmlReader[] { mslReader });

                // Initialize the view generator to generate views in C#
                EntityViewGenerator viewGenerator = new EntityViewGenerator();
                viewGenerator.LanguageOption = LanguageOption.GenerateCSharpCode;
                IList<EdmSchemaError> errors = viewGenerator.GenerateViews(mappingItems, writer);

                foreach (EdmSchemaError e in errors)
                {
                    // log error
                    this.Error(e.Message);
                }

                MemoryStream memStream = writer.BaseStream as MemoryStream;
                generatedViews = Encoding.UTF8.GetString(memStream.ToArray());
            }
        }
        catch (Exception ex)
        {
            // log error
            this.Error(ex.ToString());
        }

        return generatedViews;
    }

    private void GetConceptualMappingAndStorageReaders(string edmxFile, out XmlReader csdlReader, out XmlReader mslReader, out XmlReader ssdlReader)
    {
        csdlReader = null;
        mslReader = null;
        ssdlReader = null;

        XNamespace edmxns = "http://schemas.microsoft.com/ado/2008/10/edmx";
        XNamespace csdlns = "http://schemas.microsoft.com/ado/2008/09/edm";
        XNamespace mslns = "http://schemas.microsoft.com/ado/2008/09/mapping/cs";
        XNamespace ssdlns = "http://schemas.microsoft.com/ado/2009/02/edm/ssdl";

        XDocument edmxDoc = XDocument.Load(edmxFile);
        if (edmxDoc != null)
        {
            XElement edmxNode = edmxDoc.Element(edmxns + "Edmx");
            if (edmxNode != null)
            {
                XElement runtimeNode = edmxNode.Element(edmxns + "Runtime");
                if (runtimeNode != null)
                {
                    // Create XmlReader over CSDL in EDMX
                    XElement conceptualModelsNode = runtimeNode.Element(edmxns + "ConceptualModels");
                    if (conceptualModelsNode != null)
                    {
                        XElement csdlContent = conceptualModelsNode.Element(csdlns + "Schema");
                        if (csdlContent != null)
                        {
                            csdlReader = csdlContent.CreateReader();
                        }
                    }

                    // Create XmlReader over MSL in EDMX
                    XElement mappingsNode = runtimeNode.Element(edmxns + "Mappings");
                    if (mappingsNode != null)
                    {
                        XElement mslContent = mappingsNode.Element(mslns + "Mapping");
                        if (mslContent != null)
                        {
                            mslReader = mslContent.CreateReader();
                        }
                    }

                    // Create XmlReader over SSDL in EDMX
                    XElement storageModelsNode = runtimeNode.Element(edmxns + "StorageModels");
                    if (storageModelsNode != null)
                    {
                        XElement ssdlContent = storageModelsNode.Element(ssdlns + "Schema");
                        if (ssdlContent != null)
                        {
                            ssdlReader = ssdlContent.CreateReader();
                        }
                    }
                }
            }
        }
    }
#>

Posté le mardi 30 juin 2009 01:56 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , , ,

POCO T4

Avec l’EF4 features CTP1, on a un super template POCO divisé en 2 tt files ce qui permet de générer les classes d’entité POCO dans un autre projet que le contexte.

C’est donc super méga cool ! Smile

Cependant, je trouve que c’est dommage de ne pas avoir une interface pour le contexte dans l’optique de le mocker.

J’ai donc modifié le .Context.tt:

 

<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.ctp.CS.ttinclude"#><#@
output extension=".cs"#><#
// Copyright (c) Microsoft Corporation.  All rights reserved.

CodeGenerationTools code = new CodeGenerationTools(this);
MetadataLoader loader = new MetadataLoader(this);
CodeRegion region = new CodeRegion(this, 1);
MetadataTools ef = new MetadataTools(this);

string inputFile = @"Northwind.edmx";
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
string namespaceName = code.VsNamespaceSuggestion();

TemplateFileManager fileManager = TemplateFileManager.Create(this);
EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
if (container == null)
{
    return "// No EntityContainer exists in the model, so no code was generated";
}

// Emit Entity Types
    string interfaceName = "I" + code.Escape(container);
    fileManager.StartNewFile(interfaceName + ".cs");
#>
using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.EntityClient;
using Entities;

<#
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#
    PushIndent(CodeRegion.GetIndent(1));
}
#>
<#=Accessibility.ForType(container)#> interface <#=interfaceName#>
{
<#
        foreach (EntitySet entitySet in container.BaseEntitySets.OfType<EntitySet>())
        {
#>
    IObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.Escape(entitySet)#> { get; }
<#
        }
#>
<#
        if (container.FunctionImports.Any())
        {
#>

<#
        }
        foreach (EdmFunction edmFunction in container.FunctionImports)
        {
            var parameters = FunctionImportParameter.Create(edmFunction.Parameters, code, ef);
            string paramList = String.Join(", ", parameters.Select(p => p.FunctionParameterType + " " + p.FunctionParameterName).ToArray());
            if(edmFunction.ReturnParameter == null)
            {
                continue;
            }
            string returnTypeElement = code.Escape(ef.GetElementType(edmFunction.ReturnParameter.TypeUsage));

#>
    IEnumerable<<#=returnTypeElement#>> <#=code.Escape(edmFunction)#>(<#=paramList#>);
<#
        }
#>
}
<#
if (!String.IsNullOrEmpty(namespaceName))
{
    PopIndent();
#>
}
<#
}
    fileManager.WriteFiles();
#>
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.EntityClient;
using Entities;

<#
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#
    PushIndent(CodeRegion.GetIndent(1));
}
#>
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : ObjectContext, <#= interfaceName #>
{
    public const string ConnectionString = "name=<#=container.Name#>";
    public const string ContainerName = "<#=container.Name#>";

    #region Constructors

    public <#=code.Escape(container)#>()
        : base(ConnectionString, ContainerName)
    {
        ContextOptions.DeferredLoadingEnabled = true;
    }

    public <#=code.Escape(container)#>(string connectionString)
        : base(connectionString, ContainerName)
    {
        ContextOptions.DeferredLoadingEnabled = true;
    }

    public <#=code.Escape(container)#>(EntityConnection connection)
        : base(connection, ContainerName)
    {
        ContextOptions.DeferredLoadingEnabled = true;
    }

    #endregion

<#
        region.Begin("ObjectSet Properties");

        foreach (EntitySet entitySet in container.BaseEntitySets.OfType<EntitySet>())
        {
#>

    <#=AccessibilityAndVirtual(Accessibility.ForReadOnlyProperty(entitySet))#> ObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.Escape(entitySet)#>
    {
        get { return <#=code.FieldName(entitySet) #>  ?? (<#=code.FieldName(entitySet)#> = CreateObjectSet<<#=code.Escape(entitySet.ElementType)#>>("<#=entitySet.Name#>")); }
    }
    private ObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.FieldName(entitySet)#>;
    IObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=interfaceName#>.<#=code.Escape(entitySet)#>
    {
        get { return <#=code.Escape(entitySet)#>; }
    }
<#
        }

        region.End();
#>

<#
        region.Begin("Function Imports");

        foreach (EdmFunction edmFunction in container.FunctionImports)
        {
            var parameters = FunctionImportParameter.Create(edmFunction.Parameters, code, ef);
            string paramList = String.Join(", ", parameters.Select(p => p.FunctionParameterType + " " + p.FunctionParameterName).ToArray());
            if(edmFunction.ReturnParameter == null)
            {
                continue;
            }
            string returnTypeElement = code.Escape(ef.GetElementType(edmFunction.ReturnParameter.TypeUsage));

#>
    <#=AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction))#> ObjectResult<<#=returnTypeElement#>> <#=code.Escape(edmFunction)#>(<#=paramList#>)
    {
<#
            foreach (var parameter in parameters)
            {
                if (!parameter.NeedsLocalVariable)
                {
                    continue;
                }
#>

        ObjectParameter <#=parameter.LocalVariableName#>;

        if (<#=parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null"#>)
        {
            <#=parameter.LocalVariableName#> = new ObjectParameter("<#=parameter.EsqlParameterName#>", <#=parameter.FunctionParameterName#>);
        }
        else
        {
            <#=parameter.LocalVariableName#> = new ObjectParameter("<#=parameter.EsqlParameterName#>", typeof(<#=parameter.RawClrTypeName#>));
        }
<#
            }
#>
        return base.ExecuteFunction<<#=returnTypeElement#>>("<#=edmFunction.Name#>"<#=code.StringBefore(", ", string.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray()))#>);
    }
    IEnumerable<<#=returnTypeElement#>> <#=interfaceName#>.<#=code.Escape(edmFunction)#>(<#=paramList#>)
    {
        return <#=code.Escape(edmFunction)#>(<#=String.Join(", ", parameters.Select(p => p.FunctionParameterName).ToArray())#>);
    }
<#
        }

        region.End();
#>
}
<#
if (!String.IsNullOrEmpty(namespaceName))
{
    PopIndent();
#>
}
<#
}
#>
<#+
string AccessibilityAndVirtual(string accessibility)
{
    if (accessibility != "private")
    {
        return accessibility + " virtual";
    }

    return accessibility;
}
#>

 

Enjoy Smile

Posté le vendredi 26 juin 2009 07:33 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , , , ,

Entity Framework : Quoi de neuf avec EF4 (VS 2010 + Feature CTP1)

Dans la vision de Microsoft, l’Entity Framework est la technologie d’accès à une base de données. Si certains sont déjà convaincus par la V1 (moi par exemple Wink), il n’en demeure pas moins que pour s’imposer face à N-Hibernate, LLBLGenPro, pour ne citer qu’eux, MS doit continuer d’investir massivement sur cette techno. Bonne nouvelle, c’est exactement ce qui est fait Smile.

Beaucoup de nouveautés seront donc apportées à la V2 d’EF, également appelée EF4 (pour prendre le numéro de .NET). La plus part ont déjà été annoncées sur le blog EFDesign. Toutes ne sont pas encore disponibles dans la beta 1 dans laquelle on retrouve également des nouveautés sur lesquelles MS n’avait pas communiqué officiellement.

Quelles sont donc les nouveautés de cette Beta :

  • le POCO bien sûr. C’est de toute évidence le principal investissement de la part de l’EF Team.

Pour les non initiés, POCO signifie Plain Old CLR Objects. Pour faire simple, il s’agit d’utiliser des entités sans aucune dépendance avec Entity Framework.

  • le lazy loading.

Pour activer le lazy loading, il faut renseigner la propriété ContextOptions.DeferredLoadingEnabled à true sur le contexte.

  • Les entité “self trackées”

L’idée est de pouvoir déterminer l’état de l’entité quand celle-ci est modifiée indépendamment du contexte. Ceci est notamment très utile dans le cas d’un scénario N-Tiers avec WCF par exemple.

  • L’utilisation des template T4 intégrée dans VS 2010.

Le template T4 permet de complètement maîtriser le code généré à partir du modèle. A noter qu’avec VS 2008, il était possible de faire du T4 mais ce n’était pas natif. VS 2010 Beta 1 + EF4 features CTP1 propose 3 templates T4 prédéfinies :

    • Le template Prescriptive classes (ie les classes d’entité héritent de classes de l’Entity Framework)
    • Le template POCO
    • Le template Self Tracking entities
  • Les fonctions définies dans le modèle

Vu que je n’en ai encore jamais parlé sur mon blog à ce jour, je vais développer un petit peu.

Imaginons que l’on veuille calculer un âge. Pour cela, il faut faire une différence entre la date courante et la date de naissance. Généralement, c’est la date du serveur qui sert de référence.

Seul problème, comment faire ça avec LINQ To Entities. En effet, si on utilise DateTime.Now, c’est la date du poste exécutant la requête LINQ qui va être prise en compte en non la date du serveur.

Avec EF4, il est possible, dans le CSDL, de définir la function suivante :

<Function Name="GetAge" ReturnType="Int32">
    <Parameter Name="person" Type="TestLINQFunctionsModel.Person" />
    <DefiningExpression>
        DiffYears(person.BirthDay, CurrentDateTime())
    </DefiningExpression>
</Function>

Ensuite, cette fonction est utilisable directement dans les requêtes esql.

Ce qui serait vraiment top c’est de rendre cela également possible pour les requêtes LINQ To Entities.

Pour cela, il faut rajouter la méthode à appeler dans la requête LINQ :

public static class PersonExtension

{

    [EdmFunction("TestLINQFunctionsModel", "GetAge")]

    public static int GetAge(this Person p)

    {

        throw new NotSupportedException();

    }

}

// Bien entendu, rien n’oblige à passer par une extension method.

Il est maintenant possible d’utiliser la méthode GetAge dans une requête LINQ to Entities :

var q = from p in context.Persons

        select new { p.LastName, p.FirstName, Age = p.GetAge() };

Cette requête génèrera la requête SQL suivante :

SELECT
1 AS [C1],
[Extent1].[FirstName] AS [FirstName],
DATEDIFF (year, [Extent1].[BirthDay], SysDateTime()) AS [C2]
FROM [dbo].[Persons] AS [Extent1]

Avec un procédé similaire, il est également possible d’utiliser directement des fonctions SQL (TSQL, PLSQL, etc.). Vu que je n’aime pas cette idée car cela casse l’abstraction apportée par EF, je ne développerai volontairement pas ce point.

  • La possibilité d’exécuter des requêtes SQL directement
  • La possibilité d’utiliser des entités sans modèle

 

A cela, il faut rajouter plein de nouveautés très utiles dans le designer :

  • La gestion des Complex Type

Les complex types sont enfin supportés par le designer même s’ils ne sont visibles que dans le model browser. Il y a même la possibilité de faire du refactoring

  • La possibilité de faire du Model First

Il est possible de générer la base à partir des entités.

  • La possibilité de “singuliariser” les noms des classes d’entité automatiquement lors de la récupération depuis la base
  • La possibilité de supprimer des éléments du SSDL
  • La correction des problèmes du style “la propriété est non mappée” pour les complex types et l’Horizontal Entity Splitting

 

Enfin, il y a plusieurs nouvelle classes et méthodes très pratiques qui ont été rajoutées. Je reviendrai dessus au fur et à mesure de mes posts.

Posté le mercredi 24 juin 2009 22:05 par Matthieu MEZIL | 6 commentaire(s)

EF4 – Features CTP1

Avec VS 2010 Beta 1, il manquait quelque features pour lesquelles l’EF team avait communiqué en disant qu’elles arriveraient prochainement.

C’est fait Smile

Enjoy Smile

Posté le mardi 23 juin 2009 00:21 par Matthieu MEZIL | 0 commentaire(s)

SubObjectSet

Avec EF, quand on utilise les mappings d’héritage TPH ou TPC, l’EntitySet est sur la classe de base.

Dans ce cas, avec EF v1, il était possible d’ajouter une propriété sur le contexte qui retourne l’EntitySet.OfType<MySubType>().

Avec EF v1, l’EntitySet est un ObjectQuery<T> et notre propriété aussi. Avec EF v2 l’EntitySet est un ObjectSet<T>. On trouve dans cette classe implémente l’interface IObjectSet<T> dans laquelle on trouve 3 méthodes pour ajouter, attacher ou supprimer des entités.

Ce qui serait cool ce serait de pouvoir utiliser les “sous-EntitySet” comme des ObjectSet.

Pour cela, j’ai écrit la classe SubObjectSet :

public class SubObjectSet<TBase, TInherited> : ObjectQuery<TInherited>, IObjectSet<TInherited>

    where TBase : class

    where TInherited : class, TBase

{

    public ObjectSet<TBase> ObjectSet { get; private set; }

 

    public SubObjectSet(ObjectSet<TBase> objectSet)

        : base(objectSet.OfType<TInherited>().CommandText, objectSet.Context)

    {

        ObjectSet = objectSet;

    }

 

    #region IObjectSet<TInherited> Members

    public void AddObject(TInherited entity)

    {

        ObjectSet.AddObject(entity);

    }

    public void Attach(TInherited entity)

    {

        ObjectSet.Attach(entity);

    }

    public void DeleteObject(TInherited entity)

    {

        ObjectSet.DeleteObject(entity);

    }

    #endregion

}

Posté le mardi 16 juin 2009 19:27 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , ,

Entity Framework v2 – comment récupérer plus simplement une seule entité ?

Alex James a écrit une extension méthode qui permet de récupérer depuis une requête une seule entité en spécifiant sa clé.

Cependant, si on a la clé, il est inutile de rendre cette extension method sur une requête, un EntitySet suffit. Avec EF4, cette méthode peut donc prendre un ObjectSet comme paramètre à la place d’un ObjectQuery.

// Dans la première version d’Entity Framework, la classe ObjectSet n’existe pas, les EntitySet sont exposés comme des ObjectQuery.

// La classe ObjectSet<T> hérite d’ObjectQuery<T>

Cela simplifie le code et rend possible l’utilisation directe de la méthode (Try)GetObjectByKey :

public static class ObjectSetExtension

{

    public static T Get<T>(this ObjectSet<T> objectSet, object key) where T : class

    {

        object value;

        objectSet.Context.TryGetObjectByKey(new EntityKey(string.Concat(objectSet.Context.DefaultContainerName, ".", objectSet.EntitySet.Name), objectSet.EntitySet.ElementType.KeyMembers.Single().Name, key), out value);

        return (T)value;

    }

    public static T Get<T>(this ObjectSet<T> objectSet, params EntityKeyMember[] keys) where T : class

    {

        object value;

        objectSet.Context.TryGetObjectByKey(new EntityKey(string.Concat(objectSet.Context.DefaultContainerName, ".", objectSet.EntitySet.Name), keys), out value);

        return (T)value;

    }

}

La deuxième extension méthode peut être utilisée pour les entités avec une clé composite.

Posté le vendredi 12 juin 2009 21:25 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , ,

Entity Framework : Undo Redo v2

Suite à ma première version de l’Undo Redo, il m’a été demandé de rajouter la possibilité de regrouper plusieurs actions par Undo / Redo.

J’ai donc rajouté deux nouvelles extension méthodes : BeginGroupOfUndoActions et EndGroupOfUndoActions.

Cela donne donc maintenant ceci :

public static class ObjectContextExtension

{

    private static Dictionary<ObjectContext, ObjectContextUndoRedo> _objectContextUndoRedo = new Dictionary<ObjectContext, ObjectContextUndoRedo>();

 

    public static void ActivateUndoRedoTracking(this ObjectContext context, int undoStackLength)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

        {

            objectContextUndoRedo = new ObjectContextUndoRedo { Context = context };

            _objectContextUndoRedo.Add(context, objectContextUndoRedo);

        }

        objectContextUndoRedo.ActivateUndoRedoTracking(undoStackLength);

    }

 

    public static bool CanUndo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        return objectContextUndoRedo.CanUndo;

    }

 

    public static void Undo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.Undo();

    }

 

    public static bool CanRedo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        return objectContextUndoRedo.CanRedo;

    }

 

    public static void Redo(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.Redo();

    }

 

    public static void BeginGroupOfUndoActions(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.MultipleActions = true;

    }

 

    public static void EndGroupOfUndoActions(this ObjectContext context)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        _objectContextUndoRedo.TryGetValue(context, out objectContextUndoRedo);

        if (objectContextUndoRedo == null)

            throw new InvalidOperationException();

        objectContextUndoRedo.MultipleActions = false;

    }

 

    private class ObjectContextUndoRedo

    {

        private List<List<UndoRedoAction>> _undo, _redo;

        private int _undoStackLength;

        private bool _trackChanges;

 

        public ObjectContext Context { get; set; }

 

        private bool _multipleActions;

        public bool MultipleActions

        {

            get { return _multipleActions; }

            set

            {

                _multipleActions = value;

                if (value)

                    _undo.Insert(0, new List<UndoRedoAction>());

            }

        }

 

        public void ActivateUndoRedoTracking(int undoStackLength)

        {

            _undoStackLength = undoStackLength;

            _undo = new List<List<UndoRedoAction>>(undoStackLength);

            _redo = new List<List<UndoRedoAction>>(undoStackLength);

            _trackChanges = true;

 

            var objectStateEntries = Context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).ToList();

 

            PropertyChangingEventHandler entityModifing = null;

            entityModifing = (sender, e) =>

            {

                var propInfo = sender.GetType().GetProperty(e.PropertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

                var value = propInfo.GetValue(sender, null);

                var undoRedoAction = new UndoRedoAction { EntityState = EntityState.Modified, UndoAction = () => propInfo.SetValue(sender, value, null) };

                var inpc = sender as INotifyPropertyChanged;

                if (inpc != null)

                {

                    PropertyChangedEventHandler entityModified = null;

                    entityModified = (s, e2) =>

                        {

                            if (e.PropertyName == e2.PropertyName && _trackChanges)

                            {

                                var newValue = propInfo.GetValue(sender, null);

                                undoRedoAction.RedoAction = () => propInfo.SetValue(sender, newValue, null);

                                if (MultipleActions)

                                    _undo[0].Add(undoRedoAction);

                                else

                                    _undo.Insert(0, new List<UndoRedoAction>() { undoRedoAction });

                                if (_undo.Count > _undoStackLength)

                                    _undo.RemoveAt(_undoStackLength);

                                _redo.Clear();

                            }

                            inpc.PropertyChanged -= entityModified;

                        };

                    inpc.PropertyChanged += entityModified;

                }

            };

            foreach (var e in objectStateEntries.Select(ose => ose.Entity as INotifyPropertyChanging).Where(inpc => inpc != null))

                e.PropertyChanging += entityModifing;

 

            Context.ObjectStateManager.ObjectStateManagerChanged += (sender, e) =>

            {

                switch (e.Action)

                {

                    case CollectionChangeAction.Add:

                        var inpc = e.Element as INotifyPropertyChanging;

                        if (inpc != null)

                            inpc.PropertyChanging += entityModifing;

                        break;

                }

            };

        }

 

        public bool CanUndo

        {

            get { return _undo != null && _undo.Any(); }

        }

 

        public void Undo()

        {

            if (!CanUndo)

                throw new InvalidOperationException();

            var undoRedoAction = _undo.First();

            _undo.RemoveAt(0);

            _trackChanges = false;

            foreach (var undoAction in undoRedoAction)

                undoAction.UndoAction();

            _trackChanges = true;

            _redo.Insert(0, undoRedoAction);

        }

 

        public bool CanRedo

        {

            get { return _redo != null && _redo.Any(); }

        }

 

        public void Redo()

        {

            if (!CanRedo)

                throw new InvalidOperationException();

            var undoRedoAction = _redo.First();

            _redo.RemoveAt(0);

            _trackChanges = false;

            foreach (var redoAction in undoRedoAction)

                redoAction.RedoAction();

            _trackChanges = true;

            _undo.Insert(0, undoRedoAction);

        }

    }

 

    private class UndoRedoAction

    {

        public EntityState EntityState { get; set; }

        public Action UndoAction { get; set; }

        public Action RedoAction { get; set; }

    }

}

J’ai pu me convaincre lors des derniers mois que pour faire une démo sur ce genre de code, il n’y avait rien de mieux que des tests unitaires.

[TestClass]

public class ObjectContextExtensionTest

{

    [TestMethod]

    public void Test()

    {

        using (var context = new NorthwindEntities())

        {

            var c = context.Categories.First();

            context.ActivateUndoRedoTracking(5);

            var cOriginalCategoryName = c.CategoryName;

            c.CategoryName = "CN";

            var c2 = context.Categories.OrderBy(c3 => c3.CategoryID).Skip(1).First();

            var c2OriginalCategoryName = c2.CategoryName;

            c2.CategoryName = "C2N";

            c.CategoryName = "CN2";

            context.Undo();

            Assert.AreEqual("CN", c.CategoryName);

            Assert.AreEqual("C2N", c2.CategoryName);

            context.Undo();

            Assert.AreEqual("CN", c.CategoryName);

            Assert.AreEqual(c2OriginalCategoryName, c2.CategoryName);

            context.Undo();

            Assert.AreEqual(cOriginalCategoryName, c.CategoryName);

            Assert.AreEqual(c2OriginalCategoryName, c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN", c.CategoryName);

            Assert.AreEqual(c2OriginalCategoryName, c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN", c.CategoryName);

            Assert.AreEqual("C2N", c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN2", c.CategoryName);

            Assert.AreEqual("C2N", c2.CategoryName);

 

            context.BeginGroupOfUndoActions();

            c.CategoryName = "CN3";

            c2.CategoryName = "C2N2";

            context.BeginGroupOfUndoActions();

            c.CategoryName = "CN4";

            c2.CategoryName = "C2N3";

            context.EndGroupOfUndoActions();

            c.CategoryName = "CN5";

            c2.CategoryName = "C2N4";

            context.Undo();

            Assert.AreEqual("CN5", c.CategoryName);

            Assert.AreEqual("C2N3", c2.CategoryName);

            context.Undo();

            Assert.AreEqual("CN4", c.CategoryName);

            Assert.AreEqual("C2N3", c2.CategoryName);

            context.Undo();

            Assert.AreEqual("CN3", c.CategoryName);

            Assert.AreEqual("C2N2", c2.CategoryName);

            context.Undo();

            Assert.AreEqual("CN2", c.CategoryName);

            Assert.AreEqual("C2N", c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN3", c.CategoryName);

            Assert.AreEqual("C2N2", c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN4", c.CategoryName);

            Assert.AreEqual("C2N3", c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN5", c.CategoryName);

            Assert.AreEqual("C2N3", c2.CategoryName);

            context.Redo();

            Assert.AreEqual("CN5", c.CategoryName);

            Assert.AreEqual("C2N4", c2.CategoryName);

        }

    }

}

Posté le mercredi 10 juin 2009 09:08 par Matthieu MEZIL | 2 commentaire(s)

Classé sous : , ,

EF : Undo Redo

Comment gérer l’Undo Redo avec EntityFramework. Ceci n’est pas géré nativement et ne le sera pas non plus dans la v2.

Il va donc falloir le code nous-même.

J’aurais pu cloner les entités et les stocker dans une Stack mais j’avais peur de faire grossir exagérément la taille de la mémoire utilisée pour la gestion du Undo / Redo.

Je suis donc parti sur une autre solution.

Dans le cadre de ce POC, je me suis limité aux modifications des propriétés scalaires.

public static class ObjectContextExtension

{

    private static Dictionary<ObjectContext, ObjectContextUndoRedo> _objectContextUndoRedo = new Dictionary<ObjectContext, ObjectContextUndoRedo>();

 

    public static void ActivateUndoRedoTracking(this ObjectContext context, int undoStackLength)

    {

        ObjectContextUndoRedo objectContextUndoRedo;

        if (_objectContextUndoRedo.ContainsKey(context))

            objectContextUndoRedo = _objectContextUndoRedo[context];

        else

        {

            objectContextUndoRedo = new ObjectContextUndoRedo { Context = context };

            _objectContextUndoRedo.Add(context, objectContextUndoRedo);

        }

        objectContextUndoRedo.ActivateUndoRedoTracking(undoStackLength);

    }

 

    public static bool CanUndo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        return _objectContextUndoRedo[context].CanUndo;

    }

 

    public static void Undo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        _objectContextUndoRedo[context].Undo();

    }

 

    public static bool CanRedo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        return _objectContextUndoRedo[context].CanRedo;

    }

 

    public static void Redo(this ObjectContext context)

    {

        if (!_objectContextUndoRedo.ContainsKey(context))

            throw new InvalidOperationException();

        _objectContextUndoRedo[context].Redo();

    }

 

    private class ObjectContextUndoRedo

    {

        private List<UndoRedoAction> _undo, _redo;

        private int _undoStackLength;

        private bool _trackChanges;

 

        public ObjectContext Context { get; set; }

 

        public void ActivateUndoRedoTracking(int undoStackLength)

        {

            _undoStackLength = undoStackLength;

            _undo = new List<UndoRedoAction>(undoStackLength);

            _redo = new List<UndoRedoAction>(undoStackLength);

            _trackChanges = true;

 

            var objectStateEntries = Context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged).ToList();

 

            PropertyChangingEventHandler entityModifing = null;

            entityModifing = (sender, e) =>

            {

                var propInfo = sender.GetType().GetProperty(e.PropertyName);

                var value = propInfo.GetValue(sender, null);

                var undoRedoAction = new UndoRedoAction { EntityState = EntityState.Modified, UndoAction = () => propInfo.SetValue(sender, value, null) };

                var inpc = sender as INotifyPropertyChanged;

                if (inpc != null)

                {

                    PropertyChangedEventHandler entityModified = null;

                    entityModified = (s, e2) =>

                        {

                            if (e.PropertyName == e2.PropertyName && _trackChanges)

                            {

                                var newValue = propInfo.GetValue(sender, null);

                                undoRedoAction.RedoAction = () => propInfo.SetValue(sender, newValue, null);

                                _undo.Insert(0, undoRedoAction);

                                if (_undo.Count > _undoStackLength)

                                    _undo.RemoveAt(_undoStackLength);

                                _redo.Clear();

                            }

                            inpc.PropertyChanged -= entityModified;

                        };

                    inpc.PropertyChanged += entityModified;

                }

            };

            foreach (var e in objectStateEntries.Select(ose => ose.Entity as INotifyPropertyChanging).Where(inpc => inpc != null))

                e.PropertyChanging += entityModifing;

 

            Context.ObjectStateManager.ObjectStateManagerChanged += (sender, e) =>

            {

                switch (e.Action)

                {

                    case CollectionChangeAction.Add:

                        var inpc = e.Element as INotifyPropertyChanging;

                        if (inpc != null)

                            inpc.PropertyChanging += entityModifing;

                        break;

                }

            };

        }

 

        public bool CanUndo

        {

            get { return _undo != null && _undo.Any(); }

        }

 

        public void Undo()

        {

            if (!CanUndo)

                throw new InvalidOperationException();

            var undoRedoAction = _undo.First();

            _undo.RemoveAt(0);

            _trackChanges = false;

            undoRedoAction.UndoAction();

            _trackChanges = true;

            _redo.Insert(0, undoRedoAction);

        }

 

        public bool CanRedo

        {

            get { return _redo != null && _redo.Any(); }

        }

 

        public void Redo()

        {

            if (!CanRedo)

                throw new InvalidOperationException();

            var undoRedoAction = _redo.First();

            _redo.RemoveAt(0);

            _trackChanges = false;

            undoRedoAction.RedoAction();

            _trackChanges = true;

            _undo.Insert(0, undoRedoAction);

        }

    }

 

    private class UndoRedoAction

    {

        public EntityState EntityState { get; set; }

        public Action UndoAction { get; set; }

        public Action RedoAction { get; set; }

    }

}

Maintenant, nous pouvons sérieusement nous poser la question de l'intérêt de l'Undo/Redo. En effet, cela pose beaucoup de problèmes : les delete en cascade, les Identity, les accés concurrents, etc.

Posté le mardi 9 juin 2009 00:50 par Matthieu MEZIL | 2 commentaire(s)

Classé sous : , ,

Comment découper ses entités en plusieurs modèles v2 ?

Suite à mon post précédent sur le sujet, mon client est revenu vers moi avec la question suivante : comment récupérer un graphe contenant l’ensemble des entités : les catégories, les fournisseurs, les produits, les lignes de commande, les commandes, les clients et les employés ?

Pour cela, il faut commencer par ajouter des “sortes de” navigation properties aux entités comme ceci :

private Supplier _supplier;

/// <remarks>

/// Changes aren't saved

/// </remarks>

public Supplier Supplier

{

    get

    {

        if (_supplier == null)

            _supplier = this.GetSupplier();

        return _supplier;

    }

    set { _supplier = value; }

}

 

private IEnumerable<Orders.OrderDetail> _orderDetails;

/// <remarks>

/// Changes aren't saved

/// </remarks>

public IEnumerable<Orders.OrderDetail> OrderDetails

{

    get

    {

        if (_orderDetails == null)

            _orderDetails = this.GetOrderDetails();

        return _orderDetails;

    }

    set { _orderDetails = value; }

}

L’idée va ensuite être de pré-renseigné ces informations.

Pour cela, nous pourrions faire ceci :

using (var stockContext = new StocksEntities())

{

    var categories = stockContext.Categories.Include("Products").ToList();

    foreach (var p in stockContext.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged).Select(e => e.Entity).OfType<Entities.Stocks.Product>())

    {

        using (var supplierContext = new SuppliersEntities())

        {

            p.Supplier = (from p2 in supplierContext.Products

                          where p2.ProductID == p.ProductID

                          select p2.Supplier).FirstOrDefault();

        }

        using (var orderContext = new OrdersEntities())

        {

            p.OrderDetails = (from od in orderContext.OrderDetails.Include("Order.Customer")

                              where od.ProductID == p.ProductID

                              select od).ToList();

            foreach (var o in orderContext.ObjectStateManager.GetObjectStateEntries(EntityState.Unchanged).Select(e => e.Entity).OfType<Entities.Orders.Order>())

            {

                using (var employeeContext = new EmployeesEntities())

                {

                    o.Employee = (from oe in employeeContext.Orders

                                  where oe.OrderID == o.OrderID

                                  select oe.Employee).FirstOrDefault();

                }

            }

        }

    }

}

Cependant, cela génèrera potentiellement énormément de requêtes. Dans l’hypothèse où nous souhaiterions récupérer qu’une partie des catégories (avec tout ce qui suit), on pourrait faire ceci :

using (var stockContext = new StocksEntities())

{

    using (var supplierContext = new SuppliersEntities())

    {

        using (var orderContext = new OrdersEntities())

        {

            using (var employeeContext = new EmployeesEntities())

            {

                var categories = stockContext.Categories.Include("Products").ToList();

                var products = categories.SelectMany(c => c.Products);

                var suppliers = supplierContext.Suppliers.Where(BuildContainsExpression<Supplier, int>(s => s.SupplierID, (from p in products

               where p.SupplierID.HasValue

               select p.SupplierID.Value).Distinct())).ToList();

                var orderDetails = orderContext.OrderDetails.Include("Order.Customer").Where(BuildContainsExpression<Entities.Orders.OrderDetail, int>(od => od.ProductID, products.Select(p => p.ProductID))).ToList();

                var orders = orderDetails.Select(od => od.Order);

                var employees = employeeContext.Employees.Where(BuildContainsExpression<Employee, int>(e => e.

EmployeeID, (from o in orders

             where o.EmployeeID.HasValue

             select o.EmployeeID.Value).Distinct())).ToList();

                foreach (var p in products)

                {

                    p.Supplier = suppliers.FirstOrDefault(s => s.SupplierID == p.SupplierID);

                    p.OrderDetails = (from od in orderDetails

                                      where od.ProductID == p.ProductID

                                      select od);

                    foreach (var o in orders)

                        o.Employee = employees.FirstOrDefault(e => o.EmployeeID == e.EmployeeID);

                }

            }

        }

    }

}

Si on souhaite récupérer l’ensemble des infos, autant faire directement ceci :

using (var stockContext = new StocksEntities())

{

    using (var supplierContext = new SuppliersEntities())

    {

        using (var orderContext = new OrdersEntities())

        {

            using (var employeeContext = new EmployeesEntities())

            {

                var categories = stockContext.Categories.Include("Products").ToList();

                var products = categories.SelectMany(c => c.Products);

                var suppliers = supplierContext.Suppliers.ToList();

                var orderDetails = orderContext.OrderDetails.Include("Order.Customer").ToList();

                var orders = orderDetails.Select(od => od.Order);

                var employees = employeeContext.Employees.ToList();

                foreach (var p in products)

                {

                    p.Supplier = suppliers.FirstOrDefault(s => s.SupplierID == p.SupplierID);

                    p.OrderDetails = (from od in orderDetails

                                      where od.ProductID == p.ProductID

                                      select od);

                    foreach (var o in orders)

                        o.Employee = employees.FirstOrDefault(e => o.EmployeeID == e.EmployeeID);

                }

            }

        }

    }

}

Posté le mardi 2 juin 2009 22:48 par Matthieu MEZIL | 2 commentaire(s)

ValidateOnBuild

Le designer fourni avec VS 2008 SP1 ne supporte pas les scenarii suivants :

  • Complex Types (supporté dans VS 2010 Beta 1)
  • TPC
  • Horizontal Entity Splitting

D’autre designer (qui plus est gratuit) le supporte Wink.

Un gros problème avec le designer de VS 2008 (cela n’est plus le cas avec 2010) est qu’il génère des erreurs qui n’en sont pas. Ceci est particulièrement énervant car ces erreurs se retrouvent à chaque compilation alors que le programme lui compile bien. En effet, l’emdx n’est pas compilé. C’est le code est généré par l’edmx qui l’est.

Cependant, cela peut être assez déroutant. Et quand les erreurs sont du style : les complex types ne sont pas supportées par le designer, il y a de quoi s’énerver : WTF! Angry

Un edmx se décompose en 4 parties :

  • les 3 parties de l’EDM :
    • SSDL // description de la base de données
    • CSDL // description des entités
    • MSL // description du mapping entre le SSDL et le CSDL
  • une partie propre au designer dont je ne me suis jamais vraiment soucié.

Quelle erreur !!!

C’est en préparant un petit outil bien sympa que je vais réaliser avec Michel, sur laquelle nous devrions communiquer mardi (hé hé suivez nos blogs) que je me suis rendu compte qu’il y avait un attribut xml ValidateOnBuild associé à un attribut Value de type bool.

<edmx:Options>

  <DesignerInfoPropertySet>

    <DesignerProperty Name="ValidateOnBuild" Value="false" />

  </DesignerInfoPropertySet>

</edmx:Options>

Vous l’aurez deviné, en le passant à false, plus de message d’erreur à la compilation et on retrouve son calme. Smile

Posté le jeudi 28 mai 2009 22:10 par Matthieu MEZIL | 3 commentaire(s)

Comment découper ses entités en plusieurs modèles

Un de mes clients veut développer un ERP avec EF. Sa base contient plus de 600 tables quasiment toutes reliées les unes avec les autres. Le problème est qu’Entity Framework a de réelles difficultés avec les gros modèles. Je vous invite d’ailleurs à regarder les deux posts de Srikanth Mandadi (working-with-large-models-in-entity-framework-part-1.aspx et working-with-large-models-in-entity-framework-part-2.aspx).

Dans ce cas, l’idéal est de découper nos entités en plusieurs modèles.

En règle générale, un ERP est composé de plusieurs modules. L’idée serait donc de réaliser un modèle par module. Le problème est qu’une même entité peut se retrouver dans plusieurs modules. Prenons un exemple simple avec Northwind qui, je le sais, n’est pas une grosse base (autant prévenir les commentaires stupides Smile). Un produit peut être utilisé :

  • dans un module de gestion des stocks

image

  • dans un module relatif aux fournisseurs

image

  • dans un module relatif aux factures

image

Cela implique donc en théorie qu’il n’est pas possible de séparer les entités Category (module Stocks), Product (module Stocks, Fournisseurs et Factures), Supplier (module Fournisseurs), OrderDetail (module Factures), Order (module Factures) et Customer (module Factures).

Nous allons également rajouter un quatrième EDM utilisé dans un module sur la gestion des employés. Celui-ci va contenir en plus de l’entité Employee, les entités Order et OrderDetail.

image

Voyons comment dans la pratique, nous allons pouvoir travailler avec 4 EDM.

Premier point, nous allons définir ces 4 EDM. Afin de limiter les conflits, nous allons partir du principe qu’une entité ne peut être modifiée que par un seul module. Pour cela, nous mettrons toutes les propriétés (hormis la clé) private dans les autres modèles.

Il est également possible “d’alléger” les entités. Par exemple, dans le module Fournisseur, on n’a pas besoin des infos relatives aux stocks.

Ok nice! Smile

Imaginons maintenant que l’on veuille faire des requêtes transverses à plusieurs modules.

Dans un premier temps, nous allons vouloir récupérer le meilleur fournisseur par catégorie.

Problème, les notions de fournisseurs et de catégories ne sont pas dans le même modèle. Si cette requête est très régulièrement appelé, il est souhaitable de rajouter l’entité Categpry dans l’EDM du module Suppliers (ou inversement). Il sera alors possible de mapper cette entité sur la vraie table ou sur une SSDL Vue.

Si la performance de cette requête n’est pas une priorité et qu’on peut se permettre d’effectuer deux requêtes en base au lieu d’une seule, il est possible de s’en sortir de la manière suivante.

Tout d’abord l’idée est, pour chaque entité “en double” lui faire implémenter une interface dans lesquelles on retrouvera les propriétés de sa ou ses clés :

public interface IProduct

{

    int ProductID { get; }

}

 

public interface IOrder

{

    int OrderID { get; }

}

 

public interface IOrderDetail

{

    int ProductID { get; }

    int OrderID { get; }

}

Ensuite il faut définir une méthode permettant de récupérer l’ensemble des relations de ces entités

public static class Product

{

    public static Category GetCategory(this IProduct product)

    {

        return GetCategory(product.ProductID);

    }

    public static Category GetCategory(int productID)

    {

        using (var context = new StocksEntities())

        {

            var p = new Stocks.Product { ProductID = productID };

            context.AttachTo("Products", p);

            p.CategoryReference.Load();

            return p.Category;

        }

    }

 

    public static Supplier GetSupplier(this IProduct product)

    {

        return GetSupplier(product.ProductID);

    }

    public static Supplier GetSupplier(int productID)

    {

        using (var context = new SuppliersEntities())

        {

            var p = new Suppliers.Product { ProductID = productID };

            context.AttachTo("Products", p);

            p.SupplierReference.Load();

            return p.Supplier;

        }

    }

 

    public static IEnumerable<IOrderDetail> GetOrderDetails(this IProduct product)

    {

        return GetOrderDetails(product.ProductID);

    }

    public static IEnumerable<IOrderDetail> GetOrderDetails(int productID)

    {

        using (var context = new OrdersEntities())

        {

            var p = new Orders.Product { ProductID = productID };

            context.AttachTo("Products", p);

            p.OrderDetails.Load();

            return p.OrderDetails.AsEnumerable().OfType<IOrderDetail>();

        }

    }

}

 

public static class Order

{

    public static Employee GetEmployee(this IOrder o)

    {

        return GetEmployee(o.OrderID);

    }

    public static Employee GetEmployee(int orderID)

    {

        using (var context = new EmployeesEntities())

        {

            var o = new Employees.Order { OrderID = orderID };

            context.AttachTo("Orders", o);

            o.EmployeeReference.Load();

            return o.Employee;

        }

    }

 

    public static Customer GetCustomer(this IOrder o)

    {

        return GetCustomer(o.OrderID);

    }

    public static Customer GetCustomer(int orderID)

    {

        using (var context = new OrdersEntities())

        {

            var o = new Orders.Order { OrderID = orderID };

            context.AttachTo("Orders", o);

            o.CustomerReference.Load();

            return o.Customer;

        }

    }

}

 

public static class OrderDetail

{

    public static IProduct GetProduct(this IOrderDetail od)

    {

        return GetProduct(od.OrderID, od.ProductID);

    }

    public static IProduct GetProduct(int orderID, int productID)

    {

        using (var context = new OrdersEntities())

        {

            var od = new Orders.OrderDetail { OrderID = orderID, ProductID = productID };

            context.AttachTo("OrderDetails", od);

            od.ProductReference.Load();

            return od.Product;

        }

    }

 

    public static Category GetCategory(this IOrderDetail od)

    {

        return Product.GetCategory(od.ProductID);

    }

 

    public static Employee GetEmployee(this IOrderDetail od)

    {

        return Order.GetEmployee(od.OrderID);

    }

 

    public static Customer GetCustomer(this IOrderDetail od)

    {

        return Order.GetCustomer(od.OrderID);

    }

}

A ce niveau là, il ne reste plus que la requête à écrire :

using (var stockContext = new StocksEntities())

{

    var bestSupplierPerCategory = from c in stockContext.Categories.Include("Products").AsEnumerable()

                                  let productSuppliers = from p in c.Products

                                                         select p.GetSupplier()

                                  let suppliers = from s in productSuppliers.Distinct()

                                                  orderby productSuppliers.Count(su => su.SupplierID == s.SupplierID) descending

                                                  select s

                                  select new { Category = c, Supplier = suppliers.FirstOrDefault() };

}

On pourra cependant regretter d’avoir fait une bonne partie de la requête en LINQ To Object. Il est possible d’améliorer cela.

Prenons un exemple un peu plus complexe : récupérer le meilleur employé par catégorie.

Afin d’optimiser la requête, il est préférable de ne va pas faire la group by en LINQ To Object (en récupérant d’abord les OrderDetails des produits de la catégorie puis l’employé de l’OrderDetail). Pour cela, nous n’allons pas utiliser les extension méthodes définies précédemment. A la place, nous allons créer deux contextes que nous allons mixer dans une seule requête LINQ comme ceci :

using (var stockContext = new StocksEntities())

{

    using (var employeesContext = new EmployeesEntities())

    {

        var bestEmployeePerCategory =

            from c in

                (from c in stockContext.Categories

                 select new

                 {

                     Category = c,

                     ProductsID = (from p in c.Products

                                   select p.ProductID)

                 }).AsEnumerable()

            let bestEmployee = (from od in employeesContext.OrderDetails.Where(BuildContainsExpression<Entities.Employees.OrderDetail, int>(od2 => od2.ProductID, c.ProductsID))

                                let e = od.Order.Employee

                                group od by new { e.EmployeeID, e.LastName, e.FirstName } into g

                                let sold = g.Sum(od => (double)od.UnitPrice * (double)od.Quantity * (1D - (double)od.Discount))

                                orderby sold descending

                                select new { g.Key.EmployeeID, g.Key.LastName, g.Key.FirstName, Sold = sold }).FirstOrDefault()

            select new { Category = c.Category, Employee = bestEmployee };

    }

}

Vous pouvez retrouver le script de génération de la base utilisée, le projet et les tests unitaires qui vont avec ici.

Posté le mercredi 27 mai 2009 01:47 par Matthieu MEZIL | 0 commentaire(s)

EF4 POCO : mes tests

Plutôt que de faire un long post inaccessible aux novices ou ennuyeux pour les experts, j’ai décidé de publier un projet de tests unitaires permettant d’illustrer le fonctionnement du POCO avec EF4. J’en ai également profité pour faire une classe de Mock permettant de tester les classes d’entités indépendamment d’EF ainsi qu’une classe BetaBugs dont le but, toujours à travers des tests unitaires, est de vous montrer également les bugs de cette Beta.

Posté le lundi 25 mai 2009 22:41 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , , ,

EF: requêter directement en SQL

Avec EF4, il est possible d’exécuter des requêtes directement en SQL. (A noter que cela était déjà possible avec les extensions d’EF avec EF v1).

Je n’aime vraiment pas cette idée. En effet, cela implique de casser l’abstraction offerte par l’edm vis-à-vis de la base de données.

Je n’aime d’autant pas ceci qu’avec la V1, il était possible, via des SSDL Functions, de spécifier la requête SQL. Pour rappel, les SSDL Functions sont définies directement dans le SSDL (la description de la base de données). Elles sont généralement associées aux procédures stockées mais il est également possible de les utiliser pour définir une requête SQL spécifique à exécuter (valable pour l’ensemble du CRUD).

Je ne doute pas de l’intérêt de pouvoir écrire des requêtes SQL spécifiques, particulièrement dans le cadre d’un développement itératif mais perdre l’abstraction est, à mon avis, regrettable.

Je viens de trouver un cas pour lequel c’est cependant très pratique : les tests unitaires. En effet, il est dommage de définir des SSDL Functions dans notre modèle si celles-ci ne sont utiliser que par nos tests. Imaginons que, dans le cadre des tests, il faille initialiser la base avec des données prédéfinies. Nous allons donc commencer par vider l’ensemble des tables. Pour cela, nous pourrions utiliser la suppression “basique” avec EF :

using (var context = new MyNorthwindEFEntities())

{

    foreach (var c in context.Categories.ToList())

        context.DeleteObject(c);

    context.SaveChanges();

}

Mais il est dommage de devoir charger les entités depuis la base uniquement pour les supprimer. De plus, il est également regrettable de supprimer les DataRow un par un pour finalement tous les supprimer.

Nous pourrions améliorer le process en ne chargeant que la clé de l’entité :

using (var context = new MyNorthwindEFEntities())

{

    foreach (var cId in context.Categories.Select(c => c.CategoryID))

    {

        var c = new Categories{ CategoryID = cId };

        context.AttachTo("Categories", c);

        context.DeleteObject(c);

    }

    context.SaveChanges();

}

C’est certes mieux mais il faut cependant commencer par une requête SELECT puis n requêtes DELETE alors qu’une seule requête DELETE suffirait.

Comme je l’ai précisé plus haut, il est possible de définir une SSDL Function qui fera le DELETE pour nous en une seule fois et sans passer par un SELECT.

Avec la EF4, il est également possible d’exécuter le requête SQL directement :

context.ExecuteStoreCommand("DELETE FROM Products");

ce qui, je dois le reconnaitre, est très pratique.

Avec EF4, il est également possible d’utiliser la méthode ExecuteStoreQuery<TResult> afin de lui laisser faire la matérialisation du ou des DataRow(s) en entité.

Posté le lundi 25 mai 2009 07:24 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , ,

POCO : CreateObject et object initializer

Lorque l’on travaille avec des entités POCO, les requêtes retournent des instances de proxy qui héritent des classes d’entité. Le fait d’avoir des instances de proxy à la place d’instance de nos entités permet, entre autre, de bénéficier du lazy loading.

Lorsque l’on crée une entité pour l’attacher au contexte, on peut faire un new sur notre entity type mais il peut être plus intéressant d’instancier le proxy. Pour cela, on a une méthode CreateObject<T>() sur le contexte.

Cependant, ce qui est dommage avec cette façon de procéder est le fait qu’on ne peut plus alors profiter des Object initializers.

J’ai donc eu l’idée de créer une méthode CreateCategory, CreateProduct, etc. et d’utiliser les paramètres de cette méthode pour l’initialisation. Dans mon cas, je profite des paramètres optionnels (nouvelle feature de C#4) afin d’avoir un système proche des Object Initializers.

Ecrire ces méthodes peut s’avérer pénible.

Aussi, j’ai mis à jour mon template de génération de code afin que cette méthode soit automatiquement générée.

Soit dit en passant, si vous regardez de plus près le code, vous pourrez remarquer l’utilisation d’une autre nouvelle feature de C#4 : la variance dont je me sers pour caster un IEnumerable<PrimitiveTypePropertyWrapper> en IEnumerable<DataPropertyWrapper>.

Maintenant, je peux écrire le code suivant :

context.CreateCategory(id:1, name:"C1", description:"D1");

Posté le vendredi 22 mai 2009 07:54 par Matthieu MEZIL | 0 commentaire(s)

POCO, les joies du T4, la suite

Hier, j’ai bloggé sur mon pattern de génération T4. En discutant un petit peu, j’ai pu me rendre compte qu’un complément d’information n'était pas superflu.

Je me suis généré le modèle d’entité suivant :

image

Première nouveauté avec VS 2010 Beta, les complex properties dans le designer (ici Address) sont supportés, même si le fait de ne pas avoir le complex type dans le designer (en dehors de la fenêtre Model Browser) est à mon avis dommage. // Si on rajoute à cela le non support des mappings TPC et de l’Horizontal Entity Splitting, je pense que mon EDM Designer devrait continuer d’être intéressant (en attendant VS 2012). Matthieu 1 - ADO .NET Team 0 Stick out tongue

Bref, EF is good quand même Big Smile

Pour réaliser mon exemple, je suis parti sur une approche Model First. C’est à dire que j’ai d’abord créer mon modèle d’entités et que je lui ai ensuite demandé de me générer la base qui va bien à partir de celles-ci. C’est est également une nouveauté de la V2. // A noter que je préfère l’approche dans laquelle le DBA conçoit la base et l’architecte conçoit les entités, le tout indépendamment. Ensuite, il suffit d’utiliser la puissance du mapping d’EDM pour mapper les deux conceptions (objet et relationnel).

Bref, revenons à nos moutons ou plutôt à nos entités. Smile

Comme l’explique Alex dans son post, il est possible d’utiliser le template T4 pour générer le code des entités.

Mon idée était de faire un template POCO. L’ADO .NET team devrait également en publier prochainement comme l’écrit Alex (après moi Stick out tongue). Matthieu 2 - ADO .NET Team 0 Stick out tongue

Avec le code généré par défaut (avec ou sans le template T4), les entités générées sont des entités Prescriptive classes (ie : devant hériter des classes de base du Framework).

Dans l’exemple, voici le code généré :

using System;

using System.Data.Objects;

using System.Data.Objects.DataClasses;

using System.Data.EntityClient;

using System.ComponentModel;

using System.Xml.Serialization;

using System.Runtime.Serialization;

 

[assembly: EdmSchemaAttribute()]

 

#region EDM Relationship Metadata

[assembly: EdmRelationshipAttribute("Test", "CategoryProduct", "Category", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(ConsoleApplication1.Category), "Product", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(ConsoleApplication1.Product))]

[assembly: EdmRelationshipAttribute("Test", "SupplierProduct", "Supplier", System.Data.Metadata.Edm.RelationshipMultiplicity.ZeroOrOne, typeof(ConsoleApplication1.Supplier), "Product", System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(ConsoleApplication1.Product))]

#endregion

 

namespace ConsoleApplication1

{

    #region Contexts

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    public partial class TestContainer : ObjectContext

    {

        #region Constructors

        /// <summary>

        /// Initializes a new TestContainer object using the connection string found in the 'TestContainer' section of the application configuration file.

        /// </summary>

        public TestContainer()

            : base("name=TestContainer", "TestContainer")

        {

            OnContextCreated();

        }

 

        /// <summary>

        /// Initialize a new TestContainer object.

        /// </summary>

        public TestContainer(string connectionString)

            : base(connectionString, "TestContainer")

        {

            OnContextCreated();

        }

 

        /// <summary>

        /// Initialize a new TestContainer object.

        /// </summary>

        public TestContainer(EntityConnection connection)

            : base(connection, "TestContainer")

        {

            OnContextCreated();

        }

        #endregion

 

        #region Partial Methods

        partial void OnContextCreated();

        #endregion

 

        #region ObjectSet Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public ObjectSet<Category> Categories

        {

            get

            {

                if ((_Categories == null))

                {

                    _Categories = base.CreateObjectSet<Category>("Categories");

                }

                return _Categories;

            }

        }

        private ObjectSet<Category> _Categories;

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public ObjectSet<Product> Products

        {

            get

            {

                if ((_Products == null))

                {

                    _Products = base.CreateObjectSet<Product>("Products");

                }

                return _Products;

            }

        }

        private ObjectSet<Product> _Products;

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public ObjectSet<Supplier> Suppliers

        {

            get

            {

                if ((_Suppliers == null))

                {

                    _Suppliers = base.CreateObjectSet<Supplier>("Suppliers");

                }

                return _Suppliers;

            }

        }

        private ObjectSet<Supplier> _Suppliers;

 

        #endregion

        #region AddTo Methods

 

        /// <summary>

        /// Deprecated Method for adding a new object to the Categories EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.

        /// </summary>

        public void AddToCategories(Category category)

        {

            base.AddObject("Categories", category);

        }

 

        /// <summary>

        /// Deprecated Method for adding a new object to the Products EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.

        /// </summary>

        public void AddToProducts(Product product)

        {

            base.AddObject("Products", product);

        }

 

        /// <summary>

        /// Deprecated Method for adding a new object to the Suppliers EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.

        /// </summary>

        public void AddToSuppliers(Supplier supplier)

        {

            base.AddObject("Suppliers", supplier);

        }

        #endregion

    }

 

    #endregion

 

 

    #region Entities

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [EdmEntityTypeAttribute(NamespaceName = "Test", Name = "Category")]

    [Serializable()]

    [DataContractAttribute(IsReference = true)]

    public partial class Category : EntityObject

    {

        #region Factory Method

        /// <summary>

        /// Create a new Category object.

        /// </summary>

        /// <param name="id">Initial value of the Id property.</param>

        /// <param name="name">Initial value of the Name property.</param>

        /// <param name="description">Initial value of the Description property.</param>

        public static Category CreateCategory(global::System.Int32 id, global::System.String name, global::System.String description)

        {

            Category category = new Category();

            category.Id = id;

 

            category.Name = name;

 

            category.Description = description;

 

            return category;

        }

        #endregion

 

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = true, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.Int32 Id

        {

            get

            {

                return _Id;

            }

            set

            {

                if (Id != value)

                {

                    OnIdChanging(value);

                    ReportPropertyChanging("Id");

                    _Id = StructuralObject.SetValidValue(value);

                    ReportPropertyChanged("Id");

                    OnIdChanged();

                }

            }

 

        }

        private global::System.Int32 _Id;

        partial void OnIdChanging(global::System.Int32 value);

        partial void OnIdChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String Name

        {

            get

            {

                return _Name;

            }

            set

            {

                OnNameChanging(value);

                ReportPropertyChanging("Name");

                _Name = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("Name");

                OnNameChanged();

            }

 

        }

        private global::System.String _Name;

        partial void OnNameChanging(global::System.String value);

        partial void OnNameChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String Description

        {

            get

            {

                return _Description;

            }

            set

            {

                OnDescriptionChanging(value);

                ReportPropertyChanging("Description");

                _Description = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("Description");

                OnDescriptionChanged();

            }

 

        }

        private global::System.String _Description;

        partial void OnDescriptionChanging(global::System.String value);

        partial void OnDescriptionChanged();

 

        #endregion

 

        #region Navigation Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [XmlIgnoreAttribute()]

        [SoapIgnoreAttribute()]

        [DataMemberAttribute()]

        [EdmRelationshipNavigationPropertyAttribute("Test", "CategoryProduct", "Product")]

        public EntityCollection<Product> Products

        {

            get

            {

                return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Product>("Test.CategoryProduct", "Product");

            }

            set

            {

                if ((value != null))

                {

                    ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Product>("Test.CategoryProduct", "Product", value);

                }

            }

        }

        #endregion

    }

 

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [EdmEntityTypeAttribute(NamespaceName = "Test", Name = "Product")]

    [Serializable()]

    [DataContractAttribute(IsReference = true)]

    public partial class Product : EntityObject

    {

        #region Factory Method

        /// <summary>

        /// Create a new Product object.

        /// </summary>

        /// <param name="id">Initial value of the Id property.</param>

        /// <param name="name">Initial value of the Name property.</param>

        public static Product CreateProduct(global::System.Int32 id, global::System.String name)

        {

            Product product = new Product();

            product.Id = id;

 

            product.Name = name;

 

            return product;

        }

        #endregion

 

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = true, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.Int32 Id

        {

            get

            {

                return _Id;

            }

            set

            {

                if (Id != value)

                {

                    OnIdChanging(value);

                    ReportPropertyChanging("Id");

                    _Id = StructuralObject.SetValidValue(value);

                    ReportPropertyChanged("Id");

                    OnIdChanged();

                }

            }

 

        }

        private global::System.Int32 _Id;

        partial void OnIdChanging(global::System.Int32 value);

        partial void OnIdChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String Name

        {

            get

            {

                return _Name;

            }

            set

            {

                OnNameChanging(value);

                ReportPropertyChanging("Name");

                _Name = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("Name");

                OnNameChanged();

            }

 

        }

        private global::System.String _Name;

        partial void OnNameChanging(global::System.String value);

        partial void OnNameChanged();

 

        #endregion

 

        #region Navigation Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [XmlIgnoreAttribute()]

        [SoapIgnoreAttribute()]

        [DataMemberAttribute()]

        [EdmRelationshipNavigationPropertyAttribute("Test", "CategoryProduct", "Category")]

        public Category Category

        {

            get

            {

                return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Category>("Test.CategoryProduct", "Category").Value;

            }

            set

            {

                ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Category>("Test.CategoryProduct", "Category").Value = value;

            }

        }

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [BrowsableAttribute(false)]

        [DataMemberAttribute()]

        public EntityReference<Category> CategoryReference

        {

            get

            {

                return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Category>("Test.CategoryProduct", "Category");

            }

            set

            {

                if ((value != null))

                {

                    ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Category>("Test.CategoryProduct", "Category", value);

                }

            }

        }

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [XmlIgnoreAttribute()]

        [SoapIgnoreAttribute()]

        [DataMemberAttribute()]

        [EdmRelationshipNavigationPropertyAttribute("Test", "SupplierProduct", "Supplier")]

        public Supplier Supplier

        {

            get

            {

                return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Supplier>("Test.SupplierProduct", "Supplier").Value;

            }

            set

            {

                ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Supplier>("Test.SupplierProduct", "Supplier").Value = value;

            }

        }

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [BrowsableAttribute(false)]

        [DataMemberAttribute()]

        public EntityReference<Supplier> SupplierReference

        {

            get

            {

                return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedReference<Supplier>("Test.SupplierProduct", "Supplier");

            }

            set

            {

                if ((value != null))

                {

                    ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedReference<Supplier>("Test.SupplierProduct", "Supplier", value);

                }

            }

        }

        #endregion

    }

 

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [EdmEntityTypeAttribute(NamespaceName = "Test", Name = "Supplier")]

    [Serializable()]

    [DataContractAttribute(IsReference = true)]

    public partial class Supplier : EntityObject

    {

        #region Factory Method

        /// <summary>

        /// Create a new Supplier object.

        /// </summary>

        /// <param name="id">Initial value of the Id property.</param>

        /// <param name="companyName">Initial value of the CompanyName property.</param>

        /// <param name="address">Initial value of the Address property.</param>

        public static Supplier CreateSupplier(global::System.Int32 id, global::System.String companyName, Address address)

        {

            Supplier supplier = new Supplier();

            supplier.Id = id;

 

            supplier.CompanyName = companyName;

 

            supplier.Address = StructuralObject.VerifyComplexObjectIsNotNull(address, "Address");

            return supplier;

        }

        #endregion

 

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = true, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.Int32 Id

        {

            get

            {

                return _Id;

            }

            set

            {

                if (Id != value)

                {

                    OnIdChanging(value);

                    ReportPropertyChanging("Id");

                    _Id = StructuralObject.SetValidValue(value);

                    ReportPropertyChanged("Id");

                    OnIdChanged();

                }

            }

 

        }

        private global::System.Int32 _Id;

        partial void OnIdChanging(global::System.Int32 value);

        partial void OnIdChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String CompanyName

        {

            get

            {

                return _CompanyName;

            }

            set

            {

                OnCompanyNameChanging(value);

                ReportPropertyChanging("CompanyName");

                _CompanyName = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("CompanyName");

                OnCompanyNameChanged();

            }

 

        }

        private global::System.String _CompanyName;

        partial void OnCompanyNameChanging(global::System.String value);

        partial void OnCompanyNameChanged();

 

        #endregion

        #region Complex Properties

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmComplexPropertyAttribute()]

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]

        [XmlElement(IsNullable = true)]

        [SoapElement(IsNullable = true)]

        [DataMemberAttribute()]

        public Address Address

        {

            get

            {

                _Address = GetValidValue(_Address, "Address", false, _AddressInitialized);

                _AddressInitialized = true;

                return _Address;

            }

            set

            {

                OnAddressChanging(value);

                ReportPropertyChanging("Address");

                _Address = SetValidValue(_Address, value, "Address");

                _AddressInitialized = true;

                ReportPropertyChanged("Address");

                OnAddressChanged();

            }

        }

        private Address _Address;

        private bool _AddressInitialized;

        partial void OnAddressChanging(Address value);

        partial void OnAddressChanged();

        #endregion

 

        #region Navigation Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [XmlIgnoreAttribute()]

        [SoapIgnoreAttribute()]

        [DataMemberAttribute()]

        [EdmRelationshipNavigationPropertyAttribute("Test", "SupplierProduct", "Product")]

        public EntityCollection<Product> Products

        {

            get

            {

                return ((IEntityWithRelationships)this).RelationshipManager.GetRelatedCollection<Product>("Test.SupplierProduct", "Product");

            }

            set

            {

                if ((value != null))

                {

                    ((IEntityWithRelationships)this).RelationshipManager.InitializeRelatedCollection<Product>("Test.SupplierProduct", "Product", value);

                }

            }

        }

        #endregion

    }

 

    #endregion

    #region ComplexTypes

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [EdmComplexTypeAttribute(NamespaceName = "Test", Name = "Address")]

    [DataContractAttribute(IsReference = true)]

    [Serializable()]

    public partial class Address : ComplexObject

    {

        #region Factory Method

        /// <summary>

        /// Create a new Address object.

        /// </summary>

        /// <param name="addressLine">Initial value of the AddressLine property.</param>

        /// <param name="postalCode">Initial value of the PostalCode property.</param>

        /// <param name="city">Initial value of the City property.</param>

        /// <param name="region">Initial value of the Region property.</param>

        /// <param name="country">Initial value of the Country property.</param>

        public static Address CreateAddress(global::System.String addressLine, global::System.String postalCode, global::System.String city, global::System.String region, global::System.String country)

        {

            Address address = new Address();

            address.AddressLine = addressLine;

 

            address.PostalCode = postalCode;

 

            address.City = city;

 

            address.Region = region;

 

            address.Country = country;

 

            return address;

        }

        #endregion

 

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String AddressLine

        {

            get

            {

                return _AddressLine;

            }

            set

            {

                OnAddressLineChanging(value);

                ReportPropertyChanging("AddressLine");

                _AddressLine = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("AddressLine");

                OnAddressLineChanged();

            }

 

        }

        private global::System.String _AddressLine;

        partial void OnAddressLineChanging(global::System.String value);

        partial void OnAddressLineChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String PostalCode

        {

            get

            {

                return _PostalCode;

            }

            set

            {

                OnPostalCodeChanging(value);

                ReportPropertyChanging("PostalCode");

                _PostalCode = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("PostalCode");

                OnPostalCodeChanged();

            }

 

        }

        private global::System.String _PostalCode;

        partial void OnPostalCodeChanging(global::System.String value);

        partial void OnPostalCodeChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String City

        {

            get

            {

                return _City;

            }

            set

            {

                OnCityChanging(value);

                ReportPropertyChanging("City");

                _City = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("City");

                OnCityChanged();

            }

 

        }

        private global::System.String _City;

        partial void OnCityChanging(global::System.String value);

        partial void OnCityChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String Region

        {

            get

            {

                return _Region;

            }

            set

            {

                OnRegionChanging(value);

                ReportPropertyChanging("Region");

                _Region = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("Region");

                OnRegionChanged();

            }

 

        }

        private global::System.String _Region;

        partial void OnRegionChanging(global::System.String value);

        partial void OnRegionChanged();

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [EdmScalarPropertyAttribute(EntityKeyProperty = false, IsNullable = false)]

        [DataMemberAttribute()]

        public global::System.String Country

        {

            get

            {

                return _Country;

            }

            set

            {

                OnCountryChanging(value);

                ReportPropertyChanging("Country");

                _Country = StructuralObject.SetValidValue(value, false);

                ReportPropertyChanged("Country");

                OnCountryChanged();

            }

 

        }

        private global::System.String _Country;

        partial void OnCountryChanging(global::System.String value);

        partial void OnCountryChanged();

 

        #endregion

    }

 

    #endregion

 

}

Pour avoir des entités POCO, il faut modifier ce template. C’est ce que j’ai fait.

Ainsi le code généré, est désormais le suivant :

using System;

using System.Data.Objects;

using System.Data.Objects.DataClasses;

using System.Data.EntityClient;

using System.ComponentModel;

using System.Xml.Serialization;

using System.Runtime.Serialization;

using System.Collections.Generic;

namespace ConsoleApplication1

{

    #region Contexts

    public interface ITestContainer

    {

        IObjectSet<Category> Categories { get; }

        IObjectSet<Product> Products { get; }

        IObjectSet<Supplier> Suppliers { get; }

    }

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    public partial class TestContainer : ObjectContext, ITestContainer

    {

        #region Constructors

        /// <summary>

        /// Initializes a new TestContainer object using the connection string found in the 'TestContainer' section of the application configuration file.

        /// </summary>

        public TestContainer()

            : base("name=TestContainer", "TestContainer")

        {

            OnContextCreated();

        }

 

        /// <summary>

        /// Initialize a new TestContainer object.

        /// </summary>

        public TestContainer(string connectionString)

            : base(connectionString, "TestContainer")

        {

            OnContextCreated();

        }

 

        /// <summary>

        /// Initialize a new TestContainer object.

        /// </summary>

        public TestContainer(EntityConnection connection)

            : base(connection, "TestContainer")

        {

            OnContextCreated();

        }

        #endregion

 

        #region Partial Methods

        partial void OnContextCreated();

        #endregion

 

        #region ObjectSet Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public ObjectSet<Category> Categories

        {

            get

            {

                if (_Categories == null)

                    _Categories = CreateObjectSet<Category>("Categories");

                return _Categories;

            }

        }

        private ObjectSet<Category> _Categories;

        IObjectSet<Category> ITestContainer.Categories { get { return Categories; } }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public ObjectSet<Product> Products

        {

            get

            {

                if (_Products == null)

                    _Products = CreateObjectSet<Product>("Products");

                return _Products;

            }

        }

        private ObjectSet<Product> _Products;

        IObjectSet<Product> ITestContainer.Products { get { return Products; } }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public ObjectSet<Supplier> Suppliers

        {

            get

            {

                if (_Suppliers == null)

                    _Suppliers = CreateObjectSet<Supplier>("Suppliers");

                return _Suppliers;

            }

        }

        private ObjectSet<Supplier> _Suppliers;

        IObjectSet<Supplier> ITestContainer.Suppliers { get { return Suppliers; } }

 

        #endregion

        #region AddTo Methods

 

        /// <summary>

        /// Deprecated Method for adding a new object to the Categories EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.

        /// </summary>

        public void AddToCategories(Category category)

        {

            base.AddObject("Categories", category);

        }

 

        /// <summary>

        /// Deprecated Method for adding a new object to the Products EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.

        /// </summary>

        public void AddToProducts(Product product)

        {

            base.AddObject("Products", product);

        }

 

        /// <summary>

        /// Deprecated Method for adding a new object to the Suppliers EntitySet. Consider using the .Add method of the associated ObjectSet&lt;T&gt; property instead.

        /// </summary>

        public void AddToSuppliers(Supplier supplier)

        {

            base.AddObject("Suppliers", supplier);

        }

        #endregion

    }

 

    #endregion

 

    #region Entities

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [DataContractAttribute(IsReference = true)]

    public partial class Category

    {

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public Int32 Id { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String Name { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String Description { get; set; }

 

        #endregion

 

        #region Navigation Properties

        private List<Product> _products;

        [DataMemberAttribute()]

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        public virtual List<Product> Products

        {

            get

            {

                if (_products == null)

                    _products = new List<Product>();

                return _products;

            }

            set { _products = value; }

        }

 

        #endregion

    }

 

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [DataContractAttribute(IsReference = true)]

    public partial class Product

    {

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public Int32 Id { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String Name { get; set; }

 

        #endregion

 

        #region Navigation Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual Category Category { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual Supplier Supplier { get; set; }

 

        #endregion

    }

 

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [DataContractAttribute(IsReference = true)]

    public partial class Supplier

    {

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public Int32 Id { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String CompanyName { get; set; }

 

        #endregion

        #region Complex Properties

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual Address Address { get; set; }

        #endregion

 

        #region Navigation Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        private List<Product> _products;

        public virtual List<Product> Products

        {

            get

            {

                if (_products == null)

                    _products = new List<Product>();

                return _products;

            }

            set { _products = value; }

        }

 

        #endregion

    }

 

    #endregion

 

    #region ComplexTypes

    /// <summary>

    /// No Metadata Documentation available.

    /// </summary>

    [DataContractAttribute(IsReference = true)]

    public partial class Address

    {

        #region Primitive Properties

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String AddressLine { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String PostalCode { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String City { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String Region { get; set; }

 

        /// <summary>

        /// No Metadata Documentation available.

        /// </summary>

        [DataMemberAttribute()]

        public virtual String Country { get; set; }

 

        #endregion

    }

 

    #endregion

 

}

Premier point, l’interface ITestContainer, va me permettre de faire du mock afin de tester mes entités.

Deuxième point, les entités n’ont plus de lien avec EF.

Troisième point, à la manière d'NHibernate, le fait de déclarer une propriété virtual va permettre de bénéficier du tracking automatique des modifications.

Enfin, pour finir ce post, dans mes tests, j’ai utilisé une autre nouveauté d’EF4 : le lazy loading. Pour cela, il suffit de le spécifier dans les options du contexte :

context.ContextOptions.DeferredLoadingEnabled = true;

Enjoy Smile

Posté le mercredi 20 mai 2009 17:42 par Matthieu MEZIL | 5 commentaire(s)

Les joies du T4

Avec EF4 (comprendre EF v2), il est possible d’utiliser le template de génération de code T4 pour générer le contexte, les entity types et les complex types à partir de l’edmx. A noter que cela est également possible avec la V1.

Et du coup, on peut vraiment “jouer”.

Un des trucs cool avec EF4 c’est le POCO (ie les entity types n’ont aucun lien avec EF).

Avoir des classes indépendantes c’est bien mais la génération de code à partir de l’edmx c’est bien aussi. En effet, dans le cas où l’on souhaiterais modifier les entités, ce n’est pas la peine de faire le travail en double.

L’idéal serait donc de générer des entités POCO. Pour cela, le template T4 va fortement nous aider. Il suffit de partir du code par défaut et d’apporter nos propres modifications.

Vous trouverez ici un template de démo.

Posté le mardi 19 mai 2009 22:35 par Matthieu MEZIL | 4 commentaire(s)

VS 2010 Beta 1 en téléchargement pour les abonnés MSDN

Soma vient de l’annoncer : la Beta1 de VS 2010 est disponible. Vous pouvez la télécharger ici.

Au programme:

Pour EF :

  • la possibilité de créer des entités POCO (indépendante d’Entity Framework)
  • la possibilité d’utiliser les complex types avec le designer
  • la possibilité de faire du lazy loading
  • plein d’autre choses sur lesquelles je reviendrai plus tard
  • plein d’autre choses vont également arriver dans la prochaine Beta mais je ne vous en dit pas plus

En gros, que du bon !

Pour C# : introduction du mot clé dynamic, des notions de variance, co-variance, etc. A priori pas de surprise, Anders avait déjà tout annoncé à la PDC.

Pour .NET : introduction du Parallel Framework qui devrait à mon avis révolutionner le développement dans les années à venir.

Pour WPF : on a aussi des nouveautés mais je laisse à Thomas le soin de vous en parler.  Smile

Pour les tests : j’ai vu qu’Etienne avait déjà bloggué sur le sujet.

Posté le lundi 18 mai 2009 21:38 par Matthieu MEZIL | 2 commentaire(s)

Démo d’Entity Framework dans une application N-Tiers

Daniel Simmons a publié la démo qu’il a faite au TechEd. Cette démo est de point de vue très intéressante avec le pattern de génération T4, des repositories avec également du mock, un service WCF et une application web MVC.

Pour ceux qui ne verrait pas le lien entre le pattern de génération T4, sachez que ce pattern de génération de code peut être utilisé pour générer les entités à partir de l’edmx. Avec la v2, l’exploitation de ce pattern permettra d’aller encore plus loin dans la génération de code, en générant par exemple, des classes POCO.

La démo proposée par Danny a l’avantage de rester assez simple, ce qui permet de la rendre très accessible.

Pour ma part, j’ai également réalisée une démo montrant l’utilisation simultanée d’Entity Framework avec WCF avec une couche DAL et une couche BLL.

Posté le vendredi 15 mai 2009 23:36 par Matthieu MEZIL | 3 commentaire(s)

Plus de Messages Page suivante »


Les 10 derniers blogs postés

- [Refactoring] ReSharper pour Visual Studio 2010 (Preview) par Thomas Jaskula le il y a 8 heures et 57 minutes

- [Refactoring] Analyser vos exceptions avec ReSharper Exceptional par Thomas Jaskula le il y a 10 heures et 11 minutes

- SharePoint 2007 : patterns & practices SharePoint Guidance par Philippe Sentenac [MVP SharePoint] le il y a 23 heures et 51 minutes

- [Visual Studio 2010] Les tests cases c’est bien, mais je vais devoir tout réécrire ? par Etienne Margraff le 07-03-2009, 09:00

- MVP[Gribouillon].AddYear par The Grib's Lair [Sébastien PICAMELOT - MVP SharePoint] le 07-03-2009, 08:45

- Clinique INSIA - Projet de fin d’Etudes (Silverlight 3 MVVM et OutOfBrowser, WCF, TFS) - Part 1 par David REI le 07-02-2009, 23:38

- C’est la crise ? Bah pourquoi cramer du budget pub alors ? par Nix's Blog le 07-02-2009, 15:31

- Soyons MVP ! par TheSaib .NET blog le 07-02-2009, 12:15

- SharePoint : Gestion des Erreurs 6398, 7076 et 6482 par Blog Technique de Romelard Fabrice le 07-02-2009, 11:53

- EF avec WPF par Matthieu MEZIL le 07-02-2009, 10:18