Sie sind hier: Weblog

Web APIs versionisieren

Foto ,
14.07.2015 01:00:00

Sobald mehr als eine Version einer Web API, zum Beispiel aus Gründen der Abwärtskompatibilität parallel zur Verfügung gestellt werden, muss der Aufrufer angeben können, welche Version er adressieren möchte. Eine dazu häufig genutzte Möglichkeit stellt der Einsatz eines HTTP-Headers, der die Versionsnummer wiederspiegelt, dar:

api-version: 3

Ab ASP.NET Web API 2.1 können solche Header vom Attribut-basierten Routing berücksichtigt werden. Dazu sind pro Route Einschränkungen zu hinterlegen. Dazu implementiert der Entwickler das Interface IHttpRouteConstraint. Dieses gibt eine Methode Match vor, welche Web API aufruft, wenn es eine Route in Erwägung zieht. Dabei übergibt Web API Informationen über den aktuellen Aufruf. Retourniert Match den Wert true, zieht Web API die damit assoziierte Action-Methode in die engere Auswahl. Liefert Match hingegen false, so wird die jeweilige Route in weiterer Folge ignoriert.

Das nachfolgende Listing zeigt eine IHttpRouteConstraint-Implementierung, welche prüft, ob im Rahmen der benutzerdefinierten Kopfzeile api-version eine bestimmte, über den Konstruktor festgelegte Versionsnummer übergeben wurde. Zu den Kontext-Informationen, die Web API an Match übergibt, gehört eine HttpRequestMessage, welche den aktuellen Aufruf beschreibt, eine IHttpRoute-Implementierung, welche Auskunft über die assoziierte Route gibt sowie ein Dictionary mit den Routing-Parametern. Zu diesen Routing-Parametern zählen sämtliche Werte, die in die Platzhalter der Route eingesetzt wurden sowie auch die restlichen URL-Parameter. Der Parameter parameterName weist einen in diesem Fall frei wählbaren Namen. Einschränkungen Routing-Parameter würden hingegen als parameterName den Namen des jeweiligen Routing-Parameters übergeben bekommen. Der Wert des Enums HttpRouteDirection informiert darüber, ob die Einschränkung beim Auflösen einer Url auf eine Route oder beim Auflösen einer Route auf eine Url zur Anwendung kommt.

public class VersionConstraint : IHttpRouteConstraint
{
    public string Version { get; set; }

    public VersionConstraint(string version)
    {
        this.Version = version;
    }

    public bool Match(
                System.Net.Http.HttpRequestMessage request, 
                IHttpRoute route, 
                string parameterName, 
                IDictionary<string, object> values, 
                HttpRouteDirection routeDirection)
    {
        var version = request
                            .Headers
                            .GetValues("api-version")
                            .FirstOrDefault();
        if (version == null) return false;
        if (version == this.Version) return true;
        return false;
    }
}

Um eine IHttpRouteConstraint-Implementierung mit einer Route zu assoziieren, leitet der Entwickler von der Klasse RouteFactoryAttribute ab und überschreibt deren Property Constraints, sodass sie ein Dictionary mit den gewünschten IHttpRouteConstraint-Implementierungen retourniert. Als Key wird dabei jener Parametername, den Web API an Match übergeben soll, verwendet. Das nächste Listing demonstriert dies für den zuvor betrachteten VersionConstraint.

public class VersionedRoute : RouteFactoryAttribute
{
    private string version;

    public VersionedRoute(string template, string version)
        : base(template)
    {
        this.version = version;
    }

    public override IDictionary<string, object> Constraints
    {
        get
        {
            var result = new Dictionary<string, object>();
            result.Add("version", new VersionConstraint(version));
            return result;
        }
    }

}

Das auf diese Weise erhaltene RouteFactoryAttribute-Derivat kann nun anstatt des Attributes Route verwendet werden, um eine Attributbasierte-Route zu definieren. Das nachfolgende Listing demonstriert dies, für das hier betrachtete Beispiel, anhand zweier Action-Methoden, welchen dieselbe Url zugewiesen wird. Um die erste der beiden Methoden zu adressieren muss der Aufrufer nun, entsprechend der oben beschriebenen Einschränkung, über den benutzerdefinierten Kopfzeileneintrag api-version den Wert 1 übergeben; für die zweite Methode hingegen den Wert 2.

[RoutePrefix("api")]
public class VoucherController : ApiController
{
    [VersionedRoute("voucher", "1")]
    public string Post([FromUri] int value)
    {
        return Guid.NewGuid().ToString();
    }

    [VersionedRoute("voucher", "2")]
    public string Post2([FromUri] int value, [FromUri] string currency)
    {
        return Guid.NewGuid().ToString();
    }

}

Die Versionisierten Action-Methoden können sich auch in unterschiedlichen Controllern befinden und dabei die selben Routen nutzen. So kann zum Beispiel der nachfolgende Controller mit dem zuvor betrachteten co-existieren:

[RoutePrefix("api")]
public class Voucher3Controller : ApiController
{
    [VersionedRoute("voucher", "3")]
    public string Post2(VoucherRequest request)
    {
        return Guid.NewGuid().ToString();
    }
}

Der von diesem Controller verwendete VoucherRequest findet sich der Vollständigkeit halber nachfolgend.

public class VoucherRequest
{
    public int Value { get; set; }
    public string Currency { get; set; }
    public string Name { get; set; }
}