Windows 8 - Metro - WCF multi-endpoint pour C# et JS.

5 minutes read

Ajourd’hui j’ai voulu m’amuser à regarder comment fonctionnait l’accès aux services WCF depuis JS et C# dans une application Metro.

Pour cela j’ai voulu n’utiliser qu’un seul et même service consommé par ces deux plateformes via divers protocoles.

Rapidement j’ai constaté la nécessité d’adopter le protocole HTTP avec Javascript. Plusieurs options s’offrent alors, exposer les données en JSON ou en XML. Pour des raisons de pure discrimination envers un format aussi verbeux que le XML, j’ai choisi JSON.

En ce qui concerne le C#, ceux ayant lu quelques-un de mes précédents articles se sont surement rendu compte de mon point faible envers les protocoles de bas niveau et tout particulièrement le netTcpBinding. Pour moi il est le juste milieu entre un protocole typé et léger sur le réseau. Il a des inconvénients tout particulièrement son non-support de la sécurité, mais pour des données non sensibles c’est la Rolls (ou Porsche pour certains expatriés français dans la bay qui se reconnaitront) des protocoles WCF. Pour la route j’ai aussi voulu exposer les données dans un protocole un peu plus haut niveau qui est le NetHttpBinding. Il offre l’avantage d’encoder les données en binaire ce qui offre une volumétrie plus faible que du XML mais malgré tout assez importante (voir détails à la fin).

Donc pour résumer on a :

  • NetHttpBinding (C#)
  • NetTcpBinding (C#)
  • WebHttpBinding (JS)

Maintenant un peu de code !

J’ai généré une solution de base avec un client C#/XAML de base, un client JS/HTML de base et un service WCF aussi de base (original non ?). J’ai pris soin d’héberger ce dernier dans mon IIS local et non dans IIS Express pour le netTcpBinding.

Voici à quoi ressemble le service WCF :

[ServiceContract]
public interface IService1
{
    [OperationContract]
    string GetData(int value);

    [OperationContract]
    CompositeType GetDataUsingDataContract(CompositeType composite);

    [OperationContract]
    [WebGet(UriTemplate = "/GetDataJSON/{value}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    string GetDataJSON(string value);

    [OperationContract]
    [WebInvoke(Method="POST", UriTemplate = "/GetDataUsingDataContractJSON", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, BodyStyle=WebMessageBodyStyle.Bare)]
    CompositeType GetDataUsingDataContractJSON(CompositeType value);
}

[DataContract]
public class CompositeType
{
    bool boolValue = true;
    string stringValue = "Hello ";

    [DataMember]
    public bool BoolValue
    {
        get { return boolValue; }
        set { boolValue = value; }
    }

    [DataMember]
    public string StringValue
    {
        get { return stringValue; }
        set { stringValue = value; }
    }
}

Et son implémentation :

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class Service1 : IService1
{
    public string GetData(int value)
    {
        System.Threading.Thread.Sleep(2000);
        return string.Format("You entered: {0}", value);
    }

    public string GetDataJSON(string value)
    {
        return GetData(Convert.ToInt32(value));
    }

    public CompositeType GetDataUsingDataContract(CompositeType composite)
    {
        System.Threading.Thread.Sleep(2000);
        if (composite == null)
        {
            throw new ArgumentNullException("composite");
        }
        if (composite.BoolValue)
        {
            composite.StringValue += "Suffix";
        }
        return composite;
    }

    public CompositeType GetDataUsingDataContractJSON(CompositeType composite)
    {
        return GetDataUsingDataContract(composite);
    }
}

Le seul truc un peu original a été d’ajouter les attributs WebGet et WebInvoke pour rendre les méthodes concernées utilisable par Javascript en REST.

Maintenant le fichier de configuration. Il ressemble beaucoup à celui que j’avais fait pour la série sur le netTcpBinding et le httpPollingDuplex en Silverlight :

<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior>
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="false"/>
      </behavior>
    </serviceBehaviors>
    <endpointBehaviors>
      <behavior name="restbehavior">
        <webHttp />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <netTcpBinding>
      <binding name="netTcpBindingConfig">
        <security mode="None" />
      </binding>
    </netTcpBinding>
  </bindings>
  <services>
    <service name="WcfService1.Service1">
      <endpoint address="" contract="WcfService1.IService1" binding="netHttpBinding" name="HttpBinding" />
      <endpoint address="rest" contract="WcfService1.IService1" binding="webHttpBinding" behaviorConfiguration="restbehavior" name="rest"/>
      <endpoint address="netTcp" contract="WcfService1.IService1" binding="netTcpBinding" bindingConfiguration="netTcpBindingConfig" name="netTcpBinding" />
      <endpoint address="mex" binding="mexHttpBinding" name="mex" contract="IMetadataExchange" />
      <host>
        <baseAddresses>
          <add baseAddress="net.tcp://xiaoba:808/WcfService1/Service1.svc" />
          <add baseAddress="http://xiaoba/WcfService1/Service1.svc" />
        </baseAddresses>
      </host>
    </service>
  </services>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="false" />
</system.serviceModel>

Maintenant qu’en est-t-il des clients ?

Je n’ai pas été bien loin dans l’investigation, le but étant juste de le faire marcher des deux côtés.

Dans chaque client j’appelle d’abord la méthode GetData puis la méthode GetDataUsingDataContract et met le résultat dans un champs texte à l’écran.

Commençons par la star du moment le dénommé Javascript :

(function () {
    "use strict";

    var app = WinJS.Application;

    app.onactivated = function (eventObject) {
        if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
            if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                GetData();
            }
            WinJS.UI.processAll();
        }
    };

    function GetData() {
        var value = 1;

        var ResultSpan = document.getElementById('SpanResult');

        var baseUrl = "http://xiaoba/WcfService1/Service1.svc/rest/GetDataJSON/";
        var urlPost = "http://xiaoba/WcfService1/Service1.svc/rest/GetDataUsingDataContractJSON";

        var url = baseUrl + value;

       WinJS.xhr({url:url}).then(function (r) {
            var result = JSON.parse(r.responseText);
            ResultSpan.innerHTML = result;
        });

        var sData = JSON.stringify({
            BoolValue: true,
            StringValue: "Ô rage ! Ô désespoir ! Ô C# ennemi !"
        });

        WinJS.xhr({
            url: urlPost,
            type: "POST",
            headers: { "Content-Type": "application/json; charset=utf-8" },
            data: sData
        }).then(function (r) {
            var result = JSON.parse(r.responseText);
            ResultSpan.innerHTML = result.StringValue;
        });
    }

    app.oncheckpoint = function (eventObject) {
    };

    app.start();
})();

L’appel au service WCF se fait ici en JSON en utilisant les objets WinJS et JSON. Attention aux deux petits fourbes ici : j’ai nommé JSON.stringify et les headers de l’objet xhr pour le post. Si vous oubliez d’encoder votre objet Javascript en une chaîne de caractère JSON avant de l’affecter au champ data de l’objet xhr, bah ça marche pas. De même pour les headers, il faut les préciser. C’est d’autant plus étrange lorsqu’on a l’habitude de JQuery mais c’est comme ça.

Je ne reviens pas sur le concept de promise utilisé ici, de très bon articles existent sur le sujet.

Et maintenant comment ça marche en C# ?

public sealed partial class BlankPage : Page
{
    public BlankPage()
    {
        this.InitializeComponent();
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        GetData();
    }

    private async void GetData()
    {
        var client = new Service1Client(Service1Client.EndpointConfiguration.HttpBinding);
        //var client = new Service1Client(Service1Client.EndpointConfiguration.netTcpBinding);

        string result = await client.GetDataAsync(1);

        resultTextBlock.Text = result;

        CompositeType c = await client.GetDataUsingDataContractAsync(new CompositeType { BoolValue = true, StringValue = "Ô rage ! Ô désespoir ! Ô Javascript ennemi !" });

        resultTextBlock.Text = c.StringValue;

        await client.CloseAsync();
    }
}

Ici j’ai déclaré la méthode GetData en async pour pouvoir profiter du pattern async await. Le fonctionnement ici n’est pas exactement identique à la version JS car ici j’attend le retour du premier appel avant de lancer le second, ça ne change fondamentalement pas les choses. On voit dans le code ci-dessus une ligne commentée. Elle permet de changer de protocole de communication pour le netTcpBinding.

Qu’est-ce que j’ai appris de ce petit exercice.

  • Il n’est pas facile de trouver les erreurs que l’on fait lorsqu’on configure mal WCF pour le JSON. Au final j’ai réussi à débuguer lorsque j’ai installé Fiddler et que j’ai forgé mes requêtes HTTP à la main.
  • C’est quand même beaucoup plus simple de consommer des services WCF en C#</li>
  • On fait très vite des erreurs en Javascript sans s’en rendre compte. Genre une majuscule à la place d’une minuscule dans une variable. Aucune erreur de “compilation” ne nous prévient de ça.
  • En jouant un peu avec Fiddler2 on se rend compte que JSON est beaucoup plus concis que le NetHttpBinding. Il faut jouer un peu des coudes avec des extensions IIS pour faire baisser la taille des données renvoyées par ce dernier.
  • Les différentes valeurs de EndPointConfiguration sont générées par le générateur de proxy de Visual Studio 11 en fonction des endpoints disponibles sur le service et compatibles avec C#/XAML.
  • Le changement de binding wcf côté client en C# ne se passe pas du tout comme en Silverlight.
  • Enfin rien à voir mais c’est juste l’enfer de faire un VerticalAlignement=”Center” en HTML…

A vot’ bon cœur m’sieurs dames et bonne soirée. Comme d’habitude le code source se trouve dans les nuages.

Updated:

Leave a Comment