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

Is there support for Razor Components (aka server side Blazor)? #676

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

Is there support for Razor Components (aka server side Blazor)? #676

dharmaturtle opened this issue Mar 20, 2019 · 38 comments

Comments

@dharmaturtle
Copy link

@dharmaturtle dharmaturtle commented Mar 20, 2019

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
Copy link
Collaborator

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

@dotnetjunkie dotnetjunkie commented Mar 28, 2019

Let's see what happens.

@dotnetjunkie
Copy link
Collaborator

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

@RyanMarcotte RyanMarcotte commented Nov 11, 2020

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

@RyanMarcotte RyanMarcotte commented Nov 11, 2020

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

@dotnetjunkie dotnetjunkie commented Nov 12, 2020

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

@dotnetjunkie dotnetjunkie commented Dec 31, 2020

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

@dotnetjunkie dotnetjunkie commented Dec 31, 2020

@dotnetjunkie
Copy link
Collaborator

@dotnetjunkie dotnetjunkie commented Jan 4, 2021

@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

@dotnetjunkie dotnetjunkie commented Jan 6, 2021

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

@dotnetjunkie dotnetjunkie commented Jan 10, 2021

@RyanMarcotte
Copy link

@RyanMarcotte RyanMarcotte commented Jan 24, 2021

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

@thepigeonfighter
Copy link

@thepigeonfighter thepigeonfighter commented Feb 10, 2021

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-wired ApplicationDBContext. 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
Copy link
Collaborator

@dotnetjunkie dotnetjunkie commented Feb 10, 2021

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.

Repository owner deleted a comment from thepigeonfighter Feb 11, 2021
Repository owner deleted a comment from thepigeonfighter Feb 11, 2021
Repository owner deleted a comment from thepigeonfighter Feb 11, 2021
@thepigeonfighter
Copy link

@thepigeonfighter thepigeonfighter commented Feb 11, 2021

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
Copy link
Collaborator

@dotnetjunkie dotnetjunkie commented Feb 11, 2021

@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
Copy link
Collaborator

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

@thepigeonfighter thepigeonfighter commented Feb 11, 2021

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

@dotnetjunkie dotnetjunkie commented Feb 26, 2021

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

@dotnetjunkie dotnetjunkie commented Mar 1, 2021

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

@J-Hauser
Copy link

@J-Hauser 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

@dotnetjunkie dotnetjunkie commented Mar 8, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
6 participants