Bagikan melalui


Injeksi Dependensi di SignalR 1.x

oleh Patrick Fletcher

Peringatan

Dokumentasi ini bukan untuk versi terbaru SignalR. Lihatlah ASP.NET Core SignalR.

Injeksi dependensi adalah cara untuk menghapus dependensi yang dikodekan secara permanen antar objek, sehingga lebih mudah untuk mengganti dependensi objek, baik untuk pengujian (menggunakan objek tiruan) atau untuk mengubah perilaku run-time. Tutorial ini menunjukkan cara melakukan injeksi dependensi pada hub SignalR. Ini juga menunjukkan cara menggunakan kontainer IoC dengan SignalR. Kontainer IoC adalah kerangka kerja umum untuk injeksi dependensi.

Apa itu Injeksi Dependensi?

Lewati bagian ini jika Anda sudah terbiasa dengan injeksi dependensi.

Injeksi dependensi (DI ) adalah pola di mana objek tidak bertanggung jawab untuk membuat dependensi mereka sendiri. Berikut adalah contoh sederhana untuk memotivasi DI. Misalkan Anda memiliki objek yang perlu mencatat pesan. Anda dapat menentukan antarmuka pengelogan:

interface ILogger 
{
    void LogMessage(string message);
}

Di objek Anda, Anda dapat membuat pesan untuk mencatat ILogger :

// Without dependency injection.
class SomeComponent
{
    ILogger _logger = new FileLogger(@"C:\logs\log.txt");

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

Ini berfungsi, tetapi ini bukan desain terbaik. Jika Anda ingin mengganti FileLogger dengan implementasi lain ILogger , Anda harus memodifikasi SomeComponent. Seandainya banyak objek lain menggunakan FileLogger, Anda harus mengubah semuanya. Atau jika Anda memutuskan untuk membuat FileLogger singleton, Anda juga harus membuat perubahan di seluruh aplikasi.

Pendekatan yang lebih baik adalah "menyuntikkan" ke ILogger dalam objek—misalnya, dengan menggunakan argumen konstruktor:

// With dependency injection.
class SomeComponent
{
    ILogger _logger;

    // Inject ILogger into the object.
    public SomeComponent(ILogger logger)
    {
        if (logger == null)
        {
            throw new NullReferenceException("logger");
        }
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

Sekarang objek tidak bertanggung jawab untuk memilih yang akan ILogger digunakan. Anda dapat beralih ILogger implementasi tanpa mengubah objek yang bergantung padanya.

var logger = new TraceLogger(@"C:\logs\log.etl");
var someComponent = new SomeComponent(logger);

Pola ini disebut injeksi konstruktor. Pola lain adalah injeksi setter, di mana Anda mengatur dependensi melalui metode atau properti setter.

Injeksi Dependensi Sederhana di SignalR

Pertimbangkan aplikasi Obrolan dari tutorial Memulai SignalR. Berikut adalah kelas hub dari aplikasi tersebut:

public class ChatHub : Hub
{
    public void Send(string name, string message)
    {
        Clients.All.addMessage(name, message);
    }
}

Misalkan Anda ingin menyimpan pesan obrolan di server sebelum mengirimnya. Anda dapat menentukan antarmuka yang mengabstraksi fungsionalitas ini, dan menggunakan DI untuk menyuntikkan antarmuka ke ChatHub kelas .

public interface IChatRepository
{
    void Add(string name, string message);
    // Other methods not shown.
}

public class ChatHub : Hub
{
    private IChatRepository _repository;

    public ChatHub(IChatRepository repository)
    {
        _repository = repository;
    }

    public void Send(string name, string message)
    {
        _repository.Add(name, message);
        Clients.All.addMessage(name, message);
    }

Satu-satunya masalah adalah bahwa aplikasi SignalR tidak secara langsung membuat hub; SignalR membuatnya untuk Anda. Secara default, SignalR mengharapkan kelas hub memiliki konstruktor tanpa parameter. Namun, Anda dapat dengan mudah mendaftarkan fungsi untuk membuat instans hub, dan menggunakan fungsi ini untuk melakukan DI. Daftarkan fungsi dengan memanggil GlobalHost.DependencyResolver.Register.

protected void Application_Start()
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    RouteTable.Routes.MapHubs();

    // ...
}

Sekarang SignalR akan memanggil fungsi anonim ini setiap kali perlu membuat ChatHub instans.

Kontainer IoC

Kode sebelumnya baik-baik saja untuk kasus sederhana. Tetapi Anda masih harus menulis ini:

... new ChatHub(new ChatMessageRepository()) ...

Dalam aplikasi kompleks dengan banyak dependensi, Anda mungkin perlu menulis banyak kode "kabel" ini. Kode ini bisa sulit dipertahankan, terutama jika dependensi berlapis. Pengujian unit juga sulit dilakukan.

Salah satu solusinya adalah menggunakan kontainer IoC. Kontainer IoC adalah komponen perangkat lunak yang bertanggung jawab untuk mengelola dependensi. Anda mendaftarkan jenis dengan kontainer, lalu menggunakan kontainer untuk membuat objek. Kontainer secara otomatis mencari tahu hubungan dependensi. Banyak kontainer IoC juga memungkinkan Anda mengontrol hal-hal seperti masa pakai dan cakupan objek.

Catatan

"IoC" adalah singkatan dari "inversi kontrol", yang merupakan pola umum di mana kerangka kerja memanggil ke dalam kode aplikasi. Kontainer IoC membangun objek untuk Anda, yang "menginversi" aliran kontrol yang biasa.

Menggunakan Kontainer IoC di SignalR

Aplikasi Obrolan mungkin terlalu sederhana untuk mendapatkan manfaat dari kontainer IoC. Sebagai gantinya, mari kita lihat sampel StockTicker .

Sampel StockTicker mendefinisikan dua kelas utama:

  • StockTickerHub: Kelas hub, yang mengelola koneksi klien.
  • StockTicker: Singleton yang memegang harga saham dan secara berkala memperbaruinya.

StockTickerHub menyimpan referensi ke StockTicker singleton, sambil StockTicker menyimpan referensi ke IHubConnectionContext untuk StockTickerHub. Ini menggunakan antarmuka ini untuk berkomunikasi dengan StockTickerHub instans. (Untuk informasi selengkapnya, lihat Siaran Server dengan ASP.NET SignalR.)

Kita dapat menggunakan kontainer IoC untuk membatalkan sedikit dependensi ini. Pertama, mari kita sederhanakan StockTickerHub kelas dan StockTicker . Dalam kode berikut, saya telah mengomentari bagian-bagian yang tidak kita butuhkan.

Hapus konstruktor tanpa parameter dari StockTicker. Sebaliknya, kita akan selalu menggunakan DI untuk membuat hub.

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly StockTicker _stockTicker;

    //public StockTickerHub() : this(StockTicker.Instance) { }

    public StockTickerHub(StockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

    // ...

Untuk StockTicker, hapus instans singleton. Nantinya, kita akan menggunakan kontainer IoC untuk mengontrol masa pakai StockTicker. Juga, buat konstruktor publik.

public class StockTicker
{
    //private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
    //    () => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

    // Important! Make this constructor public.
    public StockTicker(IHubConnectionContext clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

    //public static StockTicker Instance
    //{
    //    get
    //    {
    //        return _instance.Value;
    //    }
    //}

Selanjutnya, kita dapat merefaktor kode dengan membuat antarmuka untuk StockTicker. Kami akan menggunakan antarmuka ini untuk memisahkan StockTickerHub dari StockTicker kelas .

Visual Studio memudahkan pemfaktoran ulang semacam ini. Buka file StockTicker.cs, klik kanan pada StockTicker deklarasi kelas, dan pilih Refaktor ... Ekstrak Antarmuka.

Cuplikan layar menu dropdown klik kanan yang ditampilkan di atas Visual Studio Code, dengan opsi Refaktor dan Ekstrak Antarmuka disorot.

Dalam dialog Ekstrak Antarmuka , klik Pilih Semua. Biarkan kolom lain bernilai defult. Klik OK.

Cuplikan layar dialog Ekstrak Antarmuka dengan opsi Pilih Semua disorot dan opsi O K ditampilkan.

Visual Studio membuat antarmuka baru bernama IStockTicker, dan juga berubah StockTicker menjadi berasal dari IStockTicker.

Buka file IStockTicker.cs dan ubah antarmuka ke publik.

public interface IStockTicker
{
    void CloseMarket();
    IEnumerable<Stock> GetAllStocks();
    MarketState MarketState { get; }
    void OpenMarket();
    void Reset();
}

StockTickerHub Di kelas , ubah dua instans menjadi StockTickerIStockTicker:

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly IStockTicker _stockTicker;

    public StockTickerHub(IStockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

IStockTicker Membuat antarmuka tidak benar-benar diperlukan, tetapi saya ingin menunjukkan bagaimana DI dapat membantu mengurangi koupling antar komponen dalam aplikasi Anda.

Menambahkan Pustaka Ninject

Ada banyak kontainer IoC sumber terbuka untuk .NET. Untuk tutorial ini, saya akan menggunakan Ninject. (Pustaka populer lainnya termasuk Castle Windsor, Spring.Net, Autofac, Unity, dan StructureMap.)

Gunakan NuGet Package Manager untuk menginstal pustaka Ninject. Di Visual Studio, dari menu Alat pilih NuGet Package Manager>Package Manager Console. Di jendela Konsol Manajer Paket, masukkan perintah berikut:

Install-Package Ninject -Version 3.0.1.10

Mengganti Pemecah Dependensi SignalR

Untuk menggunakan Ninject dalam SignalR, buat kelas yang berasal dari DefaultDependencyResolver.

internal class NinjectSignalRDependencyResolver : DefaultDependencyResolver
{
    private readonly IKernel _kernel;
    public NinjectSignalRDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override object GetService(Type serviceType)
    {
        return _kernel.TryGet(serviceType) ?? base.GetService(serviceType);
    }

    public override IEnumerable<object> GetServices(Type serviceType)
    {
        return _kernel.GetAll(serviceType).Concat(base.GetServices(serviceType));
    }
}

Kelas ini mengambil alih metode GetService dan GetServices dari DefaultDependencyResolver. SignalR memanggil metode ini untuk membuat berbagai objek pada waktu proses, termasuk instans hub, serta berbagai layanan yang digunakan secara internal oleh SignalR.

  • Metode GetService membuat satu instans jenis. Ambil alih metode ini untuk memanggil metode TryGet kernel Ninject. Jika metode tersebut mengembalikan null, kembali ke resolver default.
  • Metode GetServices membuat kumpulan objek dari jenis tertentu. Ambil alih metode ini untuk menggabungkan hasil dari Ninject dengan hasil dari resolver default.

Mengonfigurasi Pengikatan Ninject

Sekarang kita akan menggunakan Ninject untuk mendeklarasikan jenis pengikatan.

Buka file RegisterHubs.cs. Dalam metode , RegisterHubs.Start buat kontainer Ninject, yang disebut Ninject kernel.

var kernel = new StandardKernel();

Buat instans resolver dependensi kustom kami:

var resolver = new NinjectSignalRDependencyResolver(kernel);

Buat pengikatan untuk IStockTicker sebagai berikut:

kernel.Bind<IStockTicker>()
    .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
    .InSingletonScope();  // Make it a singleton object.

Kode ini mengatakan dua hal. Pertama, setiap kali aplikasi membutuhkan IStockTicker, kernel harus membuat instans StockTicker. Kedua, StockTicker kelas harus dibuat sebagai objek singleton. Ninject akan membuat satu instans objek, dan mengembalikan instans yang sama untuk setiap permintaan.

Buat pengikatan untuk IHubConnectionContext sebagai berikut:

kernel.Bind<IHubConnectionContext>().ToMethod(context =>
    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();

Kode ini membuat fungsi anonim yang mengembalikan IHubConnection. Metode WhenInjectedInto memberi tahu Ninject untuk menggunakan fungsi ini hanya saat membuat IStockTicker instans. Alasannya adalah bahwa SignalR membuat instans IHubConnectionContext secara internal, dan kami tidak ingin mengambil alih bagaimana SignalR membuatnya. Fungsi ini hanya berlaku untuk kelas kami StockTicker .

Teruskan resolver dependensi ke dalam metode MapHubs :

RouteTable.Routes.MapHubs(config);

Sekarang SignalR akan menggunakan resolver yang ditentukan di MapHubs, bukan resolver default.

Berikut adalah daftar kode lengkap untuk RegisterHubs.Start.

public static class RegisterHubs
{
    public static void Start()
    {
        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()
            .InSingletonScope();

        kernel.Bind<IHubConnectionContext>().ToMethod(context =>
                resolver.Resolve<IConnectionManager>().
                    GetHubContext<StockTickerHub>().Clients
            ).WhenInjectedInto<IStockTicker>();

        var config = new HubConfiguration()
        {
            Resolver = resolver
        };

        // Register the default hubs route: ~/signalr/hubs
        RouteTable.Routes.MapHubs(config);
    }
}

Untuk menjalankan aplikasi StockTicker di Visual Studio, tekan F5. Di jendela browser, navigasikan ke http://localhost:*port*/SignalR.Sample/StockTicker.html.

Cuplikan layar Sampel Ticker A S P dot NET Signal R Stock ditampilkan di jendela browser Internet Explorer.

Aplikasi ini memiliki fungsionalitas yang sama persis seperti sebelumnya. (Untuk deskripsi, lihat Siaran Server dengan ASP.NET SignalR.) Kami belum mengubah perilaku; hanya membuat kode lebih mudah diuji, dirawat, dan berkembang.