vendredi 13 mai 2011 13:45
tja
[WCF REST Starter Kit] Lire le contenu d’un message dans RequestInterceptor
Si vous avez la “chance” de travailler avec WCF REST Starter Kit, vous-vous heurterez très souvent à des limites, qui non seulement vous frustrent mais également qui vous laissent un sentiment de “pas fini” ou fait à l’arrache. Comme excuse, on peut dire que WCF Rest Starter Kit est en Preview 2 donc il a le droit d’être pas fini, mais à vrai dire je ne vois pas son avenir en rose avec bientôt une arrivée des vraies nouvelles API pour faire du REST (WCF Web API) made by Glenn Block. En attendant ceux qui sont en Framework 3.5 comme moi en ce moment n’ont le choix que de faire avec l’ancien.
Bon, allons dans le vif du sujet.
Une des extensions de WCF REST Starter Kit est l’introduction de RequestInterceptor qui est un composant s’exécutant au niveau du channel et qui a accès à la stack entière de l’appel au service. A la différence d’autres points d’extension, request interceptors sont exécutés par un channel unique et très tôt dans le channel pipline WCF et avant tous les autres composants d’extension. C’est important à avoir en tête.
Pour résumer, le RequestInterceptor est un point d’extension qui permet donc d’intercepter l’appel vers votre service où vous pouvez effectuer certaines actions comme par exemple :
- Authentification
- Caching
- Centent-Type dispaching
- X-HTTP-Method-Override
Cela permet de gérer le code en un seul endroit au lieu de le faire dans les opérations des implémentations des services (enfin avant ça on pouvait le faire ailleurs).
Comme son nom indique, WCF REST Starter Kit est là pour faire des service RESTFul. Techniquement c’est une exposition de service en webHttpBinding (protocole http comme son nom indique) et la sérialisation des message soit en JSON, soit en XML.
Pour un cas bien particulier, un des services REST devait être également exposé en SOAP. Vue que par défaut WCF (pour la partie Http) communique en SOAP alors ce ne devrait pas être un problème. WCF REST Starter kit est une couche qui dérive des implémentations de base qui elles communiquent en SOAP. D’ailleurs si vous exposez le même service avec basicHttpBinding, les clients SOAP peuvent consommer le service sans aucun problème.
Le problème
J’ai donc implémenté mon AuthenticationRequestInterceptor qui me servait à parcourir les Headers de la requête entrante pour identifier le client appelant et le laisser effectuer l’appel ou pas. Là où ça coince ce qu’on m’a demandé d’inspecter le contenu de message SOAP pour récupérer la clé d’identification.
Tentative 1 : Lecture directe
Dans votre RequestInterceptor vous avez accès au contexte de l’appel sous forme de l’objet RequestContext. Une des propriétés qui nous intéresse, c’est RequestMessage. La méthode qui nous intéresse c’est la méthode GetReaderAtBodyContents() qui permet d’obtenir un reader XML afin de lire le contenu de l’enveloppe Body de SOAP. Je ne vais pas vous détailler tout le code nécessaire car ce n’est pas le plus important. En tout cas je fais mon test unitaire pour pour valider que tout fonctionne, et ça passe. Je fais en suite un petit test en live et là…ça passe pas ! J’ai un message “This message cannot support the operation because it has been read”. WTF

La ligne dans mon AuthenticationRequestInterceptor de code qui était en cause était la suivante :
1: XmlReader reader = requestContext.RequestMessage.GetReaderAtBodyContents();
En fait l’exception ne se produit pas sur cette ligne, mais plus tard lorsque WCF essaie de le lire en interne pour le désérialiser. Quelle en est la cause ? Si on se réfère à l’excellent article WCF Messaging Fundamentals nous apprenons dans la section “Message Lifetime” que la classe Message (dans notre propriété requestContext.RequestMessage) afin de supporter le streaming le corps du message (body) ne peut être traité qu’une seule fois. Pour cela vous avez une propriété State qui défini l’état courant du message :
1: public enum MessageState
2: {
3: Created,
4: Read,
5: Written,
6: Copied,
7: Closed
8: }
Le seul état valide pour traiter le message est “Created”. Dans tous les autres états vous aurez l’exception ci-dessus. Donc l’appel de la méthode requestContext.RequestMessage.GetReaderAtBodyContents() modifie l’état du message en “Read” d’où l’exception. Plus loin dans l’article on apprend que le seul moyen de traiter le message est de créer un copie en mémoire et de récréer un nouveau message.
Tentative 2 : Copier le message
Donc si on veut traiter le message plusieurs fois on a le choix de créer une copie avec la méthode CreateBufferedCopy() Cette fois ci je me dis que je suis sur la bonne piste. Je donc écris le code pour copier le message qui ressemble à ceci (pseudo code) :
1: var messageBuffer = requestContext.RequestMessage.CreateBufferedCopy(Int32.MaxValue);
2: var originalMessage = messageBuffer.CreateMessage();
3: var processMessage = messageBuffer.CreateMessage();
4: // lire le message d'origine
5:
6: requestContext.RequestMessage = originalMessage;
Sauf que cela ne marche pas ! La propriété
requestContext.RequestMessage est en lecture seule !
POURQUOI CETTE LIMITATION ? !!!
Tentative 3: IDispatchMessageInspector
Un point d’extension intéressant de WCF qui est similaire à celui de RequestInterceptor. Je me suis dit que finalement je pourrais me brancher ici. La signature de l’interface est la suivante :
1: public interface IDispatchMessageInspector
2: {
3: object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext);
4: void BeforeSendReply(ref Message reply, object correlationState);
5: }
Comme on peut le constater nous avons accès directement à l’objet Message, donc pas de problème avec la propriété en lecture seule. Le seul problème, comme je le dis il ne s’exécute qu’après les RequestInterceptor ! Donc je ne peux rien en faire.
Tentative 4 : Reflection
C’est le désespoir d’en arriver là. Mais au pire, je me suis dit qu’avec la Reflection je pourrais arriver à faire ce que je veux :)
Tout d’abord, l’inspection du code avec Reflector (version pas encore expirée). Je me suis dit qu’au lieu d’appeler directement la méthode requestContext.RequestMessage.GetReaderAtBodyContents() qui modifie l’état du message, je vais appeler une méthode en interne.

En jaune – l’appel classique, l’état du message est modifié et on ne peut rien faire avec le message.
En rouge – ce que je veux appeler.
Un petit code vite fait :
1: var method = requestContext.RequestMessage.GetType().GetMethod("OnGetReaderAtBodyContents", BindingFlags.Instance | BindingFlags.NonPublic);
2: var bodyReader = (XmlDictionaryReader)method.Invoke(requestContext.RequestMessage, null);
3: var ms = new MemoryStream();
4: XmlWriter xmlWriter = XmlWriter.Create(ms);
5: XmlDictionaryWriter writer = XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter);
6: writer.Flush();
7: ms.Position = 0;
8: XmlReader xmlReader = XmlReader.Create(ms);
9: if (xmlReader.ReadToDescendant("accessKeyId"))
10: authorization = xmlReader.ReadString();
Comme vous le voyez j’essaie d’appeler la méthode interne OnGetreaderAtBodyContents()et lire le message sans modifier l’état du message. Le test unitaire passe. je lance mon client SOAP et la booom, exception “The reader cannot be null”. A un moment donné le XmlReader a été disposé et voilà, WCF ne peut plus lire le message. Pas bon.
Tentative 5 : Reflection version 2.
Si on ne peut pas évoquer même par la reflection les méthodes, autant lire le contenu directement des propriétés. Après des recherches avec Reflector (version non encore expirée
) j’ai vu que WCF lisait principalement une propriété Buffer qui est sur une propriété privé MessageData de la classe Message. On va faire pareil :
1: var p = requestContext.RequestMessage.GetType().GetProperty("MessageData", BindingFlags.Instance | BindingFlags.NonPublic);
2: var messageData = p.GetValue(requestContext.RequestMessage, null);
3: var bufferProperty = messageData.GetType().GetProperty("Buffer", BindingFlags.Instance | BindingFlags.Public);
4: var bufferPropertyValue = (ArraySegment<byte>)bufferProperty.GetValue(messageData, null);
5: var memoryStream = new MemoryStream(bufferPropertyValue.Array, bufferPropertyValue.Offset, bufferPropertyValue.Count);
6: memoryStream.Position = 0;
7:
8: var xmlReader = XmlDictionaryReader.CreateTextReader(memoryStream, XmlDictionaryReaderQuotas.Max);
9: while (xmlReader.Read())
10: {
11: var temp = xmlReader.ReadString();
12: if (!String.IsNullOrEmpty(temp) && xmlReader.LocalName == "accessKeyId")
13: {
14: authorization = temp;
15: break;
16: }
17: }
Le test unitaire passe ! Le client SOAP passe ! Yes ça marche
.
Conclusion
S’il n’y avait pas cette limite sous la forme de la propriété en lecture seule pour requestContext.RequestMessage je m’en serait sorti avec 2 tentatives. Sinon il m’a fallu 3 autres pour arriver à garder mon code cohérent et ne pas disperser le code d’authentification à plusieurs endroits. WCF Starter kit a d’autres limites que ça. Mais ce qui est frustrant ce que vous avez en main un framewrok qui pourrait être bien agréable à utiliser pour faire du REST mais de temps en temps on se heurte aux incohérences comme celle-ci. Finalement je m’en suis sorti mais à quel prix 
// Thomas
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :