MVC C# - FluentValidation Composite Validator for multiple AbstractValidator<T> - c#

I have an MVC application that has many AbstractValidator<T> per view model. This is by design because I use SimpleInjector to inject queries to hit our database and I don't want to have constructor over injection in one validator. I created the class below that is the validator for every view model, but it does not call my When and other custom validation rules, only the simple rules are called. Any help will be appreciated. Thank you.
public class CompositeValidator<T> : AbstractValidator<T>
{
private ICollection<IValidator> _validators = new List<IValidator>();
public CompositeValidator(IEnumerable<IValidator<T>> validators = null)
{
if (validators == null) return;
foreach (var validator in validators)
{
_validators.Add(validator);
var enumerator = validator.GetEnumerator();
while (enumerator.MoveNext())
{
AddRule(enumerator.Current);
}
}
}
public override ValidationResult Validate(ValidationContext<T> context)
{
var errorsFromOtherValidators = _validators.SelectMany(x => x.Validate(context).Errors);
return new ValidationResult(errorsFromOtherValidators );
}
}
Here is an example of two validators
public class PersonValidator : AbstractValidator<PersonVm>
{
public PersonValidator()
{
RuleFor(model => model.FirstName).NotEmpty();
RuleFor(model => model.LastName).NotEmpty();
}
}
public class PersonMustBeRegisteredValidator : AbstractValidator<PersonVm>
{
private readonly IQuery<PersonExists> _query;
public PersonMustBeRegisteredValidator(IQuery<ReturnPerson> query)
{
_query = query;
Custom(model =>
{
var person = _query.Select(new { model.Id });
if (person == null) return new ValidationFailure("Id", "Person does not exist");
return null;
});
}
}}

this actually works. I found a validation interceptor I forgot about that was not firing my validation.

Related

How do I write my Aspect classes and my problem solves?

I have 2 errors because of my little knowledge of PostSharp (last version). My project's three aspect classes, but I only get 2 errors. When the getall method runs in my mvc project, I want log information to be generated in my database and C:/Log/Log.txt. But no logs are created. Here is what I want from you. No matter how I write the code block, my problem solves? I have some validation and Transaction processes, but I don't think it has anything to do with the error I'm getting, so there's no need for details. Firstly, I've gotten the following warning.
enter image description here
To solve this, I followed the procedure below. LogAspect and FluentValidationAspect classes have been giving error.
[LogAspect(AspectPriority = 1), FluentValidationAspect(AspectPriority = 2), TransactionScopeAspect(AspectPriority = 3)]
And again, I got the following error. (CS 7036)
enter image description here
I want to do some operations in my BookManager class. (Log, Validation, Transaction). Here are the codes;
[LogAspect(AspectPriority = 1), FluentValidationAspect(AspectPriority = 2), TransactionScopeAspect(AspectPriority = 3)]
public class BookManager : IBookService
{
private IBookDal _bookDal;
private IPersonDal _personDal;
/* private readonly IQueryableRepository<Book> _queryable; */
public BookManager(IBookDal bookDal, IPersonDal personDal/*IQueryableRepository<Book> queryable */)
{
_personDal = personDal;
/*_queryable = queryable; */
_bookDal = bookDal;
}
[FluentValidationAspect(typeof(BookValidator))]
public Book Add(Book book)
{
return _bookDal.Add(book);
}
public void Delete(Book book)
{
_bookDal.Delete(book);
}
[LogAspect(typeof(DatabaseLogger))]
public List<Book> GetAll()
{
return _bookDal.GetList();
}
[TransactionScopeAspect]
public void TransactionalOperation(Person person, Book book)
{
_personDal.Delete(person);
// Business Codes
_bookDal.Add(book);
}
public Book GetById(int bookId)
{
return _bookDal.Get(p=>p.BookId==bookId);
}
[FluentValidationAspect(typeof(BookValidator))]
public Book Update(Book book)
{
return _bookDal.Update(book);
}
}
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method, TargetMemberAttributes = MulticastAttributes.Instance)]
public class LogAspect : OnMethodBoundaryAspect
{
private Type _loggerType;
[NonSerialized]
private LoggerService _loggerService;
public LogAspect(Type loggerType)
{
_loggerType = loggerType;
}
public override void RuntimeInitialize(MethodBase method)
{
if (_loggerType.BaseType != typeof(LoggerService))
{
throw new Exception("Wrong logger type.");
}
_loggerService = (LoggerService)Activator.CreateInstance(_loggerType);
base.RuntimeInitialize(method);
}
public override void OnEntry(MethodExecutionArgs args)
{
if (!_loggerService.IsInfoEnabled)
{
return;
}
try
{
var logParameters = args.Method.GetParameters().Select((t, i) => new LogParameter
{
Name = t.Name,
Type = t.ParameterType.Name,
Value = args.Arguments.GetArgument(i)
}).ToList();
var logDetail = new LogDetail
{
FullName = args.Method.DeclaringType == null ? null : args.Method.DeclaringType.Name,
MethodName = args.Method.Name,
Parameters = logParameters
};
_loggerService.Info(logDetail);
}
catch (Exception)
{
}
}
}
[Serializable]
public class FluentValidationAspect : OnMethodBoundaryAspect
{
Type _validatorType;
public FluentValidationAspect(Type validatorType)
{
_validatorType = validatorType;
}
public override void OnEntry(MethodExecutionArgs args)
{
var validator = (IValidator)Activator.CreateInstance(_validatorType);
var entityType = _validatorType.BaseType.GetGenericArguments()[0];
var entities = args.Arguments.Where(t => t.GetType() == entityType);
foreach (var entity in entities)
{
ValidatorTool.FluentValidate(validator, entity);
}
}
}
I want to tell you something that you have to consider. Also, I did assembly level logging. This is the code.
[assembly: LogAspect(typeof(JsonFileLogger), AttributeTargetTypes = "ELibrary.Library.Business.Managers.BookManager*")]
Finally I want to add, can you explain the solution of this error by rewriting the wrong block?
Ordering aspects is documented here. You are getting the warning because PostSharp does not have information on the ordering of some transformations. In your case I'd add the following attribute on LogAspect:
[AspectTypeDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, typeof(FluentValidationAspect))]
And then on FluentValidationAspect:
[AspectTypeDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, typeof(TransactionScopeAspect))]
This should totally order those three aspects and you should get rid of warnings.
Afterwards, you were getting C# errors because your aspect constructors simply have a parameter that you did not specify. AspectPriority needs to be specified on individual attributes.

How to trigger model validation inside IValidateOptions<T> implementation?

I have a .Net 5 app and want to add validators for my configurations. Given this sample options
public sealed class DatabaseOptions
{
public string ConnectionString { get; set; }
}
I currently validate it with this implementation
public sealed class DatabaseOptionsValidator : IValidateOptions<DatabaseOptions>
{
public ValidateOptionsResult Validate(string name, DatabaseOptions databaseOptions)
{
List<string> validationFailures = new List<string>();
if (string.IsNullOrEmpty(databaseOptions.ConnectionString))
validationFailures.Add($"{nameof(databaseOptions.ConnectionString)} is required.");
// ...
if (validationFailures.Any())
{
return ValidateOptionsResult.Fail(validationFailures);
}
return ValidateOptionsResult.Success;
}
}
I would like to avoid implementing my own validation checks and error messages since I know data annotations already get the job done.
I modified the options model to this
public sealed class DatabaseOptions
{
[Required]
[MinLength(9999999)] // for testing purposes
public string ConnectionString { get; set; }
}
and was hoping to find a way to trigger the model validation
public sealed class DatabaseOptionsValidator : IValidateOptions<DatabaseOptions>
{
public ValidateOptionsResult Validate(string name, DatabaseOptions databaseOptions)
{
List<string> validationFailures = new List<string>();
// trigger the model validation and add every error to the validationFailures list
if (validationFailures.Any())
{
return ValidateOptionsResult.Fail(validationFailures);
}
return ValidateOptionsResult.Success;
}
}
but unfortunately I wasn't able to do so. The debugger hits the validator but how can I trigger the validation inside the Validate method?
Please have a look at the comments since my solution is already available!
Based on Rodrigo Rodrigues answer I created my own options validator based on data annotations
public sealed class OptionsValidator<TOptions> : IValidateOptions<TOptions> where TOptions : class
{
public ValidateOptionsResult Validate(string name, TOptions options)
{
ValidationContext validationContext = new ValidationContext(options);
List<ValidationResult> validationResults = new List<ValidationResult>();
bool noValidationErrorsOccured = Validator.TryValidateObject(options, validationContext, validationResults, true);
if (noValidationErrorsOccured) {
return ValidateOptionsResult.Success;
}
IEnumerable<string> validationFailures = validationResults.Select(validationResult => validationResult.ErrorMessage);
return ValidateOptionsResult.Fail(validationFailures);
}
}
So whenever I want to add a validator to my DI container I can make use of this extension method
public static IServiceCollection AddOptionsValidator<TOptions>(this IServiceCollection serviceCollection) where TOptions : class
=> serviceCollection.AddSingleton<IValidateOptions<TOptions>, OptionsValidator<TOptions>>();
There is a technique I use for validating data annotations in my netcore apps, not using IValidateOptions, but implementing a custom validator, and registering it as PostConfigure.
You can find valuable assets in the namespace System.ComponentModel.DataAnnotations.
Something like this:
// Custom validator for data annotations
public static class Validation {
public static void ValidateDataAnotations<TOptions>(TOptions options) {
var context = new ValidationContext(options);
var results = new List<ValidationResult>();
Validator.TryValidateObject(options, context, results, validateAllProperties: true);
if (results.Any()) {
var aggrErrors = string.Join(' ', results.Select(x => x.ErrorMessage));
var count = results.Count;
var configType = typeof(TOptions).Name;
throw new ApplicationException($"Found {count} configuration error(s) in {configType}: {aggrErrors}");
}
}
}
Then, you register this static method in you composition root (probably Startup.cs):
public void ConfigureServices(IConfiguration configuration, IServiceCollection serviceCollection) {
// (...)
serviceCollection.Configure<DatabaseOptions>(configuration.GetSection(nameof(DatabaseOptions)));
// invalid configuration values will break at this point
serviceCollection.PostConfigure<DatabaseOptions>(Validation.ValidateDataAnotations);
}

Using custom navigation service, how to pass a parameter to the next view model?

I am coming up to speed on Xamarin. I am using "Mastering Xamarin.Forms: App architecture techniques for building multi-platform, native mobile apps with Xamarin.Forms 4, 3rd Edition" as a guide. This had me create a custom navigation service.
Here is the implementation (I skipped the interface for brevity)
namespace wfw_dispenser.Services
{
public class XamarinFormsNavService : INavService
{
readonly IDictionary<Type, Type> _map = new Dictionary<Type, Type>();
public event PropertyChangedEventHandler CanGoBackChanged;
public INavigation XamarinFormsNav { get; set; }
public bool CanGoBack => XamarinFormsNav.NavigationStack?.Any() == true;
public async Task GoBack()
{
if (CanGoBack)
{
await XamarinFormsNav.PopAsync(true);
OnCanGoBackChanged();
}
}
public async Task NavigateTo<TVM>()
where TVM : BaseViewModel
{
await NavigateToView(typeof(TVM));
if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel)
{
((BaseViewModel)XamarinFormsNav.NavigationStack.Last().BindingContext).Init();
}
}
public async Task NavigateTo<TVM, TParameter>(TParameter parameter)
where TVM : BaseViewModel
{
await NavigateToView(typeof(TVM));
if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel<TParameter>)
{
((BaseViewModel<TParameter>)XamarinFormsNav.NavigationStack.Last().BindingContext).Init(parameter);
}
}
public void RemoveLastView()
{
if (XamarinFormsNav.NavigationStack.Count< 2)
{
return;
}
var lastView = XamarinFormsNav.NavigationStack[XamarinFormsNav.NavigationStack.Count - 2];
XamarinFormsNav.RemovePage(lastView);
}
public void ClearBackStack()
{
if (XamarinFormsNav.NavigationStack.Count < 2)
{
return;
}
for (var i = 0; i < XamarinFormsNav.NavigationStack.Count - 1; i++)
{
XamarinFormsNav.RemovePage(XamarinFormsNav.NavigationStack[i]);
}
}
public void NavigateToUri(Uri uri)
{
if (uri == null)
{
throw new ArgumentException("Invalid URI");
}
Device.OpenUri(uri);
}
async Task NavigateToView(Type viewModelType)
{
if (!_map.TryGetValue(viewModelType, out Type viewType))
{
throw new ArgumentException("No view found in view mapping for " + viewModelType.FullName + ".");
}
// Use reflection to get the View's constructor and create an instance of the View
var constructor = viewType.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(dc => !dc.GetParameters().Any());
var view = constructor.Invoke(null) as Page;
var vm = ((App)Application.Current).Kernel.GetService(viewModelType);
view.BindingContext = vm;
await XamarinFormsNav.PushAsync(view, true);
}
public void RegisterViewMapping(Type viewModel, Type view)
{
_map.Add(viewModel, view);
}
void OnCanGoBackChanged() => CanGoBackChanged?.Invoke(this, new PropertyChangedEventArgs("CanGoBack"));
}
}
It appears to me that there is a NavigateTo that takes a parameter. I tried it and it kind of goes nowhere without any errors in the log. There's nothing in the text about this method to explain how to use it.
I probably have to do something in the "catching" view model for this. Can someone help me out?
First, you must extend from the parameterized version of BaseViewModel. In your case, since you are passing in a PaymentRequest, this would be:
public class CheckoutViewModel : BaseViewModel<PaymentRequest>
Then BaseViewModel<T> has a virtual Init method that you can implement
public class BaseViewModel<TParameter> : BaseViewModel
{
protected BaseViewModel(INavService navService, IAnalyticsService analyticsService)
: base(navService, analyticsService)
{
}
public override void Init()
{
Init(default(TParameter));
}
public virtual void Init(TParameter parameter)
{
}
}

Abstract class model binding in asp.net core web api 2

I have been trying to figure out how to use custom model binding with .net Core 2 web api but have not been able to get it working.
I have been through some articles as below
http://www.palmmedia.de/Blog/2018/5/13/aspnet-core-model-binding-of-abstract-classes
Asp net core rc2. Abstract class model binding
In my case, the bindingContext.ModelName is always empty. Can anybody explain why this could be?
Sample implementation below
Controller
public IActionResult SomeAction([ModelBinder(BinderType = typeof(BlahTypeModelBinder))][FromBody]TheBaseClass theBase)
{
return Ok();
}
Models
public abstract class TheBaseClass
{
public abstract int WhatType { get; }
}
public class A : TheBaseClass
{
public override int WhatType { get { return 1; } }
}
public class B : TheBaseClass
{
public override int WhatType { get { return 2; } }
}
Provider
public class BhalTypeBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (context.Metadata.ModelType == typeof(TheBaseClass))
{
var assembly = typeof(TheBaseClass).Assembly;
var abstractSearchClasses = assembly.GetExportedTypes()
.Where(t => t.BaseType.Equals(typeof(TheBaseClass)))
.Where(t => !t.IsAbstract)
.ToList();
var modelBuilderByType = new Dictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder>();
foreach (var type in abstractSearchClasses)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
var metadata = context.MetadataProvider.GetMetadataForType(type);
foreach (var property in metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
modelBuilderByType.Add(type, new Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder(propertyBinders));
}
return new BlahTypeModelBinder(modelBuilderByType, context.MetadataProvider);
}
return null;
}
}
Binder
public class BlahTypeModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly IDictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder> _binders;
public BlahTypeModelBinder(IDictionary<Type, Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder> binders, IModelMetadataProvider metadataProvider)
{
_metadataProvider = metadataProvider;
_binders = binders;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var modelTypeValue = bindingContext.ValueProvider.GetValue(ModelNames.CreatePropertyModelName(bindingContext.ModelName, "WhatType"));
if (modelTypeValue != null && modelTypeValue.FirstValue != null)
{
Type modelType = Type.GetType(modelTypeValue.FirstValue);
if (this._binders.TryGetValue(modelType, out var modelBinder))
{
ModelBindingContext innerModelBindingContext = DefaultModelBindingContext.CreateBindingContext(
bindingContext.ActionContext,
bindingContext.ValueProvider,
this._metadataProvider.GetMetadataForType(modelType),
null,
bindingContext.ModelName);
/*modelBinder*/
this._binders.First().Value.BindModelAsync(innerModelBindingContext);
bindingContext.Result = innerModelBindingContext.Result;
return Task.CompletedTask;
}
}
//More code
}
}
I finally managed to solve the issue. You dont need the provider. Just the following binder works
public class BlahTypeModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var json = ExtractRequestJson(bindingContext.ActionContext);
var jObject = Newtonsoft.Json.Linq.JObject.Parse(json);
var whatTypeInt = (int)jObject.SelectToken("WhatType");
if (whatTypeInt == 1)
{
var obj = DeserializeObject<A>(json);
bindingContext.Result = ModelBindingResult.Success(obj);
}
else if (whatTypeInt == 2)
{
var obj = DeserializeObject<B>(json);
bindingContext.Result = ModelBindingResult.Success(obj);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
return Task.CompletedTask;
}
private static string ExtractRequestJson(ActionContext actionContext)
{
var content = actionContext.HttpContext.Request.Body;
return new StreamReader(content).ReadToEnd();
}
private static T DeserializeObject<T>(string json)
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json, new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto
});
}
}
The examples you linked to use an external query string parameter to determine the type.
If you call your action like this: SomeAction?WhatType=YourNamespaceName.A the binding works as expected.
bindingContext.ModelName being empty is just fine, it would be set after model binding. You can set it after setting bindingContext.Result if you want. Parameter WhatType comes from the QueryStringValueProvider, so no prefix is fine.
How to accomplish abstract model binding based on the JSON alone
To do this, we need:
A value provider to read the JSON and provide us with some "WhatType" value, in place of the QueryStringValueProvider.
Some reflection to map the extracted numbers to the Type-s.
1. ValueProvider
There is a detailed article on creating ValueProviders here:
As a starting point here is some code that successfully extracts the WhatType integers from the body json:
public class BlahValueProvider : IValueProvider
{
private readonly string _requestBody;
public BlahValueProvider(string requestBody)
{
_requestBody = requestBody;
}
private const string PROPERTY_NAME = "WhatType";
public bool ContainsPrefix(string prefix)
{
return prefix == PROPERTY_NAME;
}
public ValueProviderResult GetValue(string key)
{
if (key != PROPERTY_NAME)
return ValueProviderResult.None;
// parse json
try
{
var json = JObject.Parse(_requestBody);
return new ValueProviderResult(json.Value<int>("WhatType").ToString());
}
catch (Exception e)
{
// TODO: error handling
throw;
}
}
}
public class BlahValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.ActionContext.HttpContext.Request;
if (request.ContentType == "application/json")
{
return AddValueProviderAsync(context);
}
return Task.CompletedTask;
}
private Task AddValueProviderAsync(ValueProviderFactoryContext context)
{
using (StreamReader sr = new StreamReader(context.ActionContext.HttpContext.Request.Body))
{
string bodyString = sr.ReadToEnd();
context.ValueProviders.Add(new BlahValueProvider(bodyString));
}
return Task.CompletedTask;
}
}
Of course you have to register this factory in Startup.cs just as you registered the model binder. And this absolutely misses converting the extracted number to the actual Type (for this, see point 2 below), but if you place a breakpoint on your line staring with if (modelTypeValue != null you can see that modelTypeValue is there for you now even without the separate GET parameter.
2. Reflection
Realize that you are trying to figure out the type based on a property that is dynamically calculated on an existing instance (they are not static). While by knowing the current implementation I know that this is possible (create an empty instance of the model, check the WhatType property, throw the instance away), this is very bad practice, as nothing guarantees that an instance property is statically constant.
The clean solution for this would be an Attribute, that contains the WhatType number for that class. Then we can reflect on that attribute and build a map that maps ints to Types. This is out of the scope if this question, but look up any custom attribute tutorial if you are not familiar, and you will be able to put it together really quickly.

FluentValidation, is possible an inline validation?

I have a the following operation:
public void Save (Customer c, IEnumerable <Product> products)
{
    // Validate that you have entered at least one product.
    if (!produtos.Any())
throw new ArgumentOutOfRangeException("products");
}
Inline, without using inheritance (eg AbstractValidator ), as would this same operation using the FluentValidation library?
This is not supported yet:
public void DoOperation(List<string> strings)
{
var validator = new InlineValidator<List<string>>();
validator.RuleFor(l => l).Must(l => l.Any()).WithMessage("No one");
validator.ValidateAndThrow(strings)
}
On this case, we have to throw ValidationException manually.
like:
public void DoOperation(List<string> strings)
{
if (!strings.Any())
{
var failures = new List<ValidationFailure>();
failures.Add(new ValidationFailure("strings", "Must have at less one."));
throw new ValidationException(failures);
}
}
See:
https://fluentvalidation.codeplex.com/discussions/579227
var validator = new InlineValidator<Person>();
validator.RuleSet("SomeRuleset", ()=>{
validator.RuleFor(x=>x.Name)...etc
});
https://github.com/FluentValidation/FluentValidation/issues/563
I think this kind of validation is impossible, if you had an object which had a property of type IEnumerable<Product> you could using FluentValidation to check if the object has at least one product.
for example
public class ProductList
{
IEnumerable<Product> Products {get;set;}
var Validator = new ProductListValidator();
public bool IsValid
{
get
{
var res = Validator.Validate(this);
return res.IsValid;
}
}
public IList<ValidationFailure> ValidationResult
{
get
{
var res = Validator.Validate(this);
return res.Errors;
}
}
}
public class ProductListValidator : AbstractValidator<ProductList>
{
public ProductListValidator()
{
RuleFor(i => i.Products).Must(i => i.HasAny()).WithMessage("Your Error Meesage");
}
}
then
public void Save (Customer c, ProductList products)
{
// Validate that you have entered at least one product.
if (!ProductList.IsValid)
{
ReturnErrorSummary(ProductList.ValidationResult);
}
}

Categories

Resources