Skip to content

Instantly share code, notes, and snippets.

@SteveSandersonMS
Created September 4, 2019 10:48
Show Gist options
  • Save SteveSandersonMS/090145d7511c5190f62a409752c60d00 to your computer and use it in GitHub Desktop.
Save SteveSandersonMS/090145d7511c5190f62a409752c60d00 to your computer and use it in GitHub Desktop.
Blazor + FluentValidation example
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; } = new Address();
public List<PaymentMethod> PaymentMethods { get; } = new List<PaymentMethod>();
}
public class Address
{
public string Line1 { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
public class PaymentMethod
{
public enum Type { CreditCard, HonourSystem }
public Type MethodType { get; set; }
public string CardNumber { get; set; }
}
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.FirstName).NotEmpty().MaximumLength(50);
RuleFor(customer => customer.LastName).NotEmpty().MaximumLength(50);
RuleFor(customer => customer.Address).SetValidator(new AddressValidator());
RuleFor(customer => customer.PaymentMethods).NotEmpty().WithMessage("You have to define at least one payment method");
RuleForEach(customer => customer.PaymentMethods).SetValidator(new PaymentMethodValidator());
}
}
public class AddressValidator : AbstractValidator<Address>
{
public AddressValidator()
{
RuleFor(address => address.Line1).NotEmpty();
RuleFor(address => address.City).NotEmpty();
RuleFor(address => address.Postcode).NotEmpty().MaximumLength(10);
}
}
public class PaymentMethodValidator : AbstractValidator<PaymentMethod>
{
public PaymentMethodValidator()
{
RuleFor(card => card.CardNumber)
.NotEmpty().CreditCard()
.When(method => method.MethodType == PaymentMethod.Type.CreditCard);
}
}
using FluentValidation;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
namespace CustomValidationSample
{
public class FluentValidator<TValidator> : ComponentBase where TValidator: IValidator, new()
{
private readonly static char[] separators = new[] { '.', '[' };
private TValidator validator;
[CascadingParameter] private EditContext EditContext { get; set; }
protected override void OnInitialized()
{
validator = new TValidator();
var messages = new ValidationMessageStore(EditContext);
// Revalidate when any field changes, or if the entire form requests validation
// (e.g., on submit)
EditContext.OnFieldChanged += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
EditContext.OnValidationRequested += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
}
private void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var validationResult = validator.Validate(editContext.Model);
messages.Clear();
foreach (var error in validationResult.Errors)
{
var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName);
messages.Add(fieldIdentifier, error.ErrorMessage);
}
editContext.NotifyValidationStateChanged();
}
private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
{
// This method parses property paths like 'SomeProp.MyCollection[123].ChildProp'
// and returns a FieldIdentifier which is an (instance, propName) pair. For example,
// it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses
// as far into the propertyPath as it can go until it finds any null instance.
var obj = editContext.Model;
while (true)
{
var nextTokenEnd = propertyPath.IndexOfAny(separators);
if (nextTokenEnd < 0)
{
return new FieldIdentifier(obj, propertyPath);
}
var nextToken = propertyPath.Substring(0, nextTokenEnd);
propertyPath = propertyPath.Substring(nextTokenEnd + 1);
object newObj;
if (nextToken.EndsWith("]"))
{
// It's an indexer
// This code assumes C# conventions (one indexer named Item with one param)
nextToken = nextToken.Substring(0, nextToken.Length - 1);
var prop = obj.GetType().GetProperty("Item");
var indexerType = prop.GetIndexParameters()[0].ParameterType;
var indexerValue = Convert.ChangeType(nextToken, indexerType);
newObj = prop.GetValue(obj, new object[] { indexerValue });
}
else
{
// It's a regular property
var prop = obj.GetType().GetProperty(nextToken);
if (prop == null)
{
throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}.");
}
newObj = prop.GetValue(obj);
}
if (newObj == null)
{
// This is as far as we can go
return new FieldIdentifier(obj, nextToken);
}
obj = newObj;
}
}
}
}
<EditForm Model="customer" OnValidSubmit="SaveCustomer">
<FluentValidator TValidator="CustomerValidator" />
<h3>Your name</h3>
<InputText placeholder="First name" @bind-Value="customer.FirstName" />
<InputText placeholder="Last name" @bind-Value="customer.LastName" />
<ValidationMessage For="@(() => customer.FirstName)" />
<ValidationMessage For="@(() => customer.LastName)" />
<h3>Your address</h3>
<div>
<InputText placeholder="Line 1" @bind-Value="customer.Address.Line1" />
<ValidationMessage For="@(() => customer.Address.Line1)" />
</div>
<div>
<InputText placeholder="City" @bind-Value="customer.Address.City" />
<ValidationMessage For="@(() => customer.Address.City)" />
</div>
<div>
<InputText placeholder="Postcode" @bind-Value="customer.Address.Postcode" />
<ValidationMessage For="@(() => customer.Address.Postcode)" />
</div>
<h3>
Payment methods
[<a href @onclick="AddPaymentMethod">Add new</a>]
</h3>
<ValidationMessage For="@(() => customer.PaymentMethods)" />
@foreach (var paymentMethod in customer.PaymentMethods)
{
<p>
<InputSelect @bind-Value="paymentMethod.MethodType">
<option value="@PaymentMethod.Type.CreditCard">Credit card</option>
<option value="@PaymentMethod.Type.HonourSystem">Honour system</option>
</InputSelect>
@if (paymentMethod.MethodType == PaymentMethod.Type.CreditCard)
{
<InputText placeholder="Card number" @bind-Value="paymentMethod.CardNumber" />
}
else if (paymentMethod.MethodType == PaymentMethod.Type.HonourSystem)
{
<span>Sure, we trust you to pay us somehow eventually</span>
}
<button type="button" @onclick="@(() => customer.PaymentMethods.Remove(paymentMethod))">Remove</button>
<ValidationMessage For="@(() => paymentMethod.CardNumber)" />
</p>
}
<p><button type="submit">Submit</button></p>
</EditForm>
@code {
private Customer customer = new Customer();
void AddPaymentMethod()
{
customer.PaymentMethods.Add(new PaymentMethod());
}
void SaveCustomer()
{
Console.WriteLine("TODO: Actually do something with the valid data");
}
}
@stevenbey
Copy link

This is a great solution; however some of my validators have dependencies, so I had to remove the new() constraint, inject an IServiceProvider and update OnInitialized (line 17) to: validator = Services.GetService<TValidator>() ?? Activator.CreateInstance<TValidator>();

Many thanks

@nssidhu
Copy link

nssidhu commented Apr 24, 2020

Validation does not happen on the tab out of field. only works at the submit level

@nssidhu
Copy link

nssidhu commented Jun 19, 2020

How can one pass in HttpClient to the CustomerValidator ?

@nssidhu
Copy link

nssidhu commented Jun 21, 2020

@stevenbey can you show how you were able to inject IserviceProvider.
I have similar need where i want to inject/pass in Httpclient from blazor page for server side validation into my CustomerValidator

@stevenbey
Copy link

@nssidhu sorry it's taken so long to reply. It's as simple as adding a property to the Validator class decorating it with the [Inject] attribute. For example: [Inject] private IServiceProvider Services { get; set; }

@icedenis
Copy link

How i can fix this error

image

@Rahul-Narayanasamy
Copy link

Rahul-Narayanasamy commented Aug 3, 2020

Hi Steve,

I implementing this FluentValidator using the above codes. but i am getting this error.
image

how can we resolve this error? Could you please check this and update the details to resolve the problem?

Regards,
Rahul

@natanaeladit
Copy link

Hi Steve,

I implementing this FluentValidator using the above codes. but i am getting this error.
image

how can we resolve this error? Could you please check this and update the details to resolve the problem?

Regards,
Rahul

please refer on the description here.. https://docs.fluentvalidation.net/en/latest/upgrading-to-9.html
you can replace that with:

var context = new ValidationContext<object>(editContext.Model);
var validationResult = validator.Validate(context);

@Rahul-Narayanasamy
Copy link

Hi Steve,
I implementing this FluentValidator using the above codes. but i am getting this error.
image
how can we resolve this error? Could you please check this and update the details to resolve the problem?
Regards,
Rahul

please refer on the description here.. https://docs.fluentvalidation.net/en/latest/upgrading-to-9.html
you can replace that with:

var context = new ValidationContext<object>(editContext.Model);
var validationResult = validator.Validate(context);

thank you for the info... it works

@ctrl-alt-d
Copy link

Adding validation just for a field:

protected override void OnInitialized()
{
    validator = new TValidator();
    var messages = new ValidationMessageStore(EditContext!);

    // Revalidate when any field changes, or if the entire form requests validation
    // (e.g., on submit)

    EditContext!.OnFieldChanged += (sender, eventArgs)
        => ValidateField((EditContext)sender!, messages, eventArgs.FieldIdentifier); //<--- THIS

    EditContext.OnValidationRequested += (sender, eventArgs)
        => ValidateModel((EditContext)sender!, messages);
}

private void ValidateField(EditContext editContext, ValidationMessageStore messages, FieldIdentifier fieldIdentifier) //<--- HERE
{
    var properties = new[] { fieldIdentifier.FieldName };
    var context = new ValidationContext<object>(editContext.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));
    var validationResult = validator!.Validate(context);
    messages.Clear(fieldIdentifier);
    messages.Add(fieldIdentifier, validationResult.Errors.Select(error => error.ErrorMessage));

    editContext.NotifyValidationStateChanged();
}

@cvphat4796
Copy link

Any solution for OnFieldChange with field of item in list ?

With code from @ctrl-alt-d we can only valid field in object. fieldIdentifier.FieldName just is field name. I need field path

@maran-baskar
Copy link

Any solution for OnFieldChange with field of item in list ?

With code from @ctrl-alt-d we can only valid field in object. fieldIdentifier.FieldName just is field name. I need field path

What do you mean by field path here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment