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");
}
}
@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