Haruskah saya menggunakan ILogger, ILogger <T>, ILoggerFactory atau ILoggerProvider untuk perpustakaan?

113

Ini mungkin agak terkait dengan Pass ILogger atau ILoggerFactory ke konstruktor di AspNet Core? , namun ini secara khusus tentang Desain Perpustakaan , bukan tentang bagaimana aplikasi sebenarnya yang menggunakan perpustakaan tersebut mengimplementasikan pencatatannya.

Saya sedang menulis Pustaka .net Standard 2.0 yang akan diinstal melalui Nuget, dan untuk memungkinkan orang yang menggunakan Pustaka itu mendapatkan beberapa info debug, saya bergantung pada Microsoft.Extensions.Logging.Abstractions untuk memungkinkan Logger standar dimasukkan.

Namun, saya melihat banyak antarmuka, dan kode contoh di web terkadang menggunakan ILoggerFactorydan membuat logger di ctor kelas. Ada juga ILoggerProvideryang terlihat seperti versi hanya-baca dari Pabrik, tetapi implementasi mungkin atau mungkin tidak mengimplementasikan kedua antarmuka, jadi saya harus memilih. (Pabrik tampaknya lebih umum daripada Penyedia).

Beberapa kode yang saya lihat menggunakan ILoggerantarmuka non-generik dan bahkan mungkin berbagi satu contoh dari logger yang sama, dan beberapa mengambil ILogger<T>di ctor mereka dan mengharapkan wadah DI untuk mendukung jenis generik terbuka atau pendaftaran eksplisit dari setiap ILogger<T>variasi perpustakaan saya penggunaan.

Saat ini, saya pikir itu ILogger<T>adalah pendekatan yang tepat, dan mungkin ctor yang tidak mengambil argumen itu dan hanya melewati Null Logger sebagai gantinya. Dengan begitu, jika tidak diperlukan logging, tidak ada yang digunakan. Namun, beberapa kontainer DI memilih ctor terbesar dan dengan demikian akan tetap gagal.

Saya ingin tahu tentang apa yang seharusnya saya lakukan di sini untuk mengurangi sakit kepala bagi pengguna, sambil tetap mengizinkan dukungan logging yang tepat jika diinginkan.

Michael Stum
sumber

Jawaban:

113

Definisi

Kami memiliki 3 interface: ILogger, ILoggerProviderdan ILoggerFactory. Mari kita lihat kode sumber untuk mengetahui tanggung jawab mereka:

ILogger : bertanggung jawab untuk menulis pesan log dari Tingkat Log tertentu .

ILoggerProvider : bertanggung jawab untuk membuat instance ILogger(Anda tidak seharusnya menggunakan ILoggerProviderlangsung untuk membuat logger)

ILoggerFactory : Anda dapat mendaftarkan satu atau lebih ILoggerProviderke pabrik, yang pada gilirannya menggunakan semuanya untuk membuat instance ILogger. ILoggerFactorymemegang koleksi ILoggerProviders.

Dalam contoh di bawah ini, kami mendaftarkan 2 penyedia (konsol dan file) dengan pabrik. Saat kami membuat logger, pabrik menggunakan kedua penyedia ini untuk membuat instance logger:

ILoggerFactory factory = new LoggerFactory().AddConsole();    // add console provider
factory.AddProvider(new LoggerFileProvider("c:\\log.txt"));   // add file provider
Logger logger = factory.CreateLogger(); // <-- creates a console logger and a file logger

Jadi, logger itu sendiri, memelihara kumpulan ILoggers, dan menulis pesan log ke semuanya. Melihat kode sumber Logger kita dapat mengonfirmasi bahwa Loggermemiliki array ILoggers(yaitu LoggerInformation[]), dan pada saat yang sama mengimplementasikan ILoggerantarmuka.


Injeksi Ketergantungan

Dokumentasi MS menyediakan 2 metode untuk menginjeksi logger:

1. Menyuntikkan pabrik:

public TodoController(ITodoRepository todoRepository, ILoggerFactory logger)
{
    _todoRepository = todoRepository;
    _logger = logger.CreateLogger("TodoApi.Controllers.TodoController");
}

membuat Logger dengan Category = TodoApi.Controllers.TodoController .

2. Menyuntikkan obat generik ILogger<T>:

public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger)
{
    _todoRepository = todoRepository;
    _logger = logger;
}

membuat logger dengan Category = nama tipe yang memenuhi syarat dari TodoController


Menurut saya, yang membuat bingung adalah dokumentasi yang tidak menyebutkan apapun tentang suntik non-generik , ILogger. Dalam contoh yang sama di atas, kami menyuntikkan non-generik ITodoRepositorynamun, itu tidak menjelaskan mengapa kami tidak melakukan hal yang sama untuk ILogger.

Menurut Mark Seemann :

Sebuah Pembuat Injeksi harus melakukan tidak lebih dari menerima ketergantungan.

Memasukkan pabrik ke dalam Controller bukanlah pendekatan yang baik, karena bukanlah tanggung jawab Pengendali untuk menginisialisasi Logger (pelanggaran SRP). Pada saat yang sama menyuntikkan generik ILogger<T>menambah kebisingan yang tidak perlu. Lihat blog Simple Injector untuk lebih jelasnya: Apa yang salah dengan abstraksi ASP.NET Core DI?

Apa yang harus disuntikkan (setidaknya menurut artikel di atas) adalah non-generik ILogger, tetapi kemudian, itu bukan sesuatu yang dapat dilakukan Kontainer DI Bawaan Microsoft, dan Anda perlu menggunakan Perpustakaan DI pihak ke-3. Ini dua dokumen menjelaskan bagaimana Anda dapat menggunakan perpustakaan pihak ke-3 dengan NET Core.


Ini adalah artikel lain oleh Nikola Malovic, di mana dia menjelaskan 5 hukum IoC miliknya.

Hukum IoC keempat Nikola

Setiap konstruktor kelas yang sedang diselesaikan tidak boleh memiliki implementasi apa pun selain menerima sekumpulan dependensinya sendiri.

Hooman Bahreini
sumber
3
Jawaban Anda dan artikel Steven adalah yang paling benar dan sekaligus paling menyedihkan.
bokibeg
30

Semuanya valid kecuali ILoggerProvider. ILoggerdan ILogger<T>apa yang seharusnya Anda gunakan untuk logging. Untuk mendapatkan ILogger, Anda menggunakan ILoggerFactory. ILogger<T>adalah jalan pintas untuk mendapatkan logger untuk kategori tertentu (jalan pintas untuk tipe sebagai kategori).

Saat Anda menggunakan ILoggeruntuk melakukan logging, setiap terdaftar ILoggerProvidermendapat kesempatan untuk menangani pesan log itu. Ini tidak benar-benar valid untuk menggunakan kode untuk dipanggil ILoggerProvidersecara langsung.

davidfowl
sumber
Terima kasih. Hal ini membuat saya berpikir bahwa an ILoggerFactoryakan menjadi cara yang bagus untuk menghubungkan DI bagi konsumen dengan hanya memiliki 1 ketergantungan ( "Beri saya Pabrik dan saya akan membuat logger saya sendiri" ), tetapi akan mencegah penggunaan logger yang ada ( kecuali konsumen menggunakan beberapa pembungkus). Mengambil ILogger- generik atau tidak - memungkinkan konsumen untuk memberi saya logger yang mereka persiapkan secara khusus, tetapi membuat pengaturan DI berpotensi jauh lebih kompleks (kecuali wadah DI yang mendukung Open Generics digunakan). Apakah itu benar? Kalau begitu, saya pikir saya akan pergi dengan pabrik.
Michael Stum
3
@MichaelStum - Saya tidak yakin saya mengikuti logika Anda di sini. Anda mengharapkan konsumen Anda untuk menggunakan sistem DI tetapi kemudian ingin mengambil alih dan mengelola dependensi secara manual di dalam perpustakaan Anda? Mengapa itu tampak pendekatan yang tepat?
Damien_The_Unbeliever
@Damienhe_Unbeliever Itu poin yang bagus. Mengambil pabrik sepertinya aneh. Saya pikir alih-alih mengambil ILogger<T>, saya akan mengambil ILogger<MyClass>atau bahkan hanya ILogger- dengan cara itu, pengguna dapat menghubungkannya hanya dengan satu pendaftaran, dan tanpa memerlukan generik terbuka dalam wadah DI mereka. Bersandar pada non-generik ILogger, tetapi akan banyak bereksperimen selama akhir pekan.
Michael Stum
10

Itu ILogger<T>yang sebenarnya dibuat untuk DI. ILogger datang untuk membantu menerapkan pola pabrik dengan jauh lebih mudah, daripada Anda menulis sendiri semua logika DI dan Pabrik, itu adalah salah satu keputusan paling cerdas dalam inti asp.net.

Anda dapat memilih antara:

ILogger<T>jika Anda memiliki kebutuhan untuk menggunakan pola pabrik dan DI dalam kode Anda atau Anda dapat menggunakan ILogger, untuk mengimplementasikan logging sederhana tanpa DI diperlukan.

mengingat, The ILoggerProviderhanyalah jembatan untuk menangani setiap pesan log terdaftar. Tidak perlu menggunakannya, karena tidak mempengaruhi apa pun yang harus Anda campur tangan dalam kode, Ia mendengarkan ILoggerProvider terdaftar dan menangani pesan. Itu saja.

Barr J
sumber
1
Apa hubungan ILogger vs ILogger <T> dengan DI? Entah akan disuntikkan, bukan?
Matthew Hostetler
Ini sebenarnya ILogger <TCategoryName> di MS Docs. Ini berasal dari ILogger dan tidak menambahkan fungsionalitas baru kecuali nama kelas untuk log. Ini juga menyediakan tipe unik sehingga DI memasukkan logger dengan nama yang benar. Ekstensi Microsoft memperluas non-generik this ILogger.
Samuel Danielson
8

Berpegang pada pertanyaan, saya yakin ILogger<T>ini adalah opsi yang tepat, dengan mempertimbangkan sisi negatif dari opsi lain:

  1. Menyuntikkan ILoggerFactorymemaksa pengguna Anda untuk memberikan kontrol pabrik logger global yang dapat berubah ke perpustakaan kelas Anda. Selain itu, dengan menerima ILoggerFactorykelas Anda sekarang dapat menulis ke log dengan sembarang nama kategori dengan CreateLoggermetode. Meskipun ILoggerFactorybiasanya tersedia sebagai singleton dalam wadah DI, saya sebagai pengguna akan meragukan mengapa perpustakaan mana pun perlu menggunakannya.
  2. Meskipun metodenya ILoggerProvider.CreateLoggerterlihat seperti itu, itu tidak dimaksudkan untuk injeksi. Ini digunakan dengan ILoggerFactory.AddProvidersehingga pabrik dapat membuat agregat ILoggeryang menulis ke beberapa ILoggerdibuat dari setiap penyedia terdaftar. Ini jelas saat Anda memeriksa penerapanLoggerFactory.CreateLogger
  3. Menerima ILoggerjuga tampak seperti cara yang harus dilakukan, tetapi tidak mungkin dengan .NET Core DI. Ini sebenarnya terdengar seperti alasan mengapa mereka perlu menyediakan ILogger<T>di tempat pertama.

Jadi bagaimanapun, kita tidak memiliki pilihan yang lebih baik daripada ILogger<T>, jika kita memilih dari kelas-kelas itu.

Pendekatan lain adalah dengan memasukkan sesuatu yang lain yang membungkus non-generik ILogger, yang dalam hal ini harus non-generik. Idenya adalah dengan membungkusnya dengan kelas Anda sendiri, Anda mengambil kendali penuh tentang bagaimana pengguna dapat mengkonfigurasinya.

tia
sumber
6

Pendekatan default dimaksudkan untuk menjadi ILogger<T>. Ini berarti bahwa dalam log log dari kelas tertentu akan terlihat jelas karena mereka akan menyertakan nama kelas lengkap sebagai konteksnya. Misalnya, jika nama lengkap kelas MyLibrary.MyClassAnda adalah, Anda akan mendapatkan ini di entri log yang dibuat oleh kelas ini. Sebagai contoh:

MyLibrary.MyClass: Informasi: Log informasi saya

Anda harus menggunakan ILoggerFactoryjika Anda ingin menentukan konteks Anda sendiri. Misalnya, semua log dari perpustakaan Anda memiliki konteks log yang sama, bukan setiap kelas. Sebagai contoh:

loggerFactory.CreateLogger("MyLibrary");

Dan kemudian log akan terlihat seperti ini:

MyLibrary: Informasi: Log informasi saya

Jika Anda melakukan itu di semua kelas maka konteksnya hanya MyLibrary untuk semua kelas. Saya membayangkan Anda ingin melakukannya untuk perpustakaan jika Anda tidak ingin mengekspos struktur kelas dalam di log.

Mengenai penebangan opsional. Saya pikir Anda harus selalu memerlukan ILogger atau ILoggerFactory di konstruktor dan menyerahkannya kepada konsumen perpustakaan untuk mematikannya atau menyediakan Logger yang tidak melakukan apa pun dalam injeksi ketergantungan jika mereka tidak ingin melakukan logging. Sangat mudah untuk mengubah logging untuk konteks tertentu dalam konfigurasi. Sebagai contoh:

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "MyLibrary": "None"
    }
  }
}
AlesD
sumber
5

Untuk desain perpustakaan, pendekatan yang baik adalah:

1. Jangan memaksa konsumen untuk memasukkan logger ke kelas Anda. Cukup buat ctor lain yang meneruskan NullLoggerFactory.

class MyClass
{
    private readonly ILoggerFactory _loggerFactory;

    public MyClass():this(NullLoggerFactory.Instance)
    {

    }
    public MyClass(ILoggerFactory loggerFactory)
    {
      this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
    }
}

2. Batasi jumlah kategori yang Anda gunakan saat Anda membuat logger untuk memungkinkan konsumen mengkonfigurasi pemfilteran log dengan mudah. this._loggerFactory.CreateLogger(Consts.CategoryName)

Ihar Yakimush
sumber