Apakah CQRS / MediatR layak ketika mengembangkan aplikasi ASP.NET?


17

Saya telah mencari CQRS / MediatR belakangan ini. Tetapi semakin saya menelusuri semakin kurang saya menyukainya. Mungkin saya salah paham tentang sesuatu / segalanya.

Jadi itu mulai luar biasa dengan mengklaim mengurangi controller Anda untuk ini

public async Task<ActionResult> Edit(Edit.Query query)
{
    var model = await _mediator.SendAsync(query);

    return View(model);
}

Yang sangat cocok dengan pedoman pengontrol yang tipis. Namun itu meninggalkan beberapa detail yang cukup penting - penanganan kesalahan.

Mari kita lihat Loginaksi default dari proyek MVC baru

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation(1, "User logged in.");
            return RedirectToLocal(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning(2, "User account locked out.");
            return View("Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Konversi yang memberi kita banyak masalah dunia nyata. Ingat tujuannya adalah untuk menguranginya menjadi

public async Task<IActionResult> Login(Login.Command command, string returnUrl = null)
{
    var model = await _mediator.SendAsync(command);

    return View(model);
}

Salah satu solusi yang mungkin untuk ini adalah mengembalikan CommandResult<T>bukan modeldan kemudian menangani CommandResultdalam filter tindakan pos. Seperti yang dibahas di sini .

Salah satu implementasi CommandResultbisa seperti ini

public interface ICommandResult  
{
    bool IsSuccess { get; }
    bool IsFailure { get; }
    object Result { get; set; }
}

sumber

Namun itu tidak benar-benar menyelesaikan masalah kita dalam Logintindakan, karena ada beberapa kondisi kegagalan. Kita dapat menambahkan status kegagalan tambahan ini ICommandResulttetapi itu merupakan awal yang bagus untuk kelas / antarmuka yang sangat besar. Orang mungkin mengatakan itu tidak sesuai dengan Tanggung Jawab Tunggal (SRP).

Masalah lainnya adalah returnUrl. Kami memiliki return RedirectToLocal(returnUrl);kode ini. Entah bagaimana kita perlu menangani argumen bersyarat berdasarkan status keberhasilan perintah. Sementara saya pikir itu bisa dilakukan (saya tidak yakin apakah ModelBinder dapat memetakan argumen FromBody dan FromQuery ( returnUrladalah FromQuery) ke model tunggal). Orang hanya bisa bertanya-tanya skenario gila macam apa yang bisa terjadi.

Validasi model juga menjadi lebih kompleks seiring dengan pengembalian pesan kesalahan. Ambil ini sebagai contoh

else
{
    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return View(model);
}

Kami melampirkan pesan kesalahan bersama dengan model. Hal semacam ini tidak dapat dilakukan dengan menggunakan Exceptionstrategi (seperti yang disarankan di sini ) karena kita memerlukan model. Mungkin Anda bisa mendapatkan modelnya dari Requesttetapi itu akan menjadi proses yang sangat terlibat.

Jadi secara keseluruhan saya mengalami kesulitan mengubah tindakan "sederhana" ini.

Saya mencari input. Apakah saya benar-benar salah di sini?


6
Sepertinya Anda sudah memahami masalah yang relevan dengan cukup baik. Ada banyak "peluru perak" di luar sana yang memiliki contoh mainan yang membuktikan kegunaannya, tetapi yang pasti jatuh ketika mereka terjepit oleh realitas aplikasi kehidupan nyata yang sebenarnya.
Robert Harvey

Lihat Perilaku MediatR. Ini pada dasarnya sebuah pipa yang memungkinkan Anda untuk mengatasi masalah lintas sektoral.
fml

Jawaban:


14

Saya pikir Anda mengharapkan terlalu banyak pola yang Anda gunakan. CQRS secara khusus dirancang untuk mengatasi perbedaan model antara permintaan dan perintah ke basis data , dan MediatR hanyalah pustaka perpesanan yang masih dalam proses. CQRS tidak mengklaim untuk menghilangkan kebutuhan akan logika bisnis seperti yang Anda harapkan. CQRS adalah pola untuk akses data, tetapi masalah Anda adalah dengan lapisan presentasi - arahan, tampilan, pengontrol.

Saya pikir Anda mungkin salah menerapkan pola CQRS ke otentikasi. Dengan login, itu tidak bisa dimodelkan sebagai perintah di CQRS karena

Perintah: Ubah status sistem tetapi jangan kembalikan nilai
- Martin Fowler CommandQuerySeparation

Menurut pendapat saya, otentikasi adalah domain yang buruk untuk CQRS. Dengan otentikasi, Anda memerlukan aliran respons permintaan yang sangat konsisten dan sinkron sehingga Anda dapat 1. memeriksa kredensial pengguna 2. membuat sesi untuk pengguna 3. menangani salah satu dari berbagai kasus tepi yang telah Anda identifikasi 4. segera berikan atau tolak pengguna sebagai tanggapan.

Apakah CQRS / MediatR layak ketika mengembangkan aplikasi ASP.NET?

CQRS adalah pola yang memiliki kegunaan yang sangat spesifik. Tujuannya adalah untuk memodelkan pertanyaan dan perintah alih-alih memiliki model untuk catatan seperti yang digunakan dalam CRUD. Ketika sistem menjadi lebih kompleks, tuntutan tampilan sering kali lebih kompleks dari sekadar menunjukkan satu catatan atau beberapa catatan, dan kueri dapat memodelkan kebutuhan aplikasi dengan lebih baik. Demikian pula perintah dapat mewakili perubahan ke banyak catatan, bukan CRUD yang Anda ubah catatan tunggal. Martin Fowler memperingatkan

Seperti pola apa pun, CQRS berguna di beberapa tempat, tetapi tidak di tempat lain. Banyak sistem yang cocok dengan model mental CRUD, dan karenanya harus dilakukan dengan gaya itu. CQRS adalah lompatan mental yang signifikan bagi semua yang berkepentingan, jadi tidak boleh ditangani kecuali manfaatnya sepadan dengan lompatan itu. Walaupun saya telah menemukan keberhasilan penggunaan CQRS, sejauh ini sebagian besar kasus yang saya alami belum begitu baik, dengan CQRS dipandang sebagai kekuatan signifikan untuk membuat sistem perangkat lunak mengalami kesulitan serius.
- Martin Fowler CQRS

Jadi untuk menjawab pertanyaan Anda CQRS tidak boleh menjadi pilihan pertama ketika merancang aplikasi saat CRUD cocok. Tidak ada dalam pertanyaan Anda yang memberi saya indikasi bahwa Anda memiliki alasan untuk menggunakan CQRS.

Adapun MediatR, ini adalah pustaka perpesanan dalam proses, ini bertujuan untuk memisahkan permintaan dari penanganan permintaan. Anda harus memutuskan lagi apakah akan meningkatkan desain Anda untuk menggunakan perpustakaan ini. Saya pribadi bukan penganjur pesan dalam proses. Pelepasan lepas dapat dicapai dengan cara yang lebih sederhana daripada perpesanan, dan saya sarankan Anda mulai dari sana.


1
Saya 100% setuju. CQRS hanya sedikit senang, jadi saya pikir "mereka" melihat sesuatu yang tidak saya lakukan. Karena saya mengalami kesulitan melihat manfaat CQRS di aplikasi web CRUD. Sejauh ini satu-satunya skenario adalah CQRS + ES yang masuk akal bagi saya.
Snæbjørn

Beberapa pria di pekerjaan baru saya memutuskan untuk menempatkan MediatR pada sistem ASP.Net baru yang mengklaimnya sebagai arsitektur. Implementasi yang dibuatnya bukan DDD, atau SOLID, atau KERING, atau KISS. Ini adalah sistem kecil yang penuh dengan YAGNI. Dan itu sudah dimulai lama setelah beberapa komentar seperti milik Anda, termasuk Anda. Saya mencoba mencari cara bagaimana saya bisa memperbaiki kode untuk menyesuaikan arsitekturnya secara bertahap. Saya memiliki pendapat yang sama tentang CQRS di luar lapisan bisnis dan saya senang ada beberapa pengembang berpengalaman berpikir seperti itu.
MFedatto

Agak ironis untuk menegaskan bahwa gagasan menggabungkan CQRS / MediatR mungkin terkait dengan banyak YAGNI dan kurangnya KISS, ketika sebenarnya beberapa alternatif populer, seperti pola Repositori, mempromosikan YAGNI dengan membengkaknya kelas repositori dan memaksa antarmuka untuk menentukan banyak operasi CRUD pada semua agregat root yang ingin mengimplementasikan antarmuka seperti itu, sering meninggalkan metode-metode tersebut baik yang tidak digunakan atau diisi dengan pengecualian "tidak diterapkan". Karena CQRS tidak menggunakan generalisasi ini, CQRS hanya dapat mengimplementasikan apa yang diperlukan.
Lesair Valmont

@LesairValmont Repository hanya dianggap CRUD. "tentukan banyak operasi CRUD" seharusnya hanya 4 (atau 5 dengan "daftar"). Jika Anda memiliki pola akses permintaan yang lebih spesifik, pola itu seharusnya tidak ada di antarmuka repositori Anda. Saya tidak pernah mengalami masalah dengan metode repositori yang tidak digunakan. Bisakah Anda memberi contoh?
Samuel

@Amuel: Saya pikir pola repositori baik-baik saja untuk skenario tertentu, seperti halnya CQRS. Sebenarnya, pada aplikasi besar, akan ada beberapa bagian yang paling cocok akan menjadi pola repositori dan yang lainnya akan lebih diuntungkan oleh CQRS. Itu tergantung pada banyak faktor yang berbeda, seperti filosofi yang diikuti pada bagian aplikasi (misalnya berbasis tugas (CQRS) vs CRUD (repo)), ORM yang digunakan (jika ada), pemodelan domain ( misalnya DDD). Untuk katalog CRUD yang sederhana, CQRS pasti berlebihan, dan beberapa fitur kolaborasi waktu nyata (seperti obrolan) tidak akan menggunakan keduanya.
Lesair Valmont

10

CQRS lebih merupakan hal manajemen data daripada dan tidak cenderung terlalu banyak berdarah ke dalam lapisan aplikasi (atau Domain jika Anda suka, karena cenderung paling sering digunakan dalam sistem DDD). Aplikasi MVC Anda, di sisi lain, adalah aplikasi lapisan presentasi dan harus dipisahkan dari inti kueri / persistensi CQRS.

Hal lain yang patut dicatat (mengingat perbandingan Anda tentang Loginmetode default dan keinginan untuk pengontrol tipis): Saya tidak akan persis mengikuti standar ASP.NET templat / kode boilerplate sebagai sesuatu yang harus kita khawatirkan untuk praktik terbaik.

Saya suka pengontrol tipis juga, karena sangat mudah dibaca. Setiap controller yang saya miliki biasanya memiliki objek "service" yang dipasangkan dengan yang pada dasarnya menangani logika yang diperlukan oleh controller:

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) {

    var result = _service.Login(model);
    switch (result) {
        case result.lockout: return View("Lockout");
        case result.ok: return RedirectToLocal(returnUrl);
        default: return View("GeneralError");
    }
}

Masih cukup tipis, tetapi kami belum benar-benar mengubah cara kode bekerja, cukup mendelegasikan penanganan ke metode layanan, yang benar-benar tidak memiliki tujuan lain selain membuat tindakan pengontrol mudah dicerna.

Ingatlah, kelas layanan ini masih bertanggung jawab untuk mendelegasikan logika ke model / aplikasi sebagaimana diperlukan, itu benar-benar hanya sedikit ekstensi dari controller untuk menjaga kode tetap rapi. Metode layanan umumnya cukup pendek juga.

Saya tidak yakin mediator akan melakukan sesuatu yang berbeda secara konseptual dari itu: memindahkan beberapa logika kontroler dasar dari controller dan ke tempat lain untuk diproses.

(Saya belum pernah mendengar MediatR ini sebelumnya, dan melihat sekilas pada halaman github tampaknya tidak mengindikasikan bahwa itu adalah sesuatu yang inovatif - tentu saja bukan sesuatu seperti CQRS - pada kenyataannya, itu terlihat seperti sesuatu seperti lapisan abstraksi lain yang Anda dapat dimasukkan untuk memperumit kode dengan cara membuatnya terlihat lebih sederhana, tapi itu hanya pandangan awal saya)


5

Saya sangat menyarankan Anda melihat presentasi NDC Jimmy Bogard tentang pendekatannya untuk memodelkan permintaan http https://www.youtube.com/watch?v=SUiWfhAhgQw

Anda kemudian akan mendapatkan gagasan yang jelas tentang apa yang digunakan Mediatr.

Jimmy tidak memiliki kepatuhan buta terhadap pola dan abstraksi. Dia sangat pragmatis. Mediatr tidak membersihkan tindakan pengontrol. Adapun penanganan pengecualian, saya mendorong itu ke kelas induk yang disebut sesuatu seperti Execute. Jadi Anda berakhir dengan tindakan pengontrol yang sangat bersih.

Sesuatu seperti:

public bool Execute<T>(Func<T> messageFunction)
{
    try
    {
        messageFunction();

        return true;
    }
    catch (ValidationException exception)
    {
        Errors = string.Join(Environment.NewLine, exception.Errors.Select(e => e.ErrorMessage));
        Logger.LogException(exception, "ValidationException caught in SiteController");
    }
    catch (SiteException exception)
    {
        Errors = exception.Message;
        Logger.LogException(exception);
    }
    catch (DbEntityValidationException dbEntityValidationException)
    {
        // Retrieve the error messages as a list of strings.
        var errorMessages = dbEntityValidationException.EntityValidationErrors
                .SelectMany(x => x.ValidationErrors)
                .Select(x => x.ErrorMessage);

        // Join the list to a single string.
        var fullErrorMessage = string.Join("; ", errorMessages);

        // Combine the original exception message with the new one.
        var exceptionMessage = string.Concat(dbEntityValidationException.Message, " The validation errors are: ", fullErrorMessage);

        Logger.LogError(exceptionMessage);

        // Throw a new DbEntityValidationException with the improved exception message.
        throw new DbEntityValidationException(exceptionMessage, dbEntityValidationException.EntityValidationErrors);                
    }
    catch (Exception exception)
    {
        Errors = "An error has occurred.";
        Logger.LogException(exception, "Exception caught in SiteController.");
    }

    // used to indicate that any transaction which may be in progress needs to be rolled back for this request.
    HttpContext.Items[UiConstants.Error] = true;

    Response.StatusCode = (int)HttpStatusCode.InternalServerError; // fail

    return false;
}

Penggunaannya terlihat seperti ini:

[Route("api/licence")]
public IHttpActionResult Post(LicenceEditModel licenceEditModel)
{
    var updateLicenceCommand = new UpdateLicenceCommand { LicenceEditModel = licenceEditModel };
    int licenceId = -1;

    if (Execute(() => _mediator.Send(updateLicenceCommand)))
    {
        return JsonSuccess(licenceEditModel);
    }

    return JsonError(Errors);
}

Semoga itu bisa membantu.


4

Banyak orang (saya juga melakukannya) mengacaukan pola dengan perpustakaan. CQRS adalah sebuah pola tetapi MediatR adalah pustaka yang dapat Anda gunakan untuk menerapkan pola itu

Anda dapat menggunakan CQRS tanpa MediatR atau pustaka perpesanan dalam proses dan Anda dapat menggunakan MediatR tanpa CQRS:

public interface IProductsWriteService
{
    void CreateProduct(CreateProductCommand createProductCommand);
}

public interface IProductsReadService
{
    ProductDto QueryProduct(Guid guid);
}

CQS akan terlihat seperti ini:

public interface IProductsService
{
    void CreateProduct(CreateProductCommand createProductCommand);
    ProductDto QueryProduct(Guid guid);
}

Bahkan, Anda tidak perlu menyebut model input Anda "Perintah" seperti di atas CreateProductCommand. Dan masukan kueri Anda "Kueri". Perintah dan pertanyaan adalah metode, bukan model.

CQRS adalah tentang pemisahan tanggung jawab (metode baca harus terpisah dari metode menulis - diisolasi). Ini adalah ekstensi untuk CQS tetapi perbedaannya adalah di CQS Anda dapat menempatkan metode ini dalam 1 kelas. (tidak ada pemisahan tanggung jawab, hanya pemisahan perintah-permintaan). Lihat pemisahan vs pemisahan

Dari https://martinfowler.com/bliki/CQRS.html :

Intinya adalah gagasan bahwa Anda dapat menggunakan model yang berbeda untuk memperbarui informasi daripada model yang Anda gunakan untuk membaca informasi.

Ada kebingungan dalam apa yang dikatakannya, ini bukan tentang memiliki model terpisah untuk input dan output, ini tentang pemisahan tanggung jawab.

Batasan CQRS dan generasi id

Ada satu batasan yang akan Anda hadapi saat menggunakan CQRS atau CQS

Secara teknis dalam perintah deskripsi asli seharusnya tidak mengembalikan nilai apa pun (void) yang menurut saya bodoh karena tidak ada cara mudah untuk mendapatkan id yang dihasilkan dari objek yang baru dibuat: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

jadi Anda harus membuat id setiap kali sendiri daripada membiarkan database melakukannya.


Jika Anda ingin mempelajari lebih lanjut: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf


1
Saya menantang penegasan Anda bahwa perintah CQRS untuk mempertahankan data baru dalam basis data karena tidak dapat mengembalikan ID yang dihasilkan basis data baru adalah "bodoh". Saya pikir ini masalah filosofis. Ingat banyak DDD dan CQRS adalah tentang kekekalan data. Ketika Anda memikirkannya dua kali, Anda mulai menyadari bahwa tindakan sekadar mempertahankan data adalah operasi mutasi data. Dan ini bukan hanya tentang ID baru, tetapi bisa juga bidang yang diisi dengan data default, pemicu, dan procs tersimpan yang mungkin juga mengubah data Anda.
Lesair Valmont

Tentu Anda dapat mengirim semacam acara seperti "ItemCreated" dengan item baru sebagai argumen. Jika Anda hanya berurusan dengan protokol permintaan-respons dan menggunakan CQRS "benar" maka id harus diketahui sebelumnya sehingga Anda dapat meneruskannya ke fungsi kueri yang terpisah - sama sekali tidak ada yang salah dengan itu. Dalam banyak kasus, CQRS hanya berlebihan. Anda bisa hidup tanpanya. Ini hanyalah cara menyusun kode Anda dan itu sebagian besar tergantung pada protokol apa yang Anda gunakan juga.
Konrad

Dan Anda dapat mencapai kekekalan data tanpa CQRS
Konrad
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.