Validate Dynamic Model using FluentValidation in ASP.Net Core

 

Introduction

In an asp.net core validating an input request is a best practice before performing any operation on it. This was generally performed in a middleware using either Microsoft data annotation attributes or using some third-party library like FluentValidation.  We write a validation rule for a static request model and all works fantastic, but what about if your input request model is dynamic? This means the request data is not the same every time. In this article we will see how to validate a model which is dynamic.

The Scenario

Let us understand the scenario first, we are going to build a generic file transfer API which copies the files to different cloud-based storage. In our example we limit to AWS S3 and Azure Storage. Our API copies the file to the respective cloud based on the settings passed in the request. This is depicted in the below diagram,



Figure 1 - File Transfer API Architecture

 

The input for our file transfer API contains the bytes of actual file and the settings for respective cloud. The settings may contain the authorization token and other details which are specific to the cloud where the file is copied.

Architecture

As shown in the below diagram the files are pushed and pulled with the respective cloud providers. In our example AWS S3 Provider and AWS Storage Provider are providers for AWS and Azure respectively.



Figure Architecture diagram shows the flow of validation

When the application starts it registers the AWS S3 Provider and Azure Storage Provider with the respective provider keys ‘aws-s3’ and ‘az-blog-storage’. When a transfer request is made the TransferController uses the keyword to load the corresponding provider validator and performs the validation.

The Request Model

Below is our ‘TransferRequest’ model for our FileTransferAPI.

namespace FileTransferApi.Model

{

    public class TransferRequest

    {

        /// <summary>

        /// Source node where the file to be copied from

        /// </summary>

        public Node Source { get; set; }

 

        /// <summary>

        /// Destination node where the file copied to

        /// </summary>

        public Node Destination { get; set; }

    }

 

    public class Node

    {

        /// <summary>

        /// Name of the file

        /// </summary>

        public string ObjectName { get; set; }

 

        /// <summary>

        /// Type of the cloud service

        /// </summary>

        public string ProviderType { get; set; }

 

        /// <summary>

        /// Settings for the respective cloud

        /// </summary>

        public dynamic Settings { get; set; }

    }

}

 

The ‘TransferRequest’ model has Source node where the file to be copied from and Destination node where the file to be copied. The ‘Node’ class has the ObjectName that is the file name and ProviderType property takes the values ‘aws-s3’ and ‘az-blob-storage’. This helps the ‘TransferServiceFactory’ to get the corresponding provider validator and provider service at runtime.

One more property ‘Settings’ is where we pass the provider specific settings. As you noted ‘Settings’ property is of type ‘dynamic’ this allows us to pass different models to the respective providers.

Here is the sample request of our transfer request,

{

    "source": {

      "objectName": "test-file.txt",

      "providerType": "aws-s3",

      "settings":{

          "regionEndPoint": "us-east-1",

          "bucketName": "dynamicvalidationbucket",

          "accessKeyId": "<<your access key id>>",

          "secretKey": "<<your secret key>>"

      }

 

    },

    "destination": {

      "objectName": "test-file.txt",

      "providerType": "az-blob-storage",

      "settings":{

          "connectionString": "<<connection string from azure storage>>",

          "containerName": "filecontainer"

      }

    }

  }

The request manifests the file to be copied form AWS S3 to Azure blob storage. The Settings model from the source node contains settings for the S3 whereas in the destination node contains settings for the azure blob storage. The details vary between each node, and it will not contain the same properties.

Settings Validator

The base interface for our validations is defined in the interfaces ‘IProviderSettings’ and ‘IProviderSettingsValidator

The below source code shows the same,

namespace FileTransferApi.Provider

{

    public interface IProviderSettings

    {

    }

 

    public interface IProviderSettingsValidator

    {

        ValidationResult Validate(string settingsJson, string prefix);

    }

}

The IProviderSettings is inherited in the provider settings below source code for AWS S3 settings model,

namespace FileTransferApi.Provider.AwsS3Provider

{

    public class AwsS3Settings : IProviderSettings

    {

 

        public string RegionEndPoint { get; set; }

        public string BucketName { get; set; }

        public string AccessKeyID { get; set; }

        public string SecretKey { get; set; }

        public string SessionToken { get; set; }

    }

}

The IProviderSettingsValidator interface has one method Validate which is implemented in each provider and it has own validation logic. Below source code is for AWS S3 settings validator

namespace FileTransferApi.Provider.AwsS3Provider

{

    public class AwsS3SettingsValidator : ProviderSettingsValidator<AwsS3Settings>

    {

        public AwsS3SettingsValidator()

        {

            RuleFor(t => t.RegionEndPoint).NotEmpty()

                .Must(IsRegionEndpointValid)

                .WithMessage($"The region endpoint is not valid. Possible values are ({string.Join(", ", RegionEndpoint.EnumerableAllRegions.Select(t => t.SystemName.ToLowerInvariant()))})");

            RuleFor(t => t.BucketName).NotEmpty();

            RuleFor(t => t.AccessKeyID).NotEmpty()

            .Matches("(?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9])");

            RuleFor(t => t.SecretKey).NotEmpty();

            //RuleFor(t => t.SessionToken).NotEmpty();

        }

        private bool IsRegionEndpointValid(string regionEndPoint)

        {

            if (string.IsNullOrEmpty(regionEndPoint)) return false;

            var names = RegionEndpoint.EnumerableAllRegions

                        .Select(t=>t.SystemName.ToLowerInvariant()).ToArray();

            return names.Contains(regionEndPoint.ToLowerInvariant());

        }

    }

}

 As you noted the AwsS3SettingsValidator does not directly implements the IProviderSettingsValidator instead it was inherited by the abstract class ‘ProviderSettingsValidator’. This class inherits the AbstractValidator form the FluentValidation and our ProviderSettingsValidator interface and implements the method ‘Validate’ as shown below,

namespace FileTransferApi.Provider

{

    public class ProviderSettingsValidator<T> : AbstractValidator<T>, IProviderSettingsValidator where T : IProviderSettings

    {

        public ProviderSettingsValidator()

        {

        }

 

        public ValidationResult Validate(string settingsJson, string prefix)

        {

            var settings = JsonConvert.DeserializeObject<T>(settingsJson);

            var vr = this.Validate(settings);

            foreach(var vf in vr.Errors)

            {

                vf.ErrorMessage = $"{prefix} {vf.ErrorMessage}";

            }

            return vr;

        }

    }

}

The validate method deserialize the json string into the respective settings model and invokes the validation from FluentValidation.

Validator Registration

The settings model and the validators are registered in the DI container while the application starts. Then these validators are then fetched from the DI container dynamically based on the request.

Below source code from the ‘RegisterProvder.cs’ shows the registration process,

 public static void RegisterProvider(this ContainerBuilder builder, Assembly assembly)

        {

 

            builder.RegisterAssemblyTypes(assembly)

                .AsImplementedInterfaces();

 

            var providerConfigurationTypes = assembly.GetTypes().Where(t => t.BaseType == typeof(ProviderConfiguration));

 

            if (providerConfigurationTypes != null)

            {

                foreach (var type in providerConfigurationTypes)

                {

                    ProviderConfiguration providerType = (ProviderConfiguration)Activator.CreateInstance(type);

 

                    //register service types

                    var providerNamespaceTypes = assembly.GetTypes().Where(t => t.Namespace == type.Namespace);

                    var transferServicetype = providerNamespaceTypes.SingleOrDefault(t=>t.GetInterfaces().Contains(typeof(ITransferService)));

                    if (transferServicetype != null)

                    {

                        builder.RegisterType(transferServicetype).Keyed<ITransferService>(providerType.Name);

                    }

 

                    //register settings types

                    var platformSettings = providerNamespaceTypes.SingleOrDefault(t => t.GetInterfaces().Contains(typeof(IProviderSettings)));

                    if (platformSettings != null)

                    {

                        builder.RegisterType(platformSettings).Keyed<IProviderSettings>(providerType.Name);

                    }

 

                    //register settings validator

                    var platformSettingsValidator = providerNamespaceTypes.SingleOrDefault(t => t.GetInterfaces().Contains(typeof(IProviderSettingsValidator)));

                    if (platformSettingsValidator != null)

                    {

                        builder.RegisterType(platformSettingsValidator).Keyed<IProviderSettingsValidator>(providerType.Name);

                    }

                }

            }

 

            builder.RegisterType<TransferServiceFactory>().As<ITransferServiceFactory>();

        }

This method loads the assembly and scans for those classes which implements IProviderSettings and IProviderSettingsValidator interfaces. These classes are automatically registered with the provider’s name.

Invoking Validation

The below source code from the ‘TransferController’ class POST method shows how the validation is invoked dynamically,

 [HttpPost]

        public async Task<TransferResponse> Post([FromBody] TransferRequest request)

        {

            IProviderSettingsValidator sSettingsValidator = _transferServiceFactory.GetPlatformSettingsValidator(request.Source.ProviderType);

            IProviderSettingsValidator dSettingsValidator = _transferServiceFactory.GetPlatformSettingsValidator(request.Destination.ProviderType);

 

            string srcSettingsJson = Convert.ToString(request.Source.Settings).Replace(Environment.NewLine, string.Empty);

            string dstSettingsJson = Convert.ToString(request.Destination.Settings).Replace(Environment.NewLine, string.Empty);

            var failure = new[]

            {

                sSettingsValidator?.Validate(srcSettingsJson, "Source"),

                dSettingsValidator?.Validate(dstSettingsJson, "Destination")

            }.SelectMany(t => t.Errors)

            .Where(error => error != null);

 

            if (failure.Any())

            {

                throw new ValidationException($"Node settings({request.Source.ProviderType}) validation error", failure);

            }

 

            //If the validation is successful then transfer the file

            var sourceTransferService = _transferServiceFactory.GetTransferService(request.Source.ProviderType);

            var destinationTransferService = _transferServiceFactory.GetTransferService(request.Destination.ProviderType);

            var dataStream = new DataStream();

            await sourceTransferService.Pull(request.Source, dataStream);

            await destinationTransferService.Push(request.Destination, dataStream);

 

            return new TransferResponse { Message = "File transferred succesfully" };

        }

Based on the provider type key the corresponding validators are fetched form the DI using TransformServiceFactory class. We then call the Validate method that will validates the settings from the respective validators.

The below screen shot shows the output with the error messages,



Figure 3 Transfer request with the error messages

Summary

This article shows how we can validate a dynamic model with an example FileTransfer service. This technique can be adopted where a dynamic model validation is required. The source code for the example discussed above can be found at github(https://github.com/arunvambur/DynamicValidation). Run the service and test the API, you need to setup the S3 bucket and Blob container in AWS and Azure respectively. User the sample request mentioned above and substitute the secret keys and other configurations from AWS and Azure.

Comments

Popular posts from this blog

Debugging and Testing Helm Charts Using VS Code

Handle Multipart Contents in Asp.Net Core

Validate appsettings in ASP.net Core using FluentValidation