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
2 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
Post a Comment