65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 中的面向方面的 RequestObject(DTO) 验证( 使用 FluentValidation 和 CastleWindsor)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (4投票s)

2016年2月15日

CPOL

2分钟阅读

viewsIcon

10900

downloadIcon

24

使用 Fluentvalidation 和 CastleWindsor 为 WCF 或 REST 服务进行自动化模拟或特定验证

引言

在本文中,我们将探讨如何使用 FluentValidation 在 WCF 或其他服务结构上自动化验证。如您所知,FluentValidation 工具由 Jeremy Skinner 开发。它是一个非常出色且有用的工具,用于 MVC 控制器的操作的验证。因此,我想,为什么我们不能在 WCF 服务调用或 DDD RequestDTO 或任何地方使用 FluentValidation 呢。

背景

实际上,验证在我们所有的应用程序中都非常重要。我们中的一些人以非常手动的方式进行此操作,尤其是在遗留项目中。如果您有兴趣将遗留代码迁移到新结构,或者您可以考虑重写验证机制,或者您可能只需要一些重构。 :)

创建 WCF 服务

首先,我们应该使用 CastleWindsor WCFIntegration Facility 创建一个 WCF 服务。 为此,我们应该编写一个 Composition Root 来安装和公开 WCF 服务。

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)
    {
        //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);
    }
}

最后是 Program.cs

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

因此,我们来到了 Bootstrapper.cs。它包含重要的内容。Bootstrapper 有一个名为 Start() 的组合根。 在这里,我们必须添加 WcfFacility,并且还应该从 nuget 下载 WcfIntegrationFacility。 然后,使用 FromAssembly.This() 安装所有 Windsor 安装程序。

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 专门用于验证。

[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);
}

简单的 MobileService 实现

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

        return new CashOrderResponse();
    }
}

用于 requestDTORequestBase

[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; }
}

简单的请求对象 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())
                ));
    }
}

请注意代码:Interceptors(InterceptorReference.ForType<ValidatorInterceptor>()).First.

验证器定义

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!");
    }
}

如果您查看 AbstractValidator <T> 实现,它同时实现了 IValidator<T>IValidator。因此,这种情况保存了 Castle Container 的注册约定。这意味着,您只能注册所有 Request Validators 以上声明。因为它们都实现了 AbstractValidator<T>,并且除了 AbstractValidator<T> 之外,还实现了 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!");
        });
    }
}

CashOrderValidator 有两个规则集。有时,您可以对多个方法使用相同的 DTORequestObject。 在这种情况下,您应该在方法接口上传递某种属性,以指定将检查哪个规则中的哪个方法。如果您不想这样做,也许您想在没有规则集的情况下进行所有验证(您可能不需要它们),这没有问题。 FluentValidator 可以运行任何不在任何规则集中的 Rule。 因此,ValidateWithRule 属性对于每个方法接口都不是必需的。 它是专门使用的。 因此,在实现验证器之后,您应该在 Castle Windsor 容器上注册此组件。

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

以及 ValidatorInstaller,用于所有编写的验证器。

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

验证部分

在完成所有安装和定义之后,我们拦截任何调用的方法并使用其自身的规则和验证器进行验证。

对于此操作,我们应该编写一个拦截器并安装它。

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)));
    }
}

整个技巧都在拦截器中。断言请求满足所有调用的请求,并查看其类型来解析它们。

关注点

  • 从未使用过 FluentValidation 属性
  • 松散耦合
  • 组合根驱动
  • 面向方面
  • 基于规则-方法的验证

历史

  • 初次发布
© . All rights reserved.