Avoir une retour null avec un service WCF REST, c’est possible!

Pour un premier article technique sur ce blog, je voulais frapper fort. Attaquons nous donc à l’un des problèmes soi-disant insurmontable de WCF.

Par défaut, si WCF est utilisé pour exposer un service REST en JSON, les méthodes retournant une valeur null, ne renvoient pas une réponse telle que les frameworks javascript les attendent.

A la place, WCF nous retourne un message HTTP 200 (statut ok) avec un corps de message vide. La plus-part des frameworks javascript interprètent cela comme une erreur.

Exemple : JQuery par exemple appel systématiquement son callback fail().

Si WCF avait la bonne idée de  nous retourner null ou {}, tout ce passerait bien.

Heureusement, il est possible d’altérer le comportement de WCF en codant un DispatcherMessageInspector qui se chargera de retourné un message convenable.

Afin d’obtenir les meilleurs performances possibles, ce DispatcherMessageInspector ne lit pas le message pour vérifier son contenu. Il n’intervient qu’en cas de réponse avec un statut OK et si il n’y a pas de corps de message.

/// <summary>
/// Inspector pour modifier les réponse de WCf pour des message JSON vides ou null
/// </summary>
public sealed class JsonNullResponseAllowedInspector : IDispatchMessageInspector
{
    /// <summary>
    /// Modification 
    /// </summary>
    /// <param name="reply"></param>
    /// <param name="correlationState"></param>
    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        // Test si la réponse est positive
        HttpResponseMessageProperty messageProperty = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name];
        // Test si la réponse est valide
        if (messageProperty.StatusCode == System.Net.HttpStatusCode.OK
            // Test si le corps du message de réponse est suprpimée par WCF
            && messageProperty.SuppressEntityBody)
        {
            // Le nouveau corps du message peut être "{}" ou "null"
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes("null"));

            // Création du message JSON
            XmlDictionaryReader reader = JsonReaderWriterFactory.CreateJsonReader(ms, XmlDictionaryReaderQuotas.Max);

            // Création du message qui est retourné
            Message newMessage = Message.CreateMessage(reader, int.MaxValue, reply.Version);

            // Ajout du status OK
            newMessage.Properties.Add(
                HttpResponseMessageProperty.Name,
                new HttpResponseMessageProperty
                {
                    StatusCode = System.Net.HttpStatusCode.OK
                });

            // Ajout du format de réponse JSON
            newMessage.Properties.Add(
                WebBodyFormatMessageProperty.Name,
                new WebBodyFormatMessageProperty(WebContentFormat.Json));

            reply = newMessage;
        }
    }

    /// <summary>
    /// Inutile
    /// </summary>
    /// <param name="request"></param>
    /// <param name="channel"></param>
    /// <param name="instanceContext"></param>
    /// <returns></returns>
    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        return null;
    }
}

Pour utiliser ce DispatcherMessageInspector, il faut bien évidement coder le Behavior qui le fournira à nos services :

/// <summary>
/// Behavior pour utiliser l'inspecteur
/// </summary>
public sealed class JsonNullResponseAllowedBehavior : IEndpointBehavior
{
    /// <summary>
    /// Application du JsonNullResponseAllowedInspector
    /// </summary>
    /// <param name="endpoint"></param>
    /// <param name="endpointDispatcher"></param>
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new JsonNullResponseAllowedInspector());
    }

    /// <summary>
    /// Inutile
    /// </summary>
    /// <param name="endpoint"></param>
    /// <param name="bindingParameters"></param>
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }

    /// <summary>
    /// Inutile
    /// </summary>
    /// <param name="endpoint"></param>
    /// <param name="clientRuntime"></param>
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }

    /// <summary>
    /// Inutile
    /// </summary>
    /// <param name="endpoint"></param>
    public void Validate(ServiceEndpoint endpoint) { }
}

Et créer une extension à WCF pour pouvoir intégrer le Behavior dans le fichier de configuration.

/// <summary>
/// Extension pour activer le Behavior
/// </summary>
public sealed class JsonNullResponseAllowedExtension : BehaviorExtensionElement
{
    /// <summary>
    /// Type de JsonNullResponseAllowedBehavior
    /// </summary>
    public override Type BehaviorType
    {
        get
        {
            return typeof(JsonNullResponseAllowedBehavior);
        }
    }

    /// <summary>
    /// Nouvelle instance de JsonNullResponseAllowedBehavior
    /// </summary>
    /// <returns></returns>
    protected override object CreateBehavior()
    {
        return new JsonNullResponseAllowedBehavior();
    }
}

Pour finir, la configuration complète. On notera, l’introduction de l’extension dans la collection behaviorExtensions, et l’utilisation de celle-ci via le <JsonNullResponseAllowed/> sur un endpointBehaviors.

<system.serviceModel>
  <extensions>
    <behaviorExtensions>
        <!-- Ajout de l'extension à la liste des extensions disponibles -->
      <add name="JsonNullResponseAllowed" type="MyLib.JsonNullResponseAllowedExtension, MyLib"/>
    </behaviorExtensions>
  </extensions>
  <!-- Proticoles à défaut pour REST-->
  <protocolMapping>
    <add scheme="http" binding="webHttpBinding" bindingConfiguration="rest.http" />
    <add scheme="https" binding="webHttpBinding" bindingConfiguration="rest.https" />
  </protocolMapping>
  <bindings>
    <!-- Bindings Rest -->
    <webHttpBinding>
      <clear />
      <binding name="rest.http">
        <security mode="None" />
      </binding>
      <binding name="rest.https">
        <security mode="Transport" />
      </binding>
    </webHttpBinding>   
  </bindings>
  <behaviors>
    <endpointBehaviors>
      <behavior name="">
        <webHttp defaultBodyStyle="Bare" defaultOutgoingResponseFormat="Json" />
        <!-- Utilisation de l'extension -->
        <JsonNullResponseAllowed/>
      </behavior>
    </endpointBehaviors>
    <serviceBehaviors>
      <behavior name="">
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="true" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>

Voilà une nouvelle preuve que WCF peut être étendu sans inventer une nouvelle usine à gaz Winking smile

Jérémy Jeanson

Comments

You have to be logged in to comment this post.