Разрешение экземпляров с помощью ASP.NET Core DI

302

Как вручную разрешить тип с помощью встроенной инфраструктуры внедрения зависимостей ASP.NET Core MVC?

Настройка контейнера достаточно проста:

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddTransient<ISomeService, SomeConcreteService>();
}

Но как я могу решить ISomeServiceбез выполнения инъекций? Например, я хочу сделать это:

ISomeService service = services.Resolve<ISomeService>();

Там нет таких методов в IServiceCollection.

Дэйв Нью
источник
1
возможный дубликат Как разрешить экземпляр внутри ConfigureServices в ASP.NET 5
Мухаммед Рехан Саид
3
Вы хотите разрешить их в ConfigureServices()методе (с IServiceCollection) или просто где-нибудь в приложении?
Хенк Моллема
2
@HenkMollema: где-нибудь в самом стартапе.
Дэйв Нью

Ответы:

486

IServiceCollectionИнтерфейс используется для построения контейнера инъекции зависимостей. После полной сборки он объединяется с IServiceProviderэкземпляром, который можно использовать для разрешения служб. Вы можете ввести IServiceProviderв любой класс. Эти IApplicationBuilderи HttpContextклассы могут предоставить поставщик услуг , а также, с помощью их ApplicationServicesили RequestServicesсвойств , соответственно.

IServiceProviderопределяет GetService(Type type)метод для разрешения службы:

var service = (IFooService)serviceProvider.GetService(typeof(IFooService));

Есть также несколько удобных методов расширения, таких как serviceProvider.GetService<IFooService>()(добавить usingдля Microsoft.Extensions.DependencyInjection).

Разрешение служб внутри класса запуска

Внедрение зависимостей

Хостинг-провайдер среды выполнения может внедрить определенные сервисы в конструктор Startupкласса, например IConfiguration, IWebHostEnvironment( IHostingEnvironmentв версиях до 3.0) ILoggerFactoryи IServiceProvider. Обратите внимание, что последний является экземпляром, созданным на уровне хостинга, и содержит только основные службы для запуска приложения .

ConfigureServices()Метод не позволяет инъекционным услуги, она только принимает IServiceCollectionаргумент. Это имеет смысл, потому что ConfigureServices()именно здесь вы регистрируете сервисы, необходимые для вашего приложения. Однако вы можете использовать сервисы, добавленные в конструктор запуска, например:

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
    // Use Configuration here
}

Любые услуги, зарегистрированные в, ConfigureServices()могут затем быть введены в Configure()метод; Вы можете добавить произвольное количество сервисов после IApplicationBuilderпараметра:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IFooService>();
}

public void Configure(IApplicationBuilder app, IFooService fooService)
{
    fooService.Bar();
}

Разрешение зависимостей вручную

Если вам необходимо разрешить службы вручную, предпочтительно использовать ApplicationServicesпредоставленный IApplicationBuilderв Configure()методе:

public void Configure(IApplicationBuilder app)
{
    var serviceProvider = app.ApplicationServices;
    var hostingEnv = serviceProvider.GetService<IHostingEnvironment>();
}

Можно передать и напрямую использовать IServiceProviderконструктор в вашем Startupклассе, но, как указано выше, он будет содержать ограниченное подмножество сервисов и, следовательно, будет иметь ограниченную полезность:

public Startup(IServiceProvider serviceProvider)
{
    var hostingEnv = serviceProvider.GetService<IWebHostEnvironment>();
}

Если вам необходимо разрешить службы в ConfigureServices()методе, требуется другой подход. Вы можете создать промежуточное звено IServiceProviderиз IServiceCollectionэкземпляра, который содержит службы, которые были зарегистрированы до этого момента :

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IFooService, FooService>();

    // Build the intermediate service provider
    var sp = services.BuildServiceProvider();

    // This will succeed.
    var fooService = sp.GetService<IFooService>();
    // This will fail (return null), as IBarService hasn't been registered yet.
    var barService = sp.GetService<IBarService>();
}

Обратите внимание: как правило, вам следует избегать разрешения служб внутри ConfigureServices()метода, поскольку именно здесь вы настраиваете службы приложения. Иногда вам просто нужен доступ к IOptions<MyOptions>экземпляру. Вы можете сделать это, связав значения из IConfigurationэкземпляра с экземпляром MyOptions(что, по сути, и делает инфраструктура опций):

public void ConfigureServices(IServiceCollection services)
{
    var myOptions = new MyOptions();
    Configuration.GetSection("SomeSection").Bind(myOptions);
}

Разрешение сервисов вручную (он же Сервисный локатор) обычно считается антишаблоном . Хотя у него есть свои варианты использования (для каркасов и / или инфраструктурных уровней), вы должны избегать его в максимально возможной степени.

Хенк Моллема
источник
14
@HenkMollema, но что, если у меня ничего не может быть введено, я имею в виду, что я не могу внедрить какой- IServiceCollectionто класс, который создается вручную ( из области промежуточного программного обеспечения ), в моем случае - планировщик, которому периодически нужны некоторые службы для генерации и отправьте электронное письмо.
Мердан Гочмурадов
52
Предупреждение, если вам нужно разрешить использование служб, ConfigureServicesи эта служба является одноэлементной, она будет отличаться от той, которая используется вами Controller! Я предполагаю , что это потому , что она использует другую IServiceProvider- чтобы избежать этого не разрешает через BuildServiceProviderи вместо того, чтобы переместить поиск в одноэлементном от ConfigureServicesк Configure(..other params, IServiceProvider serviceProvider)вStartup.cs
вали
3
@ Хорошая мысль. Поскольку это другой IServiceProviderэкземпляр, он создаст новый экземпляр Singleton. Вы можете избежать этого, возвращая экземпляр поставщика услуг из ConfigureServicesметода, чтобы он также был контейнером, используемым вашим приложением.
Хенк Моллема
1
Вызывающий collection.BuildServiceProvider();было то , что мне было нужно, спасибо!
Крис Марисич
2
@HenkMollema, как заставить его работать только с одним экземпляром поставщика услуг? Как правило, вы: 1) зарегистрируете некоторые из ваших зависимостей 2) создадите временный экземпляр поставщика услуг 3) используйте этого поставщика услуг для решения чего-то, что вам нужно для регистрации некоторых других зависимостей. После этого вы не можете вернуть временный экземпляр, так как в нем отсутствуют некоторые ваши зависимости (зарегистрированные в 3). Я что-то упускаю?
Филипп
109

Разрешение экземпляров вручную включает использование IServiceProviderинтерфейса:

Разрешение зависимости в Startup.ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();

    var serviceProvider = services.BuildServiceProvider();
    var service = serviceProvider.GetService<IMyService>();
}

Разрешение зависимостей в Startup.Configure

public void Configure(
    IApplicationBuilder application,
    IServiceProvider serviceProvider)
{
    // By type.
    var service1 = (MyService)serviceProvider.GetService(typeof(MyService));

    // Using extension method.
    var service2 = serviceProvider.GetService<MyService>();

    // ...
}

Устранение зависимостей в Startup.Configure в ASP.NET Core 3

public void Configure(
    IApplicationBuilder application,
    IWebHostEnvironment webHostEnvironment)
{
    app.ApplicationServices.GetService<MyService>();
}

Использование Runtime Injected Services

Некоторые типы могут быть введены как параметры метода:

public class Startup
{
    public Startup(
        IHostingEnvironment hostingEnvironment,
        ILoggerFactory loggerFactory)
    {
    }

    public void ConfigureServices(
        IServiceCollection services)
    {
    }

    public void Configure(
        IApplicationBuilder application,
        IHostingEnvironment hostingEnvironment,
        IServiceProvider serviceProvider,
        ILoggerFactory loggerfactory,
        IApplicationLifetime applicationLifetime)
    {
    }
}

Разрешение зависимостей в действиях контроллера

[HttpGet("/some-action")]
public string SomeAction([FromServices] IMyService myService) => "Hello";
Мухаммед Рехан Саид
источник
1
@AfsharMohebbi, GetServiceкоторый является универсальным, является методом расширения в Microsoft.Extensions.DependencyInjectionпространстве имен.
Ахмадали Шафи
О методах расширения: Метод расширения - это статический метод, который добавляет функциональность в класс. Вы можете объявить общедоступное статическое TheReturnType TheMethodName (this TheTypeYouExtend theTypeYouExtend {// BODY}, а затем использовать его следующим образом: TheTypeYouExtend.TheMethodName (); стал очень распространенным подходом к .NET Core, так что разработчики могут расширять базовую функциональность ... хорошие примеры здесь: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Хуан
17

Если вы создадите приложение с шаблоном, у вас будет что-то вроде этого в Startupклассе:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddApplicationInsightsTelemetry(Configuration);

    services.AddMvc();
}

Затем вы можете добавить зависимости, например:

services.AddTransient<ITestService, TestService>();

Если вы хотите получить доступ ITestServiceк вашему контроллеру, вы можете добавить IServiceProviderконструктор, и он будет добавлен :

public HomeController(IServiceProvider serviceProvider)

Затем вы можете разрешить добавленную вами услугу:

var service = serviceProvider.GetService<ITestService>();

Обратите внимание, что для использования универсальной версии вы должны включить пространство имен в расширения:

using Microsoft.Extensions.DependencyInjection;

ITestService.cs

public interface ITestService
{
    int GenerateRandom();
}

TestService.cs

public class TestService : ITestService
{
    public int GenerateRandom()
    {
        return 4;
    }
}

Startup.cs (ConfigureServices)

public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationInsightsTelemetry(Configuration);
    services.AddMvc();

    services.AddTransient<ITestService, TestService>();
}

HomeController.cs

using Microsoft.Extensions.DependencyInjection;

namespace Core.Controllers
{
    public class HomeController : Controller
    {
        public HomeController(IServiceProvider serviceProvider)
        {
            var service = serviceProvider.GetService<ITestService>();
            int rnd = service.GenerateRandom();
        }
BrunoLM
источник
10

Если вам просто нужно разрешить одну зависимость с целью передачи ее конструктору другой зависимости, которую вы регистрируете, вы можете сделать это.

Допустим, у вас есть сервис, который принимает строку и ISomeService.

public class AnotherService : IAnotherService
{
    public AnotherService(ISomeService someService, string serviceUrl)
    {
        ...
    }
}

Когда вы зарегистрируетесь в Startup.cs, вам нужно будет сделать следующее:

services.AddScoped<IAnotherService>(ctx => 
      new AnotherService(ctx.GetService<ISomeService>(), "https://someservice.com/")
);
raterus
источник
ОП не
указывал
1
На самом деле это должен быть принятый ответ ... Хотя ответ Хенка Моллемы очень показателен, в настоящее время ваш ответ более чистый и не создает проблем, связанных с созданием промежуточного IServiceProvider (различные случаи синглетонов ...). Возможно, это решение не было доступно в 2015 году, когда Хенк ответил, но теперь это путь.
Vi100
Пробовал это, но ISomeServiceдля меня все равно было пустым.
ajbeaven
2 вопроса: 1) Если конструктор параметров класса обслуживания AnotherService изменяется (удаляются или добавляются службы), то мне нужно изменить сегмент регистра службы IAnotherService и он продолжает меняться? 2) Вместо этого я могу добавить только один конструктор для AnotherService с одним параметром, например public AnotherService (IServiceProvider serviceProvider), и получать нужные мне услуги от конструктора. И мне просто нужно зарегистрировать класс службы AnotherService в классе запуска, например services.AddTransient <IAnotherService, AnotherService> (sp => {var service = new AnotherService (sp); return service;});
Thomas.Benz
2

Таким способом вы можете внедрить зависимости в такие атрибуты, как AuthorizeAttribute.

var someservice = (ISomeService)context.HttpContext.RequestServices.GetService(typeof(ISomeService));
Бора Айдын
источник
Это то, что я искал .. Спасибо
Рейан Чогл
0

Я знаю, что это старый вопрос, но я удивлен, что здесь нет довольно очевидного и отвратительного взлома.

Вы можете использовать возможность определения своей собственной функции ctor для извлечения необходимых значений из ваших служб по мере их определения ... очевидно, это будет выполняться каждый раз, когда запрашивается служба, если вы явно не удалите / очистите и не добавите заново определение эта услуга в рамках первой конструкции эксплуатирующего ctor .

Преимущество этого метода заключается в том, что вам не требуется создавать дерево служб или использовать его во время настройки службы. Вы все еще определяете, как будут настроены сервисы.

public void ConfigureServices(IServiceCollection services)
{
    //Prey this doesn't get GC'd or promote to a static class var
    string? somevalue = null;

    services.AddSingleton<IServiceINeedToUse, ServiceINeedToUse>(scope => {
         //create service you need
         var service = new ServiceINeedToUse(scope.GetService<IDependantService>())
         //get the values you need
         somevalue = somevalue ?? service.MyDirtyHack();
         //return the instance
         return service;
    });
    services.AddTransient<IOtherService, OtherService>(scope => {
         //Explicitly ensuring the ctor function above is called, and also showcasing why this is an anti-pattern.
         scope.GetService<IServiceINeedToUse>();
         //TODO: Clean up both the IServiceINeedToUse and IOtherService configuration here, then somehow rebuild the service tree.
         //Wow!
         return new OtherService(somevalue);
    });
}

Способ исправления этого паттерна состоял бы в том, чтобы дать OtherServiceявную зависимость IServiceINeedToUse, а не либо неявно зависеть от него или возвращаемого значения его метода ... или явно разрешить эту зависимость каким-либо другим способом.

Иззи
источник
-4
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddDbContext<ConfigurationRepository>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("SqlConnectionString")));

    services.AddScoped<IConfigurationBL, ConfigurationBL>();
    services.AddScoped<IConfigurationRepository, ConfigurationRepository>();
}
Натан Алард
источник
5
Ваши ответы с большей вероятностью будут приняты и оценены, если вы предоставите краткое объяснение того, почему это хороший ответ, а не просто фрагмент кода. Это также помогает спрашивающему быть уверенным, что он действительно отвечает на заданный вопрос.
Джим Л
Кто-то неправильно пометил ваш ответ как некачественный. Вы должны добавить сопроводительный текст, чтобы объяснить, как ваш ответ работает, чтобы предотвратить дальнейшую пометку и / или понижение голосов. Ответ только на код не является некачественным . Пытается ли он ответить на вопрос? Если нет, пометьте как «не ответ» или порекомендуйте удаление (если оно находится в очереди на проверку). б) Это технически неверно? Downvote или комментарий. Из обзора .
Вай Ха Ли