Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

Utiliser Roslyn pour améliorer le compilateur C# ou VB

J’ai publié un POC qui permet d’enrichir le langage C# en utilisant Roslyn.

Enjoy! :)

Posté le jeudi 12 avril 2012 11:21 par Matthieu MEZIL | 5 commentaire(s)

Classé sous : , ,

Après-midi du dev Roslyn

J’aurais le plaisir d’animer un après-midi du dev sur Roslyn le jeudi 12 avril avec Mitsu, Jean-Baptiste, Florent, Simon et David.

Si vous voulez voir du code qui pique, inscrivez-vous !

Posté le mercredi 14 mars 2012 21:34 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : ,

Ma session TechDays sur Roslyn

Ma session des TechDays 2012 sur Roslyn que j’ai co-animé avec Léonard Labat est disponible ici.

Posté le mercredi 14 mars 2012 21:25 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : ,

Podcast sur T4 pour Visual Studio Talk Show

J’ai récemment enregistré un podcast sur T4 avec Guy Barrette et Mario Cardinal que vous pouvez retrouver ici.

Posté le samedi 10 mars 2012 00:32 par Matthieu MEZIL | 0 commentaire(s)

Classé sous :

WCF Async Queryable Services : the architecture

Si vous me suivez sur twitter, vous devez probablement savoir que je travaille sur un projet que j’ai appelé WCF Async Queryable Services.

Je viens de faire une première vidéo sur l’architecture de WCF Async Queryable Services.

WCF Async Queryable Services - Architecture

Merci d’avance pour vos feedbacks

Posté le vendredi 9 mars 2012 23:05 par Matthieu MEZIL | 0 commentaire(s)

Article sur Roslyn

Si vous êtes intéressé par Roslyn, je vous invite à lire l’article que j’ai écrit sur le sujet.

Posté le vendredi 25 novembre 2011 08:26 par Matthieu MEZIL | 2 commentaire(s)

Classé sous : , , ,

Roslyn : générer des InvokeActivity avec T4

Le titre peut paraître bizarre mais c’est un vrai besoin rencontré chez un client pour lequel je vais utiliser Roslyn.

Imaginez le scénario suivant : dans un projet, il y a des développeurs et des experts fonctionnels. Les experts fonctionnels doivent "développer" des Workflows. Les développeurs vont développer des méthodes qui seront utilisées dans les workflows.

Le problème avec ça c’est les InvokeActivity. Pour les utiliser, ils doivent connaîtrent la signature de la méthode pour spécifier les bons types des paramètres / résultat. Un autre problème c’est qu’il faut écrire manuellement le nom de la méthode avec potentiellement le risque de faute de frappe / mise à jour.

Les experts fonctionnels préfèreraient faire du drag and drop des activités dans le workflow.

Donc mon idée est d’encapsuler les InvokeActivity dans des Activity. Maintenant, ça peut être très pénible pour les développeurs de faire ces activités.

Pour cela, il serait plus simple que les développeurs décorent les méthodes en utilisant un Attribut et ils pourraient utiliser un T4 pour générer ces activités.

Le T4 utilise Roslyn pour récupérer les informations sur les méthodes.

Ce T4 va utiliser le ttinclude d’Entity Framework EF.Utility.CS.ttinclude qui intègre déjà la création de plusieurs fichiers depuis un T4 (dans mon casje veux un fichier par méthode).

<#@ include file="EF.Utility.CS.ttinclude"#>

Ensuite, le T4 référence les assemblies de Roslyn :

<#@ assembly name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Roslyn\v1.0\Roslyn.Compilers.dll"#>

<#@ assembly name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Roslyn\v1.0\Roslyn.Compilers.CSharp.dll"#>

<#@ assembly name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Roslyn\v1.0\Roslyn.Services.dll"#>

<#@ assembly name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Roslyn\v1.0\Roslyn.Services.CSharp.dll"#>

<#@ assembly name="C:\Program Files (x86)\Reference Assemblies\Microsoft\Roslyn\v1.0\Roslyn.Services.VisualBasic.dll"#>

Le chemin vers la solution et le nom de projet sont spécifiés au début du T4. Donc maintenant, on peut charger la solution en utilisant Roslyn et récupérer les méthodes décorées avec l’attribut dans le projet spécifié.

var solution = Solution.Load(solutionPath);

var project = solution.Projects.First(p => p.AssemblyName == projectAssemblyName);

foreach (var document in project.Documents)

{

    //…

}

Ensuite, pour identifier les méthodes, on utilisera un SyntaxVisitor:

public class InvokeActivityAttributeVisitor : SyntaxVisitor<object>

{

    protected override object VisitCompilationUnit(CompilationUnitSyntax node)

    {

        foreach (var n in node.ChildNodes())

            Visit(n);

        return null;

    }

 

    protected override object VisitNamespaceDeclaration(NamespaceDeclarationSyntax node)

    {

        foreach (var n in node.ChildNodes())

            Visit(n);

        return null;

    }

 

    protected override object VisitTypeDeclaration(TypeDeclarationSyntax node)

    {

        foreach (var n in node.ChildNodes())

            Visit(n);

        return null;

    }

 

    protected override object VisitMethodDeclaration(MethodDeclarationSyntax node)

    {

        if (node.Modifiers.Any(st => st.Kind == SyntaxKind.PublicKeyword) && node.Attributes.Any(a => a.Attributes.Any(a2 => Regex.IsMatch(a2.Name.GetFullText(), @"^(Roslyn.WF.ActivityGenerator.)?InvokeActivity$"))))

        {

            string methodName = node.Identifier.GetText();
            var returnTypeAsPredefinedTypeSyntax = node.ReturnType as PredefinedTypeSyntax;
            if (returnTypeAsPredefinedTypeSyntax == null || returnTypeAsPredefinedTypeSyntax.Keyword.Kind != SyntaxKind.VoidKeyword)
            {
                //…       
            }
            foreach (var parameter in node.ParameterList.Parameters)
            {
                //…       
            }

        }

    }

}

Maintenant le point important est de connaître le namespace et l’assembly des types utilisés dans les méthodes. Pour cela, on utilisera les symbols de compilation :

_syntaxTree = (SyntaxTree)_document.GetSyntaxTree();
_semanticModel = _document.Project.GetCompilation().GetSemanticModel(_syntaxTree);
var returnTypeSymbol = _semanticModel.GetSemanticInfo(node.ReturnType).Symbol;
string returnTypeAssemblyName = returnTypeSymbol.ContainingAssembly.AssemblyName.Name;
string returnTypeNamespaceName = returnTypeSymbol.ContainingNamespace.ToString();

Il faut également savoir si les paramètres sont en In/Out/InOut:

parameter.Modifiers.Any(st => st.Kind == SyntaxKind.RefKeyword) ? Direction.InOut : (parameter.Modifiers.Any(st => st.Kind == SyntaxKind.OutKeyword) ? Direction.Out : Direction.In)

Le reste du code est basique, utilisé pour générer les activités.

Vous pouvez télécharger le code ici.

Posté le jeudi 20 octobre 2011 09:02 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , ,

Comment ajouter des comportements à l’ObjectContext ?

Imaginons que l’on veuille ajouter un comportement à la classe ObjectContext dans le but de catché certaines exceptions par exemple. Si vous essayez de supprimer une entité déjà supprimée en base, vous ne voulez pas avoir d’exception. Dans la même idée, si vous supprimez une entité modifiée par un tiers, vous ne voulez pas non plus avoir d’exception.

Comme faire cela avec un code réutilisable ?

Une solution pourrait passer par une classe ExceptionHandlingObjectContext qui hérite d’ObjectContext et qui serait héritée par l’ObjectContext spécifique (ex : NorthwindEntities).

Cependant, on peut imaginer d’avoir également un TracingObjectContext qui serait ou non utiliser en fonction d’une configuration, en utilisant Unity par exemple, ce qui rend cette solution non viable.

Je vais donc opter pour une solution différente.

Je veux créer une classe CustomizableObjectContext qui ne connait pas les behaviors. Mon ObjectContext spécifique va en hériter et je vais ensuite lui renseigner mes behaviors en utilisant les delegates.

public class CustomizableObjectContext : ObjectContext
{
public CustomizableObjectContext(EntityConnection
connection)
:
base
(connection)
{
}
public CustomizableObjectContext(string
connectionString)
:
base
(connectionString)
{
}
protected CustomizableObjectContext(EntityConnection connection, string
defaultContainerName)
:
base
(connection, defaultContainerName)
{
}
protected CustomizableObjectContext(string connectionString, string
defaultContainerName)
:
base
(connectionString, defaultContainerName)
{
}

private List<Func<ObjectContext, SaveOptions, Func<SaveOptions, int>, int
>> _saveActions =
new List<Func<ObjectContext, SaveOptions, Func<SaveOptions, int>,int
>>();

public void AddSaveAction(Func<ObjectContext, SaveOptions, Func<SaveOptions, int>, int
> saveAction)
{
_saveActions.Add(saveAction);
}

public override int SaveChanges(SaveOptions
options)
{
Func<int, SaveOptions, int> saveAction = null
;
saveAction = (index, saveOptions) =>
{
if
(index == -1)
return base
.SaveChanges(saveOptions);
return _saveActions[index](this
, saveOptions, so => saveAction(index - 1, so));
};
return saveAction(_saveActions.Count - 1, options);
}
}

Maintenant je peux rajouter mon ExceptionHandling behavior :

public abstract class ObjectContextCustomizerBase<T> 
    where T : ObjectContextCustomizerBase<T>
{
    protected ObjectContext ObjectContext { get; set; }
   
    protected abstract int SaveChanges(ObjectContext context, SaveOptions options, Func<SaveOptions, int> baseSaveChanges);
}
public abstract class ObjectContextCustomizer<T>  : ObjectContextCustomizerBase<T>
    where T : ObjectContextCustomizer<T>, new()
{
    public static OC CreateObjectContext<OC>(OC objectContext)
        where OC : CustomizableObjectContext
    {
        objectContext.AddSaveAction(new T { ObjectContext = objectContext }.SaveChanges);
        return objectContext;
    }
}
public class ExceptionHandlerObjectContext : ObjectContextCustomizer<ExceptionHandlerObjectContext>
{
protected override int SaveChanges(ObjectContext context, SaveOptions options, Func<SaveOptions, int
> baseSaveChanges)
{
int
value = 0;
try
{
value = baseSaveChanges(options);
}
catch (OptimisticConcurrencyException
e)
{
bool canHandle = true
;
if
(e.StateEntries.Any() && e.StateEntries.Select(se =>
{
if (se.State != System.Data.EntityState
.Deleted)
return false
;
object
dbEntity;
if (ObjectContext.TryGetObjectByKey(se.EntityKey, out
dbEntity))
ObjectContext.ApplyOriginalValues(se.EntitySet.Name, dbEntity);
else
ObjectContext.Detach(se.Entity);
return true
;
}).TakeWhile(b => canHandle).Aggregate((b1, b2) => canHandle = b2))
// Aggregate for execute select on each of them
try
{
value = baseSaveChanges(options);
}
catch (System.Exception
e2)
{
throw e2;
}
else
throw e2;
}
return value;
}
}

Enfin, pour l’utiliser avec Unity, je peux utiliser le code suivant :

unityContainer.RegisterType<NorthwindEntities, NorthwindEntities>(new InjectionFactory(c => ExceptionHandlerObjectContext.CreateObjectContext(new NorthwindEntities())));

Posté le mardi 13 septembre 2011 21:36 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : , ,

SL5 : ICustomTypeProvider

On a souvent le problème suivant avec SL4 : dans le Model on récupère une collection d’entités et dans le ViewModel, on veut récupérer cette collection en y ajoutant de l’intelligence (par exemple des propriétés calculées).

Dans ce cas, il faut gérer deux collections et propager les modifications dans les deux sens. Une bonne pratique consiste à utiliser des "RelayCollection" pour s’occuper de cette propagation.

En .NET, on peut ajouter de "fausses" propriétés utilisées pour le binding en implémentant l’interface ICustomTypeDescriptor mais celle-ci n’existe pas dans Silverlight.

Cependant, avec SL5, on peut utiliser l’interface ICustomTypeProvider.

J’ai repris le code d’Alexandra et j’ai tenté de l’améliorer.

Cela nous donne le code suivant :

public abstract class DynamicBaseType
{
   
public abstract object GetPropertyValue(string
propertyName);
   
public abstract void SetPropertyValue(string propertyName, object
value);
}
public abstract class DynamicBaseType<T> : DynamicBaseType, ICustomTypeProvider, INotifyPropertyChanged
    where T : DynamicBaseType
<T> {
   
private static List<CustomPropertyInfo> _customProperties = new List<CustomPropertyInfo
>();
   
private Dictionary<string, object
> _customPropertyValues;
   
private CustomType
_customtype;

   
protected
DynamicBaseType()
    {
        _customPropertyValues =
new Dictionary<string, object
>();
       
foreach (var property in
_customProperties)
            _customPropertyValues.Add(property.Name,
null
);
    }

   
public static void AddProperty(string name, Type type, object value = null, List<Attribute> attributes = null
)
    {
       
if
(!CheckIfNameExists(name))
            _customProperties.Add(
new CustomPropertyInfo
(name, type, value, attributes));
    }

   
public static void AddProperty<V>(string name, Func<T, V> get, Action<T, V> set = null, List<Attribute> attributes = null, string[] properties = null
)
    {
       
if
(!CheckIfNameExists(name))
            _customProperties.Add(
new CustomPropertyInfo
<V>(name, get, set, attributes, properties));
    }

   
private static bool CheckIfNameExists(string
name)
    {
       
if (_customProperties.Select(p => p.Name).Contains(name) || typeof
(T).GetProperties().Select(p => p.Name).Contains(name))
           
throw new Exception("The property with this name already exists: "
+ name);
       
return false
;
    }

   
private bool ValidateValueType(object value, Type
type)
    {
       
if (value == null
)
        {
           
if
(!type.IsValueType)
               
return true
;
           
return (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable
<>));
        }
       
return
type.IsAssignableFrom(value.GetType());
    }

   
public override object GetPropertyValue(string
propertyName)
    {
       
object
customPropertyValue;
       
if (_customPropertyValues.TryGetValue(propertyName, out
customPropertyValue))
           
return customPropertyValue ?? _customProperties.First(p => p.Name == propertyName).GetDefaultValue(this
);
       
throw new Exception("There is no property "
+ propertyName);
    }

   
public override void SetPropertyValue(string propertyName, object
value)
    {
       
CustomPropertyInfo
propertyInfo = _customProperties.FirstOrDefault(prop => prop.Name == propertyName);
       
object
customPropertyValue;
       
if (!_customPropertyValues.TryGetValue(propertyName, out
customPropertyValue))
           
throw new Exception("There is no property "
+ propertyName);
       
if
(ValidateValueType(value, propertyInfo.PropertyType))
        {
           
if
(customPropertyValue != value)
            {
                _customPropertyValues[propertyName] = value;
                OnPropertyChanged(propertyName);
            }
        }
       
else throw new Exception("Value is of the wrong type or null for a non-nullable type."
);
    }

   
public PropertyInfo
[] GetProperties()
    {
       
return this
.GetCustomType().GetProperties();
    }

   
public Type
GetCustomType()
    {
       
return _customtype ?? (_customtype = new CustomType(typeof
(T)));
    }

   
protected virtual void OnPropertyChanged(string
propertyName)
    {
       
if (PropertyChanged != null
)
        {
            PropertyChanged(
this, new PropertyChangedEventArgs
(propertyName));
           
foreach (var dependantCustomPropertyInfo in _customProperties.OfType<IDependantCustomPropertyInfo
>().Where(dcpi => dcpi.Properties.Contains(propertyName)))
                PropertyChanged(
this, new PropertyChangedEventArgs
(dependantCustomPropertyInfo.Name));
        }
    }
   
public event PropertyChangedEventHandler
PropertyChanged;

   
private class CustomType : Type
    {
       
Type
_baseType;
       
public CustomType(Type
delegatingType)
        {
            _baseType = delegatingType;
        }
       
public override Assembly
Assembly
        {
           
get { return
_baseType.Assembly; }
        }

       
public override string
AssemblyQualifiedName
        {
           
get { return
_baseType.AssemblyQualifiedName; }
        }

       
public override Type
BaseType
        {
           
get { return
_baseType.BaseType; }
        }

       
public override string
FullName
        {
           
get { return
_baseType.FullName; }
        }

       
public override Guid
GUID
        {
           
get { return
_baseType.GUID; }
        }

       
protected override TypeAttributes
GetAttributeFlagsImpl()
        {
           
throw new NotImplementedException
();
        }

       
protected override ConstructorInfo GetConstructorImpl(BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier
[] modifiers)
        {
           
throw new NotImplementedException
();
        }

       
public override ConstructorInfo[] GetConstructors(BindingFlags
bindingAttr)
        {
           
return
_baseType.GetConstructors(bindingAttr);
        }

       
public override Type
GetElementType()
        {
           
return
_baseType.GetElementType();
        }

       
public override EventInfo GetEvent(string name, BindingFlags
bindingAttr)
        {
           
return
_baseType.GetEvent(name, bindingAttr);
        }

       
public override EventInfo[] GetEvents(BindingFlags
bindingAttr)
        {
           
return
_baseType.GetEvents(bindingAttr);
        }

       
public override FieldInfo GetField(string name, BindingFlags
bindingAttr)
        {
           
return
_baseType.GetField(name, bindingAttr);
        }

       
public override FieldInfo[] GetFields(BindingFlags
bindingAttr)
        {
           
return
_baseType.GetFields(bindingAttr);
        }

       
public override Type GetInterface(string name, bool
ignoreCase)
        {
           
return
_baseType.GetInterface(name, ignoreCase);
        }

       
public override Type
[] GetInterfaces()
        {
           
return
_baseType.GetInterfaces();
        }

       
public override MemberInfo[] GetMembers(BindingFlags
bindingAttr)
        {
           
return
_baseType.GetMembers(bindingAttr);
        }

       
protected override MethodInfo GetMethodImpl(string name, BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier
[] modifiers)
        {
           
throw new NotImplementedException
();
        }

       
public override MethodInfo[] GetMethods(BindingFlags
bindingAttr)
        {
           
return
_baseType.GetMethods(bindingAttr);
        }

       
public override Type GetNestedType(string name, BindingFlags
bindingAttr)
        {
           
return
_baseType.GetNestedType(name, bindingAttr);
        }

       
public override Type[] GetNestedTypes(BindingFlags
bindingAttr)
        {
           
return
_baseType.GetNestedTypes(bindingAttr);
        }

       
public override PropertyInfo[] GetProperties(BindingFlags
bindingAttr)
        {
           
PropertyInfo
[] clrProperties = _baseType.GetProperties(bindingAttr);
           
if (clrProperties != null
)
               
return
clrProperties.Concat(_customProperties).ToArray();
           
return
_customProperties.ToArray();
        }

       
protected override PropertyInfo GetPropertyImpl(string name, BindingFlags bindingAttr, Binder binder, Type returnType, Type[] types, ParameterModifier
[] modifiers)
        {
           
return
GetProperties(bindingAttr).FirstOrDefault(prop => prop.Name == name) ?? _customProperties.FirstOrDefault(prop => prop.Name == name);
        }

       
protected override bool
HasElementTypeImpl()
        {
           
throw new NotImplementedException
();
        }

       
public override object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string
[] namedParameters)
        {
           
return
_baseType.InvokeMember(name, invokeAttr, binder, target, args, modifiers, culture, namedParameters);
        }

       
protected override bool
IsArrayImpl()
        {
           
throw new NotImplementedException
();
        }

       
protected override bool
IsByRefImpl()
        {
           
throw new NotImplementedException
();
        }

       
protected override bool
IsCOMObjectImpl()
        {
           
throw new NotImplementedException
();
        }

       
protected override bool
IsPointerImpl()
        {
           
throw new NotImplementedException
();
        }

       
protected override bool
IsPrimitiveImpl()
        {
           
return
_baseType.IsPrimitive;
        }

       
public override Module
Module
        {
           
get { return
_baseType.Module; }
        }

       
public override string
Namespace
        {
           
get { return
_baseType.Namespace; }
        }

       
public override Type
UnderlyingSystemType
        {
           
get { return
_baseType.UnderlyingSystemType; }
        }

       
public override object[] GetCustomAttributes(Type attributeType, bool
inherit)
        {
           
return
_baseType.GetCustomAttributes(attributeType, inherit);
        }

       
public override object[] GetCustomAttributes(bool
inherit)
        {
           
return
_baseType.GetCustomAttributes(inherit);
        }

       
public override bool IsDefined(Type attributeType, bool
inherit)
        {
           
return
_baseType.IsDefined(attributeType, inherit);
        }

       
public override string
Name
        {
           
get { return
_baseType.Name; }
        }
    }

   
private class CustomPropertyInfo : PropertyInfo
    {
       
private string
_name;
       
private Type
_type;
       
private object
_defaultValue;
       
private List<Attribute
> _attributes;

       
public CustomPropertyInfo(string name, Type type, object defaultValue = null, List<Attribute> attributes = null
)
            :
this
(name, type, attributes)
        {
            _defaultValue = defaultValue;
        }

       
protected CustomPropertyInfo(string name, Type type, List<Attribute> attributes = null
)
        {
            _name = name;
            _type = type;
            _attributes = attributes;
        }

       
public virtual object GetDefaultValue(DynamicBaseType
entity)
        {
           
return
_defaultValue;
        }

       
public override PropertyAttributes
Attributes
        {
           
get { throw new NotImplementedException
(); }
        }

       
public override bool
CanRead
        {
           
get { return true
; }
        }

       
public override bool
CanWrite
        {
           
get { return true
; }
        }

       
public override MethodInfo[] GetAccessors(bool
nonPublic)
        {
           
throw new NotImplementedException
();
        }

       
public override MethodInfo GetGetMethod(bool
nonPublic)
        {
           
throw new NotImplementedException
();
        }

       
public override ParameterInfo
[] GetIndexParameters()
        {
           
throw new NotImplementedException
();
        }

       
public override MethodInfo GetSetMethod(bool
nonPublic)
        {
           
throw new NotImplementedException
();
        }

       
public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo
culture)
        {
           
return ((DynamicBaseType
)obj).GetPropertyValue(_name);
        }

       
public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo
culture)
        {
            ((
DynamicBaseType
)obj).SetPropertyValue(_name, value);
        }

       
public override Type
PropertyType
        {
           
get { return
_type; }
        }

       
public override Type
DeclaringType
        {
           
get { throw new NotImplementedException
(); }
        }

       
public override object[] GetCustomAttributes(Type attributeType, bool
inherit)
        {
           
return _attributes == null ? new object
[0] : _attributes.Where(a => a.GetType() == attributeType).ToArray();
        }

       
public override object[] GetCustomAttributes(bool
inherit)
        {
           
return _attributes == null ? new object
[0] : _attributes.ToArray();
        }

       
public override bool IsDefined(Type attributeType, bool
inherit)
        {
           
throw new NotImplementedException
();
        }

       
public override string
Name
        {
           
get { return
_name; }
        }

       
public override Type
ReflectedType
        {
           
get { throw new NotImplementedException
(); }
        }
    }

   
private interface IDependantCustomPropertyInfo
    {
       
string Name { get
; }
       
string[] Properties { get
; }
    }

   
private class CustomPropertyInfo<V> : CustomPropertyInfo, IDependantCustomPropertyInfo
    {
       
private Func
<T, V> _get;
       
private Action
<T, V> _set;
       
private string
[] _properties;

       
public CustomPropertyInfo(string name, Func<T, V> get, Action<T, V> set = null, List<Attribute> attributes = null, string[] properties = null
)
            :
base(name, typeof
(V), attributes)
        {
            _get = get;
            _set = set;
            _properties = properties;
        }

       
public string
[] Properties
        {
           
get { return
_properties; }
        }

       
public override object GetDefaultValue(DynamicBaseType
entity)
        {
           
return
_get((T)entity);
        }

       
public override bool
CanWrite
        {
           
get { return _set != null
; }
        }

       
public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo
culture)
        {
           
return
_get((T)obj);
        }

       
public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo
culture)
        {
           
if (_set == null
)
               
throw new InvalidOperationException();
            _set((T)obj, (V)value);
        }
    }
}

Que je peux utiliser très facilement.

Avec ma classe Customer par exemple :

public class Customer : DynamicBaseType<Customer>
{
   
private String
firstName;
   
public String
FirstName
    {
       
get
{ return firstName; }
       
set
        {
            firstName =
value
;
            OnPropertyChanged(
"FirstName"
);
        }
    }

   
private String
lastName;
   
public String
LastName
    {
       
get
{ return lastName; }
       
set
        {
            lastName =
value
;
            OnPropertyChanged(
"LastName");
        }
    }
}

Dans le ViewModel, je peux désormais utiliser le code suivant :

Customer.AddProperty("Age", typeof(int));
Customer.AddProperty("Married", typeof(bool));
Customer.AddProperty("FullName", c => string.Format("{0} {1}", c.LastName, c.FirstName), properties:new string[] { "LastName", "FirstName" });
 
customers[0].SetPropertyValue("Age", 40);
customers[0].SetPropertyValue("Married", true);
 
customers[1].SetPropertyValue("Age", 45);
customers[1].SetPropertyValue("Married", true);

Ce qui est nouveau dans ma solution par rapport au code de départ c’est que FullName est calculé à partir de d’autres propriétés. Si on change la propriété LastName ou FirstName d’un Customer, celui-ci lèvera également l’évènement PropertyChanged avec FullName.

Posté le lundi 12 septembre 2011 23:58 par Matthieu MEZIL | 0 commentaire(s)

Classé sous :

Utiliser un repository depuis WF

Imaginons le repository suivant :

public class FooRepository
{
public void Add(FooEntity entity) { /*TODO*/ }
public void Update(FooEntity entity) { /*TODO*/ }
public void Delete(FooEntityentity) { /*TODO*/ }
public void SaveChanges() { /*TODO*/ }
public void Dispose() { /*TODO*/ }
}

L’idée est de l’utiliser dans un contexte Workflow.

Pour cela j’utilise un WCF workflow service.

clip_image002[4]

Le receive de la méthode GetFooEntity retourne une instance de FooEntity que j’affecte ensuite dans la variable fooEntity du type FooEntity.

Imaginons qu’entre le Receive et le SendReply, je veuille ajouter une instance de fooEntity en utilisant mon repository, puis persister mon repository et enfin le disposer.

On peut le faire en instanciant et initialisant une variable de type FooRepository et en utilisant des activités de type InvokeMethod :

clip_image004[4]

clip_image006[4]

Comme vous pouvez le voir, c’est un peu pénible…

Mon idée est donc de définir une activité RepositoryScope pour simplifier cela.

Tout d’abord, nous allons créer une interface IRepository que ma classe FooRepository va implémenter :

public interface IRepository : IDisposable
{
     void Add(object entity);
     void Update(object entity);
     void Delete(object entity);
     void SaveChanges(); } public class FooRepository : IRepository {
     public void Add(FooEntity entity) { /*TODO*/ }
     public void Update(FooEntity entity) { /*TODO*/ }
     public void Delete(FooEntity entity) { /*TODO*/ }
     public void SaveChanges() { /*TODO*/ }
     public void Dispose() { /*TODO*/ }


     void IRepository.Add(object entity)
     {
         Add((FooEntity)entity);
     }
     void IRepository.Update(object entity)
     {
         Update((FooEntity)entity);
     }
     void IRepository.Delete(object entity)
     {
         Delete((FooEntity)entity);
     } }

Maintenant, nous allons définir notre activité RepositoryScope qui hérite de NativeActivity.

Dans le RepositoryScope, il faudrait être capable d’ajouter plusieurs entités. Pour cela, on pourrait utiliser une propriété de type Activity et utiliser une séquence mais autant directement utiliser une collection d’activité.

public class RepositoryScope<RepositoryType> : NativeActivity
     where RepositoryType : IRepository, new() {
     private Collection<Activity> _activities;
     public Collection<Activity> Activities
     {
         get { return _activities ?? (_activities = new Collection<Activity>()); }
     }

     protected override void Execute(NativeActivityContext context)
     {
         // TODO
     } }

Maintenant la première question est : comment renseigner cette propriété dans le designer ? Pour cela, nous allons définir notre propre ActivityDesigner et ajouter l’attribut Designer dans notre classe RepositoryScope<RepositoryType>.

[Designer(typeof(RepositoryScopeDesigner))]
 
 

<sap:ActivityDesigner x:Class="MyNamespace.RepositoryScopeDesigner"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"

    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">

 

    <sap:ActivityDesigner.Resources>

        <Style TargetType="sap:WorkflowItemsPresenter">

            <Setter Property="SpacerTemplate">

                <Setter.Value>

                    <DataTemplate>

                        <Rectangle Height="1"

                                   Stroke="Gray"

                                   Width="50"

                                   Margin="0,10,0,10" />

                    </DataTemplate>

                </Setter.Value>

            </Setter>

            <Setter Property="ItemsPanel">

                <Setter.Value>

                    <ItemsPanelTemplate>

                        <StackPanel Orientation="Vertical" />

                    </ItemsPanelTemplate>

                </Setter.Value>

            </Setter>

        </Style>

    </sap:ActivityDesigner.Resources>

 

    <Grid>

        <sap:WorkflowItemsPresenter Items="{Binding Path=ModelItem.Activities}"

                                    HintText="Insert Activities Here" />

    </Grid>

</
sap:ActivityDesigner
>

Ensuite, on peut maintenant l’utiliser comme ceci :

clip_image008[4]

clip_image010[4]

Maintenant notre activité ScopeRepository doit scheduler ses activités. La manière la plus aisée est d’utiliser une séquence, de la scheduler dans la méthode Execute, de les retirer les activités des metadatas, de les ajouter dans la séquence et d’ajouter la Sequence dans les enfants de l’activité :

[Designer(typeof(RepositoryScopeDesigner))]
public class RepositoryScope<RepositoryType> : NativeActivity
     where RepositoryType : IRepository, new() {
     private Collection<Activity> _activities;
     public Collection<Activity> Activities
     {
         get { return _activities ?? (_activities = new Collection<Activity>()); }
     }


     private Sequence _scopeSequence;
     private Sequence ScopeSequence
     {
         get { return _scopeSequence ?? (_scopeSequence = new Sequence()); }
     }

     protected override void Execute(NativeActivityContext context)
     {
         context.ScheduleActivity(ScopeSequence);     }

     protected override void CacheMetadata(NativeActivityMetadata metadata)
     {
         base.CacheMetadata(metadata);
         metadata.SetChildrenCollection(null);         foreach (Activity activity in Activities)              ScopeSequence.Activities.Add(activity);         metadata.AddChild(ScopeSequence);     } }

Notez que le metadata.SetChildrenCollection(null) est obligatoire pour les affecter à la séquence.

Maintenant l’idée est d’utiliser ce scope pour faire plus que ce que ne fait déjà la séquence. Le scope va instancier le repository et va le disposer à la fin. Nous allons également utiliser un bloc Try / Finally pour disposer le repository quoi qu’il arrive.

[Designer(typeof(RepositoryScopeDesigner))]
public class RepositoryScope<RepositoryType> : NativeActivity
     where RepositoryType : IRepository, new() {
     public OutArgument<RepositoryType> Repository { get; set; }

     private Collection<Activity> _activities;
     public Collection<Activity> Activities
     {
         get { return _activities ?? (_activities = new Collection<Activity>()); }
     }

     private Sequence _scopeSequence;
     private Sequence ScopeSequence
     {
         get { return _scopeSequence ?? (_scopeSequence = new Sequence()); }
     }

     private TryCatch _tryCatch;
     private TryCatch TryCatch
     {
         get { return _tryCatch ?? (_tryCatch = new TryCatch()); }
     }

     private Activity _finallyActivity;
     public Activity FinallyActivity
     {
         get { return _finallyActivity ?? (_finallyActivity = new DisposeActivity()); }
     }

     protected override void Execute(NativeActivityContext context)
     {
         var repo = new RepositoryType();
         Repository.Set(context, repo);         context.ScheduleActivity(TryCatch);
     }

     protected override void CacheMetadata(NativeActivityMetadata metadata)
     {
         metadata.AddChild(FinallyActivity);
         base.CacheMetadata(metadata);
         metadata.SetChildrenCollection(null);
         foreach (Activity activity in Activities)
             ScopeSequence.Activities.Add(activity);
         TryCatch.Try = ScopeSequence;
         TryCatch.Finally = FinallyActivity;
         metadata.AddChild(TryCatch);
     } }

C’est un bon début mais encore faudrait-il que l’activité DisposeActivity connaisse le repository.

On pourrait utiliser un InArgument<IRepository>, cependant, il y a un meilleur moyen avec les NativeActivity : utiliser la propriété Properties. En effet, les instances contenues dans la propriété Properties sont partagées entre l’activité et ses descendants.

Nous pouvons donc procéder comme ceci :

[Designer(typeof(RepositoryScopeDesigner))]
public class RepositoryScope<RepositoryType> : NativeActivity
     where RepositoryType : IRepository, new() {
     public OutArgument<RepositoryType> Repository { get; set; }

     private Collection<Activity> _activities;
     public Collection<Activity> Activities
     {
         get { return _activities ?? (_activities = new Collection<Activity>()); }
     }

     private Sequence _scopeSequence;
     private Sequence ScopeSequence
     {
         get { return _scopeSequence ?? (_scopeSequence = new Sequence()); }
     }

     private TryCatch _tryCatch;
     private TryCatch TryCatch
     {
         get { return _tryCatch ?? (_tryCatch = new TryCatch()); }
     }

     private DisposeActivity _disposeActivity;
     public Activity DisposeActivity
     {
         get { return _disposeActivity ?? (_disposeActivity = new DisposeActivity()); }
     }

     protected override void Execute(NativeActivityContext context)
     {
         var repo = new RepositoryType();
         Repository.Set(context, repo);
         context.Properties.Add("Repository", repo);         context.ScheduleActivity(TryCatch);
     }

     protected override void CacheMetadata(NativeActivityMetadata metadata)
     {
         base.CacheMetadata(metadata);
         metadata.SetChildrenCollection(null);
         foreach (Activity activity in Activities)
             ScopeSequence.Activities.Add(activity);
         TryCatch.Try = ScopeSequence;
         TryCatch.Finally = DisposeActivity;
         metadata.AddChild(TryCatch);
     } }




internal class DisposeActivity : NativeActivity {
     protected override void Execute(NativeActivityContext context)
     {
         ((IRepository)context.Properties.Find("Repository")).Dispose();     } }

Cool ! Maintenant, nous n’avons plus besoin d’initialiser la variable fooRepository que nous allons récupérer comme OutArgument de notre RepositoryScope. Nous n’avons plus besoin de l’InvokeMethod sur Dispose (puisque le RepositoryScope s’en charge désormais).

clip_image012[4]

clip_image014[4]

A présent nous allons améliorer l’utilisation du RepositoryScope.

Tout d’abord, je ne pense pas qu’il soit souhaitable de passer la méthode à appeler sous la forme d’une string.

Nous allons donc créer des activités AddEntity, UpdateEntity, DeleteEntity et SaveChanges que nous allons utiliser dans le workflow.

public class AddEntity : NativeActivity
{
     [RequiredArgument]
     public InArgument<IRepository> Repository { get; set; }

     [RequiredArgument]
     public InArgument<object> Entity { get; set; }

     protected override void Execute(NativeActivityContext context)
     {
         Repository.Get(context).Add(Entity.Get(context));
     } } public class SaveChanges : NativeActivity {
     [RequiredArgument]
     public InArgument<IRepository> Repository { get; set; }

     protected override void Execute(NativeActivityContext context)
     {
         Repository.Get(context).SaveChanges();
     } }

clip_image016[4]

Maintenant l’idée est de définir l’argument Repository optionnel et de le déduire, dans ce cas, du RepositoryScope parent en utilisant la propriété Properties, comme nous avons fait avec la DisposeActivity :

public abstract class RepositoryActivity : NativeActivity
{
     public InArgument<IRepository> Repository { get; set; }

     protected virtual IRepository GetRepository(NativeActivityContext context)
     {
         IRepository repo = Repository.Get(context);
         if (repo == null)
             repo = ((IRepository)context.Properties.Find("Repository"));
         return repo;
     } } public class AddEntity : RepositoryActivity {
     [RequiredArgument]
     public InArgument<object> Entity { get; set; }

     protected override void Execute(NativeActivityContext context)
     {
         GetRepository(context).Add(Entity.Get(context));
     } } public class SaveChanges : RepositoryActivity {
     protected override void Execute(NativeActivityContext context)
     {
         GetRepository(context).SaveChanges();
     } } internal class DisposeActivity : RepositoryActivity {
     protected override void Execute(NativeActivityContext context)
     {
         GetRepository(context).Dispose();
     } }

Maintenant il va y avoir un point à prendre en compte : être capable de gérer l’encapsulation d’un RepositoryScope par un autre RepositoryScope. Pour cela, notre propriété “Repository” n’est pas suffisante.

Il va donc falloir changer notre code et nous allons utiliser une Stack<IRepository> à la place.

Dans notre code, on peut regretter la manière d’utiliser la properties “Repository”. Même s’il serait mieux d’utiliser une constante à la place de "Repository", ça reste pas terrible selon moi, et, de plus, c’est dommage de devoir faire un cast. Selon moi, on devrait utiliser des variables typés à la place.

Pour cela nous allons utiliser une nouvelle classe :

public class RepositoryContext
{
     private Stack<IRepository> _repositories;
     public Stack<IRepository> Repositories
     {
         get { return _repositories ?? (_repositories = new Stack<IRepository>()); }
     }

     public static RepositoryContext GetContext(NativeActivityContext context)
     {
         return (RepositoryContext)context.Properties.FirstOrDefault(kv => kv.Key == typeof(RepositoryContext).Name).Value;
     }

     public static RepositoryContext CreateContext(NativeActivityContext context)
     {
         RepositoryContext c = new RepositoryContext();
         context.Properties.Add(typeof(RepositoryContext).Name, c);
         return c;
     }

     public static RepositoryContext GetOrCreateContext(NativeActivityContext context)
     {
         return GetContext(context) ?? CreateContext(context);
     } }

Maintenant dans la méthode Execute de notre RespositoryScope, nous allons l’initialiser comme ceci :

RepositoryContext.CreateContext(context).Repositories.Push(repo);

à la place de

context.Properties.Add("Repository", repo);

Dans les RepositoryActivity, nous allons l’utiliser comme ceci :

repo = RepositoryContext.GetContext(context).Repositories.Peek();

au lieu de

repo = ((IRepository)context.Properties.Find("Repository"));

Dans le Dispose, nous allons dépiler notre pile:

internal class DisposeActivity : RepositoryActivity
{
     protected override void Execute(NativeActivityContext context)
     {
         GetRepository(context).Dispose();
         RepositoryContext.GetContext(context).Repositories.Pop();
     } }

Je trouve cela beaucoup mieux et je pourrais vouloir reproduire ce fonctionnement dans d’autre cas, d’où l’idée de le factoriser au maximum.

Je vais donc utiliser une classe de base :

public class WFPropertiesContext<T> 
where T : WFPropertiesContext<T>, new() {
     public static T GetContext(NativeActivityContext context)
     {
         return (T)context.Properties.FirstOrDefault(kv => kv.Key == typeof(T).Name).Value;
     }

     public static T CreateContext(NativeActivityContext context)
     {
         T c = new T();
         c.Initialize(context);
         context.Properties.Add(typeof(T).Name, c);
         return c;
     }

     public static T GetOrCreateContext(NativeActivityContext context)
     {
         return GetContext(context) ?? CreateContext(context);
     }

     protected virtual void Initialize(NativeActivityContext context)
     {
     } } public class RepositoryContext : WFPropertiesContext<RepositoryContext> {
     private Stack<IRepository> _repositories;
     public Stack<IRepository> Repositories
     {
         get { return _repositories ?? (_repositories = new Stack<IRepository>()); }
     } }

Posté le jeudi 1 septembre 2011 23:59 par Matthieu MEZIL | 2 commentaire(s)

Classé sous : ,

EF et récursivité

A travers ce post, je vais vous présenter un cas concret de mauvaise utilisation d’EF observée récemment chez un client.

Mon modèle est le suivant :

image

Nous voulons simplement récupérer un arbre avec une contrainte : si le type du noeud est "G" et qu’il n’a pas d’enfant, il ne doit pas apparaître dans l’arborescence.

Comment faire ceci.

Voici le code que j’ai pu observer :

public static List<Node> GetTree()
{
using (var context = new TreeEntities())
{
List<Node> nodes = context.Nodes.Where(n => n.Parent == null).ToList();
foreach (var n in nodes)
LoadChildren(context, n);
foreach (var n in nodes.Where(n => n.Type == "G" && n.Children.Count == 0).ToList())
nodes.Remove(n);
return nodes;
} } public static void LoadChildren(TreeEntities context, Node parentNode) {
List<Node> nodes = context.Nodes.Where(n => n.ParentId == parentNode.Id).ToList();
     foreach (var n in nodes)
         LoadChildren(context, n);
     foreach (var n in nodes.Where(n => n.Type == "G" && n.Children.Count == 0).ToList())
         context.Nodes.Detach(n); }

J’ai effectué un test avec le jeu d’entités suivant :

using (var context = new TreeEntities())
{
     for (int i0 = 0; i0 < 10; i0++)
     {
         Node n0 = new Node { Name = i0.ToString(), Type = i0 == 9 ? null : "G" };
         context.Nodes.AddObject(n0);
         if (i0 > 7)
             break;
         for (int i1 = 0; i1 < 10; i1++)
         {
             Node n1 = new Node { Name = n0.Name + i1.ToString(), Type = i1 == 9 ? null : "G", Parent = n0 };
             context.Nodes.AddObject(n1);
             if (i1 > 7)
                 break;
             for (int i2 = 0; i2 < 10; i2++)
             {
                 Node n2 = new Node { Name = n1.Name + i2.ToString(), Type = i2 == 9 ? null : "G", Parent = n1 };
                 context.Nodes.AddObject(n2);
                 if (i2 > 7)
                     break;
                 for (int i3 = 0; i3 < 10; i3++)
                 {
                     Node n3 = new Node { Name = n2.Name + i3.ToString(), Type = i3 == 9 ? null : "G", Parent = n2 };
                     context.Nodes.AddObject(n3);
                     if (i3 > 7)
                         break;
                     for (int i4 = 0; i4 < 10; i4++)
                         context.Nodes.AddObject(new Node { Name = n3.Name + i4.ToString(), Parent = n3 });
                 }
             }
         }
     }
     context.SaveChanges(); }

Pour cette première version, ce code s’exécute en 5 minutes 41 secondes et génère 46 226 requêtes en base !

 

Il n’y a pas besoin d’être DBA pour savoir que si le nombre de requêtes dépend du nombre de datarows, le code ne supporte pas la charge.

Comment améliorer les choses ?

 

Un des gros avantages d’EF est le fait qu’il fait tout seul les relations. C’est d’ailleurs pour cela que ce code fonctionne. A aucun moment on a affecté les enfants / parents d’un des noeuds. EF l’a fait pour nous.

 

On pourrait être tenter par le code suivant :

public static List<Node> GetTree()
{
using (var context = new TreeEntities())
{


return context.Nodes.Where(n => n.Type != "G" || n.Children.Any()).AsEnumerable().Where(n => n.ParentId == null).ToList();
} }

Petite remarque avant de continuer : attention à bien utiliser ParentId != null et pas Parent != null. En effet, au moment où on énumère sur les entités, rien ne nous indique que le parent a bien été chargé. Si ce n’est pas le cas, ParentId sera différent de null alors que Parent sera égal à null.

D’autre part, ce code ne fonctionne pas comme il devrait. En effet, si j’ai un noeud de type G avec un enfant de type G sans enfant, le premier noeud doit être supprimé puisque son seul enfant doit être supprimé. Or avec mon système à un seul niveau, ce ne sera pas le cas.

 

SQL n’est cependant pas réputé pour la récursivité.

Je vais donc tenter deux approches :

  • une première dans laquelle je vais faire cette récursivité dans mon code. A noter que cela implique de charger trop d’entités (comme c’est le cas avec la première version).
  • une deuxième dans laquelle c’est SQL Server qui va se charger de la récursivité

 

Ma première solution est la suivante :

public static IEnumerable<T> GetEntitiesInCache<T>(this ObjectContext context, EntityState entityState = EntityState.Unchanged | EntityState.Modified | EntityState.Added)
{
    return context.ObjectStateManager.GetObjectStateEntries(entityState).Select(ose => ose.Entity).OfType<T>(); } public static List<Node> GetTree(TreeEntities context) {
using (var context = new TreeEntities())
{
   foreach (var n in context.Nodes);

bool continueLoop;
do
{
continueLoop = false;
foreach (var n in context.GetEntitiesInCache<Node>().Where(n => n.Type == "G" && n.Children.Count == 0).ToList())
{
continueLoop = true;
context.Nodes.Detach(n);
}
} while (continueLoop);

return context.GetEntitiesInCache<Node>().Where(n => n.ParentId == null).ToList();
} }

Dans ce cas, je suis passé de plus de 5 minutes 41 secondes à 1 seconde avec une seule requête exécutée en base.

A noter que je travaille avec une base locale. Dans le cas contraire l’écart de temps aurait été encore plus important.

Remarquez également le

       foreach (var n in context.Nodes);

qui permet de charger l’ensemble des noeuds.

 

Pour la récursivité avec SQL server, cela fera l’objet d’un post futur.

Posté le jeudi 28 juillet 2011 21:02 par Matthieu MEZIL | 3 commentaire(s)

Classé sous : , ,

EF nouvelle CTP

Une nouvelle CTP d’EF et de WCF Data Services est disponible.

Contrairement aux CTPs précédentes qui se concentraient essentiellement sur l’ajout d’une nouvelle approche, Code-First, celle-ci apporte des améliorations sur le moteur même d’Entity Framework.

Le designer a également eu droit à son lot de nouveautés / améliorations :

  • le support des enums, types spatial et Table Valued Functions
  • la possibilité d’avoir plusieurs diagrammes du même edmx avec les petites options qui vont bien pour intégrer les entités liées par exemple
  • la possibilité de choisir la couleur de chaque entité
  • Le report automatique du StoreGeneratedPattern du CSDL dans le SSDL
  • La possibilité d’importer les procédures stockées dans le CSDL directement lors de l’import depuis la base
  • L’amélioration de l’association visuelle entre association et navigation properties
  • La possibilité d’externaliser dans un fichier séparé les infos relatives aux diagrammes, ce qui a le mérite de simplifier considérablement les problématiques de merge d’edmx avec un contrôleur de sources.

Mes tests m’ont amené à identifier un bug avec les Table Valued Functions : il n’est pas possible, dans la version actuelle, de placer le résultat d’une Table Valued Functions dans un let.

Plus d’info sur Announcing the Microsoft Entity Framework June 2011 CTP.

Posté le vendredi 1 juillet 2011 02:07 par Matthieu MEZIL | 4 commentaire(s)

L2E : impact sur le SQL

Comme je l’écrivais hier,

“Derrière la magie d’Entity Framework se cache la réalité du SQL. Je persiste et je signe, si vous ne connaissez pas le SQL, il sera très compliqué d’écrire des requêtes L2E optimales.

J’ai trouvé une bonne illustrations avec les fonctions de grouping.”

Hier, je vous ai montré comment exécuter un Count et un Max en une seule requête LINQ.

Maintenant imaginons que je veuille toujours récupérer le nombre de commandes et la date de la dernière commande mais par client.

Comment récupérer cela?

Si vous êtes sûr que chaque client à au moins une commande ou que vous voulez ignorer les clients sans commande, la meilleure façon d’écrire cette requête est probablement celle-ci:

from o in context.Orders
group o by o.Customer into g
select new { Customer = g.Key, OrdersCount = g.Count(), LastOrderDate = g.Max(o => o.OrderDate) };

Dans ce cas, le SQL est très proche de celui de la requête L2E :

SELECT

[GroupBy1].[K1] AS [CustomerId],

[GroupBy1].[K2] AS [LastName],

[GroupBy1].[K3] AS [FirstName],

[GroupBy1].[K4] AS [BirthDay],

[GroupBy1].[K5] AS [AddressLine],

[GroupBy1].[K6] AS [City],

[GroupBy1].[K7] AS [PostalCode],

[GroupBy1].[K8] AS [Region],

[GroupBy1].[K9] AS [Country],

[GroupBy1].[A1] AS [C1],

[GroupBy1].[A2] AS [C2]

FROM ( SELECT

[Extent2].[CustomerId] AS [K1],

[Extent2].[LastName] AS [K2],

[Extent2].[FirstName] AS [K3],

[Extent2].[BirthDay] AS [K4],

[Extent2].[AddressLine] AS [K5],

[Extent2].[City] AS [K6],

[Extent2].[PostalCode] AS [K7],

[Extent2].[Region] AS [K8],

[Extent2].[Country] AS [K9],

COUNT(1) AS [A1],

MAX([Extent1].[OrderDate]) AS [A2]

FROM [dbo].[Orders] AS [Extent1]

INNER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerId] = [Extent2].[CustomerId]

GROUP BY [Extent2].[CustomerId], [Extent2].[LastName], [Extent2].[FirstName], [Extent2].[BirthDay], [Extent2].[AddressLine], [Extent2].[City], [Extent2].[PostalCode], [Extent2].[Region], [Extent2].[Country]

) AS [GroupBy1]

Avec un plan d’exécution assez simple :

image_thumb[7]

Si vous pouvez avoir des clients sans commandes, la requêtes devient plus compliquée.

Une idée assez simple serait d’utiliser la requête précédente union les clients sans commandes :

(from o in context.Orders
 group o by o.Customer into g
 select new { Customer = g.Key, OrdersCount = g.Count(), LastOrderDate = (DateTime?)g.Max(o => o.OrderDate) }).Union(
from c in context.Customers
where !c.Orders.Any()
select new { Customer = c, OrdersCount = 0, LastOrderDate = (DateTime?)null });

Dans ce cas, la requête SQL n’est pas performante :

SELECT

[Distinct1].[C1] AS [C1],

[Distinct1].[C2] AS [C2],

[Distinct1].[C3] AS [C3],

[Distinct1].[C4] AS [C4],

[Distinct1].[C5] AS [C5],

[Distinct1].[C6] AS [C6],

[Distinct1].[C7] AS [C7],

[Distinct1].[C8] AS [C8],

[Distinct1].[C9] AS [C9],

[Distinct1].[C10] AS [C10],

[Distinct1].[C11] AS [C11],

[Distinct1].[C12] AS [C12]

FROM ( SELECT DISTINCT

[UnionAll1].[C1] AS [C1],

[UnionAll1].[CustomerId] AS [C2],

[UnionAll1].[LastName] AS [C3],

[UnionAll1].[FirstName] AS [C4],

[UnionAll1].[BirthDay] AS [C5],

[UnionAll1].[AddressLine] AS [C6],

[UnionAll1].[City] AS [C7],

[UnionAll1].[PostalCode] AS [C8],

[UnionAll1].[Region] AS [C9],

[UnionAll1].[Country] AS [C10],

[UnionAll1].[C2] AS [C11],

[UnionAll1].[C3] AS [C12]

FROM (SELECT

1 AS [C1],

[GroupBy1].[K1] AS [CustomerId],

[GroupBy1].[K2] AS [LastName],

[GroupBy1].[K3] AS [FirstName],

[GroupBy1].[K4] AS [BirthDay],

[GroupBy1].[K5] AS [AddressLine],

[GroupBy1].[K6] AS [City],

[GroupBy1].[K7] AS [PostalCode],

[GroupBy1].[K8] AS [Region],

[GroupBy1].[K9] AS [Country],

[GroupBy1].[A1] AS [C2],

CAST( [GroupBy1].[A2] AS datetime2) AS [C3]

FROM ( SELECT

[Extent2].[CustomerId] AS [K1],

[Extent2].[LastName] AS [K2],

[Extent2].[FirstName] AS [K3],

[Extent2].[BirthDay] AS [K4],

[Extent2].[AddressLine] AS [K5],

[Extent2].[City] AS [K6],

[Extent2].[PostalCode] AS [K7],

[Extent2].[Region] AS [K8],

[Extent2].[Country] AS [K9],

COUNT(1) AS [A1],

MAX([Extent1].[OrderDate]) AS [A2]

FROM [dbo].[Orders] AS [Extent1]

INNER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerId] = [Extent2].[CustomerId]

GROUP BY [Extent2].[CustomerId], [Extent2].[LastName], [Extent2].[FirstName], [Extent2].[BirthDay], [Extent2].[AddressLine], [Extent2].[City], [Extent2].[PostalCode], [Extent2].[Region], [Extent2].[Country]

) AS [GroupBy1]

UNION ALL

SELECT

1 AS [C1],

[Extent3].[CustomerId] AS [CustomerId],

[Extent3].[LastName] AS [LastName],

[Extent3].[FirstName] AS [FirstName],

[Extent3].[BirthDay] AS [BirthDay],

[Extent3].[AddressLine] AS [AddressLine],

[Extent3].[City] AS [City],

[Extent3].[PostalCode] AS [PostalCode],

[Extent3].[Region] AS [Region],

[Extent3].[Country] AS [Country],

0 AS [C2],

CAST(NULL AS datetime2) AS [C3]

FROM [dbo].[Customers] AS [Extent3]

WHERE NOT EXISTS (SELECT

1 AS [C1]

FROM [dbo].[Orders] AS [Extent4]

WHERE [Extent3].[CustomerId] = [Extent4].[CustomerId]

)) AS [UnionAll1]

) AS [Distinct1]

image_thumb[10]

Vous vous en doutez, j’espère, ce n’est probablement pas le meilleur moyen d’écrire cette requête…

Un autre serait d’utiliser ce que j’ai présenté hier : un group by sur une constant pour récupérer le count et le max sur c.Orders:

from c in context.Customers
let ordersInfo = (from o in c.Orders
                  group o by 1 into g
                  select new { OrdersCount = g.Count(), LastOrderDate = g.Max(o => o.OrderDate) }).FirstOrDefault()
select new { Customer = c, OrdersCount = (int?)ordersInfo.OrdersCount ?? 0, OrderDate = (DateTime?)ordersInfo.LastOrderDate };

Remarquez le cast en int? ou DateTime?. Contrairement à .NET, SQL retourne null pour OrdersCount et OrderDate si un client n’a pas de commande.

Si cette requête est bonne en L2O, elle ne l’est pas en L2E au vu du SQL généré !

SELECT

[Extent1].[CustomerId] AS [CustomerId],

[Extent1].[LastName] AS [LastName],

[Extent1].[FirstName] AS [FirstName],

[Extent1].[BirthDay] AS [BirthDay],

[Extent1].[AddressLine] AS [AddressLine],

[Extent1].[City] AS [City],

[Extent1].[PostalCode] AS [PostalCode],

[Extent1].[Region] AS [Region],

[Extent1].[Country] AS [Country],

CASE WHEN ([Limit1].[C1] IS NULL) THEN 0 ELSE [Limit1].[C1] END AS [C1],

CAST( [Limit1].[C2] AS datetime2) AS [C2]

FROM [dbo].[Customers] AS [Extent1]

OUTER APPLY (SELECT TOP (1)

[GroupBy1].[A1] AS [C1],

[GroupBy1].[A2] AS [C2]

FROM ( SELECT

[Project1].[K1] AS [K1],

COUNT([Project1].[A1]) AS [A1],

MAX([Project1].[A2]) AS [A2]

FROM ( SELECT

1 AS [K1],

1 AS [A1],

[Project1].[OrderDate] AS [A2]

FROM ( SELECT

[Extent2].[OrderDate] AS [OrderDate]

FROM [dbo].[Orders] AS [Extent2]

WHERE [Extent1].[CustomerId] = [Extent2].[CustomerId]

) AS [Project1]

) AS [Project1]

GROUP BY [K1]

) AS [GroupBy1] ) AS [Limit1]

image_thumb[12]

Comme vous pouvez le voir, cette requête est pire que la précédente. Donc contrairement à mon post d’hier, le group par une constant n’est pas une bonne idée ici.

On peut également essayer comme ceci :

from c in context.Customers
let orders = c.Orders
select new { Customer = c, OrdersCount = (int?)orders.Count ?? 0, LastOrderDate = (DateTime?)orders.Max(o => o.OrderDate) };

Mais le SQL généré n’est pas meilleur :

SELECT

[Project3].[CustomerId] AS [CustomerId],

[Project3].[LastName] AS [LastName],

[Project3].[FirstName] AS [FirstName],

[Project3].[BirthDay] AS [BirthDay],

[Project3].[AddressLine] AS [AddressLine],

[Project3].[City] AS [City],

[Project3].[PostalCode] AS [PostalCode],

[Project3].[Region] AS [Region],

[Project3].[Country] AS [Country],

CASE WHEN ([Project3].[C1] IS NULL) THEN 0 ELSE [Project3].[C2] END AS [C1],

CAST( [Project3].[C3] AS datetime2) AS [C2]

FROM ( SELECT

[Project2].[CustomerId] AS [CustomerId],

[Project2].[LastName] AS [LastName],

[Project2].[FirstName] AS [FirstName],

[Project2].[BirthDay] AS [BirthDay],

[Project2].[AddressLine] AS [AddressLine],

[Project2].[City] AS [City],

[Project2].[PostalCode] AS [PostalCode],

[Project2].[Region] AS [Region],

[Project2].[Country] AS [Country],

[Project2].[C1] AS [C1],

[Project2].[C2] AS [C2],

(SELECT

MAX([Extent4].[OrderDate]) AS [A1]

FROM [dbo].[Orders] AS [Extent4]

WHERE [Project2].[CustomerId] = [Extent4].[CustomerId]) AS [C3]

FROM ( SELECT

[Project1].[CustomerId] AS [CustomerId],

[Project1].[LastName] AS [LastName],

[Project1].[FirstName] AS [FirstName],

[Project1].[BirthDay] AS [BirthDay],

[Project1].[AddressLine] AS [AddressLine],

[Project1].[City] AS [City],

[Project1].[PostalCode] AS [PostalCode],

[Project1].[Region] AS [Region],

[Project1].[Country] AS [Country],

[Project1].[C1] AS [C1],

(SELECT

COUNT(1) AS [A1]

FROM [dbo].[Orders] AS [Extent3]

WHERE [Project1].[CustomerId] = [Extent3].[CustomerId]) AS [C2]

FROM ( SELECT

[Extent1].[CustomerId] AS [CustomerId],

[Extent1].[LastName] AS [LastName],

[Extent1].[FirstName] AS [FirstName],

[Extent1].[BirthDay] AS [BirthDay],

[Extent1].[AddressLine] AS [AddressLine],

[Extent1].[City] AS [City],

[Extent1].[PostalCode] AS [PostalCode],

[Extent1].[Region] AS [Region],

[Extent1].[Country] AS [Country],

(SELECT

COUNT(1) AS [A1]

FROM [dbo].[Orders] AS [Extent2]

WHERE [Extent1].[CustomerId] = [Extent2].[CustomerId]) AS [C1]

FROM [dbo].[Customers] AS [Extent1]

) AS [Project1]

) AS [Project2]

) AS [Project3]

Quant-au plan d’exécution, il pique les yeux :

image_thumb[16]

Ok j’arrête de jouer avec votre patience. Quelle est la meilleure requête ?

Je pense que c’est la suivante :

from c in context.Customers
from o in
    (from o2 in context.Orders
     where o2.CustomerId == c.CustomerId
     group o2 by o2.CustomerId into g
     select new { OrdersCount = g.Count(), LastOrderDate = (DateTime?)g.Max(o => o.OrderDate) }).DefaultIfEmpty()
select new { Customer = c, OrdersCount = (int?)o.OrdersCount ?? 0, o.LastOrderDate };

Avec celle-ci la requête SQL et le plan d’exécution reste simple et efficace :

SELECT

[Extent1].[CustomerId] AS [CustomerId],

[Extent1].[LastName] AS [LastName],

[Extent1].[FirstName] AS [FirstName],

[Extent1].[BirthDay] AS [BirthDay],

[Extent1].[AddressLine] AS [AddressLine],

[Extent1].[City] AS [City],

[Extent1].[PostalCode] AS [PostalCode],

[Extent1].[Region] AS [Region],

[Extent1].[Country] AS [Country],

CASE WHEN ([GroupBy1].[A1] IS NULL) THEN 0 ELSE [GroupBy1].[A1] END AS [C1],

CASE WHEN ([GroupBy1].[K1] IS NULL) THEN CAST(NULL AS datetime2) ELSE CAST( [GroupBy1].[A2] AS datetime2) END AS [C2]

FROM [dbo].[Customers] AS [Extent1]

OUTER APPLY (SELECT

[Extent2].[CustomerId] AS [K1],

COUNT(1) AS [A1],

MAX([Extent2].[OrderDate]) AS [A2]

FROM [dbo].[Orders] AS [Extent2]

WHERE [Extent2].[CustomerId] = [Extent1].[CustomerId]

GROUP BY [Extent2].[CustomerId] ) AS [GroupBy1]

image_thumb[18]

Notez le coût de la requête 11%, comme la première requête qui ne remontait pas les clients sans commande ! Ca rocks !

 

Pourquoi ais-je écris ce post ? Pourquoi ais-je montré le SQL généré ou les plans d’exécution liés ?

Je pense que c’est important. Contrairement au discours marketing, je ne pense pas qu’EF soit magique et facile à utiliser. Bien sûr, pour une base de démo avec 3 rows dans 3 tables, vous pouvez utiliser la requête que vous voulez mais pour une application de la vraie vie avec de vraies problématique de performance, ce n’est pas si simple… Attention, je continue de trouver EF fantastique et je continue d’adorer ça. Cependant, je pense qu’il est important de comprendre les impactes des requêtes L2E sur le SQL et ce n’est pas simple…

Avec ce post, je voulais montrer à quel point les plans d’exécution du SQL peuvent être différent pour un même résultat en fonction de la manière d’écrire la requête L2E.

Je réalise souvent des audits sur Entity Framework, particulièrement pour résoudre des problèmes de performance et je n’ai, à ce jour, jamais vu de vrais problèmes liés à EF. Les problèmes de performance avec EF que j’ai pu observer étaient tous liés à une méconnaissance d’EF et / ou une mauvaise conception de la base.

Posté le mercredi 15 juin 2011 00:48 par Matthieu MEZIL | 3 commentaire(s)

Comment effectuer plusieurs fonctions de grouping en une seule requête LINQ ?

Derrière la magie d’Entity Framework se cache la réalité du SQL. Je persiste et je signe, si vous ne connaissez pas le SQL, il sera très compliqué d’écrire des requêtes L2E optimales.

J’ai trouvé une bonne illustrations avec les fonctions de grouping.

Comment récupérer la dernière date de commande et le nombre de commandes ?

En SQL, on peut écrire la requête suivante :

SELECT COUNT(1), MAX(OrderDate)

FROM Orders

Mais comment faire la même chose en LINQ ?

Bien sûr, on pourrait utiliser deux requêtes mais il y a un point très important à prendre en compte avant de faire ça : la performance. En effet, le coût pour récupérer le count ou le max ou le count et le max est le même :

image_thumb[1]

Cela signifie que, même en ignorant le coût des deux connections à la base, avoir deux requêtes pour récupérer le nombre de commande et la date de la dernière commande est deux fois plus important que de récupérer ces informations en une seule requête !

Si l’on pense SQL, la fonction COUNT et MAX sont des fonctions de grouping.

On obtient le même plan d’exécution en utilisant un group by sur une constante :

SELECT COUNT(1), MAX(OrderDate)

FROM

(

SELECT OrderDate, 1 AS G

FROM Orders

) O

GROUP BY O.G

image_thumb[7]

Et ça on peut l’écrire en LINQ :

from o in context.Orders
group o by 1 into g
select new { Count = g.Count(), MaxOrderDate = g.Max(o => o.OrderDate) };

Posté le mardi 14 juin 2011 21:53 par Matthieu MEZIL | 0 commentaire(s)

Après-midi du développement sur la Task Parallel Library

J’aurai le plaisir de co-animer l’après-midi du développement sur la TPL jeudi prochain accompagné de David, Eric et Bruno.

Pour ma part, je me concentrerai sur des exemples de la vraie vie utilisant EF4.1 Code First et T4.

Posté le mercredi 8 juin 2011 21:07 par Matthieu MEZIL | 2 commentaire(s)

Classé sous : ,

Comment récupérer par code les WorkItems d’un ChangeSet dans un contexte de branche ?

Imaginons que l’on ait 3 branches : dev / integration / prod.

On va faire plusieurs check-ins dans la branche de dev qu’on va relier avec des WorkItems.

Ensuite, on va appliquer un certains nombre de ces changesets sur l’integration, ce qui va générer un nouveau changeset (sur la branche d’intégration).

On peut répéter plusieurs fois cette opération puis, on va appliquer tout ou partie de ces changesets de l’integration sur la branche de prod (ce qui va générer un nouveau changeset sur la branche de prod).

Maintenant, pour un changesetid donné, je veux récupérer tous les workitems qui lui sont liés directement ou indirectement (ie : liés aux changesets de la branche précédente qu’on a intégré).

Comment faire ceci ?

Après avoir un peu galéré à trouver la bonne méthode à utiliser dans les API TFS, j’ai écrit le code suivant :

public static IEnumerable<WorkItem> GetWorkItems(string serverURL, string collectionName, NetworkCredential networkCredential, int changeSetId, params string[] branches)
{
    TfsConfigurationServer tfsConfigurationServer = new TfsConfigurationServer(new Uri(serverURL), networkCredential);
    tfsConfigurationServer.Authenticate();
    return GetWorkItems(tfsConfigurationServer.GetTeamProjectCollection(new Guid(tfsConfigurationServer.CatalogNode.QueryChildren(new[] { CatalogResourceTypes.ProjectCollection },
false, CatalogQueryOptions.None).FirstOrDefault(cn => cn.Resource.DisplayName == collectionName).Resource.Properties["InstanceId"])).GetService<VersionControlServer>(),
changeSetId, branches).Distinct(); } private static IEnumerable<WorkItem> GetWorkItems(VersionControlServer versionControlServer, int changeSetId, IEnumerable<string> branches) {
    foreach (WorkItem wi in versionControlServer.GetChangeset(changeSetId).WorkItems)
        yield return wi;
    string[] subBranches = branches.Take(2).ToArray();
    if (subBranches.Length == 2)
        foreach (WorkItem wi in versionControlServer.QueryMerges(subBranches[1], VersionSpec.Latest, subBranches[0], VersionSpec.Latest, new ChangesetVersionSpec(changeSetId),
new ChangesetVersionSpec(changeSetId), RecursionType.Full).SelectMany(cm => GetWorkItems(versionControlServer, cm.SourceVersion, branches.Skip(1))))
            yield return wi; }

Enjoy Sourire

Posté le vendredi 27 mai 2011 12:12 par Matthieu MEZIL | 0 commentaire(s)

Classé sous : ,

EF CanDelete : comment savoir si vous avez des FK sur une entité?

Aujourd’hui, j’ai eu la chance d’être confronté à un problème intéressant.

Avant de tenter de supprimer une entité, mon client voulait checker s’il n’y avait pas de foreign key sur celle-ci.

Une première solution pour faire cela est ce que j’appelle la méthode “bourrin” : on charge les relations enfants de l’entité et on regarde s’il y en a.

Cette première solution fonctionne mais présente deux gros défauts :

  • le chargement potentiel de nombreuses entités inutiles
  • le fait que le développeur doit lui même préciser les relations enfants avec un impact important lors des mise à jour…

Pour palier ce deuxième point, il serait possible d’utiliser un T4, cependant la perspective de charger les entités pour rien est une très mauvaise pratique.

On pourrait bien sûr n’en charger qu’une par relation fille de façon à limiter l’impact mais, si je fais l’effort d’écrire ce post, c’est que je propose une meilleure solution.

 

Comment pourrait-on faire cela ?

La génération des T4 se base sur les metadata du modèle. Avec la classe ObjectContext, il est possible d’accéder aux metadata et c’est ce que je vais utiliser pour générer ma requête et ainsi ne récupérer qu’un booléen depuis la base.

EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
 
var navigationProperties = entityType.NavigationProperties.Where(enp =>
enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList(); if (navigationProperties.Count == 0)
return true;

Il y a trois façons de requêter avec Entity Framework :

  • LINQ To Entity
  • ESQL
  • Query Builder

Je vais vous présenter les 3 dans ce post.

 

Query Builder

Commençons par le plus facile pour notre cas : l’approche QueryBuilder.

L’idée de l’approche Query Builder consiste à intégrer des portions d’esql dans les méthodes de requêtage de la classe ObjectQuery.

string any = navigationProperties.Select(np =>
{ 
    if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
        return string.Format("EXISTS(SELECT 1 FROM it.{0})", np.Name);
    return string.Format("it.{0} <> null", np.Name); }).Aggregate((exp1, exp2) => string.Format("{0} OR {1}", exp1, exp2)); List<ObjectParameter> objectParameters = new List<ObjectParameter>(); string where = entityType.KeyMembers.Select(km => {
    PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
    objectParameters.Add(new ObjectParameter(km.Name, propertyInfo.GetValue(entity, null)));
    return string.Format("it.{0} == @{0}", km.Name); }).Aggregate((k1, k2) => string.Format("{0} AND {1}", k1, k2)); return !context.CreateObjectSet<TEntity>().Where(any).Where(where, objectParameters.ToArray()).SelectValue<bool>("true").FirstOrDefault();

ESQL

Il est aussi possible de n’utiliser qu’une chaine de caractères en ESQL.

string any = navigationProperties.Select(np =>
{ 
    if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
        return string.Format("EXISTS(SELECT 1 FROM it.{0})", np.Name);
    return string.Format("it.{0} <> null", np.Name); }).Aggregate((exp1, exp2) => string.Format("{0} OR {1}", exp1, exp2)); List<ObjectParameter> objectParameters = new List<ObjectParameter>(); string where = entityType.KeyMembers.Select(km => {
    PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
    objectParameters.Add(new ObjectParameter(km.Name, propertyInfo.GetValue(entity, null)));
    return string.Format("it.{0} == @{0}", km.Name); }).Aggregate((k1, k2) => string.Format("{0} AND {1}", k1, k2)); DbDataRecord value = context.CreateQuery<DbDataRecord>(string.Format("SELECT false FROM {0}.{1} AS it WHERE {2} AND ({3})", entityContainer.Name, entitySet.Name, where, any), objectParameters.ToArray()).FirstOrDefault(); if (value == null)
    return true; return value.GetBoolean(0);

L2E

Il est également possible de générer son Expression et utiliser LINQ To Entities.

ParameterExpression expressionAnyParameter = Expression.Parameter(typeof(TEntity));
 
Expression expressionAnyBody = navigationProperties.Select(np =>
{ 
    PropertyInfo propertyInfo = typeof(TEntity).GetProperty(np.Name);
    if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
    {
        return (Expression)Expression.Call(    
            null,    
            typeof(Enumerable).GetMethods().First(m => m.Name == "Any" && m.GetParameters().Length == 1).MakeGenericMethod(propertyInfo.PropertyType.GetGenericArguments()[0]),    
            Expression.MakeMemberAccess(    
                expressionAnyParameter,    
                propertyInfo));
    }
    return (Expression)Expression.NotEqual(
        Expression.MakeMemberAccess(
            expressionAnyParameter,
            propertyInfo),
        Expression.Constant(
            null,
            typeof(object))); }).Aggregate((exp1, exp2) => Expression.OrElse(exp1, exp2)); Expression<Func<TEntity, bool>> expressionAny = Expression.Lambda<Func<TEntity, bool>>(expressionAnyBody, expressionAnyParameter); ParameterExpression expressionWhereParameter = Expression.Parameter(typeof(TEntity)); Expression expressionWhereBody = entityType.KeyMembers.Select(km => {
    Expression<Func<TEntity>> exp = () => entity;
    PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
    ParameterExpression expressionWhereKeyParameter = Expression.Parameter(propertyInfo.PropertyType);
    return Expression.Equal(
        Expression.MakeMemberAccess(
            expressionWhereParameter,
            propertyInfo),
     Expression.MakeMemberAccess(
        exp.Body,
         propertyInfo)); }).Aggregate((k1, k2) => Expression.AndAlso(k1, k2)); Expression<Func<TEntity, bool>> expressionWhere = Expression.Lambda<Func<TEntity, bool>>(expressionWhereBody, expressionWhereParameter); return ! context.CreateObjectSet<TEntity>().Where(expressionWhere).Where(expressionAny).Select(e => true).FirstOrDefault();

Revenons un peu sur la génération de l’Expression.

Si la génération de l’expressionAny est “classique”, la deuxième (l’expressionWhere) est plus intéressante.

En effet, afin d’éviter tout risque d’injection SQL, je ne veux pas comparer les clés de mon entité avec des constantes mais avec une variable de façon à obtenir une requête SQL paramétrée.

L’autre point intéressant est la variable exp. Pourquoi ais-je besoin de cette variable ?

Pour répondre à cette question, il faut comprendre le fonctionnement des lambdas.

Dans une lambda expression ou un délégué anonyme, il est possible d’utiliser une variable de la méthode parente. Ceci est réellement géniale et super pratique mais comment cela fonctionne-t-il ?

En réalité, dans ce cas, le compilateur crée une classe avec un champ public qui sera utilisé par la méthode parente et la lambda.

Le problème ici, c’est que je n’ai pas accès à cette classe puisque celle-ci est générée par le compilateur. D’où l’idée de récupérer l’expression permettant d’accéder à ce champ à travers la variable exp.

 

C’est cool, ça marche. Cependant, on peut apporter quelques modifications.

Tout d’abord, ces différentes versions ne fonctionnent pas avec l’héritage. En effet, nous pouvons alors avoir un décalage entre l’EntityType et l’EntitySet.

Pour résoudre ce problème sans passer par la Reflection, nous allons utiliser l’approche ESQL pour récupérer l’ObjectQuery<TEntity>.

ESQL

EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
EntityContainer entityContainer = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityContainer>().First();
EntitySet entitySet = null;
EntityType entityTypeLoop = entityType;
while (entityTypeLoop != null && (entitySet = entityContainer.BaseEntitySets.OfType<EntitySet>().FirstOrDefault(es => es.ElementType == entityTypeLoop)) == null)     
entityTypeLoop = (EntityType)entityTypeLoop.BaseType; if (entitySet == null)    
throw new InvalidOperationException();
var navigationProperties = entityType.NavigationProperties.Where(enp =>     
enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne || 
    enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList(); if (navigationProperties.Count == 0)
    return true; string any = navigationProperties.Select(np => {
    if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
        return string.Format("EXISTS(SELECT 1 FROM it.{0})", np.Name);
    return string.Format("it.{0} <> null", np.Name); }).Aggregate((exp1, exp2) => string.Format("{0} OR {1}", exp1, exp2)); List<ObjectParameter> objectParameters = new List<ObjectParameter>(); string where = entityType.KeyMembers.Select(km => {
    PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
    objectParameters.Add(new ObjectParameter(km.Name, propertyInfo.GetValue(entity, null)));
    return string.Format("it.{0} == @{0}", km.Name); }).Aggregate((k1, k2) => string.Format("{0} AND {1}", k1, k2)); DbDataRecord value = context.CreateQuery<DbDataRecord>(string.Format("SELECT false FROM OFTYPE({0}.{1}, {2}) AS it WHERE {3} AND ({4})", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName, where, any),
objectParameters.ToArray()).FirstOrDefault();
if (value == null)
     return true; return value.GetBoolean(0);

Il est aussi possible d’utiliser ESQL pour générer l’ObjectQuery<TEntity> puis utiliser un autre mode de requêtage.

Query Builder

return !context.CreateQuery<TEntity>(string.Format("SELECT VALUE entity FROM OFTYPE({0}.{1}, {2}) AS entity", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName)).
Where(any).Where(where, objectParameters.ToArray()).SelectValue<bool>("true").FirstOrDefault();

Notez le VALUE dans la requête ESQL signifiant que l’on veut récupérer l’entité complète et non pas des colonnes.

L2E

return !context.CreateQuery<TEntity>(string.Format("SELECT VALUE entity FROM OFTYPE({0}.{1}, {2}) AS entity", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName)).
Where(expressionWhere).Where(expressionAny).Select(e => true).FirstOrDefault();

Une autre piste d’amélioration consisterait à mettre en cache la génération de la requête. Cela est plus ou moins facile en fonction du mode de requêtage.

En effet, la génération de cette requête entre deux entités de même type est la même (aux paramètres prêt).

Ma première idée consiste à me créer une classe statique CanDeleteImplementation<TEntity>. Ce qui est très important ici c’est le fait d’utiliser une classe générique. Ainsi les champs statiques seront différents d’une entité à l’autre.

ESQL

public static class CanDeleteImplementation<TEntity> 
    where TEntity : class {
    private static string _query;
    private static List<Func<TEntity, ObjectParameter>> _parametersFactories;
    public static bool CanDelete(ObjectContext context, TEntity entity)
    {
        if (_query == null)
        {
            _parametersFactories = new List<Func<TEntity, ObjectParameter>>();
            EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
            EntityContainer entityContainer = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityContainer>().First();
            EntitySet entitySet = null;
            EntityType entityTypeLoop = entityType;
            while (entityTypeLoop != null && (entitySet = entityContainer.BaseEntitySets.OfType<EntitySet>().FirstOrDefault(es => es.ElementType == entityTypeLoop)) == null)
                entityTypeLoop = (EntityType)entityTypeLoop.BaseType;
            if (entitySet == null)
                throw new InvalidOperationException();
            var navigationProperties = entityType.NavigationProperties.Where(enp =>
                enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
                enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList();
            if (navigationProperties.Count == 0)
                return true;
            ParameterExpression expressionAnyParameter = Expression.Parameter(typeof(TEntity));
            string any = navigationProperties.Select(np =>
            {
                if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                    return string.Format("EXISTS(SELECT 1 FROM it.{0})", np.Name);
                return string.Format("it.{0} <> null", np.Name);
            }).Aggregate((exp1, exp2) => string.Format("{0} OR {1}", exp1, exp2));
            string where = entityType.KeyMembers.Select(km =>
            {
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
                ParameterExpression parameterFactoryParameterExpression = Expression.Parameter(typeof(TEntity));
                _parametersFactories.Add(Expression.Lambda<Func<TEntity, ObjectParameter>>(Expression.New(
                    typeof(ObjectParameter).GetConstructor(new Type[] { typeof(string), typeof(object) }),
                    Expression.Constant(km.Name),
                    Expression.Convert(
                        Expression.MakeMemberAccess(
                            parameterFactoryParameterExpression,
                            typeof(TEntity).GetProperty(km.Name)),
                        typeof(Object))), parameterFactoryParameterExpression).Compile());
                return string.Format("it.{0} == @{0}", km.Name);
            }).Aggregate((k1, k2) => string.Format("{0} AND {1}", k1, k2));
            _query = string.Format("SELECT false FROM OFTYPE({0}.{1}, {2}) AS it WHERE {3} AND ({4})", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName, where, any);
        }         DbDataRecord value = context.CreateQuery<DbDataRecord>(_query, _parametersFactories.Select(p => p(entity)).ToArray()).FirstOrDefault(); 
        if (value == null)
            return true;
        return value.GetBoolean(0);
    } }

Query Builder

La même chose en Query Builder :

public static class CanDeleteImplementation<TEntity> 
    where TEntity : class {
    private static string _query;
    private static string _any;
    private static string _where;
    private static List<Func<TEntity, ObjectParameter>> _parametersFactories;
    public static bool CanDelete(ObjectContext context, TEntity entity)
    {
        if (_query == null)
        {
            _parametersFactories = new List<Func<TEntity, ObjectParameter>>();
            EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
            EntityContainer entityContainer = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityContainer>().First();
            EntitySet entitySet = null;
            EntityType entityTypeLoop = entityType;
            while (entityTypeLoop != null && (entitySet = entityContainer.BaseEntitySets.OfType<EntitySet>().FirstOrDefault(es => es.ElementType == entityTypeLoop)) == null)
                entityTypeLoop = (EntityType)entityTypeLoop.BaseType;
            if (entitySet == null)
                throw new InvalidOperationException();
            var navigationProperties = entityType.NavigationProperties.Where(enp =>
                enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
                enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList();
            if (navigationProperties.Count == 0)
                return true;
            ParameterExpression expressionAnyParameter = Expression.Parameter(typeof(TEntity));
            _any = navigationProperties.Select(np =>
            {
                if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                    return string.Format("EXISTS(SELECT 1 FROM it.{0})", np.Name);
                return string.Format("it.{0} <> null", np.Name);
            }).Aggregate((exp1, exp2) => string.Format("{0} OR {1}", exp1, exp2));
            _where = entityType.KeyMembers.Select(km =>
            {
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
                ParameterExpression parameterFactoryParameterExpression = Expression.Parameter(typeof(TEntity));
                _parametersFactories.Add(Expression.Lambda<Func<TEntity, ObjectParameter>>(Expression.New(
                    typeof(ObjectParameter).GetConstructor(new Type[] { typeof(string), typeof(object) }),
                    Expression.Constant(km.Name),
                    Expression.Convert(
                        Expression.MakeMemberAccess(
                            parameterFactoryParameterExpression,
                            typeof(TEntity).GetProperty(km.Name)),
                        typeof(Object))), parameterFactoryParameterExpression).Compile());
                return string.Format("it.{0} == @{0}", km.Name);
            }).Aggregate((k1, k2) => string.Format("{0} AND {1}", k1, k2));
            _query = string.Format("SELECT VALUE entity FROM OFTYPE({0}.{1}, {2}) AS entity", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName);
        }         return ! context.CreateQuery<TEntity>(_query).Where(_where, _parametersFactories.Select(p => p(entity)).ToArray()).Where(_any).SelectValue<bool>("true").FirstOrDefault();
    } }

L2E

Avec la génération d’expression, cela s’avère beaucoup plus délicat. En effet, on n’a pas accès aux paramètres de l’Expression.

On pourrait naïvement penser que le code suivant fonctionne

public static class CanDeleteImplementation<TEntity> 
    where TEntity : class {
    private static string _query;
    private static Expression<Func<TEntity, bool>> _expressionAny;
    private static Expression<Func<TEntity, bool>> _expressionWhere;
    public static bool CanDelete(ObjectContext context, TEntity entity)
    {
        if (_query == null)
        {
            EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
            EntityContainer entityContainer = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityContainer>().First();
            EntitySet entitySet = null;
            EntityType entityTypeLoop = entityType;
            while (entityTypeLoop != null && (entitySet = entityContainer.BaseEntitySets.OfType<EntitySet>().FirstOrDefault(es => es.ElementType == entityTypeLoop)) == null)
                entityTypeLoop = (EntityType)entityTypeLoop.BaseType;
            if (entitySet == null)
                throw new InvalidOperationException();
            var navigationProperties = entityType.NavigationProperties.Where(enp =>
                enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList();
            if (navigationProperties.Count == 0)
                return true;
            ParameterExpression expressionAnyParameter = Expression.Parameter(typeof(TEntity));
            Expression expressionAnyBody = navigationProperties.Select(np =>
            {
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(np.Name);
                if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                {
                    return (Expression)Expression.Call(
                        null,
                        typeof(Enumerable).GetMethods().First(m => m.Name == "Any" && m.GetParameters().Length == 1).MakeGenericMethod(propertyInfo.PropertyType.GetGenericArguments()[0]),
                        Expression.MakeMemberAccess(
                            expressionAnyParameter,
                            propertyInfo));
                }
                return (Expression)Expression.NotEqual(
                        Expression.MakeMemberAccess(
                            expressionAnyParameter,
                            propertyInfo),
                        Expression.Constant(
                            null,
                            typeof(object)));
            }).Aggregate((exp1, exp2) => Expression.OrElse(exp1, exp2));
            _expressionAny = Expression.Lambda<Func<TEntity, bool>>(expressionAnyBody, expressionAnyParameter);
            ParameterExpression expressionWhereParameter = Expression.Parameter(typeof(TEntity));
            Expression expressionWhereBody = entityType.KeyMembers.Select(km =>
            {
                Expression<Func<TEntity>> exp = () => entity;
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
                ParameterExpression expressionWhereKeyParameter = Expression.Parameter(propertyInfo.PropertyType);
                return Expression.Equal(
                    Expression.MakeMemberAccess(
                        expressionWhereParameter,
                        propertyInfo),
                Expression.MakeMemberAccess(
                    exp.Body,
                    propertyInfo));
            }).Aggregate((k1, k2) => Expression.AndAlso(k1, k2));
            _expressionWhere = Expression.Lambda<Func<TEntity, bool>>(expressionWhereBody, expressionWhereParameter);
            _query = string.Format("SELECT VALUE entity FROM OFTYPE({0}.{1}, {2}) AS entity", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName);
        }
        return !context.CreateQuery<TEntity>(_query).Where(_expressionWhere).Where(_expressionAny).Select(e => true).FirstOrDefault();
    } }

Mais que nenni, ce code ne fonctionne pas. Il renverra toujours la même valeur (celle pour la première entité passée en paramètre).

Autre problématique, les Expressions sont immutables.

On pourrait regénérer la whereExpression à chaque fois.

On peut aussi passer par un champ statique qui référencerait le paramètre entity.

public static class CanDeleteImplementation<TEntity> 
    where TEntity : class {
    private static string _query;
    private static Expression<Func<TEntity, bool>> _expressionAny;
    private static Expression<Func<TEntity, bool>> _expressionWhere;
    private static TEntity _entity;
    public static bool CanDelete(ObjectContext context, TEntity entity)
    {
        if (_query == null)
        {
            EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
            EntityContainer entityContainer = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityContainer>().First();
            EntitySet entitySet = null;
            EntityType entityTypeLoop = entityType;
            while (entityTypeLoop != null && (entitySet = entityContainer.BaseEntitySets.OfType<EntitySet>().FirstOrDefault(es => es.ElementType == entityTypeLoop)) == null)
                entityTypeLoop = (EntityType)entityTypeLoop.BaseType;
            if (entitySet == null)
                throw new InvalidOperationException();
            var navigationProperties = entityType.NavigationProperties.Where(enp =>
                enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList();
            if (navigationProperties.Count == 0)
                return true;
            ParameterExpression expressionAnyParameter = Expression.Parameter(typeof(TEntity));
            Expression expressionAnyBody = navigationProperties.Select(np =>
            {
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(np.Name);
                if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                {
                    return (Expression)Expression.Call(
                        null,
                        typeof(Enumerable).GetMethods().First(m => m.Name == "Any" && m.GetParameters().Length == 1).MakeGenericMethod(propertyInfo.PropertyType.GetGenericArguments()[0]),
                        Expression.MakeMemberAccess(
                            expressionAnyParameter,
                            propertyInfo));
            }
                return (Expression)Expression.NotEqual(
                        Expression.MakeMemberAccess(
                            expressionAnyParameter,
                            propertyInfo),
                        Expression.Constant(
                            null,
                            typeof(object)));
            }).Aggregate((exp1, exp2) => Expression.OrElse(exp1, exp2));
            _expressionAny = Expression.Lambda<Func<TEntity, bool>>(expressionAnyBody, expressionAnyParameter);
            ParameterExpression expressionWhereParameter = Expression.Parameter(typeof(TEntity));
            Expression expressionWhereBody = entityType.KeyMembers.Select(km =>
            {
                Expression<Func<TEntity>> exp = () => _entity;
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
                ParameterExpression expressionWhereKeyParameter = Expression.Parameter(propertyInfo.PropertyType);
                return Expression.Equal(
                    Expression.MakeMemberAccess(
                        expressionWhereParameter,
                        propertyInfo),
                Expression.MakeMemberAccess(
                    exp.Body,
                    propertyInfo));
            }).Aggregate((k1, k2) => Expression.AndAlso(k1, k2));
            _expressionWhere = Expression.Lambda<Func<TEntity, bool>>(expressionWhereBody, expressionWhereParameter);
            _query = string.Format("SELECT VALUE entity FROM OFTYPE({0}.{1}, {2}) AS entity", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName);
        }
        _entity = entity;
        return !context.CreateQuery<TEntity>(_query).Where(_expressionWhere).Where(_expressionAny).Select(e => true).FirstOrDefault();
    } }

Cependant, attention, ce code n’est pas thread-safe!!!

Il faudrait pour cela poser un lock entre les deux dernières lignes de la méthode.

Cependant, poser un lock qui en plus peut durer longtemps (le temps de la requête SQL) ne me paraît pas être une bonne solution.

Il apparaît donc nécessaire de regénérer la whereExpression. Cependant, même avec cette approche, il est possible de mettre en cache le processus de génération de l’expression.

public static class CanDeleteImplementation<TEntity> 
where TEntity : class {
    private static string _query;
    private static Expression<Func<TEntity, bool>> _expressionAny;
    private static Func<TEntity, Expression<Func<TEntity, bool>>> _getExpressionWhere;


   public static bool CanDelete(ObjectContext context, TEntity entity)
    {   
      if (_query == null)
        {
            EntityType entityType = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityType>().First(et => et.Name == typeof(TEntity).Name);
            EntityContainer entityContainer = context.MetadataWorkspace.GetItems(DataSpace.CSpace).OfType<EntityContainer>().First();
            EntitySet entitySet = null;
            EntityType entityTypeLoop = entityType; 
            while (entityTypeLoop != null && (entitySet = entityContainer.BaseEntitySets.OfType<EntitySet>().FirstOrDefault(es => es.ElementType == entityTypeLoop)) == null)
                entityTypeLoop = (EntityType)entityTypeLoop.BaseType;
            if (entitySet == null
                throw new InvalidOperationException();
            var navigationProperties = entityType.NavigationProperties.Where(enp =>
                enp.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One && enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne ||
                enp.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many).ToList();
            if (navigationProperties.Count == 0) 
                return true; ParameterExpression expressionAnyParameter = Expression.Parameter(typeof(TEntity));
            Expression expressionAnyBody = navigationProperties.Select(np =>
            {
                PropertyInfo propertyInfo = typeof(TEntity).GetProperty(np.Name);
                if (np.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
                {
                    return (Expression)Expression.Call(
                        null,
                        typeof(Enumerable).GetMethods().First(m => m.Name == "Any" && m.GetParameters().Length == 1).MakeGenericMethod(propertyInfo.PropertyType.GetGenericArguments()[0]),
                        Expression.MakeMemberAccess(expressionAnyParameter, propertyInfo));
                }
                return (Expression)Expression.NotEqual(
                    Expression.MakeMemberAccess(
                        expressionAnyParameter,
                        propertyInfo), 
                    Expression.Constant( 
                    null,
                    typeof(object)));
            }).Aggregate((exp1, exp2) => Expression.OrElse(exp1, exp2));
            _expressionAny = Expression.Lambda<Func<TEntity, bool>>(expressionAnyBody, expressionAnyParameter);             ParameterExpression entityParameter = Expression.Parameter(typeof(TEntity));
            ParameterExpression expressionWhereParameter = Expression.Parameter(typeof(TEntity)); 
            _getExpressionWhere = Expression.Lambda<Func<TEntity, Expression<Func<TEntity, bool>>>>(
                Expression.Lambda<Func<TEntity, bool>>(
                 entityType.KeyMembers.Select(km =>
                    {
                        PropertyInfo propertyInfo = typeof(TEntity).GetProperty(km.Name);
                        ParameterExpression expressionWhereKeyParameter = Expression.Parameter(propertyInfo.PropertyType);
                        return Expression.Equal(
                            Expression.MakeMemberAccess(
                                expressionWhereParameter,
                                propertyInfo),
                            Expression.MakeMemberAccess(
                                entityParameter,
                                propertyInfo));
                    }).Aggregate((k1, k2) => Expression.AndAlso(k1, k2)),
                    expressionWhereParameter),
                entityParameter).Compile();
            _query = string.Format("SELECT VALUE entity FROM OFTYPE({0}.{1}, {2}) AS entity", entityContainer.Name, entitySet.Name, typeof(TEntity).FullName);
        }
        return !context.CreateQuery<TEntity>(_query).Where(_getExpressionWhere(entity)).Where(_expressionAny).Select(e => true).FirstOrDefault(); 
    } }

En conclusion, bien qu’on ait tendance à l’oublier, LINQ To Entities n’est pas toujours le meilleur moyen de requêter des entités, surtout quand il s’agit de générer les requêtes à la volée.

Dans le cas présent, l’approche ESQL ou Query Builder me paraît beaucoup plus adaptée.

Posté le samedi 21 mai 2011 02:59 par Matthieu MEZIL | 4 commentaire(s)

Classé sous : , ,

L’après-midi du dev EF

Les webcasts de l’après-midi du dev sur EF que j’avais animé avec Michel sont en ligne sur http://msdn.microsoft.com/fr-fr/netframework/hh134069.aspx

Posté le mercredi 18 mai 2011 21:49 par Matthieu MEZIL | 0 commentaire(s)

Et si on parlait un peu de T4

Julien Dollon m’a récemment invité dans la webtv supinfo pour parler de T4 dans le cadre d’une série d’émission sur .NET.

Vous pouvez dès à présent visualiser la video ici.

Posté le mardi 3 mai 2011 11:19 par Matthieu MEZIL | 1 commentaire(s)

Session EF TechDays Genève

J’ai animé la semaine dernière une session sur Entity Framework ciblée DBA avec Christian Robert.

Vous pouvez dès à présent revoir cette session sur channel 9.

Posté le mercredi 13 avril 2011 00:39 par Matthieu MEZIL | 0 commentaire(s)

Plus de Messages Page suivante »


Les 10 derniers blogs postés

- Les actualités de la semaine sur c2i.fr (14 mai - 20 mai) par Richard Clark le il y a 5 heures et 0 minutes

- Reactive Extensions : Consommer des services avec Rx Partie 3, les pièges à éviter par Léonard Labat le il y a 14 heures et 5 minutes

- SharePoint Blog Site, problème d’archives par Le Blog (Vert) d'Arnaud JUND le 05-20-2012, 13:09

- Soirée ALT.NET Mai - 3 présentations par #Rui le 05-18-2012, 11:59

- [ #SharePoint 2010][ #SQLServer 2012] AlwaysOn pour SharePoint (2/4) : Configuration (2e partie)… par Le blog de Patrick [MVP SharePoint] le 05-18-2012, 11:31

- Team Foundation Server 11: tous les trésors cachés du site d’équipe par Philess le 05-16-2012, 19:01

- [PowerShell 3] Télécharger et installer la documentation en ligne par Blog de SPBrouillet (Pierrick BROUILLET) le 05-16-2012, 17:36

- [#SharePoint 2010][#SQLServer 2012] AlwaysOn pour SharePoint (1/4) : Configuration (1ère partie)… par Le blog de Patrick [MVP SharePoint] le 05-16-2012, 12:10

- Job Day @MIC Brussels - .Net Developers on Mobile applications par Le Blog (Vert) d'Arnaud JUND le 05-15-2012, 20:26

- [SharePoint 2010] – SharePoint 2010, Windows (Server) 8 et des erreurs IIS sont dans une VM… par Blog de SPBrouillet (Pierrick BROUILLET) le 05-14-2012, 12:10