2011-11-01 21 views
5

Tengo un conjunto de servicios alojados con WCF Web Api, lo que tengo que hacer es validar las propiedades dentro de los modelos de la aplicación.Validación de propiedades del modelo WCF web APi

En MVC 3 por ejemplo I decorar propiedades en el modelo como este:

[StringLength(30)] 
    public string UserName { get; set; } 

y luego en el controlador procedo como este para verificar os el modelo ha cumplido con los parámetros de validación:

[HttpPost] 
    ActionResult Create(Model myModel) 
    { 
     if(ModelState.IsValid(){ 
      Post the model 
     } 
     else 
     { 
      Don't post the model 
     } 
    } 

¿Hay alguna manera de hacer algo similar en WCF Web Api?

Respuesta

1

En primer lugar debo decir impresionante pregunta a responder + Daniel

Sin embargo, he tomado un poco más lejos, lo refinó y se añade a la misma.

ValidationHander

he refinado esto un poco. Ahora se basa en un genérico HttpOperationHandler por lo que puede tomar el HttpRequestMessage. El motivo es que puedo devolver mensajes de error formateados con el tipo de medio correcto (desde el encabezado accept).

public class ValidationHandler<TResource> : HttpOperationHandler<TResource, HttpRequestMessage, HttpRequestMessage> 
{ 
    public ValidationHandler() : base("response") { } 

    protected override HttpRequestMessage OnHandle(TResource model, HttpRequestMessage requestMessage) 
    { 
     var results = new List<ValidationResult>(); 
     var context = new ValidationContext(model, null, null); 
     Validator.TryValidateObject(model, context, results, true); 

     if (results.Count == 0) 
     { 
      return requestMessage; 
     } 

     var errorMessages = results.Select(x => x.ErrorMessage).ToArray(); 

     var mediaType = requestMessage.Headers.Accept.FirstOrDefault(); 
     var response = new RestValidationFailure(errorMessages); 
     if (mediaType != null) 
     { 
      response.Content = new ObjectContent(typeof (string[]), errorMessages, mediaType); 
     } 
     throw new HttpResponseException(response); 
    } 
} 

métodos de extensión

La 2 que ya ha proporcionado estancia prácticamente lo mismo acerca del parametro desc ya no se necesita cuando se añade el ValidationHandler en el método ModelValidationFor

He añadido una método de extensión extra. Esto es para asegurarse de que todas las clases de "Recursos" estén validadas. Esto es principalmente yo siendo flojo y olvidadizo. Siempre me olvido de agregar alguna clase a una lista en alguna parte. (Es por eso que escribo instaladores genéricos Windsor!)

public static void ValidateAllResourceTypes(this WebApiConfiguration config, string assemblyFilter = "MyCompany*.dll") 
{ 
    var path = Path.GetDirectoryName((new Uri(Assembly.GetExecutingAssembly().CodeBase)).AbsolutePath); 
    var dc = new DirectoryCatalog(path, assemblyFilter); 
    var assemblies = dc.LoadedFiles.Select(Assembly.LoadFrom).ToList(); 
    assemblies.ForEach(assembly => 
    { 
     var resourceTypes = assembly.GetTypes() 
      .Where(t => t.Namespace != null && t.Namespace.EndsWith("Resources")); 

     foreach (var resourceType in resourceTypes) 
     { 
      var configType = typeof(Extensions); 
      var mi = configType.GetMethod("ModelValidationFor"); 
      var mi2 = mi.MakeGenericMethod(resourceType); 
      mi2.Invoke(null, new object[] { config }); 
     } 
    });    
} 

hice uso del espacio de nombres System.ComponentModel.Composition.Hosting (anteriormente conocido como MEF) para la clase DirectoryCatalog. En este caso, acabo de utilizar el espacio de nombres que termina con "Recursos" para encontrar mis clases de "Recursos". No tomaría mucho trabajo cambiarlo para usar un atributo personalizado o cualquier otra forma en la que prefiera identificar qué clases son sus "Recursos".

RestValidationFailure

Ésta es una pequeña clase de ayuda que hice para permitir un comportamiento coherente para las respuestas de error de validación.

public class RestValidationFailure : HttpResponseMessage 
{ 
    public RestValidationFailure(string[] messages) 
    { 
     StatusCode = HttpStatusCode.BadRequest; 
     foreach (var errorMessage in messages) 
     { 
      Headers.Add("X-Validation-Error", errorMessage); 
     } 
    } 
} 

Por lo tanto, ahora tengo una lista muy bien (en mi MediaType preferido) de todos los errores de validación.

¡Disfrútalo! :)

+0

¡Excelente respuesta! – Daniel

3

Hay un ejemplo de esto posted on MSDN de crear un comportamiento para esto que debería funcionar. También puede llamar a los validadores manualmente con Validator.ValidateObject (o envolverlo como un método de extensión) y devolver los errores de validación, que es esencialmente lo que está haciendo ese comportamiento.

5

Actualmente estoy trabajando en un HttpOperationHandler que hace exactamente lo que necesita. Ya no está hecho, pero este código psuedo puede darte una idea de cómo puedes hacerlo.

public class ValidationHandler : HttpOperationHandler 
{ 
    private readonly HttpOperationDescription _httpOperationDescription; 
    private readonly Uri _baseAddress; 

    public ValidationHandler(HttpOperationDescription httpOperationDescription, Uri baseAddress) 
    { 
     _httpOperationDescription = httpOperationDescription; 
     _baseAddress = baseAddress; 
    } 

    protected override IEnumerable<HttpParameter> OnGetInputParameters() 
    { 
     return new[] { HttpParameter.RequestMessage }; 
    } 

    protected override IEnumerable<HttpParameter> OnGetOutputParameters() 
    { 
     var types = _httpOperationDescription.InputParameters.Select(x => x.ParameterType); 

     return types.Select(type => new HttpParameter(type.Name, type)); 
    } 

    protected override object[] OnHandle(object[] input) 
    { 
     var request = (HttpRequestMessage)input[0]; 
     var uriTemplate = _httpOperationDescription.GetUriTemplate(); 

     var uriTemplateMatch = uriTemplate.Match(_baseAddress, request.RequestUri); 

     var validationResults = new List<ValidationResult>(); 

     //Bind the values from uriTemplateMatch.BoundVariables to a model 

     //Do the validation with Validator.TryValidateObject and add the results to validationResults 

     //Throw a exception with BadRequest http status code and add the validationResults to the message 

     //Return an object array with instances of the types returned from the OnGetOutputParmeters with the bounded values 
    } 
} 

El valor OnGetInputParameters dice lo que se espera en el método OnHandle, y los OnGetOutputParameters dice lo que está la salida esperada del método OnHandle (que más tarde se inyecta en el método en el servicio).

A continuación, puede agregar el controlador para el enrutamiento con un HttpConfiguration de la siguiente manera:

var httpConfiguration = new HttpConfiguration 
      { 
       RequestHandlers = (collection, endpoint, operation) => collection.Add(new ValidationHandler(operation, endpoint.Address.Uri)) 
      }; 
RouteTable.Routes.MapServiceRoute<MyResource>("MyResource", httpConfiguration); 
+0

OK, hay un par de cosas que no entiendo. Por ejemplo, ¿dónde pasas estos parámetros al constructor ?. Otra cosa es, ¿puedes usar _httpOperationDescripton.KnownTypes en lugar de escribir un método de extensión? ¿Por qué le proporciona al manejador un IEnumerable y por qué el manejador proporciona la operación con este mismo tipo? – Daniel

+0

He actualizado mi respuesta y espero que responda todas sus preguntas. Como puede ver, no estoy llamando al método de extensión para obtener todos los tipos de parámetros, me di cuenta de que puede obtenerlos de _httpOperationDescription.InputParameters (no estoy seguro si puede obtenerlos de KnownTypes). – Thern

+0

Sí, he estado leyendo mucho sobre los controladores de operaciones porque sabía que ese era el camino a seguir, pero simplemente no entendía su propósito. Finalmente logré hacer que esto funcione ... ¡Muchas gracias! Voy a publicar mi controlador, espero que lo veas y me des tu opinión. – Daniel

6

Ok finalmente lograron obtener las validaciones de mis modelos de trabajo. Escribí un controlador de validación y un par de métodos de extensiones. Lo primero que el manejador de validación:

public class ValidationHandler<T> : HttpOperationHandler 
{ 
    private readonly HttpOperationDescription _httpOperationDescription; 

    public ValidationHandler(HttpOperationDescription httpOperationDescription) 
    { 
     _httpOperationDescription = httpOperationDescription; 
    } 

    protected override IEnumerable<HttpParameter> OnGetInputParameters() 
    { 
     return _httpOperationDescription.InputParameters 
      .Where(prm => prm.ParameterType == typeof(T)); 
    } 

    protected override IEnumerable<HttpParameter> OnGetOutputParameters() 
    { 
     return _httpOperationDescription.InputParameters 
      .Where(prm => prm.ParameterType == typeof(T)); 
    } 

    protected override object[] OnHandle(object[] input) 
    { 
     var model = input[0]; 
     var validationResults = new List<ValidationResult>(); 
     var context = new ValidationContext(model, null, null); 
     Validator.TryValidateObject(model, context, validationResults,true); 
     if (validationResults.Count == 0) 
     { 
      return input; 
     } 
     else 
     { 
      var response = new HttpResponseMessage() 
      { 
       Content = new StringContent("Model Error"), 
       StatusCode = HttpStatusCode.BadRequest 
      }; 
      throw new HttpResponseException(response); 
     } 
    } 
} 

Nótese cómo el controlador recibe un objeto T, esto es principalmente porque me gustaría validar todos los tipos de modelo dentro de la API. Entonces OnGetInputParameters especifica que el manejador necesita recibir un objeto de tipo T, y OnGetOutputParameters especifica que el manejador necesita devolver un objeto con el mismo tipo de T en caso de que se cumplan las políticas de validación, de lo contrario, vea cómo el método on handle arroja un excepción, para que el cliente sepa que ha habido un problema de validación.

Ahora tengo que registrar el controlador, para esto escribí un par de métodos de extensiones, siguiendo un ejemplo de un blog de Pedro Felix http://pfelix.wordpress.com/2011/09/24/wcf-web-apicustom-parameter-conversion/ (este blog me ayudó mucho, hay algunas explicaciones agradables sobre las operaciones de todo el controlador) Así que estos son los métodos de extensiones:

public static WebApiConfiguration ModelValidationFor<T>(this WebApiConfiguration conf) 
    { 
     conf.AddRequestHandlers((coll, ep, desc) => 
      { 
       if (desc.InputParameters.Any(p => p.ParameterType == typeof(T))) 
       { 
        coll.Add(new ValidationHandler<T>(desc)); 
       } 
      }); 
     return conf; 
    } 

lo que este Methos comprueba si hay un parámetro de tipo T en las operaciones, y si es así, se añade el manejador a tal operación.

Éste llama al otro método de extensión AddRequestHandler, y ese método agrega el nuevo controlador sin eliminar los registros anteriores, si existen.

public static WebApiConfiguration AddRequestHandlers(
     this WebApiConfiguration conf, 
     Action<Collection<HttpOperationHandler>,ServiceEndpoint,HttpOperationDescription> requestHandlerDelegate) 
    { 
     var old = conf.RequestHandlers; 
     conf.RequestHandlers = old == null ? requestHandlerDelegate : 
             (coll, ep, desc) => 
             { 
              old(coll, ep, desc); 
             }; 
     return conf; 
    } 

Lo último es registrar el manejador:

 var config = new WebApiConfiguration(); 
     config.ModelValidationFor<T>(); //Instead of passing a T object pass the object you want to validate 
     routes.SetDefaultHttpConfiguration(config); 

     routes.MapServiceRoute<YourResourceObject>("SomeRoute"); 

Así que eso es todo .. Espero que ayuda a otra persona !!

+0

+1 - deberías empacar esto y ponerlo en nuget –

Cuestiones relacionadas