.NET Core DI, cara melewatkan parameter ke konstruktor

105

Memiliki konstruktor layanan berikut

public class Service : IService
{
     public Service(IOtherService service1, IAnotherOne service2, string arg)
     {

     }
}

Apa saja pilihan untuk meneruskan parameter menggunakan mekanisme .NET Core IOC

_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService>(x=>new Service( _serviceCollection.BuildServiceProvider().GetService<IOtherService>(), _serviceCollection.BuildServiceProvider().GetService<IAnotherOne >(), "" ));

Apakah ada cara lain?

boris
sumber
3
Ubah desain Anda. Ekstrak arg ke Objek Parameter dan masukkan itu.
Steven

Jawaban:

124

Parameter ekspresi ( x dalam kasus ini), dari delegasi pabrik adalah a IServiceProvider.

Gunakan itu untuk menyelesaikan dependensi,

_serviceCollection.AddSingleton<IService>(x => 
    new Service(x.GetRequiredService<IOtherService>(),
                x.GetRequiredService<IAnotherOne>(), 
                ""));

Delegasi pabrik adalah permintaan yang tertunda. Kapan pun jenisnya akan diselesaikan, ia akan meneruskan penyedia yang telah diselesaikan sebagai parameter delegasi.

Nkosi
sumber
1
ya, ini adalah cara saya melakukannya sekarang, tetapi apakah ada cara lain? mungkin lebih elegan? Maksud saya Akan terlihat sedikit aneh memiliki parameter lain yang merupakan layanan terdaftar. Saya mencari sesuatu yang lebih seperti mendaftarkan layanan secara normal dan hanya meneruskan argumen non-layanan, dalam hal ini arg. Sesuatu seperti yang dilakukan Autofac .WithParameter("argument", "");
boris
1
Tidak, Anda membangun penyedia secara manual, itu buruk. Delegasi tersebut adalah doa yang tertunda. Kapan pun jenisnya akan diselesaikan, ia akan meneruskan penyedia lengkap sebagai parameter delegasi.
Nkosi
@MCR yang merupakan pendekatan default dengan Core DI di luar kotak.
Nkosi
12
@Nkosi: Lihat ActivatorUtilities.CreateInstance , bagian dari Microsoft.Extensions.DependencyInjection.Abstractionspaket (jadi tidak ada dependensi khusus container)
Tseng
Terima kasih, @Tseng, sepertinya jawaban sebenarnya yang kami cari di sini.
BrainSlugs83
63

Perlu dicatat bahwa cara yang disarankan adalah dengan menggunakan Pola Opsi . Tetapi ada kasus penggunaan yang tidak praktis (ketika parameter hanya diketahui saat runtime, bukan pada waktu startup / kompilasi) atau Anda perlu mengganti dependensi secara dinamis.

Ini sangat berguna saat Anda perlu mengganti satu dependensi (baik itu string, integer, atau jenis dependensi lain) atau saat menggunakan library pihak ketiga yang hanya menerima parameter string / integer dan Anda memerlukan parameter waktu proses.

Anda dapat mencoba CreateInstance (IServiceProvider, Object []) sebagai jalan pintas (tidak yakin ini berfungsi dengan parameter string / tipe nilai / primitif (int, float, string), belum diuji) (Baru saja mencobanya dan mengonfirmasi kerjanya, bahkan dengan beberapa parameter string) daripada menyelesaikan setiap dependensi dengan tangan:

_serviceCollection.AddSingleton<IService>(x => 
    ActivatorUtilities.CreateInstance<Service>(x, "");
);

Parameter (parameter terakhir dari CreateInstance<T>/CreateInstance ) menentukan parameter yang harus diganti (tidak diselesaikan dari penyedia). Mereka diterapkan dari kiri ke kanan saat muncul (yaitu string pertama akan diganti dengan parameter tipe string pertama yang akan dipakai).

ActivatorUtilities.CreateInstance<Service> digunakan di banyak tempat untuk menyelesaikan layanan dan menggantikan salah satu registrasi default untuk aktivasi tunggal ini.

Misalnya jika Anda memiliki kelas bernama MyService, dan memiliki IOtherService, ILogger<MyService>sebagai dependensi dan Anda ingin menyelesaikan layanan tetapi mengganti layanan default IOtherService(katakanlah OtherServiceA) dengan OtherServiceB, Anda dapat melakukan sesuatu seperti:

myService = ActivatorUtilities.CreateInstance<Service>(serviceProvider, new OtherServiceB())

Kemudian parameter pertama IOtherServiceakan OtherServiceBdiinjeksikan, bukan OtherServiceAparameter yang tersisa akan datang dari container.

Ini berguna ketika Anda memiliki banyak dependensi dan hanya ingin memperlakukan satu dependensi secara khusus (yaitu mengganti penyedia khusus database dengan nilai yang dikonfigurasi selama permintaan atau untuk pengguna tertentu, sesuatu yang hanya Anda ketahui pada waktu proses dan selama permintaan dan bukan saat aplikasi dibangun / dimulai).

Anda juga dapat menggunakan Metode ActivatorUtilities.CreateFactory (Type, Type []) untuk membuat metode pabrik, karena metode ini menawarkan Referensi dan Tolok Ukur GitHub dengan kinerja yang lebih baik .

Nanti berguna ketika tipe diselesaikan sangat sering (seperti di SignalR dan skenario permintaan tinggi lainnya). Pada dasarnya Anda akan membuat ObjectFactorymelalui

var myServiceFactory = ActivatorUtilities.CreateFactory(typeof(MyService), new[] { typeof(IOtherService) });

kemudian cache (sebagai variabel dll) dan panggil jika diperlukan

MyService myService = myServiceFactory(serviceProvider, myServiceOrParameterTypeToReplace);

## Pembaruan: Coba sendiri untuk mengonfirmasi bahwa ini juga berfungsi dengan string dan integer, dan memang berhasil. Berikut contoh konkret yang saya uji dengan:

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        services.AddTransient<HelloWorldService>();
        services.AddTransient(p => p.ResolveWith<DemoService>("Tseng", "Stackoverflow"));

        var provider = services.BuildServiceProvider();

        var demoService = provider.GetRequiredService<DemoService>();

        Console.WriteLine($"Output: {demoService.HelloWorld()}");
        Console.ReadKey();
    }
}

public class DemoService
{
    private readonly HelloWorldService helloWorldService;
    private readonly string firstname;
    private readonly string lastname;

    public DemoService(HelloWorldService helloWorldService, string firstname, string lastname)
    {
        this.helloWorldService = helloWorldService ?? throw new ArgumentNullException(nameof(helloWorldService));
        this.firstname = firstname ?? throw new ArgumentNullException(nameof(firstname));
        this.lastname = lastname ?? throw new ArgumentNullException(nameof(lastname));
    }

    public string HelloWorld()
    {
        return this.helloWorldService.Hello(firstName, lastName);
    }
}

public class HelloWorldService
{
    public string Hello(string name) => $"Hello {name}";
    public string Hello(string firstname, string lastname) => $"Hello {firstname} {lastname}";
}

// Just a helper method to shorten code registration code
static class ServiceProviderExtensions
{
    public static T ResolveWith<T>(this IServiceProvider provider, params object[] parameters) where T : class => 
        ActivatorUtilities.CreateInstance<T>(provider, parameters);
}

Cetakan

Output: Hello Tseng Stackoverflow
Tseng
sumber
6
Ini juga bagaimana ASP.NET Core memberi contoh Pengontrol secara default ControllerActivatorProvider , mereka tidak langsung diselesaikan dari IoC (kecuali .AddControllersAsServicesdigunakan, yang menggantikan ControllerActivatorProviderdenganServiceBasedControllerActivator
Tseng
16

Jika Anda merasa tidak nyaman dengan layanan baru, Anda dapat menggunakan Parameter Object polanya.

Jadi ekstrak parameter string ke dalam tipenya sendiri

public class ServiceArgs
{
   public string Arg1 {get; set;}
}

Dan konstruktornya sekarang akan terlihat seperti

public Service(IOtherService service1, 
               IAnotherOne service2, 
               ServiceArgs args)
{

}

Dan penyiapannya

_serviceCollection.AddSingleton<ServiceArgs>(_ => new ServiceArgs { Arg1 = ""; });
_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService, Service>();

Manfaat pertama adalah jika Anda perlu mengubah konstruktor Layanan dan menambahkan layanan baru padanya, Anda tidak perlu mengubah new Service(...panggilannya. Manfaat lainnya adalah pengaturannya sedikit lebih bersih.

Untuk konstruktor dengan satu atau dua parameter, ini mungkin terlalu banyak.

Adrian Iftode
sumber
2
Akan lebih intuitif bagi parameter yang kompleks untuk menggunakan pola Opsi dan merupakan cara yang disarankan untuk pola opsi, namun kurang cocok untuk parameter yang hanya diketahui saat runtime (yaitu dari permintaan atau klaim)
Tseng
0

Anda juga dapat memasukkan dependensi dengan proses ini

_serviceCollection.AddSingleton<IOtherService , OtherService>();
_serviceCollection.AddSingleton<IAnotherOne , AnotherOne>();
_serviceCollection.AddSingleton<IService>(x=>new Service( x.GetService<IOtherService>(), x.GetService<IAnotherOne >(), "" ));
Alamgir
sumber