Open Bouke opened 3 years ago
Can you post a somewhat more elaborate example that I can use to reproduce the issue?
Sure, the repository is here: https://github.com/Bouke/SimpleInjectorIssue928, and this is the relevant commit: https://github.com/Bouke/SimpleInjectorIssue928/commit/be4b4303364a6f5d8d09d77b9699d00eb445d7f1. Thank you.
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:
IRazorPageActivator
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.
Hi Steven,
Thank you for the investigation and the example code. This appears to be working fine.
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)
...
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?
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
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?
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>()));
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.I'm assuming
IPropertySelectionBehavior
andIComponentActivator
would work here as well, but it doesn't. The property remainsnull
. Furthermore if I useRazorInject
I'm getting an exception from Microsoft DI thatITenantContextProvider<TenantContext>
is not a registered service:(split from https://github.com/simpleinjector/SimpleInjector/issues/860#issuecomment-937591783)