simpleinjector / SimpleInjector.Integration.AspNetCore

MIT License
2 stars 3 forks source link

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

Open dharmaturtle opened 5 years ago

dharmaturtle commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

Let's see what happens.

dotnetjunkie commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 code snippet shows how to register this class:

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

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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:

dotnetjunkie commented 3 years ago

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 commented 3 years ago

See https://github.com/dotnet/aspnetcore/issues/28957

dotnetjunkie commented 3 years ago

@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 commented 3 years ago

@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:

Stay tuned.

dotnetjunkie commented 3 years ago

@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 commented 3 years ago

Also https://github.com/dotnet/aspnetcore/issues/29194

RyanMarcotte commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

@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.

thepigeonfighter commented 3 years ago

So in further testing, I am running across this error and have tried to fix it in many different ways but have been unable to:

SimpleInjector.ActivationException: Error resolving the cross-wiredApplicationDBContext. You are trying to resolve a cross-wired service, but are doing so outside the context of an active (Async Scoped) scope

Not sure if the unique lifetime of Blazor/SignalR has something to do with this or more likely my own ignorance. Apologies if this is not the correct place for this post. Here is a specific instance of when the error occurs this method is called on a button click.

private async Task OnDeleteUserAsync(string userId)
{
    if (await IsCurrentUserAsync(userId))
    {
        ShowErrorMessage("Can not delete this user, because they are currently signed in.");
        return;
    }
    var result = await ShowConfirmBox("Delete User?", "Are you sure you want to delete this user?");
    if (result)
    {
        //throws on this line
        Processor.Delete<IdentityUser>(userId);

        LoadUsers();
        ShowNotification("User deleted.");
    }
}

Here is the Processor.Delete method. I wrapped it in the disposable Scope variable trying to troubleshoot but it was initially unwrapped.

public CommandResult Delete<T>(object key) where T : class
{
    using (Scope scope = AsyncScopedLifestyle.BeginScope(_container))
    {
        Type commandType = typeof(DeleteCommand<>).MakeGenericType(typeof(T));
        Type handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);
        dynamic deletor = _container.GetInstance(handlerType);
        return deletor.Handle(new DeleteCommand<T>(key));

    }
}

Lastly the DeleteCommandHandler

public class DeleteCommandHandler<T> : ICommandHandler<DeleteCommand<T>> where T : class
{
    private readonly IRepository<T> _repo;

    public DeleteCommandHandler(IRepository<T> repo)
    {
        _repo = repo;
    }

    public CommandResult Handle(DeleteCommand<T> command)
    {
        if (command.Entity != null)
        {
            _repo.Delete(command.Entity);
        }
        else
        {
            _repo.Delete(command.ID);
        }
        _repo.Save();
        return CommandResult.Ok;
    }
}

The IRepository<T> is the reference that depends on the ApplicationDBContext. I assume it has something to do with either my set up being wrong or a setting needing changed in the Blazor integration setup but I am too unfamiliar with the internals of SimpleInjector to be able to tell if it is the later.

In further testing if I change async Task OnDeleteUserAsync(string userId) to async void OnDeleteUserAsync(string userId) this error goes away.

dotnetjunkie commented 3 years ago

In further testing if I change async Task OnDeleteUserAsync(string userId) to async void OnDeleteUserAsync(string userId) this error goes away.

That doesn't sound right. Now everything runs on a background thread; this will likely cause trouble in the future.

Your code looks valid and unsuspicious to me. Would you be able to come up with a Minimal, Reproducible Example, something that demonstrates the problem and that I can copy-paste and run as is to analyze what's going on here? If it's too much code to post, it's fine to upload a zip or link to a repository.

thepigeonfighter commented 3 years ago

So I have reproduced my project setup on as small of a scale as possible. If you run the app and navigate to the users page deleting/creating users throws the error we are talking about. Let me know if you have any questions and sorry I couldn't make the app simpler I just wanted to make sure the set up was as similar to my production app as possible.

TestBlazorApp.zip

dotnetjunkie commented 3 years ago

@thepigeonfighter,

Let's start with the bad news.

It seems we are screwed. I raised an issue with Microsoft about the addition of an interception point around Blazor events. When a blazor event (such as your @onclick) is raised, it is done so in a clean asynchronous context. The integration code you used hooks into two other interception points to ensure Simple Injector's scope is restored. But as such interception point seems to miss for Blazor events, there is no way that we can ensure that a scope is automatically restored. This of course sucks.

UPDATE: After some discussion with the Blazor team, they moved this issue to the Next sprint planning milestone for future evaluation / consideration. Fingers crossed.

UPDATE 2: @J-Hauser created a workaround that involves writing a custom base class.

dotnetjunkie commented 3 years ago

@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:

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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.

dotnetjunkie commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

J-Hauser commented 3 years ago

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"
                }
            });
        }
    }
}
dotnetjunkie commented 3 years ago

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.

Bouke commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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.