code it

Martins Tech Blog

Constraints für WebAPI-Parameter

Seit die WebAPI in der Version 2 vorliegt, kann man dort nicht nur basierend auf Konventionen agieren, sondern die Routen auch mit Hilfe der Attribute RoutePrefix und Route definieren. Schauen wir uns ein Beispiel an:

[RoutePrefix("api/v1/books")]
public class BooksController : ApiController {

    [HttpGet]
    [Route("{id}")]
    public IHttpActionResult GetItem(int id)
    {
        // in production mode we would search for the book in database
        if (id == 87653)
        {
            var book = new Book
            {
                Author = "John Grisham",
                Title = "Gray Mountain",
                Id = 87653,
                Isbn = "978-0385537148"
            };
            return Ok(book);
        }

        return NotFound();
    }
}

Aufrufbar ist das Ganze nun über die Route /api/books/87653.


In der Action habe ich definiert, dass die Id vom Datentyp int sein soll. Was passiert, wenn hier kein int übergeben wird, also wenn hier z.B. die ISBN übergeben wird?


ASP.NET kann den übergebenen Wert nicht mappen und unser Parameter ist nicht Nullable. Deswegen wird der Request mit dem Statuscode 400 abgewiesen.

WebAPI unterstützt auch sogenannte Constraints. Damit kann näher spezifiziert werden, welche Bedingungen die Parameter erfüllen müssen. In meinem Fall ist der Parameter id ein int. Das definiere ich nun zusätzlich in der Route.
[HttpGet]
[Route("{id:int}")]
public IHttpActionResult GetItem(int id)
{
    // do all the fancy stuff
}
Was passiert nun, wenn ein Request mit der ISBN ankommt?


Der Request wird noch immer abgewiesen, aber dieses mal nicht mit Statuscode 400 sondern mit dem Statuscode 404, denn nun wird bereits bei der Auswertung der Routentabelle erkannt, dass keine Route vorliegt, die für diesen Request passt.

Solche Constraints können auch kombiniert werden. Dazu verbindet man verschiedene Constraints einfach mit einem Doppelpunkt. 

[HttpGet]
[Route("{id:int:min(1):max(100000)}")]
public IHttpActionResult GetItem(int id)
{
    // do all the fancy stuff
}

Eine detaillierte Liste der möglichen Constraints können in der MSDN nachgelesen werden.

Wem die möglichen Constraints nicht ausreichen, dem steht es frei, eigene Constraints zu schreiben. Ich möchte das am Beispiel eines ISBN13-Constraints machen. Das Vorgehen ist ganz einfach. 

Zunächst erstellt man eine neue Klasse die das Interface IHttpRouteConstraint implementiert:
public class Isbn13Constraint : IHttpRouteConstraint
{
    private static bool IsIsbn13Valid(string inputValue) { 
        // remove delimiters 
        var isbn = Regex.Replace(inputValue, @"-|\.| ", ""); 

        // validate number of digits 
        if (isbn.Length != 13)
        {
            return false;
        }
        // calculate the product of the digit multiplication operations 
        var product = 0;
        for (var i = 0; i < (isbn.Length - 1); i++)
        {
            product += Convert.ToInt16(isbn.Substring(i, 1)) * (1 + (2 * (i % 2)));
        } 

        // calculate the check digit 
        var  checkdigit = Convert.ToString(10 - (product % 10)); 

        // validate check digit 
        return (checkdigit == isbn.Substring(isbn.Length - 1, 1)); 
    } 

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
        IDictionary values, HttpRouteDirection routeDirection)
    {
        object value;
        if (!values.TryGetValue(parameterName, out value) || value == null)
        {
            return false;
        }

        var stringValue = value as String;
        return stringValue != null && IsIsbn13Valid(stringValue);
    }
}

Der Rückgabewert der einzige Methode Match ist selbsterklärend - true wenn alles passt, false wenn nicht. Der hier eben erzeigte Constraint übernimmt gleich noch die Validierung der Gültigkeit der ISBN. Zugegeben - man kann sich darüber streiten, ob das in das Aufgabengebiet eines Constraints fällt. Schon allein wegen des Rückgabecodes "Not Found" statt "Bad Request" würde ich diese Prüfung eher woanders ansiedeln.

Dieser neue Constraint muss nun in der WebApiConfig noch registriert werden. Statt MapHttpAttributeRoutes ohne Parameter aufzurufen, übergeben wir hier einen neuen ConstraintResolver, der den eben erstellten Constraint enthält.

var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("isbn13", typeof(Isbn13Constraint));

config.MapHttpAttributeRoutes(constraintResolver);
Das war's auch schon fast. Der Constraint kann nun ebenso wie auch die anderen Constraints verwendet werden.

[HttpGet]
[Route("{isbn:isbn13}")]
public IHttpActionResult GetItem(string isbn)
{
    // do all the fancy stuff
}
Happy coding.
Sie benötigen Unterstützung bei der Umsetzung Ihrer Projekte?
Ich bin käuflich. Nehmen Sie mit mir Kontakt auf.

Kommentar schreiben

Loading