Custom response when using WCF and the Validation Block

Topics: Validation Application Block
Aug 22, 2012 at 3:20 PM

I'm creating a new API for our application that will be consumed by .Net and non .NET clients

I want to use the Validation Application block, however I need to customise the response when validation fails so I can standardise the response when there is an error, all errors including validation errors need to return a response with the HTTP Code set appropriately (validation for example will return 400) along with either as JSON or XML body describing the error e.g:

{"Code":400,"Message":"Validation failed"}

How is this achieved?

 

Aug 23, 2012 at 7:33 AM

Enterprise Library provides Exception Shielding but that is just handling exceptions and translating them into faults instead of returning an results object like you want.

I think you will have to write a custom IErrorHandler to process the FaultException<ValidationFault> from the validation application block as well as general exceptions.

Then you can create a Message with the information and return it.

A naive example for a method ProcessResult Process(...){}:

        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            // if FaultException<ValidationFault> with have to extract details
            ProcessResult result = new ProcessResult() { Code = 500, Message = error.Message }; 
            MessageVersion ver = OperationContext.Current.IncomingMessageVersion;
            
            fault = Message.CreateMessage(ver, 
                OperationContext.Current.IncomingMessageHeaders.Action + "Response", 
                result,
                new System.Runtime.Serialization.DataContractSerializer(
                    typeof(ProcessResult), "ProcessResponse", "http://tempuri.org/"));

        }

I think the 2 areas that may cause headaches are handling JSON/REST vs. SOAP since it looks like there are some differences (OperationContext.Current.IncomingMessageHeaders.Action vs. OperationContext.Current.IncomingMessageProperties["HttpOperationName"]) as well as ensuring that objects are serialized properly since the runtime can be very unforgiving in its messaging when encountering unexpected data.

--
Randy Levy
Enterprise Library support engineer
entlib.support@live.com 

Aug 24, 2012 at 9:32 PM
Edited Aug 24, 2012 at 9:37 PM

Hey Randy,

I have this implementation working well for non FaultException<T> exceptions, however any FaultException do not return the correct response I get a HTML error page. I have noticed that the fault property is already populated when I receive a validation exception.

I have also tried using the Exception application block to shield the exception but this also does not work - again 'normal' exceptions are correctly shielded but validation exceptions simple result in an error.

Maybe I am missing something here, but I have now been trying to get this resolved for 2 days.

Any ideas?

Marc

Aug 25, 2012 at 9:26 AM

That is a little bit of a problem.  There is something going on in the bowels of WCF because I'm having issues even just changing any outgoing values in the IErrorHandler.

There might be an elegant (and simple) WCF solution but I wasn't able to find it either so here is a way that can get you up and running.  Basically, The idea is to use the Enterprise Library source code and create our own behavior that plays nice with JSON and REST (which is what I think you are doing).

I created my own versions of ValidationBehavior, ValidationElement, and ValidationParameterInspector.  The code looks like this (apologies for the long code posting) (with changes in bold):

    /// <summary>
    /// The behavior class that set up the validation contract behavior
    /// for implementing the validation process.
    /// </summary>
    public class ValidationBehavior : IEndpointBehavior, IContractBehavior, IOperationBehavior
    {
        #region ValidationBehavior Members

        private string ruleSet;
        private bool enabled;
        private bool enableClientValidation;

        /// <summary>
        /// Internal use initializer that set the client validation flag.
        /// </summary>
        /// <param name="enabled">if set to <c>true</c> [enabled].</param>
        /// <param name="enableClientValidation">if set to <c>true</c> [enable client validation].</param>
        /// <param name="ruleSet"></param>
        internal ValidationBehavior(bool enabled, bool enableClientValidation, string ruleSet)
        {
            this.enableClientValidation = enableClientValidation;
            this.enabled = enabled;
            this.ruleSet = ruleSet;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:ValidationBehavior"/> class.
        /// The <see cref="Enabled"/> property will be set as 'true'.
        /// </summary>
        public ValidationBehavior()
            : this(true, false, string.Empty)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:ValidationBehavior"/> class.
        /// The <see cref="Enabled"/> property will be set to 'true'.
        /// </summary>
        /// <param name="ruleSet">The name of the validation ruleset to apply.</param>
        public ValidationBehavior(string ruleSet)
            : this(true, false, ruleSet)
        {

        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:ValidationBehavior"/> class.
        /// </summary>
        /// <param name="enabled">if set to <c>true</c> [enabled].</param>
        public ValidationBehavior(bool enabled)
            : this(enabled, enabled, string.Empty)
        {
        }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="T:ValidationBehavior"/> is enabled.
        /// </summary>
        /// <value><c>true</c> if enabled; otherwise, <c>false</c>. The dafault value is true.</value>
        public bool Enabled
        {
            get { return enabled; }
            set { enabled = value; }
        }

        #endregion

        #region IEndpointBehavior Members

        /// <summary>
        /// Implement to pass data at runtime to bindings to support custom behavior.
        /// </summary>
        /// <param name="endpoint">The endpoint to modify.</param>
        /// <param name="bindingParameters">The objects that binding elements require to support the behavior.</param>
        public void AddBindingParameters(
            ServiceEndpoint endpoint,
            BindingParameterCollection bindingParameters)
        {
            if (endpoint == null) throw new ArgumentNullException("endpoint");

            AddBindingParameters(endpoint.Contract, endpoint, bindingParameters);
        }

        /// <summary>
        /// Implements a modification or extension of the client across an endpoint.
        /// </summary>
        /// <param name="endpoint">The endpoint that is to be customized.</param>
        /// <param name="clientRuntime">The client runtime to be customized.</param>
        public void ApplyClientBehavior(
            ServiceEndpoint endpoint,
            ClientRuntime clientRuntime)
        {
            if (endpoint == null) throw new ArgumentNullException("endpoint");

            ApplyClientBehavior(endpoint.Contract, endpoint, clientRuntime);
        }

        /// <summary>
        /// Implements a modification or extension of the service across an endpoint.
        /// </summary>
        /// <param name="endpoint">The endpoint that exposes the contract.</param>
        /// <param name="endpointDispatcher">The endpoint dispatcher to be modified or extended.</param>
        public void ApplyDispatchBehavior(
            ServiceEndpoint endpoint,
            EndpointDispatcher endpointDispatcher)
        {
            if (endpoint == null) throw new ArgumentNullException("endpoint");
            if (endpointDispatcher == null) throw new ArgumentNullException("endpointDispatcher");

            ApplyDispatchBehavior(endpoint.Contract, endpoint, endpointDispatcher.DispatchRuntime);
        }

        /// <summary>
        /// Implement to confirm that the endpoint meets some intended criteria.
        /// </summary>
        /// <param name="endpoint">The endpoint to validate.</param>
        public void Validate(ServiceEndpoint endpoint)
        {
            if (endpoint == null) throw new ArgumentNullException("endpoint");

            Validate(endpoint.Contract, endpoint);
        }

        #endregion

        #region IContractBehavior Members

        /// <summary>
        /// Configures any binding elements to support the contract behavior.
        /// </summary>
        /// <param name="contractDescription">The contract description to modify.</param>
        /// <param name="endpoint">The endpoint to modify.</param>
        /// <param name="bindingParameters">The objects that binding elements require to support the behavior.</param>
        public void AddBindingParameters(
            ContractDescription contractDescription,
            ServiceEndpoint endpoint,
            BindingParameterCollection bindingParameters)
        {
        }

        /// <summary>
        /// Implements a modification or extension of the client across a contract.
        /// </summary>
        /// <param name="contractDescription">The contract description for which the extension is intended.</param>
        /// <param name="endpoint">The endpoint.</param>
        /// <param name="clientRuntime">The client runtime.</param>
        public void ApplyClientBehavior(
            ContractDescription contractDescription,
            ServiceEndpoint endpoint,
            ClientRuntime clientRuntime)
        {
            if (false == enabled ||
                false == enableClientValidation)
            {
                return;
            }

            // perform validation on client side
            foreach (ClientOperation clientOperation in clientRuntime.Operations)
            {
                OperationDescription operationDescription =
                    contractDescription.Operations.Find(clientOperation.Name);
                ApplyClientBehavior(operationDescription, clientOperation);
            }
        }

        /// <summary>
        /// Implements a modification or extension of the client across a contract.
        /// </summary>
        /// <param name="contractDescription">The contract description to be modified.</param>
        /// <param name="endpoint">The endpoint that exposes the contract.</param>
        /// <param name="dispatchRuntime">The dispatch runtime that controls service execution.</param>
        public void ApplyDispatchBehavior(
            ContractDescription contractDescription,
            ServiceEndpoint endpoint,
            DispatchRuntime dispatchRuntime)
        {
            if (false == enabled)
            {
                return;
            }

            // perform validation on server side.
            // add the faultdescription and validation parameters
            foreach (DispatchOperation dispatchOperation in dispatchRuntime.Operations)
            {
                OperationDescription operationDescription =
                    contractDescription.Operations.Find(dispatchOperation.Name);
                ApplyDispatchBehavior(operationDescription, dispatchOperation);
            }
        }

        /// <summary>
        /// Implement to confirm that the contract and endpoint can support the contract behavior.
        /// </summary>
        /// <param name="contractDescription">The contract to validate.</param>
        /// <param name="endpoint">The endpoint to validate.</param>
        public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
        {
            // by pass validation if this behavior is disabled
            if (false == enabled)
            {
                return;
            }

            // check of all operations with validators has the FaultContract attribute with 
            // a ValidationFault type
            foreach (OperationDescription operation in contractDescription.Operations)
            {
                Validate(operation);
            }
        }

        #endregion

        #region IOperationBehavior Members

        /// <summary>
        /// Configures any binding elements to support the operation behavior.
        /// </summary>
        /// <param name="operationDescription">The operation being examined. Use for examination only. If the operation 
        /// description is modified, the results are undefined.</param>
        /// <param name="bindingParameters">The objects that binding elements require to support the behavior.</param>
        public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
        {
            // nothing to do
        }

        /// <summary>
        /// Implements a modification or extension of the client accross an operation.
        /// </summary>
        /// <param name="operationDescription">The operation being examined. Use for examination only. If the operation 
        /// description is modified, the results are undefined.</param>
        /// <param name="clientOperation">The run-time object that exposes customization properties for the operation 
        /// described by <paramref name="operationDescription"/>.</param>
        public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
        {
            clientOperation.ParameterInspectors.Add(new ValidationParameterInspector(operationDescription, ruleSet));
        }

        /// <summary>
        /// Implements a modification or extension of the service accross an operation.
        /// </summary>
        /// <param name="operationDescription">The operation being examined. Use for examination only. If the operation 
        /// description is modified, the results are undefined.</param>
        /// <param name="dispatchOperation">The run-time object that exposes customization properties for the operation 
        /// described by <paramref name="operationDescription"/>.</param>
        public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            dispatchOperation.ParameterInspectors.Add(new ValidationParameterInspector(operationDescription, ruleSet));
        }

        /// <summary>
        /// Implement to confirm that the operation meets some intended criteria.
        /// </summary>
        /// <param name="operationDescription">The operation being examined. Use for examination only. If the operation 
        /// description is modified, the results are undefined.</param>
        public void Validate(OperationDescription operationDescription)
        {
            if (HasValidationAssertions(operationDescription) &&
               !HasFaultDescription(operationDescription))
            {
                throw new InvalidOperationException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        "The operation '{0}' has no FaultContract defined for validation. Add the  [FaultContract(typeof(ValidationFault))] attribute to this operation.",
                        operationDescription.Name));
            }
        }

        #endregion

        private bool HasValidationAssertions(OperationDescription operation)
        {
            MethodInfo methodInfo = operation.SyncMethod;
            if (methodInfo == null)
            {
                throw new ArgumentNullException("operation.SyncMethod");
            }
            return methodInfo.GetCustomAttributes(typeof(ValidatorAttribute), false).Length > 0 ||
                HasParametersWithValidationAssertions(methodInfo.GetParameters());
        }

        private bool HasFaultDescription(OperationDescription operation)
        {
            foreach (FaultDescription fault in operation.Faults)
            {
                if (fault.DetailType == typeof(ValidationFault))
                {
                    return true;
                }
            }
            return false;
        }

        private bool HasParametersWithValidationAssertions(ParameterInfo[] parameters)
        {
            foreach (ParameterInfo parameter in parameters)
            {
                if (parameter.GetCustomAttributes(typeof(ValidatorAttribute), false).Length > 0)
                {
                    return true;
                }
            }
            return false;
        }
    }

    /// <summary>
    /// 
    /// </summary>
    public class ValidationParameterInspector : IParameterInspector
    {
        private List<Validator> inputValidators = new List<Validator>();
        private List<string> inputValidatorParameterNames = new List<string>();
        /// <summary>
        /// 
        /// </summary>
        /// <param name="operation"></param>
        /// <param name="ruleSet"></param>
        public ValidationParameterInspector(OperationDescription operation, string ruleSet)
        {
            MethodInfo method = operation.SyncMethod;

            foreach (ParameterInfo param in method.GetParameters())
            {
                switch (param.Attributes)
                {
                    case ParameterAttributes.Out:
                    case ParameterAttributes.Retval:
                        break;

                    default:
                        inputValidators.Add(CreateInputParameterValidator(param, ruleSet));
                        inputValidatorParameterNames.Add(param.Name);
                        break;
                }
            }
        }

        private Validator CreateInputParameterValidator(ParameterInfo param, string ruleSet)
        {
            Validator paramAttributeValidator = ParameterValidatorFactory.CreateValidator(param);
            Validator typeValidator = ValidationFactory.CreateValidator(param.ParameterType, ruleSet);
            return new AndCompositeValidator(paramAttributeValidator, typeValidator);
        }

        /// <summary>
        /// 
        /// </summary>
        public IList<Validator> InputValidators
        {
            get { return inputValidators; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="operationName"></param>
        /// <param name="inputs"></param>
        /// <returns></returns>
        public object BeforeCall(string operationName, object[] inputs)
        {
            ValidationFault fault = new ValidationFault();
            for (int i = 0; i < inputValidators.Count; ++i)
            {
                ValidationResults results = inputValidators[i].Validate(inputs[i]);
                AddFaultDetails(fault, inputValidatorParameterNames[i], results);
            }

            if (!fault.IsValid)
            {
                // Construct string with all validation details from fault
                var result = new ProcessResult() { Message = fault.Details.First().Message, Code = (int) HttpStatusCode.BadRequest };
                throw new WebFaultException<ProcessResult>(result, HttpStatusCode.BadRequest);
            }

            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="operationName"></param>
        /// <param name="outputs"></param>
        /// <param name="returnValue"></param>
        /// <param name="correlationState"></param>
        public void AfterCall(
            string operationName, object[] outputs, object returnValue, object correlationState)
        {
            // Deliberate noop - we don't need to do anything after the call
        }

        private void AddFaultDetails(ValidationFault fault, string parameterName, ValidationResults results)
        {
            if (!results.IsValid)
            {
                foreach (ValidationResult result in results)
                {
                    fault.Add(CreateValidationDetail(result, parameterName));
                }
            }
        }

        private ValidationDetail CreateValidationDetail(ValidationResult result, string parameterName)
        {
            return new ValidationDetail(result.Message, result.Key, parameterName);
        }
    }

    /// <summary>
    /// Represents a configuration element that specifies validation features 
    /// for a Windows Communication Foundation (WCF) service.
    /// </summary>
    public class ValidationElement : BehaviorExtensionElement
    {
        private const string EnabledAttributeName = "enabled";
        private const string RulesetAttributeName = "ruleset";

        /// <summary>
        /// 
        /// </summary>
        [ConfigurationProperty(EnabledAttributeName, DefaultValue = true, IsRequired = false)]
        public bool Enabled
        {
            get { return (bool)base[EnabledAttributeName]; }
            set { base[EnabledAttributeName] = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        [ConfigurationProperty(RulesetAttributeName, DefaultValue = null, IsRequired = false)]
        public string Ruleset
        {
            get { return (string)base[RulesetAttributeName]; }
            set { base[RulesetAttributeName] = value; }
        }

        /// <summary>
        /// Gets the type of behavior.
        /// </summary>
        /// <value></value>
        /// <returns>A <see cref="ValidationBehavior"/> <see cref="T:System.Type"></see>.</returns>
        public override Type BehaviorType
        {
            get { return typeof(ValidationBehavior); }
        }

        /// <summary>
        /// Creates a behavior extension based on the current configuration settings.
        /// </summary>
        /// <returns>The behavior extension.</returns>
        protected override object CreateBehavior()
        {
            if (Ruleset == null)
            {
                Ruleset = string.Empty;
            }

            return new ValidationBehavior(Enabled, Enabled, Ruleset);
        }
    }

Basically the change is to use WebFaultException<ProcessResult> where ProcessResult is this class:

    [DataContract]
    public class ProcessResult
    {
        [DataMember]
        public int Code
        {
            get;
            set;
        }

        [DataMember]
        public string Message
        {
            get;
            set;
        }
    }

In web.config I add the new behavior:

  <system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="validation" type="ValidationHOL.Service.ValidationElement, ValidationHOL.Service"/>
      </behaviorExtensions>
    </extensions>

This could be improved and "genericized" to handle both FaultException and WebFaultException's, formatting the fault details, setting the HTTP status code etc.  

The response should look like this:

Status Code: 400
Date: Sat, 25 Aug 2012 09:04:47 GMT
X-AspNet-Version: 4.0.30319
Connection: Close
Content-Length: 68
Server: ASP.NET Development Server/10.0.0.0
Content-Type: application/json; charset=utf-8
Cache-Control: private

{"Code":400,"Message":"The notes must be 1 to 100 characters long."}

Hope that gets you moving.

--
Randy Levy
Enterprise Library support engineer
entlib.support@live.com