Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable interception of edit context model type when attempting to retrieve validator instances #74

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,28 @@ You can control this behaviour using the `DisableAssemblyScanning` parameter. If

You can find examples of different configurations in the sample projects. The Blazor Server project is configured to load validators from DI only. The Blazor WebAssembly project is setup to load validators using reflection.

**Note:** When scanning assemblies the component will swallow any exceptions thrown by that process. This is to stop exceptions thrown by scanning third party dependencies crashing your app.
**Note:** When scanning assemblies the component will swallow any exceptions thrown by that process. This is to stop exceptions thrown by scanning third party dependencies crashing your app.

## Intercepting the Model Type Used to Find Validators
By default, the component will use the type of the model being validated to determine what validator to resolve from the DI container. In most scenarios, this will be the model's compile-time type; however, if your model is being proxied (by [Castle Project's `DynamicProxy`](http://www.castleproject.org/projects/dynamicproxy/), say), the component will fail to resolve validators from the DI container or from scanning assemblies because the runtime type differs from the compile-time type used to implement the validator.

You can control this behaviour using the `ModelTypeFunc` parameter.
```csharp
// using System;
// using Castle.DynamicProxy;

public static class ModelTypeInterceptor
{
public static Type Execute(object model)
{
if (model is IProxyTargetAccessor proxy)
return proxy.DynProxyGetTarget().GetType();

return model.GetType();
}
}
```

```html
<FluentValidationValidator ModelTypeFunc='@ModelTypeInterceptor.Execute' />
```
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,40 @@ public static class EditContextFluentValidationExtensions
private static readonly List<AssemblyScanResult> AssemblyScanResults = new List<AssemblyScanResult>();

public static EditContext AddFluentValidation(this EditContext editContext, IServiceProvider serviceProvider, bool disableAssemblyScanning, IValidator validator, FluentValidationValidator fluentValidationValidator)
=> editContext.AddFluentValidation(serviceProvider, disableAssemblyScanning, validator, fluentValidationValidator, FluentValidationValidator.ModelTypePassthrough);

public static EditContext AddFluentValidation(this EditContext editContext, IServiceProvider serviceProvider, bool disableAssemblyScanning, IValidator validator, FluentValidationValidator fluentValidationValidator, MakeTypeUsingEditContextModelDelegate modelTypeFunc)
{
if (editContext == null)
{
throw new ArgumentNullException(nameof(editContext));
}

if (modelTypeFunc == null)
{
throw new ArgumentNullException(nameof(modelTypeFunc));
}

var messages = new ValidationMessageStore(editContext);

editContext.OnValidationRequested +=
(sender, eventArgs) => ValidateModel((EditContext)sender, messages, serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator);
(sender, eventArgs) => ValidateModel((EditContext)sender, messages, serviceProvider, disableAssemblyScanning, fluentValidationValidator, validator, modelTypeFunc);

editContext.OnFieldChanged +=
(sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier, serviceProvider, disableAssemblyScanning, validator);
(sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier, serviceProvider, disableAssemblyScanning, validator, modelTypeFunc);

return editContext;
}

private static async void ValidateModel(EditContext editContext,
ValidationMessageStore messages,
IServiceProvider serviceProvider,
bool disableAssemblyScanning,
FluentValidationValidator fluentValidationValidator,
IValidator validator = null)
ValidationMessageStore messages,
IServiceProvider serviceProvider,
bool disableAssemblyScanning,
FluentValidationValidator fluentValidationValidator,
IValidator validator,
MakeTypeUsingEditContextModelDelegate modelTypeFunc)
{
validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning);
validator ??= GetValidatorForModel(serviceProvider, editContext.Model, disableAssemblyScanning, modelTypeFunc);

if (validator is object)
{
Expand All @@ -61,16 +69,17 @@ private static async void ValidateModel(EditContext editContext,
}

private static async void ValidateField(EditContext editContext,
ValidationMessageStore messages,
FieldIdentifier fieldIdentifier,
IServiceProvider serviceProvider,
bool disableAssemblyScanning,
IValidator validator = null)
ValidationMessageStore messages,
FieldIdentifier fieldIdentifier,
IServiceProvider serviceProvider,
bool disableAssemblyScanning,
IValidator validator,
MakeTypeUsingEditContextModelDelegate modelTypeFunc)
{
var properties = new[] { fieldIdentifier.FieldName };
var context = new ValidationContext<object>(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));

validator ??= GetValidatorForModel(serviceProvider, fieldIdentifier.Model, disableAssemblyScanning);
validator ??= GetValidatorForModel(serviceProvider, fieldIdentifier.Model, disableAssemblyScanning, modelTypeFunc);

if (validator is object)
{
Expand All @@ -83,9 +92,10 @@ private static async void ValidateField(EditContext editContext,
}
}

private static IValidator GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning)
private static IValidator GetValidatorForModel(IServiceProvider serviceProvider, object model, bool disableAssemblyScanning, MakeTypeUsingEditContextModelDelegate modelTypeFunc)
{
var validatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
var modelType = modelTypeFunc.Invoke(model.GetType());
var validatorType = typeof(IValidator<>).MakeGenericType(modelType);
if (serviceProvider != null)
{
try
Expand Down Expand Up @@ -119,7 +129,7 @@ private static IValidator GetValidatorForModel(IServiceProvider serviceProvider,
}


var interfaceValidatorType = typeof(IValidator<>).MakeGenericType(model.GetType());
var interfaceValidatorType = typeof(IValidator<>).MakeGenericType(modelType);

Type modelValidatorType = AssemblyScanResults.FirstOrDefault(i => interfaceValidatorType.IsAssignableFrom(i.InterfaceType))?.ValidatorType;

Expand Down
5 changes: 4 additions & 1 deletion src/Blazored.FluentValidation/FluentValidationsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ namespace Blazored.FluentValidation
{
public class FluentValidationValidator : ComponentBase
{
internal static readonly MakeTypeUsingEditContextModelDelegate ModelTypePassthrough = model => model.GetType();

[Inject] private IServiceProvider ServiceProvider { get; set; }

[CascadingParameter] private EditContext CurrentEditContext { get; set; }

[Parameter] public IValidator Validator { get; set; }
[Parameter] public bool DisableAssemblyScanning { get; set; }
[Parameter] public MakeTypeUsingEditContextModelDelegate ModelTypeFunc { get; set; }

internal Action<ValidationStrategy<object>> Options;

Expand All @@ -40,7 +43,7 @@ protected override void OnInitialized()
$"inside an {nameof(EditForm)}.");
}

CurrentEditContext.AddFluentValidation(ServiceProvider, DisableAssemblyScanning, Validator, this);
CurrentEditContext.AddFluentValidation(ServiceProvider, DisableAssemblyScanning, Validator, this, ModelTypeFunc ?? ModelTypePassthrough);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Blazored.FluentValidation
{
public delegate Type MakeTypeUsingEditContextModelDelegate(object model);
}