Search code examples
c#asp.net-coreblazorblazor-server-side.net-8.0

Blazored.FluentValidation issue in Blazor Server .NET 8


I am encountering issues with Blazored.FluentValidation in my Blazor Server application while implementing login functionality. Below is my login component code:

<EditForm class="needs-validation" Model="@Input" OnValidSubmit="@SubmitAsync" FormName="login" novalidate>
<FluentValidationValidator @ref="@_fluentValidationValidator" />
<DataAnnotationsValidator />

<AuthInput Id="email" ErrorFeedback="" Label="Email Address"
           TextFieldType="TextFieldType.EmailAddress"
           @bind-Value="Input.Email" PlaceHolder="Email Address here"
           ValidationFor="@(() => Input.Email)" />

<AuthInput Id="password" ErrorFeedback="" Label="Password" 
           TextFieldType="TextFieldType.Password"
           PlaceHolder="********" @bind-Value="Input.Password"
           ValidationFor="@(() => Input.Password)" />

<div class="d-lg-flex justify-content-between align-items-center mb-4">
    <div class="form-check"></div>
    <a href="/Identity/Account/ForgotPassword">Forgot your password?</a>
</div>

<Button Text="Sign In" ButtonType="ButtonType.Submit" IsFullWidth="true" />

My AuthInput Component

@using Microsoft.AspNetCore.Components.Forms
@using System.Linq.Expressions
@inherits InputBase<string>

<div class="@ColSize">
    <div class="mb-3">
        <label for="@Id" class="form-label">@LabelText</label>
        <input id="@Id" name="@FieldName" @attributes="@AdditionalAttributes" class="form-control" type="@_type"
               required="@IsRequired" @bind="@Value" placeholder="@PlaceHolder" />
        <ValidationMessage For="ValidationFor" />
        <div class="invalid-feedback"></div>
    </div>
</div>

@code{
[Parameter] public string Id { get; set; }
[Parameter] public string LabelText { get; set; }
[Parameter] public string ColSize { get; set;  }
[Parameter] public string FieldName { get; set; }
[Parameter] public string PlaceHolder { get; set; }
[Parameter] public bool IsRequired { get; set; } = false;
[Parameter] public TextFieldType TextFieldType { get; set; } = TextFieldType.Text;
[Parameter] public Expression<Func<string>> ValidationFor { get; set; } = default!;

private string _type => TextFieldType == TextFieldType.Text ? "text" :
                        TextFieldType == TextFieldType.Number ? "number" :
                        TextFieldType == TextFieldType.Password ? "password" :
                        TextFieldType == TextFieldType.EmailAddress ? "email" :
                        "text";

protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
    result = value;
    validationErrorMessage = null;
    return true;
}
}

In the code-behind, I've declared:

private FluentValidationValidator? _fluentValidationValidator;
private bool Validated => _fluentValidationValidator!.Validate(options => { 
                          options.IncludeAllRuleSets(); });
private TokenRequestModel Input { get; set; } = new();

Null Reference Exception: The Validate method, as per the documentation, should not result in a null reference exception. Despite including all rule sets with options.IncludeAllRuleSets(), I'm still facing this error. Any insights into the cause or suggestions for resolution based on the Blazored.FluentValidation documentation would be appreciated.

The Null Exception

I've followed the documentation on the Blazored.FluentValidation source control, but these issues persist. Any help, recommendations, or code examples to resolve these Blazored.FluentValidation problems.


Solution

  • You have two questions:

    Why am I getting a null exception?

    _fluentValidationValidator is null until the component is first rendered: sub components [such as FluentValidationValidator] don't exist before the first render.

    private FluentValidationValidator? _fluentValidationValidator;
    

    So if you try to use Validated before that first render, you get an exception as there's no _fluentValidationValidator object to call Validate on. _fluentValidationValidator! tells the compiler to ignore null validation checking because you know better and at this point in the code _fluentValidationValidator can't be null. Obviously not the case here.

    private bool Validated => _fluentValidationValidator!.Validate(options => { 
                              options.IncludeAllRuleSets(); });
    

    You can use the null-conditional operator like this which returns false if _fluentValidationValidator is null.

    private bool Validated => _fluentValidationValidator?.Validate(options => { 
                              options.IncludeAllRuleSets(); }) ?? false;
    

    Or refactor your logic to not attempt to use _fluentValidationValidator until you know it's not null.

    In the documentation you see this. At this point you're submitting the form, so _fluentValidationValidator must exist.

        private void SubmitFormAsync()
        {
            if (await _fluentValidationValidator!.ValidateAsync())
            {
                Console.WriteLine("Form Submitted Successfully!");
            }
        }
    

    Personally, I don't like using the null-suppression operator unless I have to. I prefer the belt-and-braces null-conditional operator.

    Why isn't my AuthInput binding working?

    You can't use Bind as you're doing. Within the component you need to assign the getter and the setter separately. As you inherit from InputBase you need to wire these into CurrentValueAsString to plug into the built in InputBase logic.

    Here's a refactored version of your component. I've added a few comments questioning why you have certain parameters because i'm guessing you're putting them in because they exist in the BootStrap example code. If you have valid reasons then ignore my comments. I've also refactored nullability in places.

    @using Microsoft.AspNetCore.Components.Forms
    @using System.Linq.Expressions
    @using System.Diagnostics.CodeAnalysis
    
    @inherits InputBase<string>
    
    <div class="@ColSize">
        <div class="mb-3">
            @if(this.LabelText is not null)
            {
                <label class="form-label">@LabelText</label>
            }
            <input @attributes="@AdditionalAttributes" class="form-control" type="@_type"
                   required="@IsRequired" value="@this.CurrentValueAsString" @onchange="this.SetValue" placeholder="@PlaceHolder" />
            <ValidationMessage For="this.ValueExpression" />
            <div class="invalid-feedback"></div>
        </div>
    </div>
    
    @code {
        // Do you need this?
        //[Parameter] public string Id { get; set; }
        [Parameter] public string? LabelText { get; set; }
        [Parameter] public string ColSize { get; set; } = "col-12";
        // Do you need this?
        //[Parameter] public string FieldName { get; set; }
        [Parameter] public string PlaceHolder { get; set; } = "Enter a value";
        [Parameter] public bool IsRequired { get; set; } = false;
        [Parameter] public TextFieldType TextFieldType { get; set; } = TextFieldType.Text;
    
        // Why include number - InputBase<int> if it's a integer
        private string _type => TextFieldType == TextFieldType.Text ? "text" :
                                TextFieldType == TextFieldType.Number ? "number" :
                                TextFieldType == TextFieldType.Password ? "password" :
                                TextFieldType == TextFieldType.EmailAddress ? "email" :
                                "text";
    
        private void SetValue(ChangeEventArgs e)
        {
            this.CurrentValueAsString = e.Value?.ToString() ?? null;
        }
    
        protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
        {
            result = value;
            validationErrorMessage = null;
            return true;
        }
    }
    

    This page then demos the control in action:

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <h1>Hello, world!</h1>
    
    Welcome to your new app.
    <EditForm Model="_model" >
        <AuthInput @bind-Value="_model.UserName" LabelText="User Name" PlaceHolder="Enter your user name" />
    
    </EditForm>
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>User Name: @_model.UserName</pre>
    </div>
    
    @code {
        private Model _model = new();
    
        public class Model {
            public string? UserName { get; set; }
    
        }
    }