Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Petunjuk / Saran
Konten ini adalah kutipan dari eBook, .NET Microservices Architecture for Containerized .NET Applications, tersedia di .NET Docs atau sebagai PDF yang dapat diunduh gratis dan dapat dibaca secara offline.
Gunakan Injeksi Dependensi untuk menyuntikkan objek infrastruktur ke lapisan aplikasi Anda
Seperti disebutkan sebelumnya, lapisan aplikasi dapat diimplementasikan sebagai bagian dari artefak (assembly) yang Anda bangun, seperti dalam proyek API Web atau proyek aplikasi web MVC. Dalam kasus layanan mikro yang dibangun dengan ASP.NET Core, lapisan aplikasi biasanya akan menjadi pustaka API Web Anda. Jika Anda ingin memisahkan apa yang berasal dari ASP.NET Core (infrastrukturnya ditambah pengontrol Anda) dari kode lapisan aplikasi kustom Anda, Anda juga dapat menempatkan lapisan aplikasi Anda di pustaka kelas terpisah, tetapi itu opsional.
Misalnya, kode lapisan aplikasi dari layanan mikro pemesanan secara langsung diimplementasikan sebagai bagian dari proyek Ordering.API (proyek ASP.NET Core Web API), seperti yang ditunjukkan pada Gambar 7-23.
Tampilan Penjelajah Solusi layanan mikro Ordering.API, memperlihatkan subfolder di bawah folder Aplikasi: Perilaku, Perintah, DomainEventHandlers, IntegrationEvents, Model, Kueri, dan Validasi.
Gambar 7-23. Lapisan aplikasi dalam proyek Ordering.API ASP.NET Core Web API
ASP.NET Core menyertakan kontainer IoC bawaan sederhana (diwakili oleh antarmuka IServiceProvider) yang mendukung injeksi konstruktor secara default, dan ASP.NET membuat layanan tertentu tersedia melalui DI. ASP.NET Core menggunakan layanan istilah untuk salah satu jenis yang Anda daftarkan yang akan disuntikkan melalui DI. Anda mengonfigurasi layanan kontainer bawaan dalam file Program.cs aplikasi Anda. Dependensi Anda diimplementasikan dalam layanan yang dibutuhkan jenis dan yang Anda daftarkan dalam kontainer IoC.
Biasanya, Anda ingin menyuntikkan dependensi yang mengimplementasikan objek infrastruktur. Dependensi khas untuk menyuntikkan adalah repositori. Tetapi Anda dapat menyuntikkan dependensi infrastruktur lain yang mungkin Anda miliki. Untuk implementasi yang lebih sederhana, Anda dapat langsung menyuntikkan objek pola Unit Kerja Anda (objek EF DbContext), karena DBContext juga merupakan implementasi dari objek persistensi infrastruktur Anda.
Dalam contoh berikut, Anda dapat melihat bagaimana .NET menyuntikkan objek repositori yang diperlukan melalui konstruktor. Kelas adalah handler perintah, yang akan tercakup di bagian berikutnya.
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
Kelas menggunakan repositori yang disuntikkan untuk menjalankan transaksi dan mempertahankan perubahan status. Tidak masalah apakah kelas tersebut adalah handler perintah, metode pengontrol ASP.NET Core Web API, atau Layanan Aplikasi DDD. Ini pada akhirnya adalah kelas sederhana yang menggunakan repositori, entitas domain, dan koordinasi aplikasi lainnya dengan cara yang mirip dengan handler perintah. Injeksi Dependensi bekerja dengan cara yang sama untuk semua kelas yang disebutkan, seperti dalam contoh menggunakan DI berdasarkan konstruktor.
Mendaftarkan jenis implementasi dependensi dan antarmuka atau abstraksi
Sebelum Anda menggunakan objek yang disuntikkan melalui konstruktor, Anda perlu mengetahui di mana harus mendaftarkan antarmuka dan kelas yang menghasilkan objek yang disuntikkan ke kelas aplikasi Anda melalui DI. (Seperti DI berdasarkan konstruktor, seperti yang ditunjukkan sebelumnya.)
Gunakan kontainer IoC bawaan yang disediakan oleh ASP.NET Core
Saat Anda menggunakan kontainer IoC bawaan yang disediakan oleh ASP.NET Core, Anda mendaftarkan jenis yang ingin Anda suntikkan dalam file Program.cs , seperti dalam kode berikut:
// Register out-of-the-box framework services.
builder.Services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration["ConnectionString"]),
ServiceLifetime.Scoped);
builder.Services.AddMvc();
// Register custom application dependencies.
builder.Services.AddScoped<IMyCustomRepository, MyCustomSQLRepository>();
Pola yang paling umum saat mendaftarkan jenis dalam kontainer IoC adalah mendaftarkan sepasang jenis—antarmuka dan kelas implementasi terkait. Kemudian ketika Anda meminta objek dari kontainer IoC melalui konstruktor apa pun, Anda meminta objek dari jenis antarmuka tertentu. Misalnya, dalam contoh sebelumnya, baris terakhir menyatakan bahwa ketika salah satu konstruktor Anda memiliki dependensi pada IMyCustomRepository (antarmuka atau abstraksi), kontainer IoC akan menyuntikkan instans kelas implementasi MyCustomSQLServerRepository.
Menggunakan pustaka Scrutor untuk pendaftaran jenis otomatis
Saat menggunakan DI di .NET, Anda mungkin ingin dapat memindai assembly dan secara otomatis mendaftarkan jenisnya berdasarkan konvensi. Fitur ini saat ini tidak tersedia di ASP.NET Core. Namun, Anda dapat menggunakan pustaka Scrutor untuk itu. Pendekatan ini nyaman ketika Anda memiliki puluhan jenis yang perlu didaftarkan dalam kontainer IoC Anda.
Sumber Daya Tambahan:
Raja Matthew. Mendaftarkan layanan dengan Scrutor
https://www.mking.net/blog/registering-services-with-scrutorKristian Hellang. Pengawas. Repositori GitHub.
https://github.com/khellang/Scrutor
Menggunakan Autofac sebagai kontainer IoC
Anda juga dapat menggunakan kontainer IoC tambahan dan menyambungkannya ke alur ASP.NET Core, seperti dalam pemesanan layanan mikro di eShopOnContainers, yang menggunakan Autofac. Saat menggunakan Autofac, Anda biasanya mendaftarkan jenis melalui modul, yang memungkinkan Anda membagi jenis pendaftaran di antara beberapa file tergantung di mana jenis Anda berada, sama seperti jenis aplikasi yang didistribusikan di beberapa pustaka kelas.
Misalnya, berikut ini adalah modul aplikasi Autofac untuk proyek Api Web Ordering.API dengan jenis yang ingin Anda suntikkan.
public class ApplicationModule : Autofac.Module
{
public string QueriesConnectionString { get; }
public ApplicationModule(string qconstr)
{
QueriesConnectionString = qconstr;
}
protected override void Load(ContainerBuilder builder)
{
builder.Register(c => new OrderQueries(QueriesConnectionString))
.As<IOrderQueries>()
.InstancePerLifetimeScope();
builder.RegisterType<BuyerRepository>()
.As<IBuyerRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<OrderRepository>()
.As<IOrderRepository>()
.InstancePerLifetimeScope();
builder.RegisterType<RequestManager>()
.As<IRequestManager>()
.InstancePerLifetimeScope();
}
}
Autofac juga memiliki fitur untuk memindai assembly dan mendaftarkan jenis menurut konvensi nama.
Proses dan konsep pendaftaran sangat mirip dengan cara Anda dapat mendaftarkan jenis dengan kontainer IoC ASP.NET Core bawaan, tetapi sintaks saat menggunakan Autofac sedikit berbeda.
Dalam kode contoh, abstraksi IOrderRepository terdaftar bersama dengan kelas implementasi OrderRepository. Ini berarti bahwa setiap kali konstruktor mendeklarasikan dependensi melalui abstraksi atau antarmuka IOrderRepository, kontainer IoC akan menyuntikkan instans kelas OrderRepository.
Jenis cakupan instans menentukan bagaimana instans dibagikan antara permintaan untuk layanan atau dependensi yang sama. Saat permintaan dibuat untuk dependensi, kontainer IoC dapat mengembalikan yang berikut:
Cakupan instans tunggal per masa pakai (disebut dalam kontainer IoC Inti ASP.NET sebagai cakupan).
Instans baru per dependensi (disebut dalam kontainer IoC ASP.NET Core sebagai sementara).
Satu instans dibagikan di semua objek menggunakan kontainer IoC (disebut dalam kontainer ASP.NET Core IoC sebagai singleton).
Sumber Daya Tambahan:
Pengantar untuk Injeksi Dependensi di ASP.NET Core
https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injectionAutofac. Dokumentasi resmi.
https://docs.autofac.org/en/latest/Membandingkan masa pakai layanan kontainer ASP.NET Core IoC dengan cakupan instans kontainer IoC Autofac.
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/
Menerapkan pola Command dan Command Handler
Dalam contoh DI-through-constructor yang ditunjukkan di bagian sebelumnya, kontainer IoC menyuntikkan repositori melalui konstruktor di kelas. Tapi tepatnya di mana mereka disuntikkan? Dalam API Web sederhana (misalnya, layanan mikro katalog di eShopOnContainers), Anda menyuntikkannya di tingkat pengontrol MVC, di konstruktor pengontrol, sebagai bagian dari alur permintaan ASP.NET Core. Namun, dalam kode awal bagian ini (kelas CreateOrderCommandHandler dari layanan Ordering.API di eShopOnContainers), injeksi dependensi dilakukan melalui konstruktor handler perintah tertentu. Mari kita jelaskan apa itu handler perintah dan mengapa Anda ingin menggunakannya.
Pola Perintah secara intrinsik terkait dengan pola CQRS yang diperkenalkan sebelumnya dalam panduan ini. CQRS memiliki dua sisi. Area pertama adalah kueri, menggunakan kueri yang disederhanakan dengan ORM mikro Dapper, yang dijelaskan sebelumnya. Area kedua adalah perintah, yang merupakan titik awal untuk transaksi, dan saluran input dari luar layanan.
Seperti yang ditunjukkan pada Gambar 7-24, pola didasarkan pada menerima perintah dari sisi klien, memprosesnya berdasarkan aturan model domain, dan akhirnya mempertahankan status dengan transaksi.
Gambar 7-24. Tampilan tingkat tinggi dari perintah atau "sisi transaksi" dalam pola CQRS
Gambar 7-24 menunjukkan bahwa aplikasi UI mengirim perintah melalui API yang masuk ke CommandHandler
, yang bergantung pada model Domain dan Infrastruktur, untuk memperbarui database.
Kelas perintah
Perintah adalah permintaan bagi sistem untuk melakukan tindakan yang mengubah status sistem. Perintah sangat penting, dan harus diproses hanya sekali.
Karena perintah sangat penting, perintah biasanya dinamai dengan kata kerja dalam suasana hati penting (misalnya, "buat" atau "perbarui"), dan mereka mungkin menyertakan jenis agregat, seperti CreateOrderCommand. Tidak seperti peristiwa, perintah bukanlah fakta dari masa lalu; itu hanya permintaan, dan dengan demikian dapat ditolak.
Perintah dapat berasal dari UI sebagai akibat dari pengguna yang memulai permintaan, atau dari manajer proses ketika manajer proses mengarahkan agregat untuk melakukan tindakan.
Karakteristik penting dari perintah adalah bahwa perintah harus diproses hanya sekali oleh satu penerima. Ini karena perintah adalah satu tindakan atau transaksi yang ingin Anda lakukan dalam aplikasi. Misalnya, perintah pembuatan pesanan yang sama tidak boleh diproses lebih dari sekali. Ini adalah perbedaan penting antara perintah dan peristiwa. Peristiwa dapat diproses beberapa kali, karena banyak sistem atau layanan mikro mungkin tertarik dengan peristiwa tersebut.
Selain itu, penting bahwa perintah hanya diproses sekali jika perintah tidak idempotensi. Perintah idempotensi jika dapat dijalankan beberapa kali tanpa mengubah hasilnya, baik karena sifat perintah, atau karena cara sistem menangani perintah.
Ini adalah praktik yang baik untuk membuat perintah dan pembaruan idempotensi ketika masuk akal di bawah aturan bisnis dan invarian domain Anda. Misalnya, untuk menggunakan contoh yang sama, jika karena alasan apa pun (logika coba lagi, peretasan, dll.) perintah CreateOrder yang sama mencapai sistem Anda beberapa kali, Anda harus dapat mengidentifikasinya dan memastikan bahwa Anda tidak membuat beberapa pesanan. Untuk melakukannya, Anda perlu melampirkan beberapa jenis identitas dalam operasi dan mengidentifikasi apakah perintah atau pembaruan sudah diproses.
Anda mengirim perintah ke satu penerima; Anda tidak menerbitkan perintah. Penerbitan adalah untuk peristiwa yang menyatakan fakta—bahwa sesuatu telah terjadi dan mungkin menarik bagi penerima peristiwa. Dalam kasus peristiwa, penerbit tidak memiliki kekhawatiran tentang penerima mana yang mendapatkan peristiwa atau apa yang mereka lakukan. Tetapi peristiwa domain atau integrasi adalah cerita yang berbeda yang sudah diperkenalkan di bagian sebelumnya.
Perintah diimplementasikan dengan kelas yang berisi bidang data atau koleksi dengan semua informasi yang diperlukan untuk menjalankan perintah tersebut. Perintah adalah jenis khusus Objek Transfer Data (DTO), yang secara khusus digunakan untuk meminta perubahan atau transaksi. Perintah itu sendiri didasarkan pada informasi yang tepat yang diperlukan untuk memproses perintah, dan tidak lebih.
Contoh berikut memperlihatkan CreateOrderCommand
kelas yang disederhanakan. Ini adalah perintah yang tidak dapat diubah yang digunakan dalam pemesanan layanan mikro di eShopOnContainers.
// DDD and CQRS patterns comment: Note that it is recommended to implement immutable Commands
// In this case, its immutability is achieved by having all the setters as private
// plus only being able to update the data just once, when creating the object through its constructor.
// References on Immutable Commands:
// http://cqrs.nu/Faq
// https://docs.spine3.org/motivation/immutability.html
// http://blog.gauffin.org/2012/06/griffin-container-introducing-command-support/
// https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties
[DataContract]
public class CreateOrderCommand
: IRequest<bool>
{
[DataMember]
private readonly List<OrderItemDTO> _orderItems;
[DataMember]
public string UserId { get; private set; }
[DataMember]
public string UserName { get; private set; }
[DataMember]
public string City { get; private set; }
[DataMember]
public string Street { get; private set; }
[DataMember]
public string State { get; private set; }
[DataMember]
public string Country { get; private set; }
[DataMember]
public string ZipCode { get; private set; }
[DataMember]
public string CardNumber { get; private set; }
[DataMember]
public string CardHolderName { get; private set; }
[DataMember]
public DateTime CardExpiration { get; private set; }
[DataMember]
public string CardSecurityNumber { get; private set; }
[DataMember]
public int CardTypeId { get; private set; }
[DataMember]
public IEnumerable<OrderItemDTO> OrderItems => _orderItems;
public CreateOrderCommand()
{
_orderItems = new List<OrderItemDTO>();
}
public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
string cardNumber, string cardHolderName, DateTime cardExpiration,
string cardSecurityNumber, int cardTypeId) : this()
{
_orderItems = basketItems.ToOrderItemsDTO().ToList();
UserId = userId;
UserName = userName;
City = city;
Street = street;
State = state;
Country = country;
ZipCode = zipcode;
CardNumber = cardNumber;
CardHolderName = cardHolderName;
CardExpiration = cardExpiration;
CardSecurityNumber = cardSecurityNumber;
CardTypeId = cardTypeId;
CardExpiration = cardExpiration;
}
public class OrderItemDTO
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount { get; set; }
public int Units { get; set; }
public string PictureUrl { get; set; }
}
}
Pada dasarnya, kelas perintah berisi semua data yang Anda butuhkan untuk melakukan transaksi bisnis dengan menggunakan objek model domain. Dengan demikian, perintah hanyalah struktur data yang berisi data baca-saja, dan tidak ada perilaku. Nama perintah menunjukkan tujuannya. Dalam banyak bahasa seperti C#, perintah direpresentasikan sebagai kelas, tetapi bukan kelas sejati dalam pengertian berorientasi objek nyata.
Sebagai karakteristik tambahan, perintah tidak dapat diubah, karena penggunaan yang diharapkan adalah bahwa perintah tersebut diproses langsung oleh model domain. Mereka tidak perlu berubah selama masa pakai yang diproyeksikan. Di kelas C#, kekekalan dapat dicapai dengan tidak memiliki setter atau metode lain yang mengubah status internal.
Perlu diingat bahwa jika Anda berniat atau mengharapkan perintah melalui proses serialisasi/deserialisasi, properti harus memiliki setter privat, dan [DataMember]
atribut (atau [JsonProperty]
). Jika tidak, deserializer tidak akan dapat membangun ulang objek di tujuan dengan nilai yang diperlukan. Anda juga dapat menggunakan properti baca-saja jika kelas memiliki konstruktor dengan parameter untuk semua properti, dengan konvensi penamaan camelCase yang biasa, dan membuat anotasi konstruktor sebagai [JsonConstructor]
. Namun, opsi ini memerlukan lebih banyak kode.
Misalnya, kelas perintah untuk membuat pesanan mungkin mirip dalam hal data dengan urutan yang ingin Anda buat, tetapi Anda mungkin tidak memerlukan atribut yang sama. Misalnya, CreateOrderCommand
tidak memiliki ID pesanan, karena pesanan belum dibuat.
Banyak kelas perintah bisa menjadi sederhana, hanya memerlukan beberapa bidang tentang beberapa status yang perlu diubah. Itu akan terjadi jika Anda hanya mengubah status pesanan dari "dalam proses" menjadi "berbayar" atau "dikirim" dengan menggunakan perintah yang mirip dengan yang berikut ini:
[DataContract]
public class UpdateOrderStatusCommand
:IRequest<bool>
{
[DataMember]
public string Status { get; private set; }
[DataMember]
public string OrderId { get; private set; }
[DataMember]
public string BuyerIdentityGuid { get; private set; }
}
Beberapa pengembang membuat objek permintaan UI mereka terpisah dari DTO perintah mereka, tetapi itu hanya masalah preferensi. Ini adalah pemisahan yang membosankan dengan tidak banyak nilai tambahan, dan objeknya hampir persis sama bentuknya. Misalnya, dalam eShopOnContainers, beberapa perintah berasal langsung dari sisi klien.
Kelas Handler perintah
Anda harus menerapkan kelas handler perintah tertentu untuk setiap perintah. Itulah cara kerja pola, dan di situlah Anda akan menggunakan objek perintah, objek domain, dan objek repositori infrastruktur. Handler perintah sebenarnya adalah jantung lapisan aplikasi dalam hal CQRS dan DDD. Namun, semua logika domain harus terkandung dalam kelas domain—dalam akar agregat (entitas akar), entitas anak, atau layanan domain, tetapi tidak dalam penangan perintah, yang merupakan kelas dari lapisan aplikasi.
Kelas handler perintah menawarkan batu loncatan yang kuat dalam cara mencapai Prinsip Tanggung Jawab Tunggal (SRP) yang disebutkan di bagian sebelumnya.
Handler perintah menerima perintah dan mendapatkan hasil dari agregat yang digunakan. Hasilnya harus berhasil menjalankan perintah, atau pengecualian. Dalam kasus pengecualian, status sistem harus tidak berubah.
Handler perintah biasanya mengambil langkah-langkah berikut:
Ini menerima objek perintah, seperti DTO (dari mediator atau objek infrastruktur lainnya).
Ini memvalidasi bahwa perintah valid (jika tidak divalidasi oleh mediator).
Ini membuat instans root agregat yang menjadi target dari perintah saat ini.
Ini menjalankan metode pada instans root agregat, mendapatkan data yang diperlukan dari perintah.
Ini mempertahankan status baru agregat ke database terkait. Operasi terakhir ini adalah transaksi aktual.
Biasanya, handler perintah menangani satu agregat yang didorong oleh root agregatnya (entitas root). Jika beberapa agregat harus terpengaruh oleh penerimaan satu perintah, Anda dapat menggunakan peristiwa domain untuk menyebarkan status atau tindakan di beberapa agregat.
Poin penting di sini adalah bahwa ketika perintah sedang diproses, semua logika domain harus berada di dalam model domain (agregat), sepenuhnya dienkapsulasi dan siap untuk pengujian unit. Handler perintah hanya bertindak sebagai cara untuk mendapatkan model domain dari database, dan sebagai langkah terakhir, untuk memberi tahu lapisan infrastruktur (repositori) untuk mempertahankan perubahan ketika model diubah. Keuntungan dari pendekatan ini adalah Anda dapat merefaktor logika domain dalam model domain yang terisolasi, sepenuhnya dienkapsulasi, kaya, dan perilaku tanpa mengubah kode dalam lapisan aplikasi atau infrastruktur, yang merupakan tingkat pipa (penangan perintah, API Web, repositori, dll.).
Ketika handler perintah menjadi kompleks, dengan terlalu banyak logika, itu bisa menjadi bau kode. Tinjau, dan jika Anda menemukan logika domain, refaktor kode untuk memindahkan perilaku domain tersebut ke metode objek domain (root agregat dan entitas turunan).
Sebagai contoh kelas handler perintah, kode berikut menunjukkan kelas yang sama CreateOrderCommandHandler
dengan yang Anda lihat di awal bab ini. Dalam hal ini, ini juga menyoroti metode Handel dan operasi dengan objek/agregat model domain.
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
Ini adalah langkah tambahan yang harus dilakukan handler perintah:
Gunakan data perintah untuk beroperasi dengan metode dan perilaku root agregat.
Secara internal dalam objek domain, ajukan peristiwa domain saat transaksi dijalankan, tetapi itu transparan dari sudut pandang handler perintah.
Jika hasil operasi agregat berhasil dan setelah transaksi selesai, ajukan peristiwa integrasi. (Ini mungkin juga dinaikkan oleh kelas infrastruktur seperti repositori.)
Sumber Daya Tambahan:
Mark Seemann. Pada Batas, Aplikasi Tidak Object-Oriented
https://blog.ploeh.dk/2011/05/31/AttheBoundaries,Aplikasi Tidak Berorientasi Objek/Perintah dan peristiwa
https://cqrs.nu/faq/Command%20and%20EventsApa yang dilakukan handler perintah?
https://cqrs.nu/faq/Command%20HandlersJimmy Bogard. Pola Perintah Domain – Handler
https://jimmybogard.com/domain-command-patterns-handlers/Jimmy Bogard. Pola Perintah Domain – Validasi
https://jimmybogard.com/domain-command-patterns-validation/
Alur proses Perintah: cara memicu handler perintah
Pertanyaan berikutnya adalah cara memanggil handler perintah. Anda dapat memanggilnya secara manual dari setiap pengontrol ASP.NET Core terkait. Namun, pendekatan itu akan terlalu digabungkan dan tidak ideal.
Dua opsi utama lainnya, yang merupakan opsi yang direkomendasikan, adalah:
Melalui artefak pola Mediator dalam memori.
Dengan antrean pesan asinkron, di antara pengontrol dan handler.
Gunakan pola Mediator (dalam memori) dalam alur perintah
Seperti yang ditunjukkan pada Gambar 7-25, dalam pendekatan CQRS Anda menggunakan mediator cerdas, mirip dengan bus dalam memori, yang cukup cerdas untuk mengalihkan ke handler perintah yang tepat berdasarkan jenis perintah atau DTO yang diterima. Panah hitam tunggal antar komponen mewakili dependensi antara objek (dalam banyak kasus, disuntikkan melalui DI) dengan interaksi terkait.
Gambar 7-25. Menggunakan pola Mediator dalam proses dalam satu layanan mikro CQRS
Diagram di atas menunjukkan zoom-in dari gambar 7-24: pengontrol ASP.NET Core mengirimkan perintah ke alur perintah MediatR, sehingga mereka sampai ke handler yang sesuai.
Alasan bahwa menggunakan pola Mediator masuk akal adalah bahwa dalam aplikasi perusahaan, permintaan pemrosesan bisa menjadi rumit. Anda ingin dapat menambahkan jumlah masalah lintas pemotongan silang yang terbuka seperti pengelogan, validasi, audit, dan keamanan. Dalam kasus ini, Anda dapat mengandalkan alur mediator (lihat pola Mediator) untuk menyediakan sarana untuk perilaku tambahan ini atau masalah lintas pemotongan.
Mediator adalah objek yang mengenkapsulasi "bagaimana" proses ini: ini mengoordinasikan eksekusi berdasarkan status, cara handler perintah dipanggil, atau payload yang Anda berikan kepada handler. Dengan komponen mediator, Anda dapat menerapkan masalah lintas pemotongan dengan cara terpusat dan transparan dengan menerapkan dekorator (atau perilaku alur sejak MediatR 3). Untuk informasi selengkapnya, lihat Pola Dekorator.
Dekorator dan perilaku mirip dengan Pemrograman Berorientasi Aspek (AOP), hanya diterapkan ke alur proses tertentu yang dikelola oleh komponen mediator. Aspek dalam AOP yang menerapkan masalah lintas pemotongan diterapkan berdasarkan penenun aspek yang disuntikkan pada waktu kompilasi atau berdasarkan intersepsi panggilan objek. Kedua pendekatan AOP yang khas terkadang dikatakan berfungsi "seperti sihir," karena tidak mudah untuk melihat bagaimana AOP melakukan pekerjaannya. Saat berhadapan dengan masalah serius atau bug, AOP bisa sulit untuk di-debug. Di sisi lain, dekorator/perilaku ini eksplisit dan hanya diterapkan dalam konteks mediator, sehingga penelusuran kesalahan jauh lebih mudah diprediksi dan mudah.
Misalnya, dalam eShopOnContainers yang memesan layanan mikro, memiliki implementasi dua perilaku sampel, kelas LogBehavior dan kelas ValidatorBehavior. Implementasi perilaku dijelaskan di bagian berikutnya dengan menunjukkan bagaimana eShopOnContainers menggunakan perilaku MediatR.
Menggunakan antrean pesan (di luar proc) dalam alur perintah
Pilihan lain adalah menggunakan pesan asinkron berdasarkan broker atau antrean pesan, seperti yang ditunjukkan pada Gambar 7-26. Opsi itu juga dapat dikombinasikan dengan komponen mediator tepat sebelum handler perintah.
Gambar 7-26. Menggunakan antrean pesan (di luar proses dan komunikasi antar-proses) dengan perintah CQRS
Alur perintah juga dapat ditangani oleh antrean pesan ketersediaan tinggi untuk mengirimkan perintah ke handler yang sesuai. Menggunakan antrean pesan untuk menerima perintah dapat lebih mempersulit alur perintah Anda, karena Anda mungkin perlu membagi alur menjadi dua proses yang terhubung melalui antrean pesan eksternal. Namun, itu harus digunakan jika Anda perlu meningkatkan skalabilitas dan performa berdasarkan pesan asinkron. Pertimbangkan bahwa dalam kasus Gambar 7-26, pengontrol hanya memposting pesan perintah ke dalam antrean dan mengembalikan. Kemudian penangan perintah memproses pesan dengan kecepatan mereka sendiri. Itu adalah manfaat besar dari antrean: antrean pesan dapat bertindak sebagai buffer dalam kasus ketika skalabilitas hiper diperlukan, seperti untuk saham atau skenario lain dengan volume data masuk yang tinggi.
Namun, karena sifat antrean pesan yang asinkron, Anda perlu mencari tahu cara berkomunikasi dengan aplikasi klien tentang keberhasilan atau kegagalan proses perintah. Sebagai aturan, Anda tidak boleh menggunakan perintah "aktifkan dan lupakan". Setiap aplikasi bisnis perlu mengetahui apakah perintah berhasil diproses, atau setidaknya divalidasi dan diterima.
Dengan demikian, mampu merespons klien setelah memvalidasi pesan perintah yang dikirimkan ke antrean asinkron menambah kompleksitas pada sistem Anda, dibandingkan dengan proses perintah dalam proses yang mengembalikan hasil operasi setelah menjalankan transaksi. Dengan menggunakan antrean, Anda mungkin perlu mengembalikan hasil proses perintah melalui pesan hasil operasi lainnya, yang akan memerlukan komponen tambahan dan komunikasi kustom dalam sistem Anda.
Selain itu, perintah asinkron adalah perintah satu arah, yang dalam banyak kasus mungkin tidak diperlukan, seperti yang dijelaskan dalam pertukaran menarik berikut antara Burtsev Alexey dan Greg Young dalam percakapan online:
[Burtsev Alexey] Saya menemukan banyak kode di mana orang menggunakan penanganan perintah asinkron atau pesan perintah satu arah tanpa alasan untuk melakukannya (mereka tidak melakukan beberapa operasi panjang, mereka tidak menjalankan kode asinkron eksternal, mereka bahkan tidak menggunakan batas lintas aplikasi untuk menggunakan bus pesan). Mengapa mereka memperkenalkan kompleksitas yang tidak perlu ini? Dan sebenarnya, saya belum melihat contoh kode CQRS dengan memblokir handler perintah sejauh ini, meskipun itu akan berfungsi dengan baik dalam banyak kasus.
[Greg Young] [...] perintah asinkron tidak ada; itu sebenarnya peristiwa lain. Jika saya harus menerima apa yang Anda kirimkan kepada saya dan mengajukan peristiwa jika saya tidak setuju, tidak lagi Anda menyuruh saya untuk melakukan sesuatu [yaitu, itu bukan perintah]. Kau mengatakan sesuatu telah dilakukan. Ini tampaknya sedikit perbedaan pada awalnya, tetapi memiliki banyak implikasi.
Perintah asinkron sangat meningkatkan kompleksitas sistem, karena tidak ada cara sederhana untuk menunjukkan kegagalan. Oleh karena itu, perintah asinkron tidak disarankan selain ketika persyaratan penskalakan diperlukan atau dalam kasus khusus saat berkomunikasi layanan mikro internal melalui olahpesan. Dalam kasus tersebut, Anda harus merancang sistem pelaporan dan pemulihan terpisah untuk kegagalan.
Dalam versi awal eShopOnContainers, diputuskan untuk menggunakan pemrosesan perintah sinkron, dimulai dari permintaan HTTP dan didorong oleh pola Mediator. Itu dengan mudah memungkinkan Anda untuk mengembalikan keberhasilan atau kegagalan proses, seperti dalam implementasi CreateOrderCommandHandler.
Bagaimanapun, ini harus menjadi keputusan berdasarkan persyaratan bisnis aplikasi atau layanan mikro Anda.
Menerapkan alur proses perintah dengan pola mediator (MediatR)
Sebagai implementasi sampel, panduan ini mengusulkan penggunaan alur dalam proses berdasarkan pola Mediator untuk mendorong perintah penyerapan dan perintah rute, dalam memori, ke penangan perintah yang tepat. Panduan ini juga mengusulkan penerapan perilaku untuk memisahkan masalah lintas pemotongan.
Untuk implementasi di .NET, ada beberapa pustaka sumber terbuka yang tersedia yang mengimplementasikan pola Mediator. Pustaka yang digunakan dalam panduan ini adalah pustaka sumber terbuka MediatR (dibuat oleh Jimmy Bogard), tetapi Anda dapat menggunakan pendekatan lain. MediatR adalah pustaka kecil dan sederhana yang memungkinkan Anda memproses pesan dalam memori seperti perintah, sambil menerapkan dekorator atau perilaku.
Menggunakan pola Mediator membantu Anda mengurangi konektor dan mengisolasi kekhawatiran pekerjaan yang diminta, sambil secara otomatis terhubung ke handler yang melakukan pekerjaan tersebut—dalam hal ini, ke penangan perintah.
Alasan bagus lain untuk menggunakan pola Mediator dijelaskan oleh Jimmy Bogard saat meninjau panduan ini:
Saya pikir mungkin perlu disebutkan pengujian di sini - ini memberikan jendela konsisten yang bagus ke dalam perilaku sistem Anda. Permintaan masuk, respons keluar. Kami telah menemukan bahwa aspek yang cukup berharga dalam membangun tes perilaku yang konsisten.
Pertama, mari kita lihat contoh pengontrol WebAPI di mana Anda benar-benar akan menggunakan objek mediator. Jika Anda tidak menggunakan objek mediator, Anda harus menyuntikkan semua dependensi untuk pengontrol tersebut, hal-hal seperti objek pencatat dan lainnya. Oleh karena itu, konstruktor akan rumit. Di sisi lain, jika Anda menggunakan objek mediator, konstruktor pengontrol Anda bisa jauh lebih sederhana, hanya dengan beberapa dependensi alih-alih banyak dependensi jika Anda memiliki satu per operasi pemotongan silang, seperti dalam contoh berikut:
public class MyMicroserviceController : Controller
{
public MyMicroserviceController(IMediator mediator,
IMyMicroserviceQueries microserviceQueries)
{
// ...
}
}
Anda dapat melihat bahwa mediator menyediakan konstruktor pengontrol Web API yang bersih dan ramping. Selain itu, dalam metode pengontrol, kode untuk mengirim perintah ke objek mediator hampir satu baris:
[Route("new")]
[HttpPost]
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand
runOperationCommand)
{
var commandResult = await _mediator.SendAsync(runOperationCommand);
return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}
Menerapkan Perintah idempotensi
Di eShopOnContainers, contoh yang lebih canggih daripada di atas adalah mengirimkan objek CreateOrderCommand dari layanan mikro Pemesanan. Tetapi karena proses bisnis Pemesanan sedikit lebih kompleks dan, dalam kasus kami, itu benar-benar dimulai di layanan mikro Ke basket, tindakan mengirimkan objek CreateOrderCommand ini dilakukan dari handler peristiwa integrasi bernama UserCheckoutAcceptedIntegrationEventHandler bukan pengontrol WebAPI sederhana yang dipanggil dari Aplikasi klien seperti dalam contoh yang lebih sederhana sebelumnya.
Namun demikian, tindakan mengirimkan Perintah ke MediatR cukup mirip, seperti yang ditunjukkan dalam kode berikut.
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items,
eventMsg.UserId, eventMsg.City,
eventMsg.Street, eventMsg.State,
eventMsg.Country, eventMsg.ZipCode,
eventMsg.CardNumber,
eventMsg.CardHolderName,
eventMsg.CardExpiration,
eventMsg.CardSecurityNumber,
eventMsg.CardTypeId);
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand,bool>(createOrderCommand,
eventMsg.RequestId);
result = await _mediator.Send(requestCreateOrder);
Namun, kasus ini juga sedikit lebih maju karena kami juga menerapkan perintah idempotensi. Proses CreateOrderCommand harus idempotensi, jadi jika pesan yang sama diduplikasi melalui jaringan, karena alasan apa pun, seperti percobaan ulang, urutan bisnis yang sama akan diproses hanya sekali.
Ini diimplementasikan dengan membungkus perintah bisnis (dalam hal ini CreateOrderCommand) dan menyematkannya ke dalam IdentifiedCommand generik, yang dilacak oleh ID dari setiap pesan yang datang melalui jaringan yang harus idempotent.
Dalam kode di bawah ini, Anda dapat melihat bahwa IdentifiedCommand tidak lebih dari DTO dengan dan ID ditambah objek perintah bisnis yang dibungkus.
public class IdentifiedCommand<T, R> : IRequest<R>
where T : IRequest<R>
{
public T Command { get; }
public Guid Id { get; }
public IdentifiedCommand(T command, Guid id)
{
Command = command;
Id = id;
}
}
Kemudian CommandHandler untuk IdentifiedCommand bernama IdentifiedCommandHandler.cs pada dasarnya akan memeriksa apakah ID yang datang sebagai bagian dari pesan sudah ada dalam tabel. Jika sudah ada, perintah tersebut tidak akan diproses lagi, sehingga bersifat sebagai perintah idempotensi. Kode infrastruktur tersebut dilakukan oleh panggilan metode di _requestManager.ExistAsync
bawah ini.
// IdentifiedCommandHandler.cs
public class IdentifiedCommandHandler<T, R> : IRequestHandler<IdentifiedCommand<T, R>, R>
where T : IRequest<R>
{
private readonly IMediator _mediator;
private readonly IRequestManager _requestManager;
private readonly ILogger<IdentifiedCommandHandler<T, R>> _logger;
public IdentifiedCommandHandler(
IMediator mediator,
IRequestManager requestManager,
ILogger<IdentifiedCommandHandler<T, R>> logger)
{
_mediator = mediator;
_requestManager = requestManager;
_logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
}
/// <summary>
/// Creates the result value to return if a previous request was found
/// </summary>
/// <returns></returns>
protected virtual R CreateResultForDuplicateRequest()
{
return default(R);
}
/// <summary>
/// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case
/// just enqueues the original inner command.
/// </summary>
/// <param name="message">IdentifiedCommand which contains both original command & request ID</param>
/// <returns>Return value of inner command or default value if request same ID was found</returns>
public async Task<R> Handle(IdentifiedCommand<T, R> message, CancellationToken cancellationToken)
{
var alreadyExists = await _requestManager.ExistAsync(message.Id);
if (alreadyExists)
{
return CreateResultForDuplicateRequest();
}
else
{
await _requestManager.CreateRequestForCommandAsync<T>(message.Id);
try
{
var command = message.Command;
var commandName = command.GetGenericTypeName();
var idProperty = string.Empty;
var commandId = string.Empty;
switch (command)
{
case CreateOrderCommand createOrderCommand:
idProperty = nameof(createOrderCommand.UserId);
commandId = createOrderCommand.UserId;
break;
case CancelOrderCommand cancelOrderCommand:
idProperty = nameof(cancelOrderCommand.OrderNumber);
commandId = $"{cancelOrderCommand.OrderNumber}";
break;
case ShipOrderCommand shipOrderCommand:
idProperty = nameof(shipOrderCommand.OrderNumber);
commandId = $"{shipOrderCommand.OrderNumber}";
break;
default:
idProperty = "Id?";
commandId = "n/a";
break;
}
_logger.LogInformation(
"----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
commandName,
idProperty,
commandId,
command);
// Send the embedded business command to mediator so it runs its related CommandHandler
var result = await _mediator.Send(command, cancellationToken);
_logger.LogInformation(
"----- Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})",
result,
commandName,
idProperty,
commandId,
command);
return result;
}
catch
{
return default(R);
}
}
}
}
Karena IdentifiedCommand bertindak seperti amplop perintah bisnis, ketika perintah bisnis perlu diproses karena itu bukan ID berulang, maka dibutuhkan perintah bisnis dalam dan mengirimkannya kembali ke Mediator, seperti di bagian terakhir kode yang ditunjukkan di atas saat menjalankan _mediator.Send(message.Command)
, dari IdentifiedCommandHandler.cs.
Saat melakukan itu, ini akan menautkan dan menjalankan handler perintah bisnis, dalam hal ini, CreateOrderCommandHandler, yang menjalankan transaksi terhadap database Pemesanan, seperti yang ditunjukkan dalam kode berikut.
// CreateOrderCommandHandler.cs
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository;
private readonly IIdentityService _identityService;
private readonly IMediator _mediator;
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;
private readonly ILogger<CreateOrderCommandHandler> _logger;
// Using DI to inject infrastructure persistence Repositories
public CreateOrderCommandHandler(IMediator mediator,
IOrderingIntegrationEventService orderingIntegrationEventService,
IOrderRepository orderRepository,
IIdentityService identityService,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
// Add Integration event to clean the basket
var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);
// Add/Update the Buyer AggregateRoot
// DDD patterns comment: Add child entities and value-objects through the Order Aggregate-Root
// methods and constructor so validations, invariants and business logic
// make sure that consistency is preserved across the whole aggregate
var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);
foreach (var item in message.OrderItems)
{
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
}
_logger.LogInformation("----- Creating Order - Order: {@Order}", order);
_orderRepository.Add(order);
return await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
}
}
Daftarkan jenis yang digunakan oleh MediatR
Agar MediatR mengetahui kelas handler perintah Anda, Anda perlu mendaftarkan kelas mediator dan kelas handler perintah di kontainer IoC Anda. Secara default, MediatR menggunakan Autofac sebagai kontainer IoC, tetapi Anda juga dapat menggunakan kontainer IoC ASP.NET Core bawaan atau kontainer lain yang didukung oleh MediatR.
Kode berikut menunjukkan cara mendaftarkan jenis dan perintah Mediator saat menggunakan modul Autofac.
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
// Register all the Command classes (they implement IRequestHandler)
// in assembly holding the Commands
builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
}
}
Di sinilah "keajaiban terjadi" dengan MediatR.
Karena setiap handler perintah mengimplementasikan antarmuka generik IRequestHandler<T>
, ketika Anda mendaftarkan rakitan menggunakan RegisteredAssemblyTypes
metode semua jenis yang ditandai sebagai IRequestHandler
juga akan didaftarkan dengan mereka Commands
. Contohnya:
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, bool>
{
Itu adalah kode yang menghubungkan perintah dengan penangan perintah. Handler hanyalah kelas sederhana, tetapi mewarisi dari RequestHandler<T>
, di mana T adalah jenis perintah, dan MediatR memastikannya dipanggil dengan payload yang benar (perintah).
Menerapkan masalah lintas pemotongan saat memproses perintah dengan Perilaku di MediatR
Ada satu hal lagi: mampu menerapkan kekhawatiran lintas pemotongan ke alur mediator. Anda juga dapat melihat di akhir kode modul pendaftaran Autofac bagaimana ia mendaftarkan jenis perilaku, khususnya, kelas LoggingBehavior kustom dan kelas ValidatorBehavior. Tetapi Anda juga dapat menambahkan perilaku kustom lainnya.
public class MediatorModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
.AsImplementedInterfaces();
// Register all the Command classes (they implement IRequestHandler)
// in assembly holding the Commands
builder.RegisterAssemblyTypes(
typeof(CreateOrderCommand).GetTypeInfo().Assembly).
AsClosedTypesOf(typeof(IRequestHandler<,>));
// Other types registration
//...
builder.RegisterGeneric(typeof(LoggingBehavior<,>)).
As(typeof(IPipelineBehavior<,>));
builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).
As(typeof(IPipelineBehavior<,>));
}
}
Kelas LoggingBehavior tersebut dapat diimplementasikan sebagai kode berikut, yang mencatat informasi tentang handler perintah yang dijalankan dan apakah berhasil atau tidak.
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) =>
_logger = logger;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next)
{
_logger.LogInformation($"Handling {typeof(TRequest).Name}");
var response = await next();
_logger.LogInformation($"Handled {typeof(TResponse).Name}");
return response;
}
}
Hanya dengan menerapkan kelas perilaku ini dan dengan mendaftarkannya di alur (di MediatorModule di atas), semua perintah yang diproses melalui MediatR akan mencatat informasi tentang eksekusi.
eShopOnContainers yang memesan layanan mikro juga menerapkan perilaku kedua untuk validasi dasar, kelas ValidatorBehavior yang bergantung pada pustaka FluentValidation, seperti yang ditunjukkan dalam kode berikut:
public class ValidatorBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
private readonly IValidator<TRequest>[] _validators;
public ValidatorBehavior(IValidator<TRequest>[] validators) =>
_validators = validators;
public async Task<TResponse> Handle(TRequest request,
RequestHandlerDelegate<TResponse> next)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(result => result.Errors)
.Where(error => error != null)
.ToList();
if (failures.Any())
{
throw new OrderingDomainException(
$"Command Validation Errors for type {typeof(TRequest).Name}",
new ValidationException("Validation exception", failures));
}
var response = await next();
return response;
}
}
Di sini perilaku meningkatkan pengecualian jika validasi gagal, tetapi Anda juga dapat mengembalikan objek hasil, yang berisi hasil perintah jika berhasil atau pesan validasi jika tidak. Ini mungkin akan memudahkan untuk menampilkan hasil validasi kepada pengguna.
Kemudian, berdasarkan pustaka FluentValidation, Anda akan membuat validasi untuk data yang diteruskan dengan CreateOrderCommand, seperti dalam kode berikut:
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(command => command.City).NotEmpty();
RuleFor(command => command.Street).NotEmpty();
RuleFor(command => command.State).NotEmpty();
RuleFor(command => command.Country).NotEmpty();
RuleFor(command => command.ZipCode).NotEmpty();
RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19);
RuleFor(command => command.CardHolderName).NotEmpty();
RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date");
RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3);
RuleFor(command => command.CardTypeId).NotEmpty();
RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found");
}
private bool BeValidExpirationDate(DateTime dateTime)
{
return dateTime >= DateTime.UtcNow;
}
private bool ContainOrderItems(IEnumerable<OrderItemDTO> orderItems)
{
return orderItems.Any();
}
}
Anda dapat membuat validasi tambahan. Ini adalah cara yang sangat bersih dan elegan untuk mengimplementasikan validasi perintah Anda.
Dengan cara yang sama, Anda dapat menerapkan perilaku lain untuk aspek tambahan atau masalah lintas pemotongan yang ingin Anda terapkan ke perintah saat menanganinya.
Sumber Daya Tambahan:
Pola mediator
-
Pola mediator
https://en.wikipedia.org/wiki/Mediator_pattern
Pola dekorator
-
Pola dekorator
https://en.wikipedia.org/wiki/Decorator_pattern
MediatR (Jimmy Bogard)
MediatR. Repositori GitHub.
https://github.com/jbogard/MediatRCQRS dengan MediatR dan AutoMapper
https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/Letakkan pengontrol Anda pada diet: POST dan perintah.
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-commands/Mengatasi masalah lintas pemotongan dengan alur mediator
https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-mediator-pipeline/CQRS dan REST: kecocokan sempurna
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-match/Contoh Alur MediatR
https://lostechies.com/jimmybogard/2016/10/13/mediatr-pipeline-examples/Perlengkapan Uji Ilis Vertikal untuk MediatR dan ASP.NET Core
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-mediatr-and-asp-net-core/Ekstensi MediatR untuk Injeksi Dependensi Microsoft Dirilis
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-microsoft-dependency-injection-released/
validasi Fluent
- Jeremy Skinner. FluentValidation. Repositori GitHub.
https://github.com/JeremySkinner/FluentValidation