locked
Blazor : DI not working for ValidationAttribute RRS feed

  • Question

  • User-1699898665 posted

    Hi,

    In the following custom attribute the dataService variable, which should pick up the DI service named, returns null:

        public class OrganisationNameIsUnique : ValidationAttribute
        {
            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                    IOrganisationDataService dataService = (IOrganisationDataService)validationContext.GetService(typeof(IOrganisationDataService));
                    Organisation organisation = (Organisation)validationContext.ObjectInstance;
                    bool isUnique;

                    isUnique = dataService.IsOrganisationNameUnique(organisation.ID, organisation.Name);

                    if (isUnique)
                    {
                        return ValidationResult.Success;
                    }
                    else
                    {
                        return new ValidationResult("The organisation name is not unique.");
                    }
            }
        }

    The same code works fine in MVC.  

    Thanks, Paul

    Wednesday, September 23, 2020 1:48 PM

Answers

All replies

  • User475983607 posted

    The same code works fine in MVC. 

    Blazor runs in the browser not the server like MVC.   I assume the design requires the HTTP Context but you did not share a complete version of the code.

    Wednesday, September 23, 2020 2:14 PM
  • User-1699898665 posted

    I'm using the so called "server" approach, not the web assembly approach...but I understand that shouldn't matter.  

    I'm sure I saw an example yesterday that uses HttpContext to inject a service into a custom validation attribute, but can't find it now.  I've found a myriad of conflicting advice and examples while googling around.  But most seem to revolve around these actions:

    1. Add services.AddHttpContextAccessor(); to Startup.ConfigureServices.

    2. Add using Microsoft.AspNetCore.Http to the code file.

    3. Add the use of HttpContext.RequestServices :

    IOrganisationDataService dataService = (IOrganisationDataService)HttpContext.RequestServices.GetService(typeof(IOrganisationDataService));

    However, this won't compile because there is no RequestServices property available.  To be sure, under this sequence of actions HttpContext has no properties or methods at all anywhere I try accessing it.

    All of the original code that would appear to be relevant. Startup:

            public void ConfigureServices(IServiceCollection services)
            {
                services.AddRazorPages();
                services.AddServerSideBlazor();

                services.AddHttpContextAccessor();

                //System services
                services.AddSingleton<WeatherForecastService>();
                services.AddTransient<IOrganisationDataService, OrganisationDataService>();
                
            }

    The model:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Http;

    namespace TestBlazorServer2020.Data.Contacts.Organisation.Model
    {
        public class Organisation
        {
            public int ID { get; set; }
            [Required]
            [OrganisationNameIsUnique]
            public string Name { get; set; }
            [Required]
            public string OrganisationTypeID { get; set; }         //nea int? 
            [Required]
            public string StatusID { get; set; }                   //nea int? 
            public string Website { get; set; }
            public string Notes { get; set; }
        }

        public class OrganisationNameIsUnique : ValidationAttribute
        {
            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                    IOrganisationDataService dataService = (IOrganisationDataService)validationContext.GetService(typeof(IOrganisationDataService));
                    Organisation organisation = (Organisation)validationContext.ObjectInstance;
                    bool isUnique;

                    //IOrganisationDataService dataService = (IOrganisationDataService)HttpContext.RequestServices.GetService(typeof(IOrganisationDataService));
                    
                    isUnique = dataService.IsOrganisationNameUnique(organisation.ID, organisation.Name);

                    if (isUnique)
                    {
                        return ValidationResult.Success;
                    }
                    else
                    {
                        return new ValidationResult("The organisation name is not unique.");
                    }
            }
        }

    }

    The component:

    @page "/Contacts/Organisation/Organisation/{Id:int}"

    @using TestBlazorServer2020.Data.Contacts.Organisation
    @using TestBlazorServer2020.Data.Code.Model

    @inject IOrganisationDataService organisationDataService

    <h2>Organisation</h2>

    @if (organisation == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <div class="form-horizontal">
            <EditForm Model="@organisation" OnValidSubmit="@Save">
                <DataAnnotationsValidator />
                <ValidationSummary />

                <div class="form-group">
                    <label class="col-md-2 control-label">Name : </label>
                    <div class="col-md-10">
                        <InputText id="name" @bind-Value="organisation.Name" />
                        <ValidationMessage For="@(() => organisation.Name)" />
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-md-2 control-label">Type : </label>
                    <div class="col-md-10">
                        <InputSelect id="website" @bind-Value="organisation.OrganisationTypeID">
                            @if (organisationTypeList != null)
                            {
                            @foreach (var item in organisationTypeList)
                                {
                                <option value="@item.ID">@item.CodeText</option>
                                }
                            }
                        </InputSelect>
                        <ValidationMessage For="@(() => organisation.OrganisationTypeID)" />
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-md-2 control-label">Status : </label>
                    <div class="col-md-10">
                        <InputSelect id="website" @bind-Value="organisation.StatusID">
                            @if (statusTypeList != null)
                            {
                            @foreach (var item in statusTypeList)
                                {
                                <option value="@item.ID">@item.CodeText</option>
                                }
                            }
                        </InputSelect>
                        <ValidationMessage For="@(() => organisation.StatusID)" />
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-md-2 control-label">Website : </label>
                    <div class="col-md-10">
                        <InputText id="website" @bind-Value="organisation.Website" />
                        <ValidationMessage For="@(() => organisation.Website)" />
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-md-2 control-label">Notes : </label>
                    <div class="col-md-10">
                        <InputTextArea id="notes" @bind-Value="organisation.Notes" Rows="6" />
                        <ValidationMessage For="@(() => organisation.Notes)" />
                    </div>
                </div>

                <button type="submit">Submit</button>
            </EditForm>
        </div>

    }


    @code {

        [Parameter]
        public int Id { get; set; }

        TestBlazorServer2020.Data.Contacts.Organisation.Model.Organisation organisation;
        IEnumerable<Code> organisationTypeList;
        IEnumerable<Code> statusTypeList;

        protected override async Task OnInitializedAsync()
        {
            organisationTypeList = await organisationDataService.GetOrganisationTypes(true);

            statusTypeList = await organisationDataService.GetStatuses(true);

            //Get the main record last to give the codes chance to load
            if (Id == 0)
            {
                organisation = new TestBlazorServer2020.Data.Contacts.Organisation.Model.Organisation();

                organisation.ID = 0;
            }
            else
            {
                organisation = await organisationDataService.GetOrganisation(Id);
            }
        }

        private async Task<bool> Save()
        {
            bool success = true;

            success = await organisationDataService.SaveOrganisation(organisation, "Paul Mason");

            return success;

        }

    }

    Thanks, Paul

    Thursday, September 24, 2020 2:31 PM
  • User475983607 posted

    You misunderstand the Blazor server lifecycle.  The HTTP Context is only available when a Blazor server application initially loads.  After the first request, SignalR takes over.  All communication travel through a Web Socket.  There's no HTTP Context.

    https://docs.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-3.1#:~:text=The%20Blazor%20framework%20includes%20synchronous,during%20component%20initialization%20and%20rendering.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, September 24, 2020 4:13 PM
  • User-1699898665 posted

    Thanks, clearly I don't understand the Blazor lifecycle.  Early days yet. 

    So, all the services I am ever going to use in the component have to be substantiated during that initial request i.e. during the OnInitialised(Async) event handler, or earlier, yes?  But, critically, a model class and it's validator attributes are inaccessible because they cannot have dependencies coming from the direction of consuming code....or can they?  I'm going to assume they can't for now.

    So the answer is that for Blazor it cannot be done as a nicely encapsulated model based custom ValidationAttribute.   That's a mark back for MVC for me (currently the score is 1-1).

    So, I'm going to tag a nice old fashioned method on to my Organisation class (organisation.GetValidationMessages()) which at least maintains a degree of encapsulation, whilst I write yet more code that I shouldn't really have to and use the ValidationMessageStore.

    Its not exactly difficult to misunderstand the documents MS have written for Blazor.  They vary from being ropey to downright rubbish (as with the link you attached).  But thankfully someone else has https://blazor-university.com/components/component-lifecycles/ done their job for them it appears.

    Thanks very...Paul

    Thursday, September 24, 2020 5:33 PM
  • User-474980206 posted

    its always handy to view the source when questions arise.

    var context = new ValidationContext(
                    instance: container ?? validationContext.Model ?? _emptyValidationContextInstance,
                    serviceProvider: validationContext.ActionContext?.HttpContext?.RequestServices,
                    items: null)
                {
                    DisplayName = metadata.GetDisplayName(),
                    MemberName = memberName
                };

    as there is no HttpContext the serviceProvider is null.

    but the real issue is you can not make async calls inside the validator (so there is not much need of a service). at some point, Blazor will probably support a remote validator, but currently you must use the server validation lifecycle event: OnValidSubmit.

    You could create a component that used reflection to find your Server validation components and call their  async validation methods in the OnSubmitValidate event. Just have the attributes implement a custom server interface and use reflection.

    note: I don't think server blazor has long future. You really should switch to client.

    Thursday, September 24, 2020 6:04 PM