Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Actualités

  • Blog de Cyril DURAND, passionné de JavaScript, Ajax, ASP.net et tout ce qui touche au developpement Web Client-Side.

    View Cyril Durand's profile on LinkedIn

    hit counters

ASP.net 3.5 SP1 : combiner les fichiers JavaScript grace au CompositeScript du ScriptManager (ex ScriptCombining du toolkitScriptManager)

ASP.net 3.5 SP1 apporte principalement 2 nouveautés pour le ScriptManager : la combinaison de script et l'historique coté client.

Qu'est-ce que la combinaison de script ?

Lorsque l'on utilise ASP.net Ajax, nous incluons de plus en plus de fichiers JavaScript dans notre page, il n'est pas rare d'avoir besoin d'une douzaine de fichiers JavaScript par page surtout lorsque l'on utilise les Ajax toolkits. Cela pose un problème de performance. En effet, télécharger pleins de petits fichiers est généralement plus coûteux que télécharger un seul gros fichier, il est donc préférable d'avoir un seul gros fichier qui regroupe les différents petits fichiers. Malheureusement travailler sur un seul gros fichier n'est pas des plus agréables, l'application sur laquelle je travaille actuellement contient plus de 60 000 lignes de JavaScript répartis dans une 60aine de fichiers... Il est donc nécessaire d'avoir un système permettant de regrouper les différents fichiers JavaScript lors de l'exécution de l'application. C'est exactement ce que fait le ScriptCombining du ScriptManager.

Comment cela se passe ? Prenons un exemple.

Ajoutons classiquement 4 fichiers JavaScript via notre ScriptManager :

<asp:ScriptManager runat="server"> <Scripts> <asp:ScriptReference Path="~/file1.js" /> <asp:ScriptReference Path="~/file2.js" /> <asp:ScriptReference Path="~/file3.js" /> <asp:ScriptReference Path="~/file4.js" /> </Scripts> </asp:ScriptManager>

Analysons le traffic réseau avec HttpWatch

image 

On voit que notre page génère 8 requêtes :

  • La 1ère pour la page elle même
  • La 2ème pour les JavaScript propres à ASP.net 2.0
  • La 3ème pour le coeur du framework ASP.net Ajax
  • La 4ème pour la liaison ASP.net <=> ASP.net Ajax. On peut ne pas inclure ce fichier en mettant EnablePartialRendering à false au niveau du ScriptManager
  • Les 4 autres pour nos différents fichiers JavaScript.

Pour regrouper les différents fichiers JavaScript avec le ScriptManager on va utiliser la propriété CompositeScript du ScriptManager à la place de la propriété Scripts :

<asp:ScriptManager runat="server"> <CompositeScript> <Scripts> <asp:ScriptReference Path="~/file1.js" /> <asp:ScriptReference Path="~/file2.js" /> <asp:ScriptReference Path="~/file3.js" /> <asp:ScriptReference Path="~/file4.js" /> </Scripts> </CompositeScript> </asp:ScriptManager>

Après analyse on obtient :

image 

On observe que l'on ne fait plus que 5 requêtes, la dernière correspond à la combinaison de nos différents fichiers JavaScript. On observe également que l'on a transféré un petit moins de données, cela vient du fait que l'on a 3 requêtes de moins et donc 3 headers HTTP en moins.

Peux t'on combiner nos fichiers JavaScript avec les fichiers inclus par ASP.net Ajax ? Oui ! Pour cela on utilise toujours le ScriptReference mais non pas avec la propriété Path mais Name.

<asp:ScriptManager ID="ScriptManager1" runat="server"> <CompositeScript> <Scripts> <asp:ScriptReference name="MicrosoftAjax.js"/> <asp:ScriptReference name="MicrosoftAjaxWebForms.js"/> <asp:ScriptReference Path="~/file1.js" /> <asp:ScriptReference Path="~/file2.js" /> <asp:ScriptReference Path="~/file3.js" /> <asp:ScriptReference Path="~/file4.js" /> </Scripts> </CompositeScript> </asp:ScriptManager>

image

Plus que 3 requêtes ! Peux t'on diminuer à 2 requêtes ? Non ! Le premier fichier JavaScript, celui qui correspond au fonctionnement de ASP.net 2.0, n'est pas inclus via le ScriptManager, on ne peut donc pas le combiner. Pour que la combinaison de script fonctionne, il faut que le fichier soit inclus via un ScriptReference. A ma connaissance, on ne peut d'ailleurs même pas le supprimer.

Utilisation du ScriptCombining avec les fichiers JavaScript automatiquement inclus via des contrôles comme les toolkits.

Lorsque l'on inclut manuellement ces différents fichiers JavaScript, le ScriptCombining semble simple à utiliser mais peut-on s'en servir avec les fichiers JavaScript automatiquement inclut lorsque l'on utilise un control des toolkits ?

<asp:ScriptManager runat="server" /> <asp:TextBox ID="tbFirstName" runat="server" /> <ajaxToolkit:TextBoxWatermarkExtender runat="server" TargetControlID="tbFirstName" WatermarkText="Type First Name Here" />

image

On voit ici qu'on a demandé 6 fichiers JavaScript, les 3 premiers correspondent à ASP.net et ASP.net Ajax alors que les 3 derniers correspondent aux différents fichiers JavaScript requis par le TextBoxWatermark

Pour combiner tous ces fichiers JavaScript, il faut les rajouter dans le ScriptManager via la propriété CompositeScript :

<asp:ScriptManager runat="server"> <CompositeScript> <Scripts> <asp:ScriptReference name="MicrosoftAjax.js"/> <asp:ScriptReference name="MicrosoftAjaxWebForms.js"/> <asp:ScriptReference name="AjaxControlToolkit.Common.Common.js" assembly="AjaxControlToolkit"/> <asp:ScriptReference name="AjaxControlToolkit.ExtenderBase.BaseScripts.js" assembly="AjaxControlToolkit"/> <asp:ScriptReference name="AjaxControlToolkit.TextboxWatermark.TextboxWatermark.js" assembly="AjaxControlToolkit"/> </Scripts> </CompositeScript> </asp:ScriptManager>

image 

Ainsi nous n'avons plus que 2 fichiers JavaScript.

Comment connaitre les différents fichiers à inclure ?

Pour cela l'équipe ASP.net a mis en place un contrôle disponible sur Codeplex nous permettant de facilement analyser une page afin de connaître les différents fichiers JavaScript à ajouter, il s'agit du ScriptReferenceProfiler disponible à cette adresse : http://www.codeplex.com/aspnet/Release/ProjectReleases.aspx?ReleaseId=13356 

Pour l'utiliser, il faut mettre l'assembly (le fichier ScriptReferenceProfiler.dll) dans le dossier /bin de votre site web puis :

<%@ Register TagPrefix="srp" Assembly="ScriptReferenceProfiler" Namespace="ScriptReferenceProfiler" %> ... <srp:ScriptReferenceProfiler runat="server" />

Lorsque vous afficherez la page, vous aurez alors la liste des différents fichiers JavaScript à rajouter :

image

On voit que le ScriptReferenceProfiler est un contrôle bien sympathique. Bertrand Leroy a mis en ligne une vidéo présentant le ScriptCombining : Using Script Combining to improve AJAX performance

Attention, il faut bien comprendre comment cela fonctionne, le CompositeScript va combiner plusieurs fichiers JavaScript en un seul puis le mettre en cache coté client. Il faut donc utiliser les mêmes combinaisons de ScriptReference entres toutes les pages. Sans cela nous allons regrouper plusieurs fois les mêmes fichiers JavaScript dans des urls différentes. Au final nous téléchargerons plusieurs fois le même fichier JavaScript mais combinés dans des urls différentes. Si vous utilisez mal le ScriptCombining vous risquez d'avoir des perfs moins bonnes qu'en ne l'utilisant pas.

Peux t'on encore optimiser le chargement des fichiers JavaScript ? Oui !

Par défaut le ScriptManager envoie tous les fichiers en gzip. Cela veut dire qu'au lieu d'envoyer le texte brut au client, ASP.net l'envoie compressé en gzip, le client se charge alors de le décompresser. On voit sur les différentes captures que cela nous fait gagner de nombreux octets. De plus le ScriptManager ajoute automatiquement les en-têtes HTTP pour mettre en cache côté client les différents fichiers JavaScript. Il s'agit d'une très bonne chose !

Malgré tout, on peut encore optimiser tout cela. Puisque nous sommes de bons développeurs nos fichiers JavaScript possèdent de nombreux commentaires et espace/saut de lignes inutiles, ces éléments prennent de la place et sont nullement nécessaire pour le client.

J'ai essayé différentes options mais le ScriptManager ne permet pas de supprimer automatiquement les différents éléments inutiles des fichiers JavaScript :

image

Bien sûr il existe de nombreux outils permettant de supprimer les commentaires inutiles : jsmin est celui que j'utilise.

Pourquoi le ScriptManager ne le fait pas automatiquement ? Aucune idée ! Il s'agit d'un énorme manque et c'est une fonctionnalité nécessaire ! A cause de ce manque je vais continuer à utiliser mon système de "Script Combining".

L'autre manque du CompositeScript est l'impossibilité de créer des groupes de regroupement. Imagions que vous ayez un site où toutes vos pages utilisent file1.js ainsi que file2.js, quelques une de vos pages utilisent en plus file3.js et file4.js. Il serait bon de pouvoir regrouper file1.js avec file2.js et file3.js avec file4.js dans 2 urls distincts, ce n'est malheureusement pas possible.
Pour moi la syntaxe logique serait de passer par le ScriptManagerProxy, ou alors modifier la propriété CompositeScript pour prendre en compte une List<List<ScriptReference>> et non pas une List<ScriptReference> comme c'est le cas actuellement.

Qu'en pensez-vous ?

Avez-vous des suppositions sur l'absence de compactage et de la création des groupes ? Un simple "oubli" de la part de Microsoft ?

Bref, cette nouveauté est une nouveauté nécessaire et bien pensée, sans compactage des fichiers JavaScript on ne peut malheureusement pas utiliser cette nouveauté ! 

Et vous ? Utilisez-vous déjà cette nouveauté ? Pensez-vous l'utiliser ? Que pensez-vous de cet ajout ?


Pour les curieux : j'utilise HttpWatch afin d'analyser le trafic réseau, il s'agit d'un plugin à IE qui permet d'indiquer de nombreuses choses ! Il a de loin détrôner Fiddler.

Deploiement de service WCF (.svc) dans IIS et "Allow this precompiled site to be updatable"

Lorsque vous déployer un service WCF sur IIS, vous devez forcément passer par un fichier .svc. Ce fichier permet d'indiquer à IIS la classe implémentant le service et ainsi faire le lien entre IIS et le runtime WCF de .net. Ce fichier est généralement composé d'une seule ligne possédant une directive de page ServiceHost

Par exemple si vous avez un service contenu dans la classe "Sample.Upload.Services.Upload" alors le fichier svc contiendra seulement cette ligne :

<%@ ServiceHost Service="Sample.Upload.Services.Upload" %>

Bien sur, il est également nécessaire d'avoir toute la config verbeuse habituelle de WCF dans le web.config. A noter que cette directive possède également l'attribut Factory qui permet de personaliser la création du ServiceHost, par défaut la classe utilisé est ServiceHostFactory.

Heberger le fichier .svc dans un site web existant ?

Ce fichier .svc est donc tout à fait semblable aux autres fichiers d'un site web ASP.net. Si vous avez un site web qui expose des données via WCF, il est tentant de mettre ce fichier .svc au sein du même projet Visual Studio.

C'est en effet une idée logique. Mais attention lors de la publication, si vous passez par le menu "publier" du site web, vous ne devez pas décocher la case "Allow this precompiled site to be updatable". Sinon Visual Studio, en fait l'outil aspnet_compiler.exe utilisé en interne par Visual Studio, va précompiler le fichier .svc, c'est à dire remplacer son contenu par "This is a marker file generated by the precompilation tool, and should not be deleted!" et mettre le code dans le dossier bin. Malheureusement un fichier svc ne se comporte pas comme un fichier ASP.net classique, il faut impérativement que le fichier contienne la directive de page, sinon le service ne peut pas fonctionner.

image

Quelles sont les impactes de ce problème ? Lorsque vous accéderez au fichier .svc publié sur IIS, vous aurez une erreur 404 ou une page blanche. Si vous obtenez un "404 : The page cannot be found" en voulant accéder à un service WCF publié dans un fichier .svc, vérifier que vous n'avez pas précompilé le fichier.

Est-ce un bug de Visual Studio ? Oui ! j'ai effectué une recherche sur connect (le site de report de bug) et j'ai trouvé cette entrée : WCF service deployment problem when "allow this precompiled site to be updatable" is not selected 

Pour le contourner :

  • soit vous laissez coché la case "Allow this precompiled site to be updatable", ce que je ne vous conseille pas pour des raisons de perfs et sécurité du code. 
  • soit après publication vous copier le fichier .svc original non "précompilé", encore faut-il penser à le faire :-)
  • Soit vous n'incluez pas le fichier .svc dans votre projet web au niveau de Visual Studio, la ligne de code du fichier se trouvera alors seulement sur le serveur
  • [non testé] soit vous n'utilisez pas le publish, mais un web deployment project avec des directives de post publication afin de copier le bon fichier .svc
ASP.net 3.5 SP1 : Could not load type 'System.Web.UI.ScriptReferenceBase'

Après publication de votre site web vous obtenez l'erreur suivante :

"System.TypeLoadException: Could not load type 'System.Web.UI.ScriptReferenceBase' from assembly 'System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'."

Cette erreur provient du SP1 de .net 3.5. Vous avez installé le SP1 sur votre machine de dev/déploiement puis publier le site web à partir de cette machine afin de le déposer sur le serveur ne possédant pas le SP1.
Pour corriger cette erreur, il faut installer le SP1 du framework 3.5 sur le serveur : Download : Microsoft .NET Framework 3.5 Service Pack 1

Mais d'où provient cette erreur ?

Dans votre site web, vous vous servez du ScriptManager afin d'insérer vos propres fichiers JavaScript, vous avez donc quelque chose ressemblant à :

<asp:ScriptManager runat="server"> <Scripts> <asp:ScriptReference Path="~/js/myfile.js" /> </Scripts> </asp:ScriptManager>

Lors de la publication du site web, vous avez la possibilité de cocher la case "Allow this precompiled site to be updatable", si vous la décochez (ce que je vous conseille) la publication va compiler les .aspx vers une assembly. La ligne du ScriptReference du contrôle ci-dessus va alors être traduite en ce code :

[DebuggerNonUserCode] private ScriptReference __BuildControl__control6() { ScriptReference reference2 = new ScriptReference(); reference2.set_Path("/WS/CSWS.asmx/js"); return reference2; }

Le SP1 de .net 3.5, amène une nouvelle classe : CompositeScriptReference. Cette classe permet d'utiliser le "script combining" une nouvelle fonctionnalité du ScriptManager permettant de regrouper plusieurs fichiers JavaScript en un seul. Cette classe a donc des fonctionnalités très proches du ScriptReference il est donc logique que ces deux classes partagent un parent commun, le SP1 a donc rajouté la classe ScriptReferenceBase.

image 

Si vous n'avez toujours pas vu le problème, regardons le code MSIL du code ci-dessus :

ldstr "/WS/CSWS.asmx/js" callvirt instance void [System.Web.Extensions]System.Web.UI.ScriptReferenceBase::set_Path(string)

On comprend maintenant pourquoi si l'on publie un site web à partir d'une machine possédant le SP1 de .net 3.5, ce site web ne fonctionne plus sur une machine ne possédant pas le SP1.

Bug ou pas bug ?
Je n'arrive pas à me décider, je ne trouve pas normal que la mis en place du SP1 sur la machine de dev entraine ce genre de problèmes, mais je ne vois pas d'autres solutions.

Et vous qu'en pensez vous ? bug ou pas bug ?

Requête XPath et namespace XML par défaut

Les requêtes XPath permettent de faire du requetage sur un fichier XML afin de trouver facilement et rapidement un ou plusieurs noeuds XML.

En .net, on peut faire une requête XPath à partir d'un objet XmlNode via les méthodes SelectSingleNode ainsi que SelecNodes.

Par exemple si on a ce fichier XML :

<?xml version="1.0" encoding="utf-8" ?> <Pouets> <Pouet>Pouet N°1</Pouet> </Pouets>

On peut obtenri le premier noeud Pouet ainsi :

XmlDocument doc = new XmlDocument(); doc.Load("pouet.xml"); Console.WriteLine(doc.SelectSingleNode("//Pouet") != null);

(Bien sur XPath est beaucoup plus puissant que ça, mais ce n'est pas l'objet du post.)

Malheureusement ça se gate si on travail avec des fichiers XML complexes : avec plusieurs namespaces XML. Par exemple si on a ce fichier :

<?xml version="1.0" encoding="utf-8" ?> <Pouets xmlns="pouetNamespace"> <Pouet>Pouet N°1</Pouet> </Pouets>

Le code plus haut ne fonctionne plus ! Après un tour dans la documentation, on peut utiliser un XmlNamespaceManager afin de résoudre les namespaces XML.

On pourrait alors écrire :

XmlDocument doc = new XmlDocument(); doc.Load("pouet.xml"); XmlNamespaceManager xmlnsmgr = new XmlNamespaceManager(doc.NameTable); xmlnsmgr.AddNamespace(String.Empty, "pouetNamespace"); Console.WriteLine(doc.SelectSingleNode("//Pouet", xmlnsmgr) != null); // => doesn't work !

Mais cela ne fonctionne toujours pas. En lisant d'avantages la doc, il y a une partie sur les requêtes XPath et namespaces par défaut.

If the XPath expression does not include a prefix, it is assumed that the namespace URI is the empty namespace. If your XML includes a default namespace, you must still add a prefix and namespace URI to the XmlNamespaceManager; otherwise, you will not get a node selected. For more information, see Select Nodes Using XPath Navigation.

Pour requêter un fichier XML ayant un namespace par défaut, on est donc obligé de rajouter un namespace avec un prefixe, ce qui donne :

XmlDocument doc = new XmlDocument(); doc.Load("pouet.xml"); XmlNamespaceManager xmlnsmgr = new XmlNamespaceManager(doc.NameTable); xmlnsmgr.AddNamespace("dummy", "pouetNamespace"); Console.WriteLine(doc.SelectSingleNode("//dummy:Pouet", xmlnsmgr) != null);

Je trouve ce comportement étonnant et absolument pas intuitif. De plus, l'impossibilité de récupérer un XmlNamespaceManager à partir d'un XmlDocument rend l'utilisation de XPath via .net sur des fichiers XML complexe très pénible. Je ne parle même pas de l'utilisation de XPath via XLinq (Linq to XML), qui est complètement impossible sauf en passant par un XmlReader ou le contenu littéral du XML.

Que contient le cache de mon navigateur ?

Lorsque l'on développe des sites internet, on aimerait bien savoir ce que contient le cache de notre navigateur, certes on peut utiliser des analyseurs de trames HTTP comme firebug, fiddler, httpwatch, mais ce n'est pas ce qu'il y a de plus pratique.

J'ai récemment découvert une astuce pour Firefox. Dans l'url il suffit de taper "about:cache", vous aurez alors la possibilité de lister le contenu du cache disque et mémoire de votre Firefox :

"about:cache"

 image

"about:cache?device=memory"

image

"about:cache-entry"

image

A noter que l'on peut utiliser, l'extension CacheViewer qui permet d'obtenir les différents éléments du cache d'une façon plus graphique.

Quel outil pour Internet Explorer ?

A partir de là, je me suis dit qu'il serait intéressant d'avoir une astuce similaire pour Internet Explorer. Malheureusement je n'ai rien trouvé de très pratique, la seule piste est une appli (avec les sources en C++) qui utilise l'API WinInet pour accéder au cache "Reading the Internet Explorer Cache"


Petite chose amusante essayer d'accéder à "about:robots" dans votre Firefox 3 ;-)

WCF : serialiser un objet non serialisable lorsque l'on n'a pas accès au type

Lorsqu'on utilise un service WCF, il se peut que l'on ne possède pas le contrôle des différents types que l'on transfère. Dans ce cas il est possible d'avoir des problèmes pour sérialiser, en effet, ne pouvant pas modifier le type, on ne peut pas rajouter des attributs utiles à la sérialisation, rajouter un constructeur vide, ... 
Ces problèmes surviennent quelques soit le binding utilisé, dans la suite de mon exemple j'utilise le netTcpBinding.

J'ai récemment été confronté à ce problème, après pas mal de recherche, j'ai trouvé une solution qui me convenait, celle-ci ne nécessite pas de réécrire une version simplifié de l'objet à transiter.

Pour cela j'ai créé une classe possédant une propriété du type de l'objet à transférer. Dans mon exemple, l'objet qui pose problème lors de la sérialisation est de type Query.

public class QueryEncapsulator{ public QueryEncapsulator() { } public QueryEncapsulator(Query q) { this.Query = q; } public Query Query { get; set; } }

Bien sur, cela ne suffit pas pour résoudre le problème, l'étape suivante est d'implémenter l'interface IXmlSerializable. Cette interface va nous permettre de personnaliser la sérialisation de cet objet, pour ne pas avoir de problème nous allons utiliser une sérialisation binaire via le BinaryFormatter puis écrire le binaire dans le XMLWriter en base64.

[Serializable] public class QueryEncapsulator : IXmlSerializable { public QueryEncapsulator() { } public QueryEncapsulator(Query q) { this.Query = q; } public Query Query { get; set; } public System.Xml.Schema.XmlSchema GetSchema() { return null; } public void ReadXml(System.Xml.XmlReader reader) { BinaryFormatter formatter = new BinaryFormatter(); Byte[] b = new Byte[1024]; using (MemoryStream ms = new MemoryStream()) { if (!reader.Read()) throw new Exception("boom"); int i; while ((i = reader.ReadContentAsBase64(b, 0, b.Length)) > 0) { ms.Write(b, 0, i); } ms.Position = 0; Query obj = (Query)formatter.Deserialize(ms); this.Query = obj; } } public void WriteXml(System.Xml.XmlWriter writer) { using (MemoryStream ms = new MemoryStream()) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(ms, this.Query); Byte[] b = ms.ToArray(); writer.WriteBase64(b, 0, b.Length); } } }

A partir de là, nous pouvons transférer notre objet QueryEncapsulator à la place de notre objet Query.

Mais pourquoi cette astuce fonctionne ? En effet, pourquoi une sérialisation binaire via le BinaryFormatter fonctionne alors que lorsqu'on utilise le NetTcpBinding le message est transféré sous forme binaire.

[disclaimer:information non confirmé] D'après ce que j'ai compris, quelque soit le binding utilisé la sérialisation WCF se fait via le DataContractSerializer. Ce DataContractSerializer utilise en interne les mécanismes sous-jacent de la sérialisation XML afin de créer le graph de la sérialisation. Cela implique que l'on peut utiliser l'interface IXmlSerializable afin de personaliser la sérialisation.

Niveau performance, le surcout de cette astuce est négligeable par rapport à la machinerie WCF, dans mon cas, le surcout ne prend pas 1ms alors que le service met entre 5 et 30ms à répondre.

[Astuce] Projet de démarrage multiple dans Visual Studio

Lorsque vous développer des grosses applications avec Visual Studio, il est parfois nécessaire de démarrer plusieurs projets en même temps pour pouvoir tester son code. C'est typiquement le cas avec WCF, on a souvent besoin de lancer l'application hébérgeant le service WCF ainsi que l'application consommant ce service.

Dans Visual Studio, il est possible de configurer au niveau de la solution plusieurs projets de démarrage. Pour cela allé dans les propriétés de la solution (clique droit sur la solution puis propriétés), il y a une page "Startup Project" qui permet de définir le ou les projets de démarrage.

image

WCF : KnownType - Comment retourner des types enfants du type définit dans le ServiceContract

Lorsque vous créez un service WCF, vous allez créer un contrat. Dans la plupart des cas, ce contrat est une interface décorée de l'attribut ServiceContract contenant des méthodes décorées de l'attribut OperationContract.

Si vous voulez retourner un objet d'un type enfant du type déclaré vous obtiendrez une exception de type : "CommunicationException".

Prenons cet exemple :

[ServiceContract] public interface IMyService { [OperationContract] Animal GetAnimal(String name); } [DataContract] public abstract class Animal { } [DataContract] public class Dog : Animal { } [DataContract] public class Cat : Animal { }

L'implémentation de ce contrat retournera soit un objet de type Dog ou un objet de type Cat suivant la valeur du paramètre name.

Lors de l'utilisation de ce service vous obtiendrez une CommunicationException :

There was an error while trying to serialize parameter http://tempuri.org/:query. The InnerException message was 'Type 'Test.Dog' with data contract name 'Dog:http://schemas.datacontract.org/2004/07/Test.Doc' is not expected. Add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.'.  Please see InnerException for more details.

La raison est que vous retournez un objet qui n'est pas connu du service, il ne sait alors pas comment le sérialiser. Pour palier à ce problème, il faut faire connaitre ces types à notre service. Il existe plusieurs solutions :

  • Utilisation de l'attribut ServiceKnownType : Cet attribut s'utilise au niveau du contrat soit sur la définition de l'interface, soit sur les différentes méthodes de l'interface :

[ServiceContract] public interface IMyService { [OperationContract] [ServiceKnownType(typeof(Dog))] [ServiceKnownType(typeof(Cat))] Animal GetAnimal(String name); }

  • Utilisation de l'attribut KnownType : Cet attribut s'utilise au niveau de la classe mère :

[DataContract] [KnownType(typeof(Dog))] [KnownType(typeof(Cat))] public abstract class Animal { }

<configuration> <system.runtime.serialization> <dataContractSerializer> <declaredTypes> <add type="Test.Animal, MyAssembly"> <knownType type="Test.Dog, MyAssembly"/> </add> <add type="Test.Animal, MyAssembly"> <knownType type="Test.Dog, MyAssembly"/> </add> </declaredTypes> </dataContractSerializer> </system.runtime.serialization> </configuration>

Il existe d'autres solutions, mais toutes dérives de ces 3 solutions. J'aime particulièrement la solution qui consiste à appeler une méthode static d'un certain type, cette solution permet d'inclure des KnownType générique ou alors d'inclure tous les enfants d'un type via de la reflection.

Pour en savoir plus sur comment référencer vos types dans le contrat, je vous conseille cet article : All about knownTypes ou cet article MSDN : Data Contract Known Types

RegexStringValidator et propriété requise dans un élément de section de configuration personnalisé

Lorsqu'on créé une section de configuration personnalisé, .net permet de valider les entrées grâce aux attributs StringValidator et RegexStringValidator.

J'ai voulu utiliser l'attribut RegexStringValidator sur un élément de ma configuration afin de vérifier que l'entrée contenait un String "simple", j'ai donc écrit ce code :

public class IndexElement : ConfigurationElement { [RegexStringValidator(@"[\w\-\.]+")] [ConfigurationProperty("Name", IsKey = true, IsRequired = true)] public String Name { get { return (String)base["Name"]; } set { base["Name"] = value; } } }

Dans mon fichier de configuration, j'ai rajouté la ligne qui va bien, mais lorsque je charge la configuration j'obtiens cette erreur :

The value for the property 'Name' is not valid. The error is: The value does not conform to the validation regex string '[\w\-\.]+'.

Après quelques recherches, je me suis rendu compte que .net valide également la valeur par défaut, dans mon cas cette valeur vaut null ce qui ne valide pas l'expression régulière.

Pour corriger le problème, il faut renseigner une valeur par défaut, on peut le faire via l'attribut ConfigurationProperty.

[RegexStringValidator(@"[\w\-\.]+")] [ConfigurationProperty("Name", IsKey = true, IsRequired = true, DefaultValue="abc")] public String Name { get { return (String)base["Name"]; } set { base["Name"] = value; } }

La valeur par défaut importe peu, en effet si on ne renseigne pas la propriété, .net va retourner une exception puique nous avons marquer cette propriété comme "Required".

Editer un fichier de config WCF : SVCConfigEditor

Toutes les personnes ayant déjà utilisé WCF savent à quel point il est pénible de faire la configuration. En effet WCF repose essentiellement sur des fichiers de config qui possèdent de très nombreuses options, éditer ce fichier de config à la main devient vite un enfer.

Lorsque vous créez un nouveau projet de type "WCF Service Library" :

Untitled

Vous pouvez modifier le fichier de config du projet via un éditeur spécial :

Untitled2

Malheureusement lorsque l'on fait un client, ou alors qu'on décide d'héberger "manuellement" notre service WCF, on ne dispose pas de cette option.

Untitled4

Pour palier à ce manque, on peut rajouter notre propre éditeur à n'importe quel fichier. Pour cela faites un click droit sur le fichier de config dans l'explorateur de solution puis "Ouvrir Avec ...", il faut ensuite rajouter un nouvel éditeur. svcConfigEditor se trouve ici : "C:\Program Files\Microsoft SDKs\Windows\v6.0\Bin\SVCConfigEditor.exe"

Untitled5

SVCConfigEditor nous permet de configurer toute les options de WCF. Si vous ne le connaissez pas et utilisez WCF, je vous invite grandement à le découvrir, attention cet outil nécessite malgré tout de comprendre le fonctionnement de la configuration WCF.

Untitled3

JSON - optimiser la sérialisation des List<T> par ASP.net AJAX

Grâce au service web et ASP.net Ajax, il est très simple de communiquer avec notre serveur à partir de JavaScript. En effet, il suffit de créer un service WCF (ou asmx) de rajouter quelques attributs et on peut appeler nos WebMethods à partir de JavaScript. En interne une sérialisation JSON (JavaScript Object Notation) est utilisée.

Malheureusement il est impossible de customiser cette sérialisation. Pourquoi vouloir personnaliser cette sérialisation ? Prenons le cas d'une méthode GetPersons qui renvoie une centaine de personne.

Le JSON aura cette tête :

{"d": [ { "__type":"Person:#", "BirthDate":"\/Date(1211790363564+0200)\/", "City":"Chénas", "FirstName":"Steven", "LastName":"Vincent" },{ "__type":"Person:#", "BirthDate":"\/Date(1211790441107+0200)\/", "City":"Légny", "FirstName":"Janet", "LastName":"laurent" },{ // etc...

On peut voir que le nom des "colonnes" se répètent inutilement. L'idée serait d'avoir un JSON ayant cette tête :

{"d":{ "Columns":[ "FirstName", "LastName", "BirthDate", "City" ],"Values":[ ["Andrew","Alex","\/Date(1211790586937+0200)\/","Saint-Didier-sur-Beaujeu"], ["Laura","Claude","\/Date(1211790697591+0200)\/","Chénas"], ["Anne","Isabelle","\/Date(1211790655756+0200)\/","Saint-Cyr-le-Chatoux"], ["Nancy","Steph","\/Date(1211790592372+0200)\/","Sainte-Paule"], // etc

Pour arriver à un tél résultat, je ne retourne plus une List<Person> mais une JsonList<Person>. Cette JsonList<T> possède 2 propriétés : Columns de type IEnumerable<String> et Values de type IEnumerable<IEnumerable<Object>>.

Dans mon exemple, pour 100 personnes, on passe de 12.8 ko à 6.8 ko, soit presque 50%. Cette optimisation nous permet alors d'obtenir une application plus réactive.

Côté client, pour retrouver un tableau d'objet plutôt qu'un objet "bizarre", on pourra utiliser ce code :