Accueil Nos publications Blog Migration d’un projet ASP.NET Web API 2 vers ASP.NET MVC 6

Migration d’un projet ASP.NET Web API 2 vers ASP.NET MVC 6

ASP.NET logoMigrer un projet d’ASP.NET MVC 5 vers ASP.NET Core et ASP.NET MVC 6 n’est pas une chose facile. En effet, la nouvelle plateforme est une réécriture profonde de tout ce qui existait jusqu’à présent, et cela a un impact sur les couches et les APIs que nous manipulons directement dans nos applications.

Ainsi, là où, historiquement, la migration d’une version à l’autre d’ASP.NET MVC se faisait assez simplement (essentiellement en jouant sur la configuration de l’application), vous devez maintenant vous attendre à un challenge technique plus important. Dans le cadre de ce billet de blog, je souhaite m’attarder sur le cas spécifique des Web APIs.

En effet, avec ASP.NET Core, la brique ASP.NET Web API disparaît et se retrouve fusionnée directement avec ASP.NET MVC 6. Ainsi, tous les types de bases, espaces de noms et extensions propres à ASP.NET Web API disparaissent également.

Pour faciliter la vie des développeurs faisant face à un scénario de migration d’une application ASP.NET Web API existante vers ASP.NET MVC 6, Microsoft a fait le choix de distribuer un paquet NuGet Microsoft.AspNet.Mvc.WebApiCompatShim. C’est ce paquet NuGet que je vous propose de découvrir et de manipuler aujourd’hui.

Création du projet

La première étape consiste à élaborer le fichier project.json qui correspond à votre application. Le point d’attention ici concerne l’expression des dépendances : référencez les paquets Microsoft.AspNet.Mvc et Microsoft.AspNet.Mvc.WebApiCompatShim.

Vous devriez alors obtenir un fichier similaire à l’exemple de fichier ci-dessous.

"dependencies": {
  "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final",
  "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
  "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
  "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
}

La seconde étape concerne la classe Startup. Dans la plupart de vos applications ASP.NET MVC <6, le point de démarrage de l’application est géré par votre fichier Global.asax. Il va falloir enlever ce fichier et traduire son contenu vers la classe Startup. Si vous aviez déjà sauté le pas d’Owin dans votre application ASP.NET Web API, alors vous disposez sûrement déjà d’une classe Startup. Néanmoins, sa syntaxe change tout de même avec ASP.NET Core et ASP.NET MVC 6.

Optez donc pour l’exemple de classe Startup proposée ci-dessous. Les points d’attention ici sont le référencement de MVC (avec les appels à AddMvc et UseMvc) mais également le référencement des Shims Web APIs avec l’instruction AddWebApiConventions.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddWebApiConventions();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseIISPlatformHandler();

        app.UseMvc();
    }

    public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}

Gestion du routage

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddWebApiConventions();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseIISPlatformHandler();

        app.UseMvc(routes =>
        {
            routes.MapWebApiRoute("ApiProducts",
                                  "api/v1/products",
                                  new
                                  {
                                      controller = "Product",
                                      action = "Index"
                                  });
        });
    }

    public static void Main(string[] args) => WebApplication.Run<Startup>(args);
}

Généralement, dans les projets qui utilisent ASP.NET Web API, l’utilisation du routage par attribut est préférée à l’alimentation manuelle de la table de routage. En revanche, le paquet Microsoft.AspNet.Mvc.WebApiCompatShim n’offre pas une compatibilité parfaite avec le routage par attribut tel qu’il était disponible dans ASP.NET Web API 2.

Ainsi, l’attribut RoutePrefix n’est pas disponible, et vous devrez alors convertir votre code pour utiliser à la place l’attribut Route, ce dernier ayant une fonction de préfixe si vous le placez au niveau d’un contrôleur.

Enfin, l’attribut Route doit obligatoirement recevoir un template en paramètre de son constructeur, même si le template en question est vide. Là encore, il s’agit d’une différence avec ASP.NET Web API 2 qui n’est pas comblée par les Shims.

Migration des contrôleurs

La prochaine étape dans la migration d’un projet ASP.NET Web API vers ASP.NET MVC 6 consiste à reprendre tous vos contrôleurs, ainsi que les modèles et services dont ils dépendent, et de les ajouter à votre projet.

Dans vos contrôleurs, une fois les attributs de routage modifiés (en suivant les instructions dans la section précédente de ce billet), vous devriez avoir obtenu un code fonctionnel pour ASP.NET MVC 6. Sauf si vos actions utilisent le type de retour IHttpActionResult. Cette interface n’a pas été portée dans les Shims, vous devez donc la remplacer par l’interface IActionResult, qui est l’interface standard d’ASP.NET MVC 6. Notez que, si dans votre usage d’ASP.NET Web API vous aviez fait le choix de typer vos actions avec un POCO ou avec le type HttpResponseMessage, vous ne rencontrerez pas de problème. En effet, ce dernier type, et tous les mécanismes permettant de générer des instances de HttpResponseMessage **sont portées et disponibles au travers de la librairie **Microsoft.AspNet.Mvc.WebApiCompatShim.

[Route("api/v1/product")]
public class ProductController: ApiController
{
    [Route(""), HttpGet]
    public IActionResult List()
    {
        return Ok(new[]
        {
            new Product { Id = 1 },
            new Product { Id = 2 },
            new Product { Id = 3 }
        });
    }
}

public class Product
{
    public int Id { get; set; }
}

Le paquet Microsoft.AspNet.Mvc.WebApiCompatShim introduit un nouveau model binder permettant de recevoir des instances de la classe HttpRequestMessage en paramètre d’une action. Ainsi, les deux exemples suivants peuvent être écrits.

[Route("product/{id:int}")]
public string Get(int id, HttpRequestMessage req)
{
    return id + " " + req.RequestUri;
}

[Route("product")]
public async Task<Product> Post(HttpRequestMessage req)
{
    return await req.Content.ReadAsAsync<Product>();
}

Comme je le disais un peu plutôt, vous avez la possibilité de générer facilement des instances de HttpResponseMessage. Vous pourriez choisir de recevoir la requête en tant que paramètre d’action comme dans l’exemple précédent. Vous pourriez également profiter du fait que vos contrôleurs dérivent du shim ApiController et que ce type expose une propriété Request de type HttpRequestMessage. Ainsi, l’exemple ci-dessous devient une approche validée pour la génération d’une réponse.

public HttpResponseMessage Post(Item item)
{
    return Request.CreateResponse(HttpSattusCode.NoContent, item);
}

Dans un des exemples précédents, j’ai utilisé la méthode Ok. Là encore il s’agit d’une méthode exposée par le shim ApiController. Il y en a plusieurs autres : Created, Conflict, NotFound, etc. Ces méthodes étaient déjà présentes dans ASP.NET Web API 2. Or, l’implémentation du shim ApiController est telle qu’un appel à ces méthodes va en fait vous retourner un type compatible ASP.NET MVC 6. Faîtes donc bien attention au type de retour de vos actions.

Gestion des erreurs

Le type HttpResponseException est lui aussi introduit par le paquet des shims. Son usage reste exactement le même qu’avec ASP.NET Web API. L’exemple ci-dessous illustre ce cas de figure.

public Product Get(int id)
{
    var product = _service.Get(id);

    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    return product;
}

En coulisse, l’exception sera catchée automatiquement par un filtre qui se chargera de la transformer en une réponse classique avec le bon code et l’éventuel contenu que vous y auriez joint.

Si vous aviez fait le choix de manipuler des instances de HttpRequestMessage et HttpResponseMessage, sachez également que la méthode d’extension CreateErrorResponse a été portée et est distribuée via les shims.

Négociation de contenu

La négociation de contenu est bel est bien présente avec ASP.NET MVC 6, mais son implémentation n’est plus la même que celle disponible avec ASP.NET Web API. Néanmoins, si votre code doit se référer au service IContentNegociatior, sachez que celui-ci est accessible grâce aux shims.

var contentNegotiator = context.RequestServices.GetRequiredService<IContentNegotiator>();

L’exemple ci-dessous peut alors être écrit et exécuté avec ASP.NET MVC 6

[Route("products/{id:int}")]
public HttpResponseMessage Get(int id)
{
    var product = new Product
    {
        Id = id
    };
    var negotiator = Configuration.Services.GetContentNegotiator();
    var result = negotiator.Negotiate(typeof(Product), Request, Configuration.Formatters);

    var bestMatchFormatter = result.Formatter;
    var mediaType = result.MediaType.MediaType;

    return new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new ObjectContent<Product>(product, bestMatchFormatter, mediaType)
    };
}

Conclusion

Le paquet Microsoft.AspNet.Mvc.WebApiCompatShim ne fera pas de miracle en migrant automatiquement à votre place une application vers ASP.NET MVC 6. Il ne vous aidera pas non plus dans la migration des personnalisations que vous auriez pu avoir apporté au tunnel de traitement des messages ou à d’autres niveaux du framework Web API (fabrique de contrôleur, sélection des actions, etc.).

Néanmoins, il doit vous permettre de pouvoir reprendre tel quel le code de vos contrôleurs d’APIs en y apportant le minimum de modifications, ce qui devrait pouvoir rapidement vous permettre de compiler votre application en la basant sur MVC 6.

Mais n’oubliez pas qu’il ne s’agit que d’un paquet de compatibilité et que vous devriez saisir la première opportunité pour réellement migrer le code de vos contrôleurs d’APIs vers une approche plus cohérente avec ASP.NET MVC 6.