Les API REST d'Azure avec ASP .NET Web API

Tags: Azure, Rest

Avec ASP .NET MVC 4.0, nous avons la joie et l'avantage de découvrir en avant première mondiale des nouvelles API pour les appels Http : ASP .NET Web API.

Ces apis seront d'ailleurs intégrées directement dans le Framework 4.5, donc on peut passer du temps dessus pour s'amuser un peu.

Suite à mes précédents articles (1 et 2) sur l'appel des API REST de Windows Azure, j'ai donc décidé de ré implémenter mon code en utilisant les Web API.

Vous pouvez télécharger les Web Api avec ASP .NET MVC 4 ici.

System.Net.Http

Donc dans mon projet, j'ai ajouté une référence vers l'assembly System.Net.Http (remarquez le nom en System.xxx prouvant que c'est bien une assembly majeure qui sera intégrée dans .NET 4.5).

Pour l'instant, vous trouverez cette assembly dans Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies.

Dans cette assembly, vous trouverez la classe principale qui est HttpClient.

Quand vous voulez effectuer une appel Http, vous utiliserez cette classe avec sa méthode SendAsync qui attend un HttpRequestMessage et retourne un HttpResponseMessage. Il existe également les méthodes GetAsync, PostAsync, PutAsync qui appellent en interne la méthode sendAsync en spécifiant le type d'appel (méthode GET, POST ou PUT). Si vous l'utilisez telle quelle, elle utilisera en interne un HttpClientHandler qui se chargera de traiter le message de requête et vous renverra la réponse.

Si vous voulez comprendre les bases de HttpClient, je vous laisse lire le billet de Henrik F Nielsen.

Mais le principe et la flexibilité de HttpClient, c'est que vous pouvez ajouter autant de HttpClientHandler que vous voulez qui traiteront l'un après l'autre la requête et la réponse. Vous pouvez donc créer une pile de handler qui pourront modifier la requête et/ou modifier la réponse.

Dans notre cas de l'appel des API REST d'Azure, on sait que l'on doit systématiquement envoyer la version de l'api que l'on souhaite utiliser (x-ms-version) dans l'en-tête du message. Plutôt que de l'ajouter dans tous nos appels, il nous suffira de créer un HttpClientHandler qui ajoutera cette version une bonne fois pour toute.

Galère, galère

Il en est de même pour le certificat X509 que l'on doit ajouter également systématiquement et je vous avouerais franchement que j'ai un peu galéré au début de cela. Mon principal soucis provenait que je ne savais pas comment ajouter mon certificat X509 à mon message HttpRequestMessage.

Pour explorer toute nouvelle assembly, j'utilise bien entendu Reflector (et/ou Just Decompile de Telerik qui à l'avantage d'être gratuit et en pleine évolution en ce moment).

J'ai donc chargé System.Net.Http pour voir comment ajouter mon certificat X509 dans le message. Mais en faisant le tour complet, je ne trouvais pas de solution. Je voyais bien que la requête Http était créée dans la méthode CreateAndPrepareWebRequest du HttpClientHandler mais cette méthode est private.

Je voyais bien également que la méthode InitializeWebRequest était appelée, mais cette méthode bien qu'étant redéfinissable était marquée private : moralité, je ne pouvais créer une classe dérivant de HttpClientHandler pour redéfinir InitializeWebRequest et pouvoir accéder enfin à l'objet HttpWebRequest pour pouvoir lui ajouter mon certificat X509. De plus je ne voyais aucunes classes dans System.Net.Http qui héritait de HttpClientHandler et qui me permettait de faire ce que je voulais.

Bref, j'étais dans une impasse.

Et puis miracle, en regardant toujours dans Reflector, je tombe sur les attributs de l'assembly et je vois :

[assembly: InternalsVisibleTo("System.Net.Http.WebRequest, PublicKey=xxx")]

InternalsVisibleTo permet à des classes d’une autre assembly d’être considérées comme faisant partie de l’assembly. Donc pour les classes dans System.Net.Http.WebRequest, InitializeWebRequest est redéfinissable.

Je charge donc l'assembly System.Net.Http.Webrequest qui ne contient qu'une seule classe : WebRequestHandler.

Et cette dernière fait exactement ce que je veux (elle est d'ailleurs prototypée pour cela) : Si vous voulez jouer avec les certificats, l'impersonnalisation, le CachePolicy, etc. elle est faite pour vous.

Alors pourquoi cette classe est dans une autre assembly, je n'en ai aucunes idées. Et utiliser InternalToVisibilityAttribute ressemble plutôt à du bricolage. Alors soit il y a une explication technique que je n'ai pas vu, soit cette classe a été ajoutée plus tard dans le processus de fabrication des Web API et qu'à terme, elle sera réintégrée, nous verrons bien...

Bref, après quelques heures passées à chercher de partout, j'avais enfin ma solution.

AzureRestRequestHandler

On peut donc enfin s'attaquer à la classe Handler d'appels aux API REST de Windows Azure. Elle hérite donc de WebRequestHandler.

Je lui ai ajouté trois propriétés :

  • CertificateThumbprint (string) : l'empreinte du certificat,
  • XmsVersion (string) : la version de l'API que l'on souhaite utiliser,

 

Je lui ajoute la méthode GetCertificate déjà définie dans un précédent article :

public class AzureRestRequestHandler : WebRequestHandler
{
    public const string XmsVersion_2009_10_01 = "2009-10-01";
    public const string XmsVersion_2010_04_01 = "2010-04-01";
    public const string XmsVersion_2010_10_28 = "2010-10-28";
    public const string XmsVersion_2011_02_25 = "2011-02-25";
    public const string XmsVersion_2011_06_01 = "2011-06-01";
    public const string XmsVersion_2011_08_01 = "2011-08-01";
    public const string XmsVersion_2011_10_01 = "2011-10-01";

    public AzureRestRequestHandler()
    {
        XmsVersion = XmsVersion_2011_10_01;
        ExpectedStatusCode = HttpStatusCode.OK;
    }

    public AzureRestRequestHandler(string certificateThumbprint) : this()
    {
        CertificateThumbprint = certificateThumbprint;
    }

    public string CertificateThumbprint { get; set; }

    public string XmsVersion { get; set; }

    private  X509Certificate2 GetCertificate()
    {
        var certificateStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        try
        {
            certificateStore.Open(OpenFlags.ReadOnly);
            var certs = certificateStore.Certificates.Find(
                X509FindType.FindByThumbprint,
                CertificateThumbprint,
                false);

            if (certs.Count != 1)
            {
                Console.WriteLine("Certificat introuvable (CurrentUser)");
                throw new InvalidOperationException("Certificat introuvable");
            }
            return certs[0];
        }
        finally
        {
            certificateStore.Close();
        }
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //TODO
    }
}

Il ne reste plus qu’à implémenter la surcharge. Donc dans un premier temps on va ajouter le certificat et la version de l’API REST dans l’appel :

public Task<List<StorageService>> GetStorageAccountInternal(string subscriptionId)
{
    var client = new HttpClient(new AzureRestRequestHandler(CertificateThumbprint));
    var requestUrl = string.Format(
        "https://management.core.windows.net/{0}/services/storageservices",
        subscriptionId);

    var tcs = new TaskCompletionSource<List<StorageService>>();

    client.GetAsync(requestUrl)
        .ContinueWith(
            task =>
                {
                    // une erreur dans l'exécution de la requête ?
                    if (task.IsFaulted)
                    {
                        tcs.TrySetException(task.Exception.GetBaseException());
                        return;
                    }

                    try
                    {
                        // j'ai une bonne réponse ?
                        task.Result.EnsureSuccessStatusCode();
                    }
                    catch (HttpRequestException exception)
                    {
                        tcs.TrySetException(exception);
                        return;
                    }

                    // réponse OK : on analyse le contenu (dans le même thread)
                    task.Result.Content.ReadAsStringAsync().ContinueWith(
                        continuationTask =>
                            {
                                // on a du mal à lire le flux http
                                if (continuationTask.IsFaulted)
                                {
                                    tcs.TrySetException(task.Exception.GetBaseException());
                                    return;
                                }

                                // ReadStorageAccountResponse converti le xml en List<StorageService>
                                tcs.SetResult(ReadStorageAccountResponse(continuationTask.Result));
                            },
                        TaskContinuationOptions.ExecuteSynchronously);
                });

    return tcs.Task;
}

 

On se concentre sur la lecture du xml et plus sur comment appeler, etc. Bref, c’est mieux (enfin je trouve Clignement d'œil)

blog comments powered by Disqus