Mostrando entradas con la etiqueta ASP.NET MVC. Mostrar todas las entradas
Mostrando entradas con la etiqueta ASP.NET MVC. Mostrar todas las entradas

domingo, 20 de noviembre de 2016

Versionado ASP.NET Web API 2

Hace unos meses tuvimos que crear una Web API y, por supuesto, uno de los requisitos principales era versionarla (también tenía que funcionar, pero eso no prometía ser tan divertido). Aunque inicialmente estuvimos sopesando la idea de usar algún paquete Nuget (como por ejemplo https://github.com/Sebazzz/SDammann.WebApi.Versioning) al final y en un alarde de “vamos a hacerlo a mano que así aprendemos más y no adquirimos una dependencia” nos tiramos al barro e implementamos el versionado 100% home-made. Si ha sido o no una decisión correcta lo sabremos con el tiempo…. Para más inri, han aparecido nuevos paquetes Nuget que prometen mucho http://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx pero ya es demasiado tarde (al menos por ahora).

Por otro lado y como hace poco me descubrí a mí mismo preguntándome qué diablos hacía este código, es ese el motivo de escribir este post, dejar por escrito que motivaciones nos empujaron en su día a tomar un montón de decisiones que parecían oportunas y llenas de razón… y aquí aprovecho para meter el disclaimer “lo hemos hecho lo mejor que hemos podido”

Viendo algunos de los distintos tipos de versionado más populares, irá discurriendo el post.

URI Path

Versionar por ruta. El más común, primigenio e intuitivo.

Aunque los ejemplos inmediatos no pueden considerarse una buena práctica (de hecho, nadie lo haría así), partiendo de la ruta “/api/Customers” lo más sencillo si queremos versionar sería crear un nuevo controlador y añadirle al nombre un sufijo de número de versión.

public class Customers2Controller : ApiController
{
    // GET api/Customers2
    public IEnumerable<Customer2> Get()
    {
    }
}

Ahora, además de “/api/Customers” también tendríamos “/api/Customers2”.

Una segunda opción más recomendable sería utilizar el atributo Route.

[Route("api/Customers2")]
public IEnumerable<Customer2> Get()
{
}

Otras rutas válidas (y seguro más convenientes que las vistas hasta ahora) serían:

  • api/v2/Customers
  • apiv2.example.com/Customers

En la primera el número de versión está en un segmento de la ruta lo más a la izquierda posible.

En la segunda es el nombre de host quien incluye el número de versión. De hecho, “dicen” que algunos incluso crean un alias para que api.example.com apunte a api<última_versión>.example.com.

En ambos casos, lo mejor para no complicarse la vida con el número de versión es que sea un simple número y no un major.minor.patch. Además, si hacemos obligatorio el uso de versión, esto es que no funcione ni api/Customers ni api.example.com, garantizamos que no habrá ninguna sorpresa con los clientes ni dejarán de funcionar cuando subamos de versión.

El versionado por URI Path nos permite cambiar drásticamente nuestra API porque todo lo que sigue a v<versión> podría cambiar de una versión a otra. Es decir, podríamos pasar de api/v1/Customers, ya no a api/v2/Customers sino a api/v2/Clientes. Lógicamente, esta libertad de cambios supone que los clientes tendrían que actualizar su código para apuntar a las nuevas rutas, y no sólo para cambiar el segmento de la versión sino también para cambiar el resto de la URL.

Además, cualquier bookmark, permalink o similar dejará de funcionar y, si no queremos romper nada, tendremos que configurar nuestra aplicación para devolver un 302 Found o un 301 Moved permanently.

Por otro lado, desde el punto de vista teórico (y poniéndonos la gorra de restafari), tenemos 2 endpoints que, en vez de devolver 2 recursos, devuelven el mismo con una representación distinta. Alguien te dirá que eso no es correcto (no seré yo).

Una solución para intentar atajar globalmente este tipo de versionado en nuestra aplicación sería indicar a la ruta que sólo buscará controladores en un espacio de nombres concreto, pero Web API no permite especificar constraint namespaces en una ruta tal y cómo sí lo hace MVC, luego tendremos que crear una implementación propia de IHttpControllerSelector que entienda rutas con la plantilla api/{namespace}/{controller}/{id} y busque controladores sólo en el espacio de nombres especificado en {namespace}.

La implementación está aquí https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/ y bastaría con añadir una ruta por convención y crear los controladores en un espacio de nombres por versión. De este modo tanto api/v1/Customers como api/v2/Customers funcionarían correctamente y no habría ambigüedad en la búsqueda del controlador.

var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{namespace}/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
config.Services.Replace(typeof(IHttpControllerSelector), 
	new NamespaceHttpControllerSelector(config));

Cada controlador en su espacio de nombres por versión:

namespace ConsoleApplication1.Controllers.v1
{
    public class CustomersController : ApiController
}	
namespace ConsoleApplication1.Controllers.v2
{
    public class CustomersController : ApiController
}

Ya lo dice el post de donde se tomó el ejemplo de IHttpControllerSelector, y es que quizás habría que hacer un fallback a un número de versión anterior si no se encuentra ningún controlador que coincida con el namespace buscado, porque si no cada vez que subiéramos versión tendríamos que copiar todos los controladores de la anterior versión a la nueva, aunque sólo haya cambiado uno. Es decir, si sólo ha cambiado api/v2/Customers quiero que api/v2/Orders siga ejecutando api/v1/Orders y no tener que copiar OrdersControllers al espacio de nombres v2 aunque no haya sufrido ningún cambio. Al final del post veremos como lo hemos resuelto en nuestro caso.

URI Parameter

En este método la versión se especifica como un parámetro de la querystring. Por ejemplo: api/Customers?version=2

Tanto URI Path como URI Parameter podrían ser la única opción disponible si queremos dar soporte a clientes que no pueden manipular las cabeceras de la petición.

Para implementar este tipo de versionado en ASP.NET Web API, tomaremos prestada la idea desde https://www.asp.net/web-api/overview/releases/whats-new-in-aspnet-web-api-21 donde hay un link a un ejemplo para versionar a través de attribute routing http://aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/RoutingConstraintsSample/ReadMe.txt

Tomando el ejemplo como base, lo haremos nuestro e iremos agregando código a medida que vayamos viendo el resto de opciones de versionado (al final todo morirá en filtrar una ruta en función de la presencia de un valor en la petición). Por ahora, sólo queremos controlar api/Customers?version=1 y api/Customers?version=2.

Los métodos de acción quedarían así:

public class CustomersController : ApiController
{
    [VersionedRoute("api/Customers", 1)]
    public IEnumerable<Customer> GetCustomers1()
    {
        return new List<Customer>()
        {
        };
    }
    [VersionedRoute("api/Customers", 2)]
    public IEnumerable<Customer2> GetCustomers2()
    {
    }
}

Código de VersionedRoute (siempre será el mismo con independencia del tipo de versionado):

class VersionedRoute : RouteFactoryAttribute
{
    private readonly int _allowedVersion;
    private const int DefaultVersion = 1;
    public VersionedRoute(string template)
     : this(template, DefaultVersion)
    {
    }
    public VersionedRoute(string template, int allowedVersion)
        : base(template)
    {
        _allowedVersion = allowedVersion;
    }
    public override IDictionary<string,object> Constraints => new HttpRouteValueDictionary
    {
        { "version", new VersionConstraint(_allowedVersion, DefaultVersion) }
    };
}

Código de VersionConstraint:

class VersionConstraint : IHttpRouteConstraint
{
    private readonly int _allowedVersion;
    private readonly int _defaultVersion;
    public VersionConstraint(int allowedVersion, int defaultVersion)
    {
        _allowedVersion = allowedVersion;
        _defaultVersion = defaultVersion;
    }
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string,object> values, HttpRouteDirection routeDirection)
    {
        if (routeDirection != HttpRouteDirection.UriResolution)
        {
            return false;
        }
        var version = GetVersionFromQueryString(request) ?? _defaultVersion;
        return version == _allowedVersion;
    }
    private static int? GetVersionFromQueryString(HttpRequestMessage request)
    {
        int version;
        if (int.TryParse(GetQueryStringValue(request, "version"), out version))
        {
            return version;
        }
        return null;
    }
    private static string GetQueryStringValue(HttpRequestMessage request, string key)
    {
        var values = request.GetQueryNameValuePairs();
        if (values.All(p => !string.Equals(p.Key, key, StringComparison.OrdinalIgnoreCase)))
        {
            return null;
        }
        return values.Single(p => string.Equals(p.Key, key, StringComparison.OrdinalIgnoreCase)).Value;
    }
}

Custom Header

Con este método la idea es incluir en la petición una cabecera personalizada del estilo X-Version o similar. El prefijo X- es una convención para cabeceras personalizadas, que no son parte del estándar.

Aunque este método no ensucia la URL (separa la información de versión del área de superficie expuesta por nuestra Web Api), el inconveniente es que ya no podremos copiar y pegar la URL, agregar un favorito o pasar la dirección por correcto electrónico. Ahora el cliente tiene que poder enviar una cabecera personalizada en la petición y nosotros, como desarrolladores, tendremos que utilizar Fiddler o una herramienta similar.

Para su implementación, tendremos que modificar la clase VersionConstraint para soporte adicionalmente el versionado por Custom Header.

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string,object> values, HttpRouteDirection routeDirection)
{
    if (routeDirection != HttpRouteDirection.UriResolution)
    {
        return false;
    }
    var version = GetVersionFromQueryString(request);
    if (version != null)
    {
        return version == _allowedVersion;
    }
    version = GetVersionFromHeaders(request) ?? _defaultVersion;
    return version == _allowedVersion;
}
private static int? GetVersionFromHeaders(HttpRequestMessage request)
{
    IEnumerable<string> headerValues;
    if (request.Headers.TryGetValues("x-version", out headerValues))
    {
        int version;
        if (int.TryParse(headerValues.First(), out version))
        {
            return version;
        }
    }
    return null;
}

Content Negotiation

Se basa en el uso de la cabecera Accept y un MIME Type personalizado donde se especifica que versión del recurso queremos obtener.

Se presenta en 2 distintas formas:

  • application/json; version=1
  • application/vnd.<compañía>.<recurso>+json; version=1

En la primera se usa el tipo MIME estándar y se le agrega un parámetro de versión.

En la segunda se usa un tipo MIME personalizado donde se especifica tanto la versión como el formato deseado.

Por cierto, vnd es de vendor https://en.wikipedia.org/wiki/Media_type

Para implementar ambas, ASP.NET nos ayudará porque cualquier cabecera acepta parámetros y el framework los parsea automáticamente:

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string,object> values, HttpRouteDirection routeDirection)
{
    if (routeDirection != HttpRouteDirection.UriResolution)
    {
        return false;
    }
    var version = GetVersionFromQueryString(request);
    if (version != null)
    {
        return version == _allowedVersion;
    }
    version = GetVersionFromHeaders(request);
    if (version != null)
    {
        return version == _allowedVersion;
    }
    version = GetVersionFromAcceptHeader(request) ?? _defaultVersion;
    return version == _allowedVersion;
}
private static int? GetVersionFromAcceptHeader(HttpRequestMessage request)
{
    var accept = request.Headers.Accept.SingleOrDefault(a =>; a.Parameters.Any(p => string.Equals(p.Name, "version", StringComparison.OrdinalIgnoreCase)));
    if (accept != null)
    {
        int version;
        if (int.TryParse(accept.Parameters.Single(p =>; string.Equals(p.Name, "version", StringComparison.OrdinalIgnoreCase)).Value, out version))
        {
            return version;
        }
    }
    return null;
}

Cuando se complica la implementación es si queremos usar la segunda opción y queremos seguir aceptando la negociación de contenido. Por ejemplo, y para nuestro tipo Customer, las siguientes peticiones devolverán siempre json (porque es el primer MediaTypeFormatter que está registrado en GlobalConfiguration.Configuration.Formatters).

  • Accept: application/vnd.example.com+json; version=2
  • Accept: application/vnd.example.com+xml; version=2

Si lo primero que hacemos al empezar un proyecto con Web API es eliminar el XmlFormatter o, lo que es lo mismo, sólo devolver json, lo anterior no es un problema. Ahora bien, si tenemos que soportar negociación de contenido, tendremos que agregar un MediaTypeFormatter para cada tipo y para cada representación. Un ejemplo completo se puede ver en http://robertgaut.com/Blog/2007/Four-Ways-to-Version-Your-MVC-Web-API

Como muestra el post, lo hay que hacer es crear un par de MediaTypeFormatter (uno para json y otro para xml) y registrar nuestros tipos. Yo entiendo que, si se opta finalmente por este tipo de versionado, la reflexión sería una solución digna frente a tener que registrar manualmente todos los tipos de nuestra aplicación. En cualquier caso, para nuestro ejemplo:

public static class TypeExtensions
{
    public static Type GetTypeFromIEnumerable(this Type type)
    {
        return IsIEnumerable(type) ? type.GetGenericArguments()[0] : null;
    }
    private static bool IsIEnumerable(Type type)
    {
        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
    }
}
class TypedXmlMediaTypeFormatter : XmlMediaTypeFormatter
{
    private readonly Type _resourceType;
    public TypedXmlMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType)
    {
        _resourceType = resourceType;
        SupportedMediaTypes.Clear();
        SupportedMediaTypes.Add(mediaType);
    }
    public override bool CanReadType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
    public override bool CanWriteType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
}
class TypedJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
    private readonly Type _resourceType;
    public TypedJsonMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType)
    {
        _resourceType = resourceType;
        SupportedMediaTypes.Clear();
        SupportedMediaTypes.Add(mediaType);
    }
    public override bool CanReadType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
    public override bool CanWriteType(Type type)
    {
        return _resourceType == type || _resourceType == type.GetTypeFromIEnumerable();
    }
}

Y el registro:

config.Formatters.Insert(0,
	new TypedXmlMediaTypeFormatter(typeof(Customer),
	new MediaTypeHeaderValue("application/vnd.example.com+xml")));
config.Formatters.Insert(0,
	new TypedJsonMediaTypeFormatter(typeof(Customer),
	new MediaTypeHeaderValue("application/vnd.example.com+json")));
config.Formatters.Insert(0,
	new TypedXmlMediaTypeFormatter(typeof(Customer2),
	new MediaTypeHeaderValue("application/vnd.example.com+xml")));
config.Formatters.Insert(0,
	new TypedJsonMediaTypeFormatter(typeof(Customer2),
	new MediaTypeHeaderValue("application/vnd.example.com+json")));

En este punto ya tenemos un atributo VersionedRoute que ha quedado bastante aparente, pero seguimos sin resolver el problema de cómo proceder cuando subamos de versión para los endpoints que no tienen cambios. Es decir, si decoramos una acción del controlador con VersionedRoute(“…”, 2), ésta sólo responderá cuando el cliente especifica la versión 2. Sin embargo, si mi API actual está en la versión 3 y el anterior endpoint no ha cambiado, no quiero tener que cambiar manualmente todos estos endpoints sin cambios a la versión 3, de hecho, no debería, si lo hago voy a romper con todos los clientes que no actualicen su código a la última versión, luego quiero que ese endpoint sin cambios responda tanto a la versión 3 como a la versión 2.

Para solucionarlo, la idea pasa porque cada endpoint sepa reconocer a cuáles versiones puede hacer fallback sin riesgo alguno. Para ello, usaremos la siguiente clase que a grandes rasgos hace:

  • Establecer el actual número de versión (el valor más alto, en mi caso el endpoint más digievolucionado).
    • Este valor hay que mantenerlo manualmente y además es el número de versión por defecto que se usará cuando en VersionedRoute no lo especifiquemos.
  • Buscar por reflexión rutas versionadas y calcular dinámicamente a que versiones de la misma ruta puede hacer fallback.
internal class Versioning
{
    public const int CurrentVersion = 3;
    public static readonly Lazy<IEnumerable<FallbackRoute>> FallbackRoutes;
    static Versioning()
    {
        FallbackRoutes = new Lazy<IEnumerable<FallbackRoute>>(GetFallbackRoutes);
    }
    private static IEnumerable<FallbackRoute> GetFallbackRoutes()
    {
        var fallbackRoutes = GetFallbackRoutesFromVersionedRoutes();
        foreach (var routeTemplate in fallbackRoutes.Select(p => p.RouteTemplate).Distinct())
        {
            var lastFallbackRouteIndexFound = 0;
            for (var version = CurrentVersion; version > 0; version--)
            {
                if (fallbackRoutes.Any(MatchFallbackRoute(routeTemplate, version)))
                {
                    lastFallbackRouteIndexFound = version;
                    continue;
                }
                fallbackRoutes.Single(MatchFallbackRoute(routeTemplate, lastFallbackRouteIndexFound))
                    .AddFallbackVersion(version);
            }
        }
        return fallbackRoutes;
    }
    private static IEnumerable<FallbackRoute> GetFallbackRoutesFromVersionedRoutes()
    {
        return Assembly.GetExecutingAssembly().GetTypes()
            .SelectMany(t => t.GetMethods())
            .Where(m => m.GetCustomAttributes(typeof(VersionedRoute), false).Length > 0)
            .Select(m =>
            {
                var route = m.GetCustomAttribute<VersionedRoute>();
                return new FallbackRoute(route.Template, route.AllowedVersion)
                    ;
            }).ToList();
    }
    private static Func<FallbackRoute, bool> MatchFallbackRoute(string routeTemplate, int allowedVersion)
    {
        return f => (f.RouteTemplate == routeTemplate) && (f.AllowedVersion == allowedVersion);
    }
    public static FallbackRoute GetFallbackRoute(string routeTemplate, int allowedVersion)
    {
        return FallbackRoutes.Value.SingleOrDefault(MatchFallbackRoute(routeTemplate, allowedVersion));
    }
}

FallbackRoute es una clase que simplemente guardar una ruta y a que versiones atiende:

internal class FallbackRoute
{
    public FallbackRoute(string routeTemplate, int allowedVersion)
    {
        RouteTemplate = routeTemplate;
        AllowedVersion = allowedVersion;
        FallbackVersions = new List();
    }
    public string RouteTemplate { get; }
    public int AllowedVersion { get; }
    public IEnumerable FallbackVersions { get; }

    public bool HasFallbackVersion(int version)
    {
        return FallbackVersions.Contains(version);
    }

    public void AddFallbackVersion(int version)
    {
        ((IList)FallbackVersions).Add(version);
    }
}

Y en VersionedConstraint hay que cambiar el código del método Match para que sepa a qué rutas de fallback responderá:

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
    if (routeDirection != HttpRouteDirection.UriResolution)
    {
        return false;
    }
    var version = GetVersion(request) ?? _defaultVersion;
    var fallbackRoute = Versioning.GetFallbackRoute(route.RouteTemplate, _allowedVersion);
    return version == _allowedVersion || (fallbackRoute != null && fallbackRoute.HasFallbackVersion(version));
}

Con estos cambios y asumiendo que CurrentVersion = 3, funcionarán las siguientes rutas:

  • VersionedRoute(“api/Customers”, 1) responderá a la versión 1.
  • VersionedRoute(“api/Customers”, 3) responderá a la versiones 2 y 3.
  • VersionedRoute(“api/Orders”, 2) responderá a la versiones 2 y 1.
  • VersionedRoute(“api/Orders”) responderá a la versión 3.

El cómo organizar el código en namespaces atendiendo a distintas representaciones versionadas de una misma entidad es cosa de cada uno, es decir, esta solución no obliga (y tampoco facilita) el buscar controladores en un espacio de nombres concreto, eso queda a libre elección del consumidor.

Si por algún motivo te parece una buena solución, te recomendaría usar el código de github https://github.com/panicoenlaxbox/WebApiVersioning donde, seguro, estará la versión más actualizada y que funciona.

Un saludo!

martes, 1 de marzo de 2016

Binding de listas en ASP.NET MVC

El binding de listas en ASP.NET MVC puede ser algo bastante sencillo y automático (en el caso de tipos simples), o por el contrario algo lioso y poco intuitivo (en el caso de tipos complejos).

Para bindear tipos simples tan sólo hay que seguir la norma HTTP de repetir el nombre del parámetro. Por ejemplo para recibir un IEnumerable foo, podríamos llamarlo con ?foo=Bar&foo=Baz y todo funcionará a la perfección.

Para el caso de tipos complejos, igualmente todo funcionará siempre y cuando el nombre de los parámetros sea el adecuado y en función del ValueProvider que recoja los datos.

Por ejemplo, si esperamos recibir un objeto Person:

    
public class Person
    {
        public string Name { get; set; }
        public IEnumerable<Address> Addresses { get; set; }
    }

    public class Address
    {
        public string City { get; set; }
        public string Country { get; set; }
    }

La url que deberíamos enviar por GET sería:

name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA

Fíjate que la norma es propiedad[índice].propiedad, por ejemplo addresses[0].country

Lo más sencillo es utilizar los helpers de Html en la vista para que los atributos name se generen acorde a esta norma.

Sin embargo, ¿Cómo enviamos un triste objeto Javascript cumpliendo esta norma?

En mi caso usaré jQuery.

Si es por POST no hay ningún problema. Cualquier de las siguientes opciones es válida (la primera funciona por la existencia de JQueryFormValueProvider que sabe interpretar cómo serializa jQuery el payload, y la segunda por JsonValueProvider).

            var data = {
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            };
            $.ajax({
                type: "POST",
                url: url,
                data: data
            });
            data = JSON.stringify(john);
            $.ajax({
                type: "POST",
                url: url,
                data: data,
                contentType: "application/json"
            });

Sin embargo, si queremos enviar ese mismo objeto por GET no funcionará.

Si probamos con el método $.param de jQuery (el que nos recomiendan para obtener un objeto serializado y enviarlo por querystring) el resultado no es el esperado:

$.param({
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            });
name=John
&addresses[0][city]=Madrid
&addresses[0][country]=Spain
&addresses[1][city]=New York
&addresses[1][country]=USA

Se parece bastante a lo espera recibir ASP.NET MVC, pero no es lo mismo. Eso lo entiende JQueryFormValueProvider, pero para GET ese proveedor no actúa.

Para solucionar esto (y este es el propósito del post) podemos usar la siguiente función (donde aumentar el objeto String con format no debería ir ahí, pero para el ejemplo y si no vas a usar la función format en ningún otro sitio, pues podría valer).

        var serialize = (function () {

            if (typeof String.format != 'function') {
                String.format = function () {
                    var format = arguments[0];
                    for (var i = 0; i < arguments.length - 1; i++) {
                        var reg = new RegExp('\\{' + i + '\\}', 'gm');
                        format = format.replace(reg, arguments[i + 1]);
                    }
                    return format;
                };
            }

            function serialize(obj) {
                var s = _serialize(obj);
                if (s !== "") {
                    s = s.substring(1, s.length);
                }
                return s;
            }

            function _serialize(obj, prefix) {
                prefix = prefix || "";
                var s = "";
                if (typeof obj !== 'object') {
                    return String.format('&{0}={1}', encodeURIComponent(prefix), encodeURIComponent(obj));
                }
                for (var prop in obj) {
                    if (!obj[prop]) {
                        continue;
                    }
                    if (Array.isArray(obj[prop])) {
                        for (var index = 0; index < obj[prop].length; index++) {
                            s += _serialize(obj[prop][index], prefix + (prefix ? '.' : '') + prop + '[' + index + ']');
                        }
                    } else if (typeof obj[prop] === "object") {
                        s += _serialize(obj[prop], (prefix ? '.' : "") + prop);
                    } else {
                        s += String.format('&{0}={1}', encodeURIComponent((prefix ? prefix + '.' : '') + prop), encodeURIComponent(obj[prop]));
                    }
                }
                return s;
            }

            return serialize;
        })();

Ahora una llamada con:

serialize({
                name: "John",
                addresses: [
                    { city: "Madrid", country: "Spain" },
                    { city: "New York", country: "USA" }
                ]
            });

Devolverá una query string que al enviar como parámetro en GET será conforme a la norma que establece ASP.NET MVC para el binding de listas de tipos complejos.

name=John
&addresses[0].city=Madrid
&addresses[0].country=Spain
&addresses[1].city=New York
&addresses[1].country=USA

Un saludo!

jueves, 19 de marzo de 2015

Distintas opciones para validar el modelo en ASP.NET MVC

La validación del modelo en ASP.NET MVC es un característica muy importante ya que garantiza que la lógica de la entidad que recibe el controlador es conforme a la reglas de validación que hayamos definido para la entidad.

Hace tiempo publiqué un post relacionado con la validación del modelo pero estaba más centrado en cómo implementar la validación en el lado del cliente Validación en cliente en ASP.NET MVC. Sin embargo, repasando ahora las distintas opciones que tenemos disponibles para validar el modelo en ASP.NET MVC, reconozco que al ser varias no siempre es sencillo decidir cual de ellas es más apropiada según qué escenario.

Que yo sepa hay hasta 4 distintas formas de implementar la validación del modelo:

La validación del modelo es lanzada por los proveedores de validación del modelo, que inicialmente es una colección con 3 proveedores:

private void DebugModelValidatorProviders()
{
foreach (var provider in ModelValidatorProviders.Providers)
{
System.Diagnostics.Debug.WriteLine(provider.GetType().Name);
}
//DataAnnotationsModelValidatorProvider
//DataErrorInfoModelValidatorProvider
//ClientDataTypeModelValidatorProvider
}

El proveedor DataAnnotationsModelValidatorProvider es quien se encargará de llamar a CustomValidationAttribute, ValidationAttribute e IValidatableObject. Por otro lado, DataErrorInfoModelValidatorProvider llamará a IDataErrorInfo.

Sabiendo ya que opciones tenemos ¿Cuál utilizar?

Reconozco que en mi caso he usado siempre ValidationAttribute, pero intentaré en este post sopesar pros y contras de todas las opciones disponibles (así después no tendré que volver a hacer el ejercicio de reflexión si más adelante lo necesito).

Para todos los ejemplos utilizaremos un ViewModel muy sencillo:

public class PersonViewModel
{
[Required]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
public int Age { get; set; }
}


Un controlador
using System.Web.Mvc;
using MyValidation.ViewModels;

namespace MyValidation.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new PersonViewModel();
return View(model);
}

[HttpPost]
public ActionResult Index(PersonViewModel model)
{
return View(model);
}
}
}

Y una vista:
@model MyValidation.ViewModels.PersonViewModel
<div class="container">
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(m => m.FirstName, new { @class = "control-label" })
@Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.FirstName)
</div>
<div class="form-group">
@Html.LabelFor(m => m.Age, new { @class = "control-label" })
@Html.TextBoxFor(m => m.Age, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Age)
</div>
<button type="submit" class="btn btn-default">Submit</button>
}
</div>


CustomValidationAttribute

La idea es crear un método estático por cada propiedad que se quiera validar. Si además queremos validar la entidad como conjunto, crearemos igualmente otro método estático. Después decoraremos la entidad y/o cada propiedad con el atributo CustomValidation donde especificaremos que método tiene que llamar (en el ejemplo que viene a continuación he llamado a los métodos IsValid, IsFirstNameValid y IsAgeValid pero podrían tener cualquier otro nombre).
using System.ComponentModel.DataAnnotations;

namespace MyValidation.ViewModels
{
[CustomValidation(typeof(PersonCustomValidation), "IsValid", ErrorMessage = "No eres yo")]
public class PersonViewModel
{
[CustomValidation(typeof(PersonCustomValidation), "IsFirstNameValid", ErrorMessage = "Nombre no es válido")]
[Required]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[CustomValidation(typeof(PersonCustomValidation), "IsAgeValid")]
[Required]
[Display(Name = "Edad")]
public int Age { get; set; }
}

public class PersonCustomValidation
{
public static ValidationResult IsValid(PersonViewModel person)
{
if (person.FirstName == "Sergio" && person.Age == 39)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}

public static ValidationResult IsFirstNameValid(string firstName)
{
if (firstName.IndexOf(" ") == -1)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}

public static ValidationResult IsAgeValid(int age)
{
if (age > 18)
{
return ValidationResult.Success;
}
return new ValidationResult("No eres mayor de edad");
}
}
}

Lo más relevante del código es que si devolvemos un ValidationResult con errorMessage establecido a null, tomará el especificado en el atributo CustomValidation, sino prevalecerá el que hayamos pasado a ValidationResult. También es importante ver que para que se llame al método que valida la entidad como un conjunto, tienen que haberse superado con éxito todas las validaciones de propiedades, en caso contrario no se llamará.

Lógicamente el código (no sólo de éste sino de los futuros ejemplos) es mejorable. No se valida por ejemplo si el parámetro firstName es null, etc. Por eso he metido [Required], para asegurarme no tener que escribir más código del necesario en los ejemplos :)

¿Qué no me gusta de CustomValidationAttribute?

Claramente tener que hardcodear el nombre de los métodos. Sólo por eso no lo utilizaré.

ValidationAttribute
using System.ComponentModel.DataAnnotations;

namespace MyValidation.ViewModels
{
[YouAreNotMe(ErrorMessage = "No eres yo")]
public class PersonViewModel
{
[Required]
[Display(Name = "Nombre")]
[StringWithoutSpaces(ErrorMessage = "{0} no puede tener espacios")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
[LegalAge(ErrorMessage = "No eres mayor de edad")]
public int Age { get; set; }
}

public class StringWithoutSpacesAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value.ToString().IndexOf(" ") == -1)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}
}

public class LegalAgeAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (((int)value) > 18)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}
}

public class YouAreNotMeAttribute: ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var person = (PersonViewModel) value;
if (person.FirstName == "Sergio" && person.Age == 39)
{
return ValidationResult.Success;
}
return new ValidationResult(null);
}
}
}

¿Qué me gusta de ValidationAttribute?

  • Ganamos seguridad de tipos (sobre todo comparado con CustomValidation).
  • Mejoramos la expresividad del código.
  • Cada atributo sólo tiene una responsabilidad, no es una colección de métodos estáticos en una misma clase como con CustomValidation.
  • Mantiene una simetría con el resto de validadores que vienen de serie.

En mi opinión, es una buena opción para validar propiedades pero no tanto para validar la entidad como conjunto.

IValidatableObject

Con IValidatableObject, en vez de trabajar con atributos ahora tenemos que implementar esta interface en clase de entidad que queremos validar.

No sirve para validar propiedades de forma individual, sino que está pensada para validar la entidad como un conjunto, es decir, si queremos validar propiedades tendremos que seguir optando por alguna de las opciones que lo soportan (de las expuestas todas excepto justo IValidatableObject).
public class PersonViewModel:IValidatableObject
{
[Required]
[Display(Name = "Nombre")]
[StringWithoutSpaces(ErrorMessage = "{0} no puede tener espacios")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
[LegalAge(ErrorMessage = "No eres mayor de edad")]
public int Age { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var result = new List<ValidationResult>();
if (FirstName == "Sergio" && Age == 39)
{
result.Add(ValidationResult.Success);
return result;
}
result.Add(new ValidationResult("No eres yo"));
if (FirstName != "Sergio")
{
result.Add(new ValidationResult("Nombre no es válido",new[] {"FirstName"}));
}
if (Age != 39)
{
result.Add(new ValidationResult("Edad no es válida", new[] { "Age" }));
}
return result;
}
}

¿Qué me gusta de IValidatableObject?

  • Qué no hay que castear un parámetro para acceder a los valores de lo que estamos validando, al estar implementado en la propia clase del modelo, podemos acceder a las propiedades de la instancia sin más.
  • Qué puede devolver una lista de ValidationResult, pudiendo así afinar (si es preciso) que propiedades contienen errores con la sobrecarga de ValidationResult que espera un IEnumerable<string> memberNames.

Por ahora (y si nadie me convence de lo contrario) es mi opción preferida para validar una entidad como conjunto.

IDataErrorInfo

Con franqueza, es para mí la mayor desconocida. No la utilizado nunca y creo no la utilizaré. Es muy distinta a cualquiera de las otras aproximaciones, no sirve para validar la entidad como un conjunto (sólo para validar propiedades), obliga a devolver un mensaje (si localizas aplicaciones sabrás que esto es un problema)… pero en cualquier caso, un ejemplo sencillo servirá para completar el post y cumplir con lo prometido.
public class PersonViewModel:IDataErrorInfo
{
[Required]
[Display(Name = "Nombre")]
public string FirstName { get; set; }
[Required]
[Display(Name = "Edad")]
public int Age { get; set; }

public string this[string columnName]
{
get
{
if (columnName == "FirstName")
{
if (FirstName.IndexOf(" ") != -1)
{
return "Nombre no es válido";
}
}
else if (columnName == "Age")
{
if (Age < 18)
{
return "No eres mayor de edad";
}
}
return null;
}
}

public string Error { get; private set; }
}

Como conclusión, a día de hoy mi resumen es el siguiente:

  • Si tengo que validar propiedades utilizo ValidationAttribute.
  • Si tengo que validar una entidad utilizo IValidatableObject.

Un saludo!

lunes, 16 de marzo de 2015

Pdf en ASP.NET MVC - MvcRazorToPdf

Tarde o temprano, generar contenido pdf en una aplicación web es algo que nos tocará hacer.

En el caso de ASP.NET MVC, yo he optado por el paquete de Nuget MvcRazorToPdf

Esta solución se basa en iTextSharp (el port de .NET de iText) e iTextSharp XML Worker, un add-on para iText que permite convertir HTML a PDF.

Lo que agrega MvcRazorToPdf es un nuevo tipo PdfActionResult que hereda de ViewResult. De este modo, crear un documento PDF es tan sencillo como crear un método de acción que devuelva una instancia del tipo PdfActionResult (que procesará nuestra vista con normalidad y convertirá a PDF el HTML resultante).

return new PdfActionResult(model);

Actualmente parece haber un problema con el paquete de Nuget y la versión de sus dependencias. Para solucionarlo (aunque entiendo que estará solucionado en futuras versiones del paquete), bastará con ejecutar los siguientes comandos en la consola de Nuget:

Install-Package iTextSharp -version 5.5.3

Install-Package itextsharp.xmlworker -version 5.5.3

Además del ejemplo básico, PdfActionResult tiene algunas sobrecargas que pueden resultarnos muy útiles, por ejemplo hay uno donde tenemos acceso al documento que se está generando, así como al writer utilizado. Lo cierto es que no quisiera tener que lidiar mucho con la generación en bruto de PDF a través de iTextSharp, pero para algunos escenarios podría ser necesario:

return new PdfActionResult(model, (writer, document) => document.SetPageSize (PageSize.A4));

Incluso podemos especificar que directamente devuelva el PDF como un archivo para su descarga:

return new PdfActionResult(model, (writer, document) => document.SetPageSize (PageSize.A4))

{

    FileDownloadName = "Example.pdf"

};

También podemos generar ficheros PDF en el servidor (gracias a un método extensor de ControllerContext) y ya después hacer con él lo que creamos oportuno:

byte[] output = ControllerContext.GeneratePdf();

var path = Server.MapPath("~/App_Data/Order.pdf");

if (System.IO.File.Exists(path))

{

    System.IO.File.Delete(path);

}

System.IO.File.WriteAllBytes(path, output);

return File(path, "application/pdf", Path.GetFileName(path));

Si hasta aquí todo ha sido contar las bondades de MvcRazorToPdf, la parte negativa la encontramos no ya en el propio paquete, sino en su dependencia con iTextSharp XML Worker. Durante el proceso de convertir el HTML a PDF, hay perdidas…He empleado más tiempo en bregar con las rarezas de iTextSharp XML Worker, que en todo lo demás. Cierto es que encontré tarde la documentación de iTextSharp XML Worker e incluso un editor WYSIWYG, pero cosas como tener que utilizar el atributo valign en vez de la propiedad CSS vertical-align, o tener que generar para las imágenes rutas absolutas en vez de relativas, son cosas que tendrás que aprender con el manido método ensayo-error. En cualquier caso, para algo sencillo seguiré apostando por esta solución.

Un saludo!

jueves, 26 de febrero de 2015

Right-to-Left

En un proyecto reciente hemos tenido el requerimiento de tener que soportar la orientación del texto de derecha a izquierda, y como intuyo que no será la última aplicación en la que tengamos que dar soporte a rtl, dejaré aquí algunas recetas y reflexiones que he ido acumulando durante el desarrollo de tan “exótica” feature.

Lo primero, como en todo, es entender que tenemos que hacer. Aunque suene de perogrullo, cambiar algo tan arraigado en nuestra cabeza como es el sentido de la escritura, puede suponer un esfuerzo de concentración extra para no perder ningún detalle. De hecho, no son pocas las veces que he llamado al cliente para que él mismo corroborara si algo tan simple como un texto o un imagen era acorde a rtl.

Al comienzo, gran parte del trabajo sucio lo hará el atributo dir de HTML. Este atributo nos permite especificar la orientación del texto de un elemento (ltr – por defecto – o rtl). Lo más sencillo es establecerlo en el body (sólo especificándolo de nuevo en los elementos en los que queramos sobrescribir el valor heredado, si es que tenemos esa necesidad). Además de dir como atributo, tenemos direction como propiedad CSS que cumple la misma función.

Por ejemplo el siguiente documento HTML se vería así si el valor de dir fuera rtl.

<body dir="rtl">

    <p>Hola mundo</p>

    <table>

        <tr>

            <td>Primera celda</td>

            <td>Segunda celda</td>

        </tr>

    </table>

</body>

image

Otro punto a tener en cuenta es la propiedad CSS float. Donde dije digo, digo Diego. Es decir, en rtl tendremos que flotar en el sentido opuesto al valor original. En el siguiente ejemplo (y aunque dir valga rtl) los divs seguirán flotante a la izquierda.

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <title>Document</title>

    <style>

        div {

            margin-right: 10px;

            float: left;

        }

    </style>

</head>

<body dir="rtl">

    <div>Div 1</div>

    <div>Div 2</div>

</body>

</html>

Para solucionarlo (así al menos lo he hecho yo), lo que hago es volver a definir la regla CSS (mismo selector pero definido después del original, así prevalecerá) y sobreescribir las propiedades que son necesarias. Digo “necesarias” en plural porque no sólo tenemos que preocuparnos por float, sino también de todas aquellas propiedades que hayamos utilizado en alguno de ambos lados del elemento (léase margin-left, margin-right, padding-left, etc.). Dicho esto, style quedaría así:

<style>

    div {

        margin-right: 10px;

        float: left;

    }

 

    div {

        margin-right: 0;

        margin-left: 10px;

        float: right;

    }

</style>

Si en algún momento la cascada de CSS nos jugara una mala pasada, siempre podemos hacer una regla con una especificad mayor que la original (aunque lo más sencillo es incluir todas estas reglas en un fichero rtl.css y asegurarse de que lo cargamos después de los estilos originales). Por ejemplo:

body[dir="rtl"] div {

    margin-right: 0;

    margin-left: 10px;

    float: right;

}

Además de float, el posicionamiento absoluto, relativo o fijo también tocará revisarlo. Si la idea era anclar algo en la esquina superior derecha (por ejemplo una x para cerrar), ahora deberemos hacerlo en la esquina superior izquierda, luego pasaremos de utilizar la propiedad left a utilizar la propiedad right.

    <style>

        .close-button {

            right: 0;

            top: 0;

        }

 

        .close-button {

            left: 0;

            right: auto;

        }

    </style>

Ya en Javascript, aunque está bien eso de separar contenido, estilo y comportamiento, me ha pasado que en algunos plugins de jQuery estaba dando estilo directamente a través de código. En este caso y para aplicar un estilo u otro en función de dir podríamos utilizar esta función:

    <script>

        function isRtl() {

            return document.body.dir === "rtl";

        }

    </script>

Algo que también tenemos que vigilar es la orientación de las imágenes. Por ejemplo, si tenemos una imagen con una flecha apuntando a la izquierda para indicar que se abrirá un panel en esa dirección, si ahora el panel se abre en la dirección opuesta lógicamente la flecha tendrá que estar apuntado en la dirección adecuada.

En este y otros muchos casos y para intentar no volverme muy loco, he creado un par de métodos de extensión para facilitar la tarea:

using System.Threading;

using System.Web;

using System.Web.Mvc;

using Antlr.Runtime.Misc;

 

namespace WebApplication1

{

    public enum Direction

    {

        Left,

        Right

    }

 

    public static class HtmlLanguageExtensions

    {

        public static HtmlString Dir(this HtmlHelper that)

        {

            return new HtmlString(!IsRtl() ? "ltr" : "rtl");

        }

 

        public static HtmlString Direction(

            this HtmlHelper that,

            Func<string> left,

            Func<string> right)

        {

            return new HtmlString(!IsRtl() ? left() : right());

        }

 

        public static HtmlString Direction(

            this HtmlHelper that,

            Direction direction)

        {

            string retval;

            if (!IsRtl())

            {

                retval =

                    direction == WebApplication1.Direction.Left ?

                    "left" :

                    "right";

            }

            else

            {

                retval =

                    direction == WebApplication1.Direction.Left ?

                    "right" :

                    "left";

            }

            return new HtmlString(retval);

        }

 

        public static HtmlString Lang(this HtmlHelper that)

        {

            return new HtmlString(Culture.GetCurrentTwoLetterISOLanguageName());

        }

 

        private static bool IsRtl()

        {

            return Thread.CurrentThread.CurrentUICulture.TextInfo.IsRightToLeft;

        }

    }

}

Ahora en vez de escribir esto:

<img src="~/Content/images/flecha-izquierda.svg" />

Escribiría esto otro:

<img src="~/Content/images/flecha-@(Html.Direction(() => "izquierda", () => "derecha")).svg" />

Y en el caso de que los literales a devolver fueran directamente “left” o “right”, escribiría esto:

<img src="~/Content/images/arrow-@(Html.Direction(Direction.Left).svg" />

Por último, estamos utilizando Bootstrap como framework CSS y de serie no soporta rtl. Si utilizamos por ejemplo la regla .list-inline veríamos esto:

image

Algo raro está pasando… esa scrollbar…, el estilo de .list-inline es:

.list-inline {

  padding-left: 0;

  margin-left: -5px;

  list-style: none;

}

.list-inline > li {

  display: inline-block;

  padding-right: 5px;

  padding-left: 5px;

}

Gracias a que siempre hay algún buen samaritano con un repo en github (desde aquí le quiero dar gracias al autor) la solución pasa por incluir una segunda hola de estilos que de soporte a rtl en Bootstrap. El repo es https://github.com/morteza/bootstrap-rtl. Para utilzar esta nueva hoja de estilos tenemos que agregar el enlace después de haber cargado Boostrap.

<link rel="stylesheet" href="//cdn.rawgit.com/morteza/bootstrap-rtl/master/dist/cdnjs/3.3.1/css/bootstrap-rtl.min.css">

Ahora el estilo de .list-inline pasará a ser:

.list-inline {

    padding-right: 0;

    padding-left: initial;

    margin-right: -5px;

    margin-left: 0;

}

Y ya entonces si tenemos una lista de Bootstrap funcionando correctamente con rtl

image

La verdad es que no he comprobado si todos los componentes y estilos de Boostrap son soportados en rtl con esta nuevo hoja de estilos, pero aquí voy a optar por ser reactivo y según vaya encontrándome problemas iré buscando soluciones.

Hasta aquí mi aventura con rtl, espero te sirva de ayuda!

Un saludo!