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

Support for Razor Components (aka server side Blazor) #28

Open
dharmaturtle opened this issue Mar 20, 2019 · 48 comments
Open

Support for Razor Components (aka server side Blazor) #28

dharmaturtle opened this issue Mar 20, 2019 · 48 comments

Comments

@dharmaturtle
Copy link

Hi!

Thanks for a great framework! I've used it in the past, and hope to get it working with my current Razor Components project.

Steps to recreate:

  1. Install the prerequisites.
  2. Run dotnet new razorcomponents -o MyRazorComponents
  3. Modify Startup.cs to match the documentation as much as possible. The before and after diff can be found here.
  4. Run dotnet watch run
  5. Observe the following exception despite .Verify() passing:

Unhandled exception rendering component: Cannot provide a value for property 'ForecastService' on type 'MyRazorComponents.Components.Pages.FetchData'. There is no registered service of type 'MyRazorComponents.Services.WeatherForecastService'.

I'm unfamiliar with crosswiring and ASP.NET; please let me know if I made any errors. Blazor initially did not support custom service providers, but that was fixed recently. AutoFac seems to work, so I feel like I'm missing something. I'm using SimpleInjector v4.4.3 (and Integration.AspNetCore.Mvc v4.4.3). Thanks again!

@dotnetjunkie dotnetjunkie reopened this Mar 20, 2019
@dotnetjunkie dotnetjunkie added the question Further information is requested label Mar 20, 2019
@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Mar 20, 2019

UPDATE: There is now a Blazor integration page in the Simple Injector documentation.


I tried to get the full stack working with VS2019prev, .NET Core v3, templates, etc, but for some reason the razorcomponents is unknown and VS 2019 doesn't have any Target framework beyond .NET Standard 2.0.

Can you provide me with a .zip file of your solution?

And don't forget to post the full stack trace.

@dharmaturtle
Copy link
Author

dharmaturtle commented Mar 20, 2019

You used to have to install the Blazor extension to get the dotnet templates working. Now, as far as I can tell, it's major benefit is giving you intellisense in .razor files.

When I run dotnet --version, I get 3.0.100-preview3-010431. Merely creating a .NET Core Console app gives me <TargetFramework>netcoreapp3.0</TargetFramework>; I had no option to change frameworks. (At least from a new start of VS19.)

I threw it on github here.

Stack trace:

Unhandled exception rendering component: Cannot provide a value for property 'ForecastService' on type 'MyRazorComponents.Components.Pages.FetchData'. There is no registered service of type 'MyRazorComponents.Services.WeatherForecastService'.
System.InvalidOperationException: Cannot provide a value for property 'ForecastService' on type 'MyRazorComponents.Components.Pages.FetchData'. There is no registered service of type 'MyRazorComponents.Services.WeatherForecastService'.
   at Microsoft.AspNetCore.Components.ComponentFactory.<>c__DisplayClass6_0.<CreateInitializer>b__2(IComponent instance)
   at Microsoft.AspNetCore.Components.ComponentFactory.PerformPropertyInjection(IComponent instance)
   at Microsoft.AspNetCore.Components.ComponentFactory.InstantiateComponent(Type componentType)
   at Microsoft.AspNetCore.Components.Rendering.Renderer.InstantiateChildComponentOnFrame(RenderTreeFrame& frame, Int32 parentComponentId)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewComponentFrame(DiffContext& diffContext, Int32 frameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InitializeNewSubtree(DiffContext& diffContext, Int32 frameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.InsertNewFrame(DiffContext& diffContext, Int32 newFrameIndex)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.AppendDiffEntriesForRange(DiffContext& diffContext, Int32 oldStartIndex, Int32 oldEndIndexExcl, Int32 newStartIndex, Int32 newEndIndexExcl)
   at Microsoft.AspNetCore.Components.RenderTree.RenderTreeDiffBuilder.ComputeDiff(Renderer renderer, RenderBatchBuilder batchBuilder, Int32 componentId, ArrayRange`1 oldTree, ArrayRange`1 newTree)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.Rendering.Renderer.Re
Process is terminating due to StackOverflowException.
nderInExistingBatch(RenderQueueEntry renderQueueEntry)
   at Microsoft.AspNetCore.Components.Rendering.Renderer.ProcessRenderQueue()
   at Microsoft.AspNetCore.Components.Rendering.Renderer.AddToRenderQueue(Int32 componentId, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.RenderHandle.Render(RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.ComponentBase.StateHasChanged()
   at Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync()
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()

I had to navigate to https://localhost:5001/fetchdata directly from a new tab to see the error in the dotnet watch run

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Mar 20, 2019

The stack trace gives some clues of what it going on. From that, we can take a peek in the actual source code.

A painful observation is that the building of these Razor Components is tightly coupled to the built-in configuration system, and I see no way to redirect the resolution of those components to a non-conforming container, such as Simple Injector.

Hopefully, this code isn't released yet and the Microsoft team can still make some changes to the design.

Hopefully @davidfowl can chime in and correct me if I'm wrong.

David, am I correct by saying the necessary Seam is missing in this part of the ASP.NET Core code base or is there a different way for non-conformers to plugin at this point? If there's no way to integrate, can you make sure this is something that will be addressed?

@dotnetjunkie
Copy link
Collaborator

Let's see what happens.

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Nov 4, 2020

ASP.NET Core 5.0 will allow the creation of Razor Components to be intercepted. Here's how to integrate it with Simple Injector. Also interesting

@RyanMarcotte
Copy link

RyanMarcotte commented Nov 11, 2020

After trying out the linked snippet in a NET 5 Blazor Server project, it looks like SimpleInjectorComponentActivator will be used to resolve instances of all Blazor components, including framework-provided ones like CascadingAuthenticationState. The CascadingAuthenticationState component also implements IDisposable, which trips the "disposable transient component" error if I register that component with Simple Injector. It would be ideal if framework/library-provided components could be resolved by Microsoft's container and I only have to consider registering my own components with Simple Injector.

From looking at the latest version of ComponentFactory, it is indeed an all-or-nothing approach to component resolution. I wrote up the following based on the implementation of DefaultComponentActivator, but I feel like there is room for improvement.

private class SimpleInjectorComponentActivator : IComponentActivator
{
    private readonly Container _container;

    public SimpleInjectorComponentActivator(Container container)
    {
        _container = container ?? throw new ArgumentNullException(nameof(container));
    }

    public IComponent CreateInstance(Type componentType)
    {
        try
        {
            return (IComponent)_container.GetInstance(componentType);
        }
        catch (ActivationException e)
        {
            // not a fan...  this still executes if a legitimate error occurred when resolving the component above
            var instance = Activator.CreateInstance(componentType);
            if (!(instance is IComponent component))
                throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));

            return component;
        }
    }
}

The above gets me part-way there. I seem to be missing a piece when it comes to resolving my own Blazor components that depend on a Blazor-provided component registered with scoped lifestyle (services.AddServerSideBlazor). NavigationManager is one such Blazor-provided component. Since Blazor Server uses SignalR under the hood, maybe adding scope similar to what is done for the SignalR Core integration would be sufficient?

It is also possible that I am missing something that renders all of the above moot. Please advise if that is indeed the case.

@RyanMarcotte
Copy link

RyanMarcotte commented Nov 11, 2020

I had forgotten that Simple Injector can resolve unregistered types via the ResolveUnregisteredType event hook. This looks better:

private class SimpleInjectorComponentActivator : IComponentActivator
{
    private readonly Container _container;

    public SimpleInjectorComponentActivator(Container container)
    {
        _container = container ?? throw new ArgumentNullException(nameof(container));
        _container.ResolveUnregisteredType += (s, e) =>
        {
            if (!e.Handled && e.UnregisteredServiceType.IsAssignableTo(typeof(IComponent)))
            {
                var registration = Lifestyle.Transient.CreateRegistration(
                    e.UnregisteredServiceType,
                    () => Activator.CreateInstance(e.UnregisteredServiceType),
                    _container);

                e.Register(registration);
            }
        };
    }

    public IComponent CreateInstance(Type componentType) => (IComponent)_container.GetInstance(componentType);
}

@RyanMarcotte
Copy link

RyanMarcotte commented Nov 11, 2020

The scoping issue appears to have been caused by me registering some scoped-lifetime Blazor services with the Microsoft DI container and with Simple Injector. Obviously, that's wrong. Interestingly, the application starts successfully if the scoped-lifetime Blazor services are only registered with the Microsoft DI container, but not if they are only registered with the Simple Injector container. The following exception is thrown in that case.

The configuration is invalid. Creating the instance for type DialogService failed. RemoteNavigationManager has not been initialized. Verification was triggered because Container.Options.EnableAutoVerification was enabled. To prevent the container from being verified on first resolve, set Container.Options.EnableAutoVerification to false.

(services.AddServerSideBlazor calls services.AddScoped<NavigationManager, RemoteNavigationManager>())

I'll keep the scoped services registered in the Microsoft container.

The following configuration needs to happen too, which I found here and here.

services.AddSimpleInjector(_container, options =>
{
    options.AddAspNetCore(ServiceScopeReuseBehavior.OnePerNestedScope);
});

It looks like everything is working after making the above two changes (resolving unregistered component types using Activator.CreateInstance(...) and overriding service scope reuse behavior). I could not have done this without all the thorough documentation and examples provided by you @dotnetjunkie , so big thanks for that!

@RyanMarcotte
Copy link

RyanMarcotte commented Nov 11, 2020

One last thing... Blazor handles disposal of components that implement IDisposable so we can suppress the diagnostic error.

private static readonly Assembly[] _blazorComponentAssemblyCollection = { ... };

// to register all Blazor components
foreach (var type in container.GetTypesToRegister<IComponent>(_blazorComponentAssemblyCollection))
{
    container.Register(type);
    container.GetRegistration(type).Registration
        .SuppressDiagnosticWarning(
            DiagnosticType.DisposableTransientComponent,
            "Disposal handled by Blazor.");
}

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Nov 11, 2020

After trying out the linked snippet in a NET 5 Blazor Server project, it looks like SimpleInjectorComponentActivator will be used to resolve instances of all Blazor components, including framework-provided ones like CascadingAuthenticationState.

You are right. I missed that. I should update the example to reflect this.

Your solution using ResolveUnregisteredType is nice. If I understand correctly, you registered your application IComponent types seperately, and you use ResolveUnregisteredType solely to fallback on the creation of framework components. The use of Activator.CreateInstance works, because (for some completely bizarre reason) those Blazor components must have a default constructor. In complete contrast with how DI works in the rest of ASP.NET Core, constructor injection is not supported, but property injection is. Microsoft's DefaultComponentActivator internally just calls Activator.CreateInstance.

Here is an alternative solution with a similar effect:

This code snippet has been updated

public sealed class SimpleInjectorComponentActivator : IComponentActivator
{
    private readonly Dictionary<Type, InstanceProducer<IComponent>> applicationProducers;

    public SimpleInjectorComponentActivator(Container container, Assembly[] assemblies)
    {
        this.applicationProducers = (
            from type in container.GetTypesToRegister<IComponent>(assemblies)
            select (type, producer: this.CreateBlazorProducer(type, container)))
            .ToDictionary(v => v.type, v => v.producer);
    }

    public IComponent CreateInstance(Type type) =>
        this.applicationProducers.TryGetValue(type, out var producer)
            ? producer.GetInstance()
            : (IComponent)Activator.CreateInstance(type);

    private InstanceProducer<IComponent> CreateBlazorProducer(Type type, Container container)
    {
        var producer = Lifestyle.Transient.CreateProducer<IComponent>(type, container);
        producer.Registration.SuppressDiagnosticWarning(
            DiagnosticType.DisposableTransientComponent,
            "Blazor will dispose components.");
        return producer;
    }
}
  • This class makes actively makes the registrations for its components in its constructor.
  • It does so by accepting a list of all application assemblies that contain components to register.
  • During resolution, the cached producers are used to create the application components.
  • When a type is not in the list, it's assumed to be a framework component and in that case it will fallback to the default creation mechanism which is a mere call to Activator.CreateInstance.
  • It suppresses the DisposableTransientComponent warning because Blazor components are expected to implement IDisposable and the framework will take care of their disposal.

This code snippet shows how to register this class:

services.AddSingleton<IComponentActivator>(
    new SimpleInjectorComponentActivator(container, new[] { typeof(Startup).Assembly });

@RyanMarcotte
Copy link

Your solution using ResolveUnregisteredType is nice. If I understand correctly, you registered your application IComponent types seperately, and you use ResolveUnregisteredType solely to fallback on the creation of framework components. The use of Activator.CreateInstance works, because (for some completely bizar reason) those Blazor components must have a default constructor. In complete contrast with how DI works in the rest of ASP.NET Core, constructor injection is not supported, but property injection is. Microsoft's DefaultComponentActivator internally just calls Activator.CreateInstance.

That is correct. I started off the original integration code and started tweaking it from there. I like your approach of bundling registration and resolution of Blazor components together. Thanks for writing that up!

Unfortunately, DefaultComponentActivator is internal. I'm not sure why that is the case. We would need to duplicate DefaultComponentActivator in our own code until that class is made public.

@RyanMarcotte
Copy link

RyanMarcotte commented Nov 11, 2020

I am still receiving errors related to RemoteNavigationManager.

System.InvalidOperationException
  HResult=0x80131509
  Message='RemoteNavigationManager' has not been initialized.
  Source=Microsoft.AspNetCore.Components
  StackTrace:
   at Microsoft.AspNetCore.Components.NavigationManager.AssertInitialized()
   at Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(String uri, Boolean forceLoad)
   at Ecofresh.WebApplication.Pages.SignupUser.Cancel() in C:\X\Ecofresh\src-webapp\Ecofresh.WebApplication\Pages\SignupUser.razor.cs:line 35
   at Ecofresh.WebApplication.Pages.SignupUser.<BuildRenderTree>b__8_1(MouseEventArgs e) in C:\X\Ecofresh\src-webapp\Ecofresh.WebApplication\Pages\SignupUser.razor:line 8

SignupUser is a component that derives from the following base class:

public abstract class PageComponentBase : ComponentBase
{
    protected PageComponentBase(NavigationManager navigationManager)
    {
        NavigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
    }

    protected NavigationManager NavigationManager { get; }

    public abstract string Title { get; }
}

The exception occurs when I click a Cancel button in that component that is supposed to use the NavigationManager property to navigate the user back to the home page.

// in SignupUser component
private void Cancel() => NavigationManager.NavigateTo("", true);

As previously mentioned, services.AddServerSideBlazor calls services.AddScoped<NavigationManager, RemoteNavigationManager>(). It looks like my own component is receiving a brand new instance of RemoteNavigationManager instead of one already initialized by Blazor within the scope of a SignalR connection used under the hood.

@RyanMarcotte
Copy link

Changing the PageComponentBase class to this works, but seems less than ideal:

public abstract class PageComponentBase : ComponentBase
{
    // back to property injection
    [Inject] protected NavigationManager NavigationManager { get; set; }

    public abstract string Title { get; }
}

This works because Blazor will still inject services for components instantiated by a custom component activator.

@dotnetjunkie
Copy link
Collaborator

Your NavigationManager problem has something to do with scoping, because if you use ctor injection, the resolution goes through Simple Injector (which will again request the instance from .NET Core), while if you use [Inject] (and didn't configure a custom IPropertyInjectionBehavior, it will be Blazor's ComponentFactory that will do the property injection by resolving the property from the .NET Core Container.

But this is probably a sign of a bigger issue with scoping. This could easily pop-up in other places as well.

I've quickly been going through the MSDN docs you provider, but I'm starting to realize that Blazor works quite differently from your typical server application. I have to investigate further, because I'm a bit in the dark right now.

@gitcob
Copy link

gitcob commented Nov 17, 2020

I've just started using Blazor and I'm a bit confused.

Using the default "Blazor Server App" (with .NET 5.0) template, after adding all the default SI integrations and the changes suggested in this issue (including the ComponentActivator that uses the default Activator as a fallback) , I still can't seem to get this to work. I also created a IPropertyInjectionBehavior that looks for InjectAttribute.

I moved the WeatherForecastService registration to Simple Injector, which is used by FetchData.razor using @inject.
When I go to that page, I get an error from the ComponentFactory.

This works because Blazor will still inject services for components instantiated by a custom component activator.

Does this mean that that path is still inaccessible to Simple Injector?

I've created this gist with my changes.

@dotnetjunkie
Copy link
Collaborator

@gitcob, @RyanMarcotte,

I finally have some spare time to dive a bit deeper into Blazor. I've been going through its documentation, and trying to add integration with Simple Injector using the default template. I'm however starting to feel that we have a serious problem here.

The problem lies in how Microsoft's internal ComponentFactory works. Even though it calls a custom IComponentActivator implementation, it will always apply property injection on @inject properties. But unfortunately it is hard-wired to use the built-in container for resolving those property dependencies . But as some of those properties will be Simple Injector-registered, the ComponentFactory will throw an exception.

As I see it, it shouldn't have been the ComponentFactory's applying property injection, but it should have been MS's DefaultComponentActivator. But this prevents us with another problem, which is that the DefaultComponentActivator is internal. Therefore, the only way I see this problem can be solved is when in v5.1:

  • The DefaultComponentActivator becomes public
  • The logic for injecting properties is moved from ComponentFactory into the DefaultComponentActivator.

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Dec 31, 2020

One workaround around this is to refrain from using the @inject tag in your Blazor components, but instead specify the injection property in the @code block, using a customly defined attribute. For instance:

@page "/fetchdata"

@using BlazorApp1.Data

<h1>Weather forecast</h1>

    <table class="table">
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
    </table>
}

@code {
    // Dependency Property with custom attribute here.
    [Dependency]
    public WeatherForecastService ForecastService { get; set; }

    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

This can be wired up as follows:

namespace BlazorApp1
{
    // Your custom attribute
    [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
    public sealed class DependencyAttribute : Attribute { }

    // custom property selection behavior that allows Simple Injector to inject properties
    // marked with [Dependency]
    class DependencyAttributePropertySelectionBehavior : IPropertySelectionBehavior
    {
        public bool SelectProperty(Type type, PropertyInfo prop) =>
            prop.GetCustomAttributes(typeof(DependencyAttribute)).Any();
    }

    public class Startup
    {
        private Container container = new Container();

        public Startup(IConfiguration configuration)
        {
            this.Configuration = configuration;

            // Instruct Simple Injector to use the custom property selection behavior
            container.Options.PropertySelectionBehavior = new DependencyAttributePropertySelectionBehavior();
        }

        [...]
    }
}

The reason you should define your own injection attribute is because ComponentFactory reacts to properties that are marked with the Microsoft.AspNetCore.Components.InjectAttribute. Simply moving from @inject to properties marked with [Inject] will, therefore, not solve the issue.

@dotnetjunkie
Copy link
Collaborator

See dotnet/aspnetcore#28957

@dotnetjunkie
Copy link
Collaborator

@RyanMarcotte, I have been able to reproduce your NavigationManager issue. The Simple Injector integration creates a new IServiceScope to resolve cross-wired services (such as the NavigationManager) from. This clearly doesn't work in the context Blazor and I'm trying to figure out how to fix this. Stay tuned...

@dotnetjunkie
Copy link
Collaborator

@RyanMarcotte, good news. I think I got to the heart of the issue concerning scoping, and now have a better understanding of how scoping works in Blazor and I think I have a solution that allows Simple Injector to be fully integrated in Blazor. It does mean I have to add a small feature to the core library. After I released a beta for the core library, I'll share the integration code that you can use to try. But long story short, Blazor scopes are long lived, while the asynchronous context (that Simple Injector's AsyncScopedLifestyle depends on) gets cleared. So the trick is to resurrect the Simple Injector Scope at the right moments (resurrection is the feature that needs to be added to the core library). As far as I can see now, those right moments are:

  • Inside the IHubActivator<T> implementation
  • Inside the IComponentActivator implementation

Stay tuned.

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Jan 8, 2021

@RyanMarcotte and others,

Below is a prototype that allows integrating Simple Injector with Blazor. It requires the following NuGet packages:

This prototype solves the problems with scoping as described by @RyanMarcotte.

I'm really interested in feedback by anyone who can try this out. When it seems to work correctly, I will likely transform this into a integration package.

This prototype does not include property injection, but this can be added using the information in this earlier comment.

LAST UPDATE: 2021-02-19

using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SimpleInjector;
using SimpleInjector.Advanced;
using SimpleInjector.Diagnostics;
using SimpleInjector.Integration.ServiceCollection;
using SimpleInjector.Lifestyles;

public class Startup
{
    private readonly Container container = new Container();

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

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

        services.AddSimpleInjector(container, options =>
        {
            // Custom extension method; see code below.
            options.AddServerSideBlazor(this.GetType().Assembly);

            // Adds the IServiceScopeFactory, required for the IServiceScope registration.
            container.Register(
                () => options.ApplicationServices.GetRequiredService<IServiceScopeFactory>(),
                Lifestyle.Singleton);
        });

        // Replace the IServiceScope registration made by .AddSimpleInjector
        // (must be called after AddSimpleInjector)
        container.Options.AllowOverridingRegistrations = true;
        container.Register<ServiceScopeAccessor>(Lifestyle.Scoped);
        this.container.Register<IServiceScope>(
            () => container.GetInstance<ServiceScopeAccessor>().Scope
                ?? container.GetInstance<IServiceScopeFactory>().CreateScope(),
            Lifestyle.Scoped);
        container.Options.AllowOverridingRegistrations = false;

        InitializeContainer();
    }

    private void InitializeContainer()
    {
        // container.RegisterSingleton<WeatherForecastService>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.ApplicationServices.UseSimpleInjector(container);

        // Default VS template stuff
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
        }

        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });

        container.Verify();
    }
}

public static class BlazorExtensions
{
    private static readonly AsyncScopedLifestyle lifestyle = new AsyncScopedLifestyle();

    public static void AddServerSideBlazor(
        this SimpleInjectorAddOptions options, params Assembly[] assemblies)
    {
        options.Container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

        options.Services.AddScoped<ScopeAccessor>();
        options.Services.AddScoped<IComponentActivator, SimpleInjectorComponentActivator>();

        // HACK: This internal ComponentHub type needs to be added for the
        // SimpleInjectorBlazorHubActivator to work.
        options.Services.AddTransient(
            typeof(Microsoft.AspNetCore.Components.Server.CircuitOptions).Assembly.GetTypes().First(
                t => t.FullName == "Microsoft.AspNetCore.Components.Server.ComponentHub"));

        options.Services.AddScoped(typeof(IHubActivator<>), typeof(SimpleInjectorBlazorHubActivator<>));

        RegisterBlazorComponents(options, assemblies);
    }

    public static void ApplyServiceScope(this Container container, IServiceProvider requestServices)
    {
        var accessor = requestServices.GetRequiredService<ScopeAccessor>();

        if (accessor.Scope is null)
        {
            accessor.Scope = AsyncScopedLifestyle.BeginScope(container);
            accessor.Scope.GetInstance<ServiceScopeAccessor>().Scope = (IServiceScope)requestServices;
        }
        else
        {
            lifestyle.SetCurrentScope(accessor.Scope);
        }
    }

    private static void RegisterBlazorComponents(SimpleInjectorAddOptions options, Assembly[] assemblies)
    {
        var types = options.Container.GetTypesToRegister(typeof(IComponent), assemblies,
            new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true });

        foreach (Type type in types.Where(t => !t.IsGenericTypeDefinition))
        {
            var registration = Lifestyle.Transient.CreateRegistration(type, options.Container);

            registration.SuppressDiagnosticWarning(
                DiagnosticType.DisposableTransientComponent,
                "Blazor will dispose components.");

            options.Container.AddRegistration(type, registration);
        }

        foreach (Type type in types.Where(t => t.IsGenericTypeDefinition))
        {
            options.Container.Register(type, type, Lifestyle.Transient);
        }
    }
}

public sealed class ScopeAccessor : IAsyncDisposable
{
    public Scope Scope { get; set; }
    public ValueTask DisposeAsync() => this.Scope.DisposeAsync();
}

public sealed class ServiceScopeAccessor
{
    public IServiceScope Scope { get; set; }
}

public sealed class SimpleInjectorComponentActivator : IComponentActivator
{
    private readonly Container container;
    private readonly IServiceProvider serviceScope;

    public SimpleInjectorComponentActivator(Container container, IServiceProvider serviceScope)
    {
        this.container = container;
        this.serviceScope = serviceScope;
    }

    public IComponent CreateInstance(Type type) =>
        (IComponent)this.GetInstance(type) ?? (IComponent)Activator.CreateInstance(type);

    private object GetInstance(Type type)
    {
        this.container.ApplyServiceScope(this.serviceScope);
        return this.container.GetRegistration(type)?.GetInstance();
    }
}

public sealed class SimpleInjectorBlazorHubActivator<T> : IHubActivator<T> where T : Hub
{
    private readonly Container container;
    private readonly IServiceProvider serviceScope;

    public SimpleInjectorBlazorHubActivator(Container container, IServiceProvider serviceScope)
    {
        this.container = container;
        this.serviceScope = serviceScope;
    }

    public T Create()
    {
        this.container.ApplyServiceScope(this.serviceScope);
        return this.container.GetInstance<T>();
    }

    public void Release(T hub) { }
}

For now, this is still quite some code, and some unfortunate ugly hacks, but this can hopefully all be tucked away in the near future. All I need is some people who can test run this prototype.

@dotnetjunkie
Copy link
Collaborator

Also dotnet/aspnetcore#29194

@RyanMarcotte
Copy link

I was able to use the new integration code without any modifications and I am also able to successfully inject scoped components (like NavigationManager) into my own Blazor components. Thank you very much for looking into this!

@thepigeonfighter
Copy link

thepigeonfighter commented Feb 9, 2021

I'm testing this out now, its working pretty great so far. A shame that it requires so many hacks, but the I gotta be able to use simple injector so it is worth the trouble 😄. One issue (not sure if it is an issue) the [Dependency] attribute appears to only work on public properties. Is there a setting somewhere that allows for private property injection?

I was able to reproduce it in a stripped down project. Just using the code you provided above for property injection and integration. Made a base component:

public class BaseComponent : ComponentBase
{
    [Dependency]
    private WeatherForecastService _weatherService { get; set; }

    protected async Task<WeatherForecast[]> GetForecasts()
    {
        return await _weatherService.GetForecastAsync(DateTime.Now);
    }
}

Then made a component that used that base component

@inherits BaseComponent
@page "/inherited"
<h3>InheritedComponent</h3>
@code {
    public TestBlazorApp.Data.WeatherForecast[] Forecasts { get; set; }

    protected override async Task OnInitializedAsync()
    {
        base.OnInitialized();

        Forecasts = await GetForecasts();
    }
}

When you execute the method GetForecasts() it throws a null reference exception, but if you change the private property _weatherService to a public property it works.

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Feb 9, 2021

@thepigeonfighter

A shame that it requires so many hacks

This will be temporary. It will make sure that integration will become easier over time.

One issue (not sure if it is an issue) the [Dependency] attribute appears to only work on public properties.

You ran into an unfortunate bug. Simple Injector incorrectly calls .NET's RuntimeReflectionExtensions.GetRuntimeProperties(Type) method, which only returns properties that are visible to the given type. The correct behavior would be to get all properties that are defined on the type and its all its base types, independently of their access modifier.

I added it to the v5.3 milestone. Until this bug gets fixed, as a workaround, you can make the property protected.

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Feb 11, 2021

@thepigeonfighter,

But the good news is that this is a problem that you not have, thanks to the design of your application. I'll explain why.

Your ApplicationDBContext is an Entity Framework DbContext. Although these objects should typically be registered as Scoped, this causes serious trouble in the context of Blazor. With Blazor, a single user gets a single Scope. Such Scope stays alive as long as the user stays on the page without manually triggering a reload. This could be hours or even days. This causes problems for Unit of Work objects because their data becomes stale and they can contain a considerable amount of memory.

But even worse, Blazor does not prevent parallel calls by the user. This means that multiple threads can access your ApplicationDBContext at the same time. Entity Framework's DbContext, however, is not thread safe and rather sooner than later will this cause your application to crash. This information is reflected in the official Blazor documentation.

The solution is to ensure a new DbContext is created for each new action you invoke. This solution, however, has some serious maintainability consequences, because it means you would have to create and dispose new scopes throughout your application in order to have all Scoped instances accessed single-threadedly and disposed deterministically. And having to do this all over the place causes serious maintainability issues, unless...

Unless you have a properly designed application—which is what you have. In your case you have two options:

  • Manage Simple Injector scopes inside your CQRSProcessor
  • Create a decorator (one for command handlers, one for query handlers) that ensure the handler is executed inside a new isolated scope.

If yo do this in your CQRSProcessor, that might look as follows:

public List<T> All<T>()
{
    using (AsyncScopedLifestyle.BeginScope(_container))
    {
        var getter = _container.GetInstance<IQueryHandler<GetAllQuery<T>, List<T>>>();
        return getter.Handle(GetAllQuery<T>.Instance);
    }
}

Decorator is perhaps a bit more work. Here's an example of how to do this.

p.s. I noticed you were using a lot of reflection inside the CQRSProcessor. This might not be needed in your case. My previous code snippet demonstrates this.

p.p.s. you should ditch the CQRSProcessor.GetQueryable<T>() method. Returning an IQueryable<T> on this level is problematic, because you can't wrap a scope around it. The scope will dispose of the DbContext before its IQueryable<T> gets actually used. That would result in an ObjectDisposedException.

@thepigeonfighter
Copy link

Thanks for all your research into this. I definitely have learned a lot from this. A couple notes.

  1. You are absolutely correct on the CQRSProcessor having unnecessary reflection! Can't believe I didn't notice that. So thanks for pointing that out, we'll get that fixed ASAP.
  2. I like your idea of a decorator maintaining scope, I already use one for logging so it would not be difficult to add another. The only issue I can foresee with this solution is when it comes to proxies and lazy loading. With the DbContext, I noticed that the lazy loading might cause issues with trying to access a disposed context.
  3. So just to confirm you would say the safest lifestyle for a DbContext in blazor would be Transient? I saw that in your integration code you made the Blazor Components transient with the justification that the framework disposes of the components. I was afraid that if I tried to make the DbContext transient it would possibly create memory leaks or something. Not sure if that fear is justified.

@dotnetjunkie
Copy link
Collaborator

dotnetjunkie commented Feb 11, 2021

So just to confirm you would say the safest lifestyle for a DbContext in blazor would be Transient?

Transient does not solve the problem. Even a Transient component will stay alive as long the Blazor component it gets injected into. Besides, it could lead to other complications, such as having multiple instances while executing a single request.

Instead, you should prevent a DbContext from becoming a Captive Dependency. You can do this by preventing its usage outside the explicitly created scope of your processor.

@J-Hauser
Copy link

J-Hauser commented Feb 26, 2021

Hello,

Thank you for this great framework and your commitment!

I think there might still be a problem with scoping or related to/the same as the issue with Blazor events.
I use a composite to filter a collection of handlers based on some runtime-data. When i inject some cross-wired services (NavigationManager and AuthenticationStateProvider in my case) they are not initialized.
If i just inject the concrete handlers into a component, then they are initialized. I tried to wrap the container.GetAllInstances() in a scope but this did not work either.

I have a repo here

In the Blazor Source the NavigationManager gets initialized here. The AuthenticationState gets also set there and also here. I could not see any hooks/extension points other than replacing the ServiceScopeFactory.

I think I can work around the AuthenticationStateProvider this by putting the required data in some app-state-container. But i am a bit lost, what to do with the NavigationManager.

Repository owner deleted a comment from thepigeonfighter Feb 26, 2021
Repository owner deleted a comment from thepigeonfighter Feb 26, 2021
@dotnetjunkie
Copy link
Collaborator

I think there might still be a problem with scoping or related to/the same as the issue with Blazor events.

Thanks for this detailed repro. The problem seems indeed identical as before; the lack of ability to intercept Blazor events. This causes the Simple Injector scope to be unrelated to the Blazor scope, which means a new NavigationManager is resolved; because this NavigationManager is new, it hasn't been initialized by Blazor, which is what causes the exception.

I'm afraid we have to wait for Microsoft to add an interception point here. It's the Conforming Container again that "is leading [...] framework developers to stop thinking about defining the right library and the right framework abstractions" as I described here years ago.

@J-Hauser
Copy link

J-Hauser commented Feb 28, 2021

Okay, I think i got it working somehow. There seems to be one place, where the scope can be applied: inside the IHandleEvent.HandleEventAsync-Method. So if one implements this interface themself, then it seems like it is possible to catch the correct ServiceScope. See the repo:

public class SimpleInjectorEventHandlerScopeProvider
{
    private readonly IServiceProvider _serviceScope;
    private readonly Container _container;

    public SimpleInjectorEventHandlerScopeProvider(
        IServiceProvider serviceScope, Container container)
    {
        _serviceScope = serviceScope;
        _container = container;
    }

    public void ApplyScope()
    {
        _container.ApplyServiceScope(_serviceScope);
    }
}
options.Services.AddScoped<SimpleInjectorEventHandlerScopeProvider>();
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
{
    _handlerFactory.ApplyScope();  //<--- here!
    
   var task = callback.InvokeAsync(arg);
    var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
        task.Status != TaskStatus.Canceled;

    // After each event, we synchronously re-render (unless !ShouldRender())
    // This just saves the developer the trouble of putting "StateHasChanged();"
    // at the end of every event callback.
    StateHasChanged();

    return shouldAwaitTask ?
        CallStateHasChangedOnAsyncCompletion(task) :
        Task.CompletedTask;  
}

With this unfortunate modification the cross-wired NavigationManager and the AuthenticationStateProvider were initialized.

@dotnetjunkie
Copy link
Collaborator

Is there missing an interface on SimpleInjectorEventHandlerScopeProvider? And where sould IHandleEvent.HandleEventAsync be defined?

@J-Hauser
Copy link

J-Hauser commented Mar 1, 2021

No, the SimpleInjectorEventHandlerScopeProvider must be created. The IHandleEvent.HandleEventAsync must be defined for each Page. A base class seems to work too.

using Microsoft.AspNetCore.Components;
using System.Threading.Tasks;

namespace BlazorSimpleInjector.Pages
{
    public class BaseComponent : ComponentBase, IHandleEvent
    {
        public BaseComponent(SimpleInjectorEventHandlerScopeProvider scopeProvider)
        {
            _scopeProvider = scopeProvider;
        }

        private readonly SimpleInjectorEventHandlerScopeProvider _scopeProvider;

        Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
        {
            _scopeProvider.ApplyScope(); //<-- here

            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            // After each event, we synchronously re-render (unless !ShouldRender())
            // This just saves the developer the trouble of putting "StateHasChanged();"
            // at the end of every event callback.
            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        }

        private async Task CallStateHasChangedOnAsyncCompletion(Task task)
        {
            try
            {
                await task;
            }
            catch // avoiding exception filters for AOT runtime support
            {
                // Ignore exceptions from task cancellations, but don't bother issuing a state change.
                if (task.IsCanceled)
                {
                    return;
                }

                throw;
            }

            StateHasChanged();
        }
    }

    public partial class FetchData : BaseComponent
    {
        public FetchData(IRequestProcessor requestHandler,
            SimpleInjectorEventHandlerScopeProvider scopeProvider) : base(scopeProvider)
        {
            _requestHandler = requestHandler;
        }

        private readonly IRequestProcessor _requestHandler;

        async Task Navigate()
        {
            await _requestHandler.Handle(new Request<Foo, Result<Foo>>
            {
                Model = new Foo()
                {
                    SomeProperty = "test"
                }
            });
        }
    }
}

Repository owner deleted a comment from thepigeonfighter Mar 5, 2021
Repository owner deleted a comment from thepigeonfighter Mar 5, 2021
@dotnetjunkie
Copy link
Collaborator

Hi guys,

I'm happy to announce the first version of the Blazor Server App Integration page in the Simple Injector documentation.

This pages combines all knowledge gathered here in this thread using your help.

If you find any new issues, please let me know. Hopefully we can improve the guidance once more and hopefully, Microsoft improves Blazor soon, which would improve our integration as well.

Thanks again.

@dotnetjunkie dotnetjunkie changed the title Is there support for Razor Components (aka server side Blazor)? Support for Razor Components (aka server side Blazor) Oct 12, 2021
@dotnetjunkie dotnetjunkie removed the question Further information is requested label Oct 12, 2021
@Bouke
Copy link

Bouke commented Nov 18, 2021

I'm just dipping my toes in Blazor and I've followed the integration page. Injection seems to work, but I'm facing issues with the injected AuthenticationStateProvider. This provider is registered with framework DI as a scoped instance. On startup of the CircuitHost the current principal (originally taken from HttpContext.User) is set on this provider. One can inject this provider into components to access the current user and perform authentication. However calling GetAuthenticationStateAsync throws InvalidOperation: "GetAuthenticationStateAsync was called before SetAuthenticationState.". The registration of the provider in the framework DI happens here.

After debugging this issue all evening, I've discovered that the AuthenticationStateProvider instance being injected into my page (through SimpleInjectorComponentActivator) isn't the same instance that the framework calls when setting the current user. As a result, I cannot get the current user through DI.

More information about authentication in Blazor Server: https://docs.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-6.0#authenticationstateprovider-service. The user's information is available through other means like the "cascading parameter" as shown in this document.

This is probably related to this issue in aspnetcore. To test this, I have subclassed ServerAuthenticationStateProvider and a breakpoint in the constructor is hit twice when starting the Blazor Server: when setting the user in CircuitHost, and when injecting into the page.

Update: after debugging some more, it seems I do get injected the correct AuthenticationStateProvider through the framework using @inject. This can be misused to copy the authentication state to the SimpleInjector's instance in App.razor like below:

@using System.Diagnostics
@code {
    [Inject] AuthenticationStateProvider FrameworkAuthenticationStateProvider { get; set; }
    [MyApp.Dependency] AuthenticationStateProvider ContainerAuthenticationStateProvider { get; set; }

    protected override void OnInitialized()
    {
        // Beware this note about AuthenticationStateProvider:
        // > The main drawback to using AuthenticationStateProvider directly is that the component
        // > isn't notified automatically if the underlying authentication state data changes.
        Debug.Assert(FrameworkAuthenticationStateProvider != ContainerAuthenticationStateProvider,
                     "Somehow the same instance was returned, making this hack unneeded");
        if (ContainerAuthenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hosted)
            hosted.SetAuthenticationState(FrameworkAuthenticationStateProvider.GetAuthenticationStateAsync());
    }
}

@J-Hauser
Copy link

J-Hauser commented Nov 18, 2021

If you are using @inject, then the component will be created by the MS-DI-Framework.
Which Frontend-Framewok do you use? There are some, which do not play so nice with a non-conforming-container?
Do you create your components at runtime?

Edit: Just a guess: what happens, if you remove the CascadingAuthenticationStateProvider?

@Bouke
Copy link

Bouke commented Nov 18, 2021

If you are using @inject, then the component will be created by the MS-DI-Framework.
Which Frontend-Framewok do you use? There are some, which do not play so nice with a non-conforming-container?
Do you create your components at runtime?

By framework I mean ASP.NET Core 6. Injecting AuthenticationStateProvider into my Razor Page gives the correct instance when using @inject, but the wrong one using SimpleInjector (through SimpleInjectorComponentActivator as per the docs). I need to be able to use SimpleInjector as the user's information is need throughout my application, and SimpleInjector performs DI of my application objects.

Edit: Just a guess: what happens, if you remove the CascadingAuthenticationStateProvider?

Doesn't make a difference.

@J-Hauser
Copy link

Yes, if you are using @inject then the whole component will be created solely by MS-DI-Framework and not by SimpleInjector afaik.
I have the same requirements. I wrapped the AuthenticationStateProvider inside a UserInfoService which is registerd as transient. This works for me so far.

Some other thing, which might be of interest: If you are using EditContext.NofiyFieldChanged somewhere there is a chance, that this exception can also occur. Circumvent it by calling the ScopeApplier beforehand.

@Bouke
Copy link

Bouke commented Nov 18, 2021

Can you share some of your code showing how you use AuthenticationStateProvider with SimpleInjector? I can't get it to work; I've created a transient UserInfoService like you said, registering with either the framework's DI or SimpleInjector, but the result is the same: GetAuthenticationStateAsync throws.

@J-Hauser
Copy link

J-Hauser commented Nov 19, 2021

Yes, but really nothing fancy:

    public class UserInfoService : IUserInfoService
    {
        private readonly AuthenticationStateProvider _authenticationStateProvider;

        public UserInfoService(AuthenticationStateProvider authenticationStateProvider)
        {
            _authenticationStateProvider = authenticationStateProvider;
        }

        private async Task<AuthenticationState> GetAuthenticationState()
        {
             return await _authenticationStateProvider.GetAuthenticationStateAsync();
        }

        public async Task<string> GetClaimValue(string claim)
        {
            return (await GetAuthenticationState()).User?.FindFirst(claim)?.Value;
        }

        public async Task<bool> UserIsAuthenticatedAsync()
        {
            return (await GetAuthenticationState()).User.Identity.IsAuthenticated;
        }
    } 

container.Register<IUserInfoService, UserInfoService>();

used in some services/handlers through constructor injection. And used in one component. But this gets called on every new Page and retrieves some values every time:

        [Dependency] IUserInfoService UserInfoService{ get; set; }

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            await base.OnAfterRenderAsync(firstRender);
             var isAuthenticated = await UserInfoService.UserIsAuthenticatedAsync();
        }

Edit: Where and When does it throw? Do you hit the ServiceScopeApplier.ApplyServiceScope() before the exception?

@Bouke
Copy link

Bouke commented Nov 19, 2021

I'm registering this class like this in SimpleInjector: container.Register<UserInfoService>();. My App.razor looks like this:

@code {
    [Dependency] UserInfoService UserInfoService { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        var isAuthenticated = await UserInfoService.UserIsAuthenticatedAsync();
    }
}

This causes the exception mentioned above ("GetAuthenticationStateAsync was called before SetAuthenticationState.").

Do you hit the ServiceScopeApplier.ApplyServiceScope() before the exception?

Yes.

@Bouke
Copy link

Bouke commented Nov 20, 2021

I believe the same scoping problem also happens when injecting NavigationManager. When injected through SimpleInjector it fails on use:

Error: System.InvalidOperationException: 'RemoteNavigationManager' has not been initialized.
at Microsoft.AspNetCore.Components.NavigationManager.AssertInitialized()
at Microsoft.AspNetCore.Components.NavigationManager.NavigateTo(String uri, Boolean forceLoad, Boolean replace)

When @injected through the framework, it works fine.

@J-Hauser
Copy link

J-Hauser commented Nov 20, 2021

I can't kill it. Please provide a sample project to reproduce this behavior.

FWIW here is a minimal working example for .net 6

SimpleInjectorIntegration.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR;
using SimpleInjector.Advanced;
using SimpleInjector.Diagnostics;
using SimpleInjector.Integration.ServiceCollection;
using SimpleInjector.Lifestyles;
using System.Reflection;

namespace SimpleInjector
{
    [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
    public sealed class DependencyAttribute : Attribute { }

    public class DependencyAttributePropertySelectionBehavior : IPropertySelectionBehavior
    {
        public bool SelectProperty(Type type, PropertyInfo prop) =>
            prop.GetCustomAttributes(typeof(DependencyAttribute)).Any();
    }

    public sealed class ScopeAccessor : IAsyncDisposable, IDisposable
    {
        public Scope Scope { get; set; }
        public ValueTask DisposeAsync() => this.Scope?.DisposeAsync() ?? default;
        public void Dispose() => this.Scope?.Dispose();
    }

    public static class BlazorExtensions
    {
        public static void AddServerSideBlazor(
            this SimpleInjectorAddOptions options, params Assembly[] assemblies)
        {
            var services = options.Services;

            // Unfortunate nasty hack. We reported this with Microsoft.
            services.AddTransient(
                typeof(Microsoft.AspNetCore.Components.Server.CircuitOptions)
                    .Assembly.GetTypes().First(
                    t => t.FullName ==
                        "Microsoft.AspNetCore.Components.Server.ComponentHub"));

            services.AddScoped(
                typeof(IHubActivator<>), typeof(SimpleInjectorBlazorHubActivator<>));
            services.AddScoped<IComponentActivator, SimpleInjectorComponentActivator>();

            RegisterBlazorComponents(options, assemblies);

            services.AddScoped<ScopeAccessor>();
            services.AddTransient<ServiceScopeApplier>();
        }

        private static void RegisterBlazorComponents(
            SimpleInjectorAddOptions options, Assembly[] assemblies)
        {
            var container = options.Container;
            var types = container.GetTypesToRegister<IComponent>(
                assemblies,
                new TypesToRegisterOptions { IncludeGenericTypeDefinitions = true });

            foreach (Type type in types.Where(t => !t.IsGenericTypeDefinition))
            {
                var registration =
                    Lifestyle.Transient.CreateRegistration(type, container);

                registration.SuppressDiagnosticWarning(
                    DiagnosticType.DisposableTransientComponent,
                    "Blazor will dispose components.");

                container.AddRegistration(type, registration);
            }

            foreach (Type type in types.Where(t => t.IsGenericTypeDefinition))
            {
                container.Register(type, type, Lifestyle.Transient);
            }
        }
    }

    public sealed class SimpleInjectorComponentActivator : IComponentActivator
    {
        private readonly ServiceScopeApplier applier;
        private readonly Container container;

        public SimpleInjectorComponentActivator(
            ServiceScopeApplier applier, Container container)
        {
            this.applier = applier;
            this.container = container;
        }

        public IComponent CreateInstance(Type type)
        {
            this.applier.ApplyServiceScope();

            IServiceProvider provider = this.container;
            var component = provider.GetService(type) ?? Activator.CreateInstance(type);
            return (IComponent)component;
        }
    }

    public sealed class SimpleInjectorBlazorHubActivator<T>
        : IHubActivator<T> where T : Hub
    {
        private readonly ServiceScopeApplier applier;
        private readonly Container container;

        public SimpleInjectorBlazorHubActivator(
            ServiceScopeApplier applier, Container container)
        {
            this.applier = applier;
            this.container = container;
        }

        public T Create()
        {
            this.applier.ApplyServiceScope();
            return this.container.GetInstance<T>();
        }

        public void Release(T hub) { }
    }

    public sealed class ServiceScopeApplier
    {
        private static AsyncScopedLifestyle lifestyle = new AsyncScopedLifestyle();

        private readonly IServiceScope serviceScope;
        private readonly ScopeAccessor accessor;
        private readonly Container container;

        public ServiceScopeApplier(
            IServiceProvider requestServices, ScopeAccessor accessor, Container container)
        {
            this.serviceScope = (IServiceScope)requestServices;
            this.accessor = accessor;
            this.container = container;
        }

        public void ApplyServiceScope()
        {
            if (this.accessor.Scope is null)
            {
                var scope = AsyncScopedLifestyle.BeginScope(this.container);

                this.accessor.Scope = scope;

                scope.GetInstance<ServiceScopeProvider>().ServiceScope = this.serviceScope;
            }
            else
            {
                lifestyle.SetCurrentScope(this.accessor.Scope);
            }
        }
    }

    public abstract class BaseComponent : ComponentBase, IHandleEvent
    {
        [Dependency] public ServiceScopeApplier Applier { get; set; }

        Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
        {
            this.Applier.ApplyServiceScope();

            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;

            StateHasChanged();

            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        }

        private async Task CallStateHasChangedOnAsyncCompletion(Task task)
        {
            try
            {
                await task;
            }
            catch
            {
                if (task.IsCanceled) return;

                throw;
            }

            base.StateHasChanged();
        }
    }
}

Program.cs

using BlazorApp1.Data;
using SimpleInjector;

var container = new Container();

container.Options.PropertySelectionBehavior =
    new DependencyAttributePropertySelectionBehavior();
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();

builder.Services.AddSimpleInjector(container, options =>
{
    options.AddServerSideBlazor(typeof(DependencyAttribute).Assembly);
});

var app = builder.Build();

app.Services.UseSimpleInjector(container);

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

container.Verify();

app.Run();

App.razor

@using SimpleInjector
<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code
{
    [Dependency] AuthenticationStateProvider AuthenticationStateProvider { get; set; }
    [Dependency] NavigationManager NavigationManager { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        var auth = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var authenticated = auth.User.Identity.IsAuthenticated;
        NavigationManager.NavigateTo("/counter");
    }
}

@Bouke
Copy link

Bouke commented Dec 3, 2021

Trying to come up with a minimal repro I've found the issue; I had AddAspNetCore() without specifying ServiceScopeReuseBehavior.OnePerNestedScope. Thank you @J-Hauser for your support and example.

As the culprit of this issue is hard to debug, I think it would be good to either change the default or tell users that they need to change the defaults when using Blazor.

@dotnetjunkie dotnetjunkie transferred this issue from simpleinjector/SimpleInjector Feb 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants