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

Injection into custom RazorPage? #25

Open
Bouke opened this issue Oct 7, 2021 · 9 comments
Open

Injection into custom RazorPage? #25

Bouke opened this issue Oct 7, 2021 · 9 comments
Labels
question Further information is requested

Comments

@Bouke
Copy link

Bouke commented Oct 7, 2021

I'm using a custom RazorPage<TModel> which I'd like to have dependencies injected into. It needs a parameterless constructor for MVC, meaning I have to use property injection.

# MyRazorPage.cs
public abstract class MyRazorPage<TModel> : RazorPage<TModel>
{
    [Import]
    public ITenantContextProvider<TenantContext> tenantProvider { get; init; }

    public TenantContext? Tenant => tenantProvider.GetContext();
}
#_ViewImports.cshtml
@inherits MyRazorPage<TModel>

I'm assuming IPropertySelectionBehavior and IComponentActivator would work here as well, but it doesn't. The property remains null. Furthermore if I use RazorInject I'm getting an exception from Microsoft DI that ITenantContextProvider<TenantContext> is not a registered service:

System.InvalidOperationException: No service for type 'ITenantContextProvider`1[TenantContext]' has been registered.
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator.<>c__DisplayClass8_0.<CreateActivateInfo>b__1(ViewContext context)
   at Microsoft.Extensions.Internal.PropertyActivator`1.Activate(Object instance, TContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator.Activate(Object page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorPageActivator.Activate(IRazorPage page, ViewContext context)

(split from simpleinjector/SimpleInjector#860 (comment))

@dotnetjunkie
Copy link
Collaborator

Can you post a somewhat more elaborate example that I can use to reproduce the issue?

@Bouke
Copy link
Author

Bouke commented Oct 7, 2021

Sure, the repository is here: https://github.com/Bouke/SimpleInjectorIssue928, and this is the relevant commit: Bouke/SimpleInjectorIssue928@be4b430. Thank you.

@dotnetjunkie
Copy link
Collaborator

Hi Bouke,

I've done some tinkering, and as far as I can see, the problem lies in the default IRazorPageActivator, which calls back into the IServiceProvider to resolve properties. You'll have to do two things:

  • Intercept the activation of Razor Pages by creating a custom IRazorPageActivator
  • Override Simple Injector's property-selection behavior such that the dependency can be injectexd into your custom MyRazorPage<TModel>.

Here's a custom IRazorPageActivator that would do the trick:

public class SimpleInjectorRazorpageActivator : IRazorPageActivator
{
    private readonly ConcurrentDictionary<Type, Registration> registrations = new();

    private readonly RazorPageActivator activator;
    private readonly Container container;

    // This implementation depends on the default RazorPageActivator, because initialization
    // of framework dependencies is required for activation to succeed.
    public SimpleInjectorRazorpageActivator(RazorPageActivator activator, Container container)
    {
        this.activator = activator;
        this.container = container;
    }

    public void Activate(IRazorPage page, ViewContext context)
    {
        this.activator.Activate(page, context);

        var reg = this.registrations.GetOrAdd(
            page.GetType(),
            type => Lifestyle.Transient.CreateRegistration(type, this.container));

        reg.InitializeInstance(page);
    }
}

You can wire everything together as follows:

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

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;

        // Change property injection behavior
        _container.Options.PropertySelectionBehavior =
            new ImportAttributePropertySelectionBehavior();
    }

    public class ImportAttributePropertySelectionBehavior : IPropertySelectionBehavior
    {
        public bool SelectProperty(Type _, PropertyInfo propertyInfo) =>
            propertyInfo.GetCustomAttribute<ImportAttribute>() != null;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();

        services.AddSimpleInjector(_container, options =>
        {
            options.AddAspNetCore()
                .AddControllerActivation()
                .AddViewComponentActivation()
                .AddPageModelActivation()
                .AddTagHelperActivation();
        });
        
        // Replace default IRazorPageActivator
        services.AddSingleton<RazorPageActivator>();
        services.AddSingleton<IRazorPageActivator, SimpleInjectorRazorpageActivator>();

        InitializeContainer();
    }

    // same old, same old
}

When you implement your MyRazorPage<T> using the ImportAttribute, everything will start to work:

public abstract class MyRazorPage<TModel> : RazorPage<TModel>
{
    [Import] public SomeDependency SomeDependency { get; set; }
}

I hope this helps.

@Bouke
Copy link
Author

Bouke commented Nov 1, 2021

Hi Steven,

Thank you for the investigation and the example code. This appears to be working fine.

@Bouke
Copy link
Author

Bouke commented Nov 18, 2021

I'm dipping my toes into Blazor, which uses a RazorPage for bootstrapping (_Host.cshtml). In order to get something injected there I'm inheriting from Microsoft.AspNetCore.Mvc.RazorPages.Page. For example:

public abstract class MyPage : Page
{
    [Import] public Container Container { get; set; }
}

Being used like so:

@page "/"
@inherits MyPage
<component type="typeof(App)" render-mode="ServerPrerendered" />

When debugging, I found that SimpleInjectorRazorpageActivator being asked to Activate a RazorPageAdapter containing my MyPage. How would I instruct SimpleInjector to also inject the dependencies into this RazorPageAdapter/Page?

This is the stack trace leading up to Activate:

at MyApp.SimpleInjectorRazorPageActivator.Activate(IRazorPage page, ViewContext context)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageResultExecutor.ExecuteAsync(PageContext pageContext, PageResult result)
at Microsoft.AspNetCore.Mvc.RazorPages.PageResult.ExecuteResultAsync(ActionContext context)
...

image

@dotnetjunkie
Copy link
Collaborator

How does the RazorPageAdapter contain your page? Is there a property of some sort? What's the relationship between the two. How do you get from the adapter to the page?

@Bouke
Copy link
Author

Bouke commented Nov 18, 2021

How does the RazorPageAdapter contain your page? Is there a property of some sort?

You can see the RazorPageAdapter containing a _page field: https://github.com/dotnet/aspnetcore/blob/ae1a6cbe225b99c0bf38b7e31bf60cb653b73a52/src/Mvc/Mvc.RazorPages/src/Infrastructure/RazorPageAdapter.cs#L20

What's the relationship between the two. How do you get from the adapter to the page?

Good question, however I'm not familiar with the design choices that went into Razor and Blazor Pages. I assume they have the adapter in place so they can share some logic. You can see the Blazor Page being created here: https://github.com/dotnet/aspnetcore/blob/ae1a6cbe225b99c0bf38b7e31bf60cb653b73a52/src/Mvc/Mvc.RazorPages/src/Infrastructure/PageActionInvoker.cs#L135

@dotnetjunkie
Copy link
Collaborator

So the adapter is passed on by the framework to the IRazorPageActivator. That's... interesting. What happens if the adapter is ignored? Does the application break? What does the default framework IRazorPageActivator implementation do when it encounters the adapter?

@Bouke
Copy link
Author

Bouke commented Nov 19, 2021

The page inside the adapter is already activated before being passed to the adapter. That is handled here through DefaultPageActivatorProvider (internal) and RazorPagePropertyActivator: https://github.com/dotnet/aspnetcore/blob/ae1a6cbe225b99c0bf38b7e31bf60cb653b73a52/src/Mvc/Mvc.RazorPages/src/Infrastructure/DefaultPageFactoryProvider.cs#L63-L67. Armed with this knowledge I've come up with a custom IPageActivatorProvider:

public class SimpleInjectorPageActivatorProvider : IPageActivatorProvider
{
    private readonly ConcurrentDictionary<Type, Registration> registrations = new();

    private readonly IPageActivatorProvider pageActivatorProvider;
    private readonly Container container;

    // This implementation depends on the default RazorPageActivator, because initialization
    // of framework dependencies is required for activation to succeed.
    public SimpleInjectorPageActivatorProvider(IPageActivatorProvider pageActivatorProvider, Container container)
    {
        this.pageActivatorProvider = pageActivatorProvider;
        this.container = container;
    }

    public Func<PageContext, ViewContext, object> CreateActivator(CompiledPageActionDescriptor descriptor)
    {
        var activator = pageActivatorProvider.CreateActivator(descriptor);
        return (context, viewContext) =>
               {
                   var page = (PageBase)activator(context, viewContext);

                   var reg = registrations.GetOrAdd(
                       page.GetType(),
                       type => Lifestyle.Transient.CreateRegistration(type, container));

                   reg.InitializeInstance(page);

                   return page;
               };
    }

    public Action<PageContext, ViewContext, object> CreateReleaser(CompiledPageActionDescriptor descriptor)
    {
        return pageActivatorProvider.CreateReleaser(descriptor);
    }
}

Registration is cumbersome as the default activator is internal:

var defaultPageActivatorProvider = typeof(IPageActivatorProvider).Assembly.GetType(
    "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.DefaultPageActivatorProvider")!;
services.AddSingleton(defaultPageActivatorProvider);
services.AddSingleton<IPageActivatorProvider>(
    provider => new SimpleInjectorPageActivatorProvider(
        (IPageActivatorProvider)provider.GetRequiredService(defaultPageActivatorProvider),
        provider.GetRequiredService<Container>()));

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

No branches or pull requests

2 participants