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 RequestDTO
s 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
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
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)
{
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
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.
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.
[Serializable]
public class ValidateWithRuleAttribute : Attribute
{
public ValidateWithRuleAttribute(params string[] ruleSets)
{
RuleSetNames = ruleSets;
}
private string[] RuleSetNames { get; set; }
}
[ServiceContract]
public interface IMobileService
{
[OperationContract]
[ValidateWithRule(ValidatorRuleSet.CashOrderMerchantRule, ValidatorRuleSet.CashOrderProductRule)]
CashOrderResponse CashOrder(CashOrderRequest request);
}
Simple MobileService
implementation:
public class MobileService : IMobileService
{
public CashOrderResponse CashOrder(CashOrderRequest request)
{
return new CashOrderResponse();
}
}
RequestBase
for requestDTO
s:
[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
:
[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
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
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>
.
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.
public static class ValidatorRuleSet
{
public const string CashOrderMerchantRule = "CashOrderMerchantRule";
public const string CashOrderProductRule = "CashOrderProductRule";
}
And ValidatorInstaller
, for all written validators.
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.
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