Click here to Skip to main content
15,886,664 members
Articles / Programming Languages / C#

Aspect Oriented RequestObject(DTO) Validation in WCF with FluentValidation and CastleWindsor

Rate me:
Please Sign up or sign in to vote.
4.83/5 (4 votes)
15 Feb 2016CPOL2 min read 10.3K   2  
Automated monkey or specific validations for WCF or REST services with Fluentvalidation and CastleWindsor

Introduction

In this article, we will look at how to automate validations with FluentValidation on WCF or other service structures. As you know, FluentValidation tool has been developed by Jeremy Skinner. It is a very good and useful tool for validation for MVC controller’s actions. So I thought, why we can't I use FluentValidation in WCF service calls or DDD RequestDTOs or anywhere.

Background

Actually; validation is very important thing in all of our applications. Some of us do this in a very manual way, especially for legacy projects. If you are interested with moving your legacy code to new structures or you may consider your validation mechanism to rewrite or maybe all you need is some refactoring. :)

Creating WCF Service

First of all, we should create a WCF Service using CastleWindsor WCFIntegration Facility. For this, we should write a Composition Root for WCF service installing and exposing.

Service1.cs

C#
internal partial class Service1 : ServiceBase
{
    public Service1()
    {
        InitializeComponent();
    }

    public Bootstrapper Bootstrapper { get; set; }

    protected override void OnStart(string[] args)
    {
        Bootstrapper = new Bootstrapper();
        Bootstrapper.Start();
    }

    protected override void OnStop()
    {
        Bootstrapper.Dispose();
    }
}

ServiceStarter.cs

C#
public static class ServiceStarter
{
    public static void CheckServiceRegisteration(string[] args)
    {
        if (args != null && args.Length > 0)
        {
            if (args[0].Equals("-i", StringComparison.OrdinalIgnoreCase))
            {
                SelfInstaller.InstallMe();
                Environment.Exit(0);
            }
            else if (args[0].Equals("-u", StringComparison.OrdinalIgnoreCase))
            {
                SelfInstaller.UninstallMe();
                Environment.Exit(0);
            }
        }
    }

    public static void StartApplication<T>(string[] args) where T : ServiceBase, new()
    {
        var servicesToRun = new ServiceBase[]
        {
            new T()
        };

        StartApplication(servicesToRun, args);
    }

    public static void StartApplication(ServiceBase[] services, string[] args)
    {
        //Check service registration before go to
        CheckServiceRegisteration(args);

        if (Environment.UserInteractive)
            RunAsConsole(services);
        else
            ServiceBase.Run(services);
    }

    private static void RunAsConsole(ServiceBase[] servicesToRun)
    {
        Console.ForegroundColor = ConsoleColor.DarkGreen;
        Console.WriteLine("Services running in console mode.");
        Console.WriteLine();

        var onStartMethod = typeof (ServiceBase).GetMethod("OnStart",
            BindingFlags.Instance | BindingFlags.NonPublic);
        foreach (var service in servicesToRun)
        {
            Console.Write("Starting {0}...", service.ServiceName);
            onStartMethod.Invoke(service, new object[] {new string[] {}});
            Console.Write("Started");
        }

        Console.WriteLine();
        Console.WriteLine();
        Console.ForegroundColor = ConsoleColor.DarkRed;
        Console.WriteLine("Press ESC to stop the services and end the process...");
        while (Console.ReadKey().Key != ConsoleKey.Escape)
            Console.ReadKey();
        Console.WriteLine();
        Console.ForegroundColor = ConsoleColor.DarkGreen;
        var onStopMethod = typeof (ServiceBase).GetMethod
				("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);

        foreach (var service in servicesToRun)
        {
            Console.Write("Stopping {0}...", service.ServiceName);
            onStopMethod.Invoke(service, null);
            Console.Write("Stopped {0}...", service.ServiceName);
        }

        Console.WriteLine("All services stopped.");
        Thread.Sleep(1000);
    }
}

Finally Program.cs

C#
public class Program
{
    private static void Main(string[] args)
    {
        ServiceStarter.StartApplication<Service1>(args);
    }
}

So, we are coming to Bootstrapper.cs. It contains important things. Bootstrapper has a composition root which is named Start(). In here, we must add WcfFacility and also WcfIntegrationFacility should be downloaded from nuget for this. Then, FromAssembly.This() for install all Windsor Installers.

C#
public class Bootstrapper
{
    public IWindsorContainer Container { get; private set; }

    public void Dispose()
    {
        Container.Dispose();
    }

    public void Start()
    {
        var metadata = new ServiceMetadataBehavior
        {
            HttpGetEnabled = true,
            HttpsGetEnabled = true
        };
        var returnFaults = new ServiceDebugBehavior {IncludeExceptionDetailInFaults = true};

        Container = new WindsorContainer()
            .AddFacility<WcfFacility>(f => f.CloseTimeout = TimeSpan.Zero)
            .Install(FromAssembly.This())
            .Register(
                Component.For<IEndpointBehavior>().ImplementedBy<WebHttpBehavior>(),
                Component.For<IServiceBehavior>().Instance(metadata),
                Component.For<IServiceBehavior>().Instance(returnFaults));
    }
}

ValidateWithRuleAttribute specific for validation.

C#
[Serializable]
public class ValidateWithRuleAttribute : Attribute
{
    public ValidateWithRuleAttribute(params string[] ruleSets)
    {
        RuleSetNames = ruleSets;
    }

    private string[] RuleSetNames { get; set; }
}
C#
[ServiceContract]
public interface IMobileService
{
    [OperationContract]
    [ValidateWithRule(ValidatorRuleSet.CashOrderMerchantRule, ValidatorRuleSet.CashOrderProductRule)]
    CashOrderResponse CashOrder(CashOrderRequest request);
}

Simple MobileService implementation:

C#
public class MobileService : IMobileService
{
    public CashOrderResponse CashOrder(CashOrderRequest request)
    {
        //todo stuff;

        return new CashOrderResponse();
    }
}

RequestBase for requestDTOs:

C#
[Serializable]
[DataContract]
public class RequestBase
{
    [DataMember]
    public string ApplicationVersion { get; set; }

    [DataMember]
    public int? BatchNo { get; set; }

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

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

    [DataMember]
    public double Latitude { get; set; }

    [DataMember]
    public double Longitude { get; set; }

    [DataMember]
    public int OriginatorInstitutionCode { get; set; }

    [DataMember]
    public int ParameterVersion { get; set; }

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

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

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

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

    [DataMember]
    public int? TraceNo { get; set; }

    [DataMember]
    public Guid TrackId { get; set; }

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

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

Simple request object CashOrderRequest:

C#
[Serializable]
[DataContract]
public class CashOrderRequest : RequestBase
{
    [DataMember]
    public List<CashOrderItem> OrderDetails { get; set; }

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

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

    [DataMember]
    public int ProvisionId { get; set; }
}

[Serializable]
[DataContract]
public class CashOrderItem
{
    [DataMember]
    public string DiscountRate { get; set; }

    [DataMember]
    public int ProductId { get; set; }

    [DataMember]
    public short Quantity { get; set; }
}

MobileServiceInstaller.cs

C#
public class MobileServiceInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        var url = "http://127.0.0.1/MobileService";
        container.Register(
            Component.For<IMobileService>().ImplementedBy<MobileService>()
            .Interceptors(InterceptorReference.ForType<ValidatorInterceptor>()).First
                .AsWcfService(new DefaultServiceModel().AddEndpoints(
                    WcfEndpoint.BoundTo(new BasicHttpBinding
                    {
                        Security = new BasicHttpSecurity
                        {
                            Mode = BasicHttpSecurityMode.None,
                            Transport = new HttpTransportSecurity
                            {
                                ClientCredentialType = HttpClientCredentialType.None
                            }
                        }
                    }).At(url)).AddBaseAddresses(url).PublishMetadata(o => o.EnableHttpGet())
                ));
    }
}

Note that on the code: Interceptors(InterceptorReference.ForType<ValidatorInterceptor>()).First.

Validator Definitions

ValidatorBase

C#
public class ValidatorBase<T> : AbstractValidator<T> where T : RequestBase
{
    public ValidatorBase()
    {
        RuleFor(x => x.LanguageCode)
            .NotNull().WithMessage("LanguageCode cannot be null")
            .NotEmpty().WithMessage("LanguageCode cannot be empty or null");

        RuleFor(x => x.TerminalCode)
            .NotNull().WithMessage("TerminalCode cannot be null")
            .NotEmpty().WithMessage("TerminalCode cannot be empty or null");

        RuleFor(x => x.TerminalSerialNumber)
            .NotNull().WithMessage("TerminalSerialNumber cannot be null")
            .NotEmpty().WithMessage("TerminalSerialNumber cannot be empty or null");

        RuleFor(x => x.TrackId)
            .NotNull().WithMessage("TrackId cannot be null")
            .Must(x => x != Guid.Empty).WithMessage("TrackId cannot be empty GUID!");

        RuleFor(x => x.TransactionDateTime).Must(t =>
        {
            DateTime dateTime;
            return DateTime.TryParse(t, out dateTime);
        }).WithMessage("Please fill CreateDateTime correctly!");
    }
}

If you look to that AbstractValidator <T> implementation, it is implemented from IValidator<T> and IValidator both. So this situation saves the register conventions to Castle Container. Which means, you can register all Request Validators only above statements. Because all of them are implemented from AbstractValidator<T> and besides AbstractValidator<T> implemented from Validator<T>.

C#
public class CashOrderRequestValidator : ValidatorBase<CashOrderRequest>
{
    public CashOrderRequestValidator()
    {
        RuleSet(ValidatorRuleSet.CashOrderMerchantRule, () =>
        {
            RuleFor(x => x.OwnerDealerCode)
                .NotNull().WithMessage("Merchant Dealercode cannot be null");

            RuleFor(x => x.OwnerTerminalCode)
                .NotNull().WithMessage("Merchant TerminalCode cannot be null");
        });

        RuleSet(ValidatorRuleSet.CashOrderProductRule, () =>
        {
            RuleFor(x => x.OrderDetails)
                .Must(x => x.Count > 0).WithMessage("CashOrder must be contains at least 1 item!")
                .NotNull().WithMessage("Order has to contains at least one product!")
                .Must(x => x.TrueForAll(p => p.Quantity > 0))
                .WithMessage("Product quantity must be greather than 0!");
        });
    }
}

The CashOrderValidator has two rulesets. Sometimes, you can use the same DTO or RequestObject for multiple methods. In this case, you should pass some kind of attributes on method interfaces in order to specify which method in with which rule will be checked. If you don’t want to that, maybe you want to make all of your validation without rulesets (you may not need them) there is no problem with that. FluentValidator can run any Rule which is not inside any rulesets. So, thus ValidateWithRule attribute is not necessary on every method interfaces. It is specific for use. So after implementation of validators, you should register this component on Castle Windsor container.

C#
public static class ValidatorRuleSet
{
    public const string CashOrderMerchantRule = "CashOrderMerchantRule";
    public const string CashOrderProductRule = "CashOrderProductRule";
}

And ValidatorInstaller, for all written validators.

C#
public class ValidatorInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(
            Classes.FromAssemblyContaining(typeof (ValidatorBase<>))
                .IncludeNonPublicTypes()
                .BasedOn(typeof (IValidator<>))
                .WithServiceAllInterfaces()
                .LifestyleTransient()
            );
    }
}

Validation Part

After all installation and definitions, we intercept any invoked method and validate with its own rule and validator.

For this operation, we should write an interceptor and install it.

C#
public class ValidatorInterceptor : IInterceptor
{
    public ValidatorInterceptor(IKernel kernel)
    {
        _kernel = kernel;
    }

    private IKernel _kernel { get; }

    public void Intercept(IInvocation invocation)
    {
        AssertRequest(invocation);
        invocation.Proceed();
    }

    private static string[] GetOrDefaultValidatorRuleSets(MethodInfo method)
    {
        var rules = new List<string> {"default"};
        var attribute = method.CustomAttributes.FirstOrDefault
			(x => x.AttributeType == typeof (ValidateWithRuleAttribute));
        if (attribute == null)
            return rules.ToArray();

        rules.AddRange((attribute.ConstructorArguments.First().Value as 
		ReadOnlyCollection<CustomAttributeTypedArgument>)
            .Select(x => x.Value.ToString())
            .ToList());

        return rules.ToArray();
    }

    private static ValidationResult ValidateTyped<T>(IValidator<T> validator, 
		T request, string[] ruleset, IValidatorSelector selector = null)
    {
        return validator.Validate(request, selector, string.Join(",", ruleset).TrimEnd(','));
    }

    private void AssertRequest(IInvocation invocation)
    {
        var requestObject = invocation.Arguments[0];
        var ruleSets = GetOrDefaultValidatorRuleSets(invocation.Method);
        var requestValidatorType = typeof (IValidator<>).MakeGenericType(requestObject.GetType());

        var validator = _kernel.Resolve(requestValidatorType);

        if (validator == null)
            return;

        var validationResult = GetType()
            .GetMethod("ValidateTyped", BindingFlags.Static | BindingFlags.NonPublic)
            .MakeGenericMethod(requestObject.GetType())
            .Invoke(null, new[] {validator, requestObject, ruleSets, null}) as ValidationResult;

        _kernel.ReleaseComponent(validator);

        if (validationResult != null && validationResult.IsValid)
            return;

        if (validationResult != null && validationResult.Errors.Any())
            throw new InvalidOperationException(string.Join
		(",", validationResult.Errors.Select(x => x.ErrorMessage)));
    }
}

The entire trick is in the interceptor. Assert request meets the all invoked requests and resolves them looking at their types.

Points of Interest

  • FluentValidation attributes never used
  • Loosely Coupled
  • Composition Root driven
  • Aspect Oriented
  • Rule-Method based validation

History

  • Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Turkey Turkey
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --