simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.22k stars 152 forks source link

@inject support in .NET Core MVC views #990

Open vkirienko opened 9 months ago

vkirienko commented 9 months ago

Hi,

I think this question was already asked before but reading through several threads I still don't have clear understanding if @inject can be used with SimpleInjector. Clearly it does not work by default but is there any workaround?

For the record I went through these.

https://github.com/simpleinjector/SimpleInjector/issues/860 https://github.com/simpleinjector/SimpleInjector/issues/362 https://github.com/simpleinjector/SimpleInjector.Integration.AspNetCore/issues/25

Thank you

dotnetjunkie commented 9 months ago

The discussion that you mentioned last actually contains code that, according to the OP, works.

vkirienko commented 9 months ago

Thank you! That's what I wanted to confirm.

I just curios why it is not part of SimpleInjector default integration with .NET Core MVC?

dotnetjunkie commented 9 months ago

I just curios why it is not part of SimpleInjector default integration with .NET Core MVC?

Mainly because of two reasons:

  1. Due to the lack of good interception points within MVC, creating and maintaining such integration point for me as a library designer might be a lot of work. There might be tons of corner cases that have to be dealt with and I'm afraid this something that might come back to me as rework over and over again.
  2. I feel that injecting dependencies into your Razor pages isn't the best approach from a design perspective and Simple Injector was always designed in an opinionated way and promotes best practices. Instead, a Razor page, MVC view, WPF view or any sort of view should rather work with a Model object that contains all data required for that view. So instead of injecting an ITimeProvider into the page, return a model that contains a DateTime CurrentTime property or something similar.

I hope this makes sense.

vkirienko commented 9 months ago

Good points, especially #2. I'm about to migrate large application to .NET Core. It has clever framework on top of MVC with again clever code in MVC views. And I agree it would be better not to have it and keep views as simple as possible.

Thank you, again for great DI library!

vkirienko commented 9 months ago

@dotnetjunkie thank you again for pointing to right direction. Code from this response works as intended and I was able to inject dependencies using [Import] attribute. It is nice to be able to do it as I have couple of properties in our base razor page class where we use service locator pattern and now we can use proper DI.

https://github.com/simpleinjector/SimpleInjector.Integration.AspNetCore/issues/25#issuecomment-1042932696

But my question was not clear and in fact I was looking to way to inject dependency using @inject directive.

Index,cshtml
@inject IDueToReminderInfoService dueToReminderInfoService

It still fails with error:

InvalidOperationException: No service for type 'MyApp.Services.Dates.IDueToReminderInfoService' has been registered.
Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator+<>c__DisplayClass8_0.<CreateActivateInfo>b__2(ViewContext context)
Microsoft.Extensions.Internal.PropertyActivator<TContext>.Activate(object instance, TContext context)
Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator.Activate(object page, ViewContext context)
MyApp.Core.SimpleInjector.SimpleInjectorRazorpageActivator.Activate(IRazorPage page, ViewContext context) in SimpleInjectorRazorpageActivator.cs
+
            activator.Activate(page, context);
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, bool invokeViewStarts)
Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, string contentType, Nullable<int> statusCode)
Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)
Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_0<TFilter, TFilterAsync>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext<TFilter, TFilterAsync>(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

In generated Index.cshtml.cs file property I'm trying to inject RazorInject attribute

        [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
        public IDueToReminderInfoService dueToReminderInfoService { get; private set; } = default!;
dotnetjunkie commented 9 months ago

From the stack trace I understand that your custom SimpleInjectorRazorpageActivator calls into the Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator class. This means that your custom SimpleInjectorRazorpageActivator does not correctly differentiate between dependencies that should be resolved from Simple Injector and dependencies that should be resolved from the built-in DI infrastructure. Clearly, IDueToReminderInfoService is something that should be pulled in from Simple Injector. This means that in this stack trace, SimpleInjectorRazorpageActivator should not call RazorPagePropertyActivator, but rather call Simple Injector instead.

vkirienko commented 9 months ago

I guess we have to call Microsoft.AspNetCore.Mvc.Razor.RazorPagePropertyActivator from SimpleInjectorRazorpageActivator. Comment in SimpleInjectorRazorpageActivator says:

    // This implementation depends on the default RazorPageActivator, because initialization
    // of framework dependencies is required for activation to succeed.

As far as I can see RazorPagePropertyActivator uses build-in service provider and not extendable,

var serviceProvider = context.HttpContext.RequestServices;
var value = serviceProvider.GetRequiredService(property.PropertyType);
dotnetjunkie commented 9 months ago

I think I'm starting to see the problem here. The implementation I proposed in 25, calls into the core behavior and let Simple Injector inject properties that are marked with the ImportAttribute. That would work great, but doesn't allow using the @inject tag. With @inject, Razor is generating an [Inject] attribute, but the framework's behavior is to resolve dependencies marked with inject. This is what is causing the exception in your case.

Since the core behavior isn't extendable, the only option is to skip calling into the core behavior. But that causes new problems, because there is a set of dependencies that seem to be injected through a different mechanism. Not initializing them might cause issues of its own.

It might be possible to work around these issues, but at this point I'm unsure how to proceed.

vkirienko commented 9 months ago

I don't have that many services used @inject. So in my case I think reasonable workaround would be to register services used with @inject in MS DI container and resolve them through SimpleInjector.

services.AddScoped<IDueToReminderInfoService>(c => SI.Container.GetInstance<IDueToReminderInfoService>());