samedi 26 mars 2011

Intégrer un composant Silverlight en WPF

Intégration

Il est parfois nécessaire d’intégrer un composant Silverlight dans une application WPF, étant donné que certains contrôles Silverlight n’existent pas en WPF. La méthode la plus courante consiste à placer un WebBrowser dans l’application WPF, et d’utiliser la méthode Navigate en indiquant l’URL d’une page contenant le composant Silverlight à intégrer. Ainsi, dans le XAML côté WPF :

<WebBrowser Name="wb"  />



Dans le constructeur correspondant côté .xaml.cs :

public MainWindow()
{
InitializeComponent();
wb.Navigate("http://aaa.bbb.com/toto.aspx");
}



Le résultat est visuellement excellent et remarquablement fluide. En plus de l’unification des développements (Xaml + C#), on dispose donc d’un moyen très simple et efficace pour tirer profit de développements réalisés en Silverlight a priori incompatibles avec WPF sans recompilation du code source.


Une fois l’intégration réalisée, le problème qui se pose immédiatement est celui de la communication entre le composant Silverlight et le reste de l’application WPF. Certains articles expliquent comment réaliser cette communication en passant par le document HTML, seul point de liaison disponible depuis l’application Silverlight et le WebBrowser WPF. Néanmoins, le moins que l’on puisse dire est que cette méthode n’est pas maintenable et qu’elle réduit l’évolutivité de l’architecture. Un véritable bricolage.


Une méthode de communication élégante


Pour faire communiquer l’application WPF avec le composant Silverlight, le mieux est encore d’utiliser la technologie prévue à cet effet : WCF. Plus précisément, il s’agit de concevoir un service WCF hébergé sur le serveur Web de l’application hébergeant le composant Silverlight. L’application WPF doit pouvoir envoyer des informations au composant Silverlight (et inversement), en passant systématiquement par le service WCF. Par conséquent, l’application WPF et l’application Silverlight doivent pouvoir :



  • Envoyer des messages au service WCF
  • Recevoir des messages provenant du service WCF.

Le service WCF agit comme un relai et doit pouvoir :



  • Recevoir des messages de l’application WPF et les relayer à l’application Silverlight
  • Recevoir des messages de l’application Silverlight et les relayer à l’application WPF.

Le service WCF a donc besoin de pouvoir recevoir ET envoyer des messages : un CallbackContract va être utilisé. D’autre part, les communications bidirectionnelles à permettre correspondent à un binding pollingDuplexHttpBinding avec Silverlight et à un binding wsDualHttpBinding avec WPF. Pour pouvoir appliquer ces 2 binding au service WCF, il faut les endpoints correspondants :

<services>
<service name="WpfSilverlightCommunication.CommunicationService">
<endpoint
address="silverlight"
binding="pollingDuplexHttpBinding"
bindingConfiguration="multipleMessagesPerPollPollingDuplexHttpBinding"
contract="WpfSilverlightCommunication.ICommunicationService">
</endpoint>
<endpoint
address="wpf"
binding="wsDualHttpBinding"
contract="WpfSilverlightCommunication.ICommunicationService">
</endpoint>
<endpoint
address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange"/>
</service>
</services>



Le reste de la configuration WCF (system.serviceModel) peut être configuré de façon classique :

<extensions>
<bindingExtensions>
<add name="pollingDuplexHttpBinding"
type="System.ServiceModel.Configuration.PollingDuplexHttpBindingCollectionElement,System.ServiceModel.PollingDuplex, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</bindingExtensions>
</extensions>
<bindings>
<wsDualHttpBinding>
<binding sendTimeout="00:00:05" />
</wsDualHttpBinding>
<pollingDuplexHttpBinding>
<binding name="multipleMessagesPerPollPollingDuplexHttpBinding"
duplexMode="SingleMessagePerPoll" sendTimeout="00:00:05"/>
</pollingDuplexHttpBinding>
</bindings>



Ainsi, l’application Silverlight pourra accéder au service WCF par l’adresse <adresseService>.svc/silverlight et l’application WPF par l’adresse <adresseService>.svc/wpf. Le service WCF fonctionne de la façon suivante :



  • Le mode d’instance est InstanceContextMode.Single, parce que l’application WPF et l’application Silverlight ne se connectent pas avec le même identifiant de session. Par conséquent, avec la configuration par défaut (PerSession), faire communiquer les 2 instances du service serait plus compliqué.
  • Le service contient une méthode Ping qui enregistre en premier lieu les canaux de communication vers l’application WPF et vers l’application Silverlight.
  • Chaque couple WPF/Silverlight est identifié par un Guid que partagent les 2 applications.

Le code du contrat de service WCF est le suivant :

[ServiceContract(CallbackContract=typeof(ICommunicationServiceHandler))]
public interface ICommunicationService
{
[OperationContract]
bool Ping(string id);

[OperationContract(IsOneWay=true)]
void Order(string appId, List<string> values);
}

[ServiceContract]
public interface ICommunicationServiceHandler
{
[OperationContract(IsOneWay = true)]
void Receive(List<string> values);
}



Et le service :

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single, ConcurrencyMode=ConcurrencyMode.Multiple)]
public class CommunicationService : ICommunicationService
{
Dictionary<string, ICommunicationServiceHandler> silverlightAppChannels = new Dictionary<string, ICommunicationServiceHandler>();
Dictionary<string, ICommunicationServiceHandler> wpfAppChannels = new Dictionary<string, ICommunicationServiceHandler>();

public void Order(string appId, List<string> values)
{
string endpointExtension = OperationContext.Current.EndpointDispatcher.EndpointAddress.Uri.Segments.Last();
if (silverlightAppChannels[appId] != null && wpfAppChannels[appId] != null)
{
switch (endpointExtension)
{
case "silverlight":
wpfAppChannels[appId].Receive(parcelleIds);
break;
case "wpf":
silverlightAppChannels[appId].Receive(parcelleIds);
break;
default:
break;
}
}
Ping(appId);
}

public bool Ping(string id)
{
string endpointExtension = OperationContext.Current.EndpointDispatcher.EndpointAddress.Uri.Segments.Last();
switch (endpointExtension)
{
case "silverlight":
silverlightAppChannels[id] = OperationContext.Current.GetCallbackChannel<ICommunicationServiceHandler>();
break;
case "wpf":
wpfAppChannels[id] = OperationContext.Current.GetCallbackChannel<ICommunicationServiceHandler>();
break;
default:
break;
}
return true;
}
}



La méthode principale, Order, effectue le relai en appelant la méthode Receive de l’autre canal de communication précédemment enregistré par la méthode Ping.


Pour faire en sorte que l’application WPF et l’application Silverlight partagent le même Guid, l’application WPF peut le créer et le transmettre en paramètre d’URL :

private static Guid appId = Guid.NewGuid();

public MainWindow()
{
InitializeComponent();
wb.Navigate("<adressePage>?guid=" + appId.ToString());
}



Pour le reste, l’application WPF agit de façon tout à fait classique comme client de l’endpoint avec le binding wsDualHttpBinding : après ajout de la référence de service, la fenêtre WPF peut implémenter le contrat de callback WCF et contenir une instance du proxy. Il faut simplement penser à appeler une fois la méthode Ping en passant le Guid au chargement de la fenêtre.


De même, l’application Silverlight agit comme un client tout à fait classique de l’endpoint ayant le binding PollingDuplexHttpBinding. Pour récupérer le Guid initialisé par l’application WPF, l’application Silverlight peut procéder de la manière suivante, par exemple pour l’appel initial à Ping :

proxy.PingAsync(HtmlPage.Document.QueryString["guid"]);



Conclusion


Une méthode originale et élégante a été conçue pour faire communiquer une application WPF avec une application Silverlight qui serait rendue dans un WebBrowser. On pourrait facilement étendre cette méthode selon plusieurs axes :



  • Réalisation d’autres types de relais, de type hub (broadcast) ou routeur par exemple.
  • Communication entre une application Winforms et un composant Silverlight avec le composant WebBrowser de WinForms.
  • Synchronisation des 2 applications qui pourraient s’échanger périodiquement leurs données.
  • Introduction d’une queue de messages et maintien des messages en attente dans le service WCF, en cas de perte de connexion.

mardi 22 mars 2011

Suivre l’activité d’une classe

Dans de nombreux cas, il peut être intéressant de suivre l’activité d’une classe quelconque, en particulier pour logger tous les appels à toutes ses méthodes. Quelques exemples :

  • Suivre l’activité d’un service Web.
  • Enregistrer tous les appels à une couche d’accès aux données.
  • Enregistrer tous les appels sur une classe réalisant un accès à une ressource externe, comme une classe réalisant les accès au disque, à un FTP, une classe encapsulant l’envoi de mails, etc.

A priori, il n’existe aucun mécanisme permettant de logger automatiquement tous les appels à une classe quelconque, de préférence sans modifier le code appelant ni le code appelé. Parmi les 4 sections suivantes, les 3 premières présentent des méthodes approchantes. La dernière présente une méthode originale basée sur les Code Contracts permettant d’atteindre le résultat recherché.

IntelliTrace & Rapports de performance

Il s’agit des méthodes les plus génériques. Elles permettent de tout savoir sur l’exécution d’un programme, mais sont limitées à la production de données de trace. Il ne sera donc pas possible de configurer totalement les données produites. De plus, seule la version Ultimate de Visual Studio 2010 possèdent ces fonctionnalités.

Concernant l’utilisation d’IntelliTrace, il est nécessaire d’activer l’option Evènements IntelliTrace et information sur les appels pour obtenir des informations sur les appels de méthodes :

image

Avec le bout de code suivant en exemple :

static void Main(string[] args)
{
Toto toto = new Toto();
toto.Operation();
}



La fenêtre IntelliTrace affiche à l’exécution :


image


On a bien une trace de tous les appels à la classe Toto. Bien entendu, dans le cas d’une application professionnelle complexe, le nombre d’appels devient tel que cette méthode n’est pas envisageable.


Concernant l’utilisation des rapports de performance, il suffit de lancer l’assistant performance :


image


Et de choisir Instrumentation :


image


Après exécution du programme, le profileur produit un fichier .vsp qui présente un grand nombre d’informations statistiques sur l’exécution. Par exemple, en basculant en mode d’affichage Fonctions, on voit entre autres que la méthode Operation a été appelée une seule fois :


image


L’IntelliTrace et les rapports de performance ne permettent cependant pas de logger explicitement les appels à une classe en particulier, et il n’est pas raisonnable de les utiliser sur une application de production pendant une durée trop longue. Il s’agit donc seulement de solutions approchantes au problème énoncé.


Méthodes existantes


Bien souvent, s’il y a besoin de suivre l’activité d’une classe en particulier, c’est que le contexte applicatif est propice à ce besoin. Dans ces cas, les technologies existantes anticipent le besoin en proposant les mécanismes nécessaires. Par exemple, pour logger les appels à un Service WCF, il est possible de créer un EndpointBehavior personnalisé dans lequel on passera avant/après chaque requête (methodes AfterReceiveRequest et BeforeSendReply). Autre exemple, en WebForms ASP.NET, tout accès à une page ASPX peut être détecté au moyen de l’évènement Page_BeginRequest. Enfin, pour logger tous les appels aux bases de données, le problème est souvent contourné par l’utilisation de SQL Server Profiler ou de tout autre outil similaire avec d’autres SGBDR.


Toutes ces méthodes sont efficaces, mais elles ne répondent au problème énoncé que dans des cas particuliers.


Design Patterns


Certains design patterns, comme l’Observer ou le Decorator, peuvent être utilisés pour ajouter une fonction de Log à une classe existante. Néanmoins, pour les mettre en place, il faut soit modifier le code de la classe à logger, soit modifier le code appelant. Par exemple, dans le cas du pattern Decorator, il faut :



  • extraire une interface ou une classe abstraite de la classe à surveiller,
  • implémenter une autre classe de log qui implémente la classe abstraite ou l’interface
  • modifier le code appelant la méthode pour ajouter la fonction de log.

On peut bien entendu utiliser le pattern Factory pour ajouter systématiquement la fonction de log, mais cela ne fait que déplacer le problème, puisque le code appelant doit alors solliciter la Factory. En outre, ceci oblige à appliquer le pattern pour chaque classe à surveiller et à “doubler” les méthodes, donc on augmente la complexité et on ajoute un niveau d’abstraction. Tout ceci est disproportionné par rapport à un problème simple a priori.


Utilisation détournée des Code Contracts


Inclus dans mscorlib à partir du Framework .NET 4.0, les Code Contracts proposent des fonctionnalités de validation de code activables par un certain nombre d’options de compilation. A la base des Code Contracts, il y a 3 mécanismes permettant de réaliser cette validation de code :



  • La méthode Contract.Requires permettant de spécifier une précondition sur une méthode
  • La méthode Contract.Ensures permettant de spécifier une postcondition sur une méthode
  • L’attribut ContractInvariantMethod appliqué à une méthode, activant une validation effectuée “à la fin de chaque méthode publique de la classe”.


C’est bien sûr l’attribut ContractInvariantMethod qui va être utilisé comme support pour réaliser la fonction de log. En effet, l’idée est de profiter de ce mécanisme pour réaliser un log plutôt qu’une validation. Pour comprendre comment fonctionnent réellement les Code Contracts, la première étape consiste à activer l’option correspondante dans l’onglet “Code Contracts” (des paramètres du projet) disponible si la fonctionnalité est installée :


image


Dans la classe Toto à surveiller, il suffit d’ajouter une méthode ayant l’attribut ContractInvariantMethod de la façon suivante :

class Toto
{
public void Operation()
{
}

[ContractInvariantMethod]
void aaa()
{
Contract.Invariant(false);
}
}



Après compilation et utilisation de Reflector sur l’assembly produite, on voit que l’activation des Code Contracts modifie la compilation du programme en ajoutant automatiquement, à la fin de chaque méthode publique, un appel à la méthode aaa du bout de code précédent, rebaptisée $InvariantMethod$. Par exemple, pour la méthode Operation qui ne faisait rien, on a en fait :


image


Par l’utilisation des Code Contracts, on a donc un mécanisme automatisé pour appeler une méthode après n’importe quel appel d’une méthode publique d’une classe. Ceci correspond bien au but recherché, encore faut-il arriver à injecter du code pour effectuer la surveillance de la classe. En effet, si on essaie de modifier la méthode aaa pour y réaliser autre chose que des appels successifs à Contract.Invariant, alors une erreur de compilation survient. Cependant, si on retourne dans les propriétés du projet, on voit qu’il est possible de définir une classe pour Custom Rewriter Methods dans laquelle on va pouvoir injecter du code quelconque :


image


Les méthodes de cette classe seront appelées uniquement si une validation par les Code Contracts échoue, c’est pourquoi il y a cette instruction dans la méthode aaa permettant de faire échouer systématiquement la validation :

Contract.Invariant(false);



Dans la classe indiquée pour Custom Rewriter Methods sont attendues des méthodes statiques ayant des signatures particulières. Pour celle qui nous intéresse ici, c’est-à-dire Invariant, on pourrait définir la méthode suivante :

public static class RuntimeFailureMethods
{
public static void Invariant(bool cond, string userMsg, string condText)
{
StackFrame[] sf = new StackTrace().GetFrames();
if (sf.Length >= 3)
{
MethodBase mb = sf[2].GetMethod();
Console.WriteLine("Appel de {0}.{1}", mb.DeclaringType.Name, mb.Name);
}
}
}



Ce code détermine quelle méthode a abouti à l’appel à Invariant en accédant au 3e élément de la pile des appels. En effet, le premier élément est toujours la méthode courante, le deuxième est la méthode aaa, et le troisième est bien la méthode d’origine appelée sur notre classe à surveiller. En exécutant toujours notre même Main, on obtient :


image



Les Code Contracts permettent donc bien de suivre automatiquement l’activité d’une classe quelconque, sans modification du code appelant ni du code appelé.

vendredi 18 mars 2011

Entity Framework 4 – Include fortement typés

Prenons un exemple simple :

EntityDesignerDiagram

Pour naviguer de Toto à Tata, Entity Framework nous propose la méthode Include :

var linq = new SafeIncludeContainer().TotoJeu.Include("Titis.Tatas");



Comme toujours, le fait de spécifier des valeurs sous forme de chaîne et non par des propriétés typées peut entraîner certains problèmes :



  • En cas de faute de frappe, une erreur est levée à l’exécution (pas très robuste)
  • En cas de changement d’un nom d’une propriété de navigation, il faut penser à mettre à jour tous les Includes concernés (pas très maintenable)

Pour pallier à ce problème, certains articles proposent de créer des méthodes d’extensions qui viennent surcharger la méthode Include, de telle sorte qu’il devient possible d’indiquer une expression Lambda en paramètre. Bien que cette méthode soit efficace et bien pensée, elle n’est pas toujours très intuitive. Par exemple, la requête précédente s’écrirait :

var linq = new SafeIncludeContainer().TotoJeu.Include(t => t.Titis.First().Tatas);



Il est possible de faire “mieux” en prenant une direction totalement différente. L’idée est de créer un Template de Texte (.tt) en se basant sur ce que peut réaliser un “générateur d’entité de suivi automatique ADO.NET” pour ajouter à chaque entité du modèle Entity Framework les propriétés et méthodes nécessaires. L’objectif est de pouvoir écrire la requête précédente de la façon suivante :

var linq = new SafeIncludeContainer().TotoJeu.Include(Toto.NTitis.Tatas);



Note : le N de NTitis est nécessaire car on ne peut pas créer une variable statique Titis ; elle entrerait en conflit avec la propriété de navigation non-statique Titis.


Pour aboutir à un tel résultat, on peut, pour chaque type d’entité :



  • Créer une classe interne Navigation. Ses objets maintiendront le texte en paramètre de l’Include. La classe contiendra des propriétés de navigation vers les autres types internes Navigation (par exemple, l’accès à .Tatas dans le bout de code précédent). Une conversion implicite vers string sera utile pour éviter les transtypages explicites ou les appels à ToString()
  • Ajouter une variable statique pour chaque propriété de navigation ayant le type <TypeDestination>.Navigation (par exemple, NTitis dans le bout de code précédent)
  • Lors de l’accès à la propriété servant à la navigation, une concaténation du texte est effectuée pour permettre la composition des Includes.

Ainsi, à titre d’exemple, la classe Tata peut être étendue par la définition d’une nouvelle classe partielle :

public partial class Tata
{
public static Titi.Navigator NTiti = new Titi.Navigator("Titi");

public sealed class Navigator
{
public string CompName { get; private set; }

public Navigator(string compName)
{
CompName = compName;
}

public static implicit operator string(Navigator n)
{
return n.CompName;
}

public Titi.Navigator Titi
{
get
{
return new Titi.Navigator(CompName + ".Titi");
}
}
}
}



Ainsi, le bout de code suivant dans une page ASPX quelconque :

protected void Page_Load(object sender, EventArgs e)
{
var linq = new SafeIncludeContainer().TotoJeu.Include(Toto.NTitis.Tatas);
foreach (var item in linq)
{
Response.Write("Toto " + item.Title);
Response.Write("<br />");
foreach (var item2 in item.Titis)
{
Response.Write("Titi " + item2.Title);
Response.Write("<br />");
foreach (var item3 in item2.Tatas)
{
Response.Write("Tata " + item3.Title);
Response.Write("<br />");
}
Response.Write("<br />");
}
Response.Write("<br />");
}
}



Produit le résultat suivant, avec quelques données de test en base de données :


image


Pour information, le Text Template utilisé est le suivant :

<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
// Generation de propriétés permettant une gestion fortement typée des Include
CodeGenerationTools code = new CodeGenerationTools(this);
MetadataLoader loader = new MetadataLoader(this);
CodeRegion region = new CodeRegion(this, 1);
MetadataTools ef = new MetadataTools(this);

string inputFile = @"SafeInclude.edmx";
MetadataWorkspace metadataWorkspace = null;
bool allMetadataLoaded =loader.TryLoadAllMetadata(inputFile, out metadataWorkspace);
EdmItemCollection ItemCollection = (EdmItemCollection)metadataWorkspace.GetItemCollection(DataSpace.CSpace);
string namespaceName = code.VsNamespaceSuggestion();

EntityFrameworkTemplateFileManager fileManager = EntityFrameworkTemplateFileManager.Create(this);

// Écrire du code de prise en charge dans le fichier de sortie du modèle principal
WriteHeader(fileManager);

// Émettre des types d'entité
foreach (EntityType entity in ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
fileManager.StartNewFile(entity.Name + "Navigator.cs");
BeginNamespace(namespaceName, code);
#>
<#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#><#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>
{
<#
region.Begin("Propriétés de navigation");

foreach (NavigationProperty navProperty in entity.NavigationProperties.Where(np => np.DeclaringType == entity))
{
NavigationProperty inverse = ef.Inverse(navProperty);
if (inverse != null && !IsReadWriteAccessibleProperty(inverse))
{
inverse = null;
}
#>

public static <#=code.Escape(navProperty.ToEndMember.GetEntityType())#>.Navigator N<#=navProperty.Name#> = new <#=code.Escape(navProperty.ToEndMember.GetEntityType())#>.Navigator("<#=navProperty.Name#>");

<#
}
#>

public sealed class Navigator
{
public string CompName { get; private set; }

public Navigator(string compName)
{
CompName = compName;
}

public static implicit operator string(Navigator n)
{
return n.CompName;
}

<#
foreach (NavigationProperty navProperty in entity.NavigationProperties.Where(np => np.DeclaringType == entity))
{
#>

public <#=code.Escape(navProperty.ToEndMember.GetEntityType())#>.Navigator <#=navProperty.Name#>
{
get {
return new <#=code.Escape(navProperty.ToEndMember.GetEntityType())#>.Navigator(CompName + ".<#=navProperty.Name#>");
}
}

<#
}
#>
}

<#
region.End();

#>
}
<#
EndNamespace(namespaceName);
}

fileManager.Process();

#>
<#+
void WriteHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
fileManager.StartHeader();
#>
//------------------------------------------------------------------------------
// <auto-generated>
// Ce code a été généré à partir d'un modèle.
//
// Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si
// le code est régénéré.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.Serialization;
<#=String.Join(String.Empty, extraUsings.Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#>
<#+
fileManager.EndBlock();
}

void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
CodeRegion region = new CodeRegion(this);
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
PushIndent(CodeRegion.GetIndent(1));
}
}

void EndNamespace(string namespaceName)
{
if (!String.IsNullOrEmpty(namespaceName))
{
PopIndent();
#>
}
<#+
}
}

bool IsReadWriteAccessibleProperty(EdmMember member)
{
string setter = Accessibility.ForWriteOnlyProperty(member);
string getter = Accessibility.ForReadOnlyProperty(member);

return getter != "private" && getter != "protected" && setter != "private" && setter != "protected";
}

#>


Pour que ce dernier fonctionne, il suffit de mettre à jour la variable inputFile pour indiquer le lien vers le fichier EDMX.

mardi 15 mars 2011

Select, Entity Framework et DTOs

Suite à une question d’un collègue concernant Entity Framework et sa capacité à transcrire ou non des clauses Select ou Where en SQL, le cas suivant s’est présenté :
using (ModelContainer ml = new ModelContainer())
{
var query = ml.Product.Select(p => new ProductDTO()
{
ProductID = p.ProductID,
CompleteName = p.Name + " - " + p.Description
}).Where(pDTO => pDTO.ProductID == 1);

var query2 = ml.Product.Select(p => new ProductDTO()
{
ProductID = p.ProductID,
CompleteName = string.Format("{0} - {1}", p.Name, p.Description)
}).Where(pDTO => pDTO.ProductID == 1);
}


Bien entendu, la question est : quelle est la différence entre query et query2?

De prime abord, query2 est légèrement mieux écrit puisque la concaténation de chaines de caractères y est mieux gérée. Seulement, query2 ne fonctionne pas puisqu’on obtient l’erreur :

Détails de l'exception: System.NotSupportedException: LINQ to Entities ne reconnaît pas la méthode « System.String Format(System.String, System.Object, System.Object) », et cette dernière ne peut pas être traduite en expression de magasin.

Ceci soulève plusieurs questions : comment savoir quelles sont les expressions que Entity Framework supporte? Comment se fait-il qu’Entity Framework soit capable de traduire en SQL un ProductDTO, une concaténation de chaînes et un “Where” additionnel?

Il est intéressant de pousser un peu plus pour voir les limites d’Entity Framework. Modifions légèrement la requête “query” :
var query = ml.Product.Select(p => new ProductDTO()
{
ProductID = p.ProductID,
CompleteName = string.Concat(p.Name.ToUpper(), " - ", p.Description.Trim())
}).Where(pDTO => pDTO.ProductID == 1);


Contre toute attente, cette requête LINQ est acceptée par Entity Framework et donne un résultat remarquable en SQL :
SELECT [Extent1].[ProductID] AS [ProductID], UPPER([Extent1].[Name]) + N' - ' + LTRIM(RTRIM([Extent1].[Description])) AS [C1]
FROM [dbo].[Product] AS [Extent1]
WHERE 1 = [Extent1].[ProductID]



Comment Entity Framework a t’il abouti à ce résultat?

La requête LINQ créée a visiblement été compilée en un seul arbre d’expression Lambda transmis à Entity Framework. A l’exécution, on voit que le type de “query” est ObjectQuery<> :

image

C’est à partir de là que l’Analyzer de Reflector entre en jeu :

image

Notre ObjectQuery<>, lorsqu’il cherche a être évalué (méthodes Execute, GetIListSouceListInternal ou encore GetEnumerator), reconstruit un plan d’exécution objet de la requête (comme le font les SGBDR). D’ailleurs, celui-ci tire profit des requêtes LINQ précompilées pour une exécution plus rapide ; tout ceci est dans la classe ObjectQueryExecutionPlan. Cette dernière sollicite ExpressionConverter. ExpressionConverter contient un nombre impressionnant de membres et surtout de classes scellées internes qui sont autant de Translator qu’il existe de possibilités de transcription d’expressions vers le magasin de données. On retrouvera par exemple DivideTranslator, LessThanTranslator, etc.

Dans notre cas, celui qui nous intéresse est logiquement MethodCallTranslator. Dans son constructeur statique, celui-ci initialise les Translators disponibles :

image

L’exemple montré ci-dessus est celui de MathPowerTranslator, mais GetCallTranslators en initialise d’autres, par exemple :

  • StartsWithTranslator
  • EndsWithTranslator
  • IndexOfTranslator
  • SubstringTranslator
  • IsNullOrEmptyTranslator
  • StringConcatTranslator
  • etc.
On retrouve bien les opérations effectuées lors de la requête LINQ effectuée au départ.
La façon dont StringConcatTranslator ou StartsWithTranslator – pour ne prendre que ces 2 exemples - génèrent leur bout de requête est une autre histoire, basée respectivement sur une transcription en fonction canonique ou un LIKE, mais dans tous les cas en utilisant le pattern Visitor.

Edit : une documentation plus complète existe sur les fonctions caniniques sur msdn.

lundi 14 mars 2011

DomainService, endpoint SOAP et méthode GetUser introuvable

L’incontournable erreur “GetUser not found” qui apparaît lorsqu’on commence à mettre en place une solution basée sur un DomainService et Silverlight, est apparemment une erreur générique dont les causes peuvent être multiples (erreur lors de l’installation de RIA Services, ou erreur quelconque de configuration). Si vous rencontrez cette erreur lors de l’ajout de l’endpoint soap sur un DomainService (exemple ci-après), lisez attentivement ce qui suit, cela pourra sauver des heures de recherches infructueuses :

<system.serviceModel>
<domainServices>
<endpoints>
<add name="OData" type="System.ServiceModel.DomainServices.Hosting.ODataEndpointFactory, System.ServiceModel.DomainServices.Hosting.OData, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add name="soap" type="Microsoft.ServiceModel.DomainServices.Hosting.SoapXmlEndpointFactory, Microsoft.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</endpoints>
</domainServices>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>



La première chose à faire est de se rendre à l’adresse du DomainService (http://<NomService>/ClientBin/<NomCompletDuService>.svc/SOAP par défaut). Normalement, avec un navigateur, une erreur 400 (invalid request) devrait être générée, puisque le navigateur ne sait pas générer une requête SOAP de lui-même. Par contre, si une erreur de type “MissingMethodException” apparait (sur System.ServiceModel.DomainServices.Server.DomainServiceDescription.get_ComplexTypes), c’est que la version de RIA Services installée n’est pas la bonne. Un coup de Reflector sur l’assembly en question permet de confirmer que la méthode n’existe pas. Il semblerait en effet qu’en fonction de la langue d’installation de RIA Services, la DLL System.ServiceModel.DomainService.Server ne soit pas la même, bien qu’elles aient toutes le même nom fort! C’est ce post qui permet de mettre sur la voie de la solution (notez le message en Allemand, et le pressentiment du rédacteur concernant un problème de langage).


En l’occurrence, l’installation de la beta 1 du SP1 de RIA Services pour Visual Studio 2010 a réglé le problème, mais il semblerait que d’une façon générale, n’importe quelle version Anglaise de RIA Services fait l’affaire. D’ailleurs, de nouvelles versions sont apparues entretemps.