天天看點

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

目錄

介紹

先決條件

深入了解基本資訊

應用解決方案結構

圖像上傳和顯示應用

MVC與JS架構之間的通信設計

在SPA中,在表示層中添加UI和PLL

用于資料讀取和寫入操作的資料通路層

軟體包安裝

讀取查詢處理程式的實作

寫入指令處理程式

将MediatR與API控制器/控制器內建

配置MediatR

将依賴項注入Web-API控制器/控制器

如何發送指令/查詢請求

介紹

如果您是軟體專業人員,那麼您将熟悉軟體增強和維護工作。這是軟體開發生命周期的一部分;這樣,您就可以糾正故障,删除/增強現有功能。軟體維護成本可以最小化如果使用軟體體系結構模式,選擇合适的技術和了解行業趨勢的未來,考慮資源可靠性/可用性為現在和未來,在代碼中使用設計模式/原則,重用代碼,保持打開你選擇未來的擴充,等等。無論如何,如果您在應用程式中使用任何已知的軟體架構模式,那麼其他人将很容易了解您的應用程式的結構/元件設計。我将使用ASP.Net Core MVC中的MediatR和Vue.js根據CQRS模式解釋示例項目實作。

先決條件

  • 需要.NET Core 3.1的Visual Studio 2019
  • 安裝Node.js,NPM和@Vue/Cli
  • 需要WebPack和NPM任務運作器
  • 推薦閱讀“在ASP.NET Core 3.1 MVC中內建/配置Vue.js”

深入了解基本資訊

  • CQRS模式:簡而言之,指令-查詢分離責任(CQRS)模式将在不更改資料庫/系統的情況下傳回資料的讀查詢操作與将資料更改到資料庫/系統的寫指令(插入/更新/删除)操作分離開來。永遠不要将讀寫操作混合在一起。
  • Mediator模式:這是一種設計模式,當您需要集中控制和多個類/對象之間的通信時,将使用中介者,這種設計模式會對代碼産生影響。例如,Facebook Messenger是向多個使用者發送消息的中介者。
  • MVC模式:這是一個應用程式的架構模式,其中模型、視圖和控制器由它們的職責分隔開。模型是對象的資料;視圖向使用者顯示資料并處理使用者互動;控制器充當視圖和模型之間的中介。

應用解決方案結構

該項目的主要目标是解釋CQRS架構模式。我正在努力實作一個小型的單頁應用程式(SPA)項目。技術的選擇很重要,您應該根據自己的要求進行選擇。對于使用者界面(UI)和表示邏輯層(PLL),我選擇ASP.NET Core MVC和Vue.js(JavaScript架構)。對于資料通路,我選擇實體架構(EF)核心代碼優先方法,并将其實作到資料通路層(DAL)中。我特意避免使用單獨的業務邏輯層(BLL)和其他層,以最大程度地減少本文的篇幅。

圖像上傳和顯示應用

在本項目中,首先考慮CQRS模式,我将上傳圖像檔案以将其儲存到資料庫中;它将說明寫指令的操作。其次,我将從資料庫中讀取資料以顯示圖像;它将說明讀取查詢操作。

我在同一解決方案中添加了兩個單獨的項目。一個是名為“HR.App.DAL.CQRS” 的ClassLibrary(.NET Core)項目,另一個是名為“HR.App.Web”的ASP.NET Core Web應用程式項目。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

MVC與JS架構之間的通信設計

在此階段,我要指出UI/PLL以及它們如何互相通信。看下面的圖。我将JS架構放在View和Web API Controller之間。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

根據上圖,ASP.NET MVC控制器呈現視圖。JS将來自視圖的HTTP請求(

GET

/

PUT

/

POST

/

DELETE

)傳遞到Web API控制器,并将Web API控制器的響應資料(JSON/XML)更新到視圖。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

注意:我正在猜測,如何在ASP.NET Core MVC項目中配置Vue.js。如果您需要分步說明在帶有示例項目的ASP.NET Core中配置Vue.js,則建議閱讀:在ASP.NET Core 3.1 MVC中內建/配置Vue.js

在SPA中,在表示層中添加UI和PLL

在“HR.App.Web”項目中,添加Index.cshtml視圖和Index.cshtml.js檔案。我為圖像上傳和圖像視圖示簽/控件添加了以下HTML腳本到Index.cshtml中。這些與讀取和寫入操作關聯。

@{
    ViewData["Title"] = "Home Page";
}

    <div id="view" v-cloak>
        <div class="card">
            <div class="card-header">
                <div class="row">
                    <div class="col-10">
                        <h5>Upload File</h5>
                    </div>
                </div>
            </div>
            <div class="card-body">
                <dropzone id="uploadDropZone" url="/HomeApi/SubmitFile"

                          :use-custom-dropzone-options="useUploadOptions"

                          :dropzone-options="uploadOptions" v-on:vdropzone-success="onUploaded"

                          v-on:vdropzone-error="onUploadError">
                    <!-- Optional parameters if any! -->
                    <input type="hidden" name="token" value="xxx">
                </dropzone>
            </div>
        </div>
        <br/>
        <div class="card">
            <div class="card-header">
                <div class="row">
                    <div class="col-10">
                        <h5>Image viewer</h5>
                    </div>
                </div>
            </div>
            <div class="card-body">
                <img v-bind:src="imageData" v-bind:alt="imageAlt" style="width:25%;
                 height:25%; display: block;margin-left: auto; margin-right: auto;" />
                <hr />
                <div class="col-6">
                    <button id="viewFile" ref="viewFileRef" type="button" 

                     class="btn btn-info" v-on:click="viewImageById">View Image</button>
                    <button type="button" class="btn btn-info" v-on:click="onClear">
                     Clear</button>
                </div>

            </div>
        </div>
    </div>
<script type="text/javascript">
</script>
<script type="text/javascript" src="~/dest/js/home.bundle.js" 

        asp-append-version="true"></script>
           

為HTTP GET添加以下Vue.js腳本,并将其POST請求到Index.cshtml.js檔案中:

import Vue from 'vue';
import Dropzone from 'vue2-dropzone';

document.addEventListener('DOMContentLoaded', function (event) {
    let view = new Vue({
        el: document.getElementById('view'),
        components: {
            "dropzone": Dropzone
        },
        data: {
            message: 'This is the index page',
            useUploadOptions: true,
            imageData: '',
            imageAlt: 'Image',
            imageId: 0,
            uploadOptions: {
                acceptedFiles: "image/*",
                //acceptedFiles: '.png,.jpg',
                dictDefaultMessage: 'To upload the image click here. Or, drop an image here.',
                maxFiles: 1,
                maxFileSizeInMB: 20,
                addRemoveLinks: true
            }
        },
        methods: {
            onClear() {
                this.imageData = '';
            },
            viewImageById() {
                try {
                    this.dialogErrorMsg = "";
                    //this.imageId = 1;
                    var url = '/HomeApi/GetImageById/' + this.imageId;

                    console.log("===URL===>" + url);
                    var self = this;

                    axios.get(url)
                        .then(response => {
                            let responseData = response.data;

                            if (responseData.status === "Error") {
                                console.log(responseData.message);
                            }
                            else {
                                self.imageData = responseData.imgData;
                                console.log("Image is successfully loaded.");
                            }
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                } catch (ex) {

                    console.log(ex);
                }
            },
            onUploaded: function (file, response) {
                if (response.status === "OK" || response.status === "200") {
                    let finalResult = response.imageId;
                    this.imageId = finalResult;
                    console.log('Successfully uploaded!');
                }
                else {
                    this.isVisible = false;
                    console.log(response.message);
                }
            },
            onUploadError: function (file, message, xhr) {
                console.log("Message ====> " + JSON.stringify(message));
            }
        }
    });
});
           

在此JS檔案中,“viewImageById”方法用于讀取請求,而“onUploaded”方法用于寫入請求。界面看起來像:

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

用于資料讀取和寫入操作的資料通路層

我猜,您知道EF核心代碼優先方法,并且您具有域模型和上下文類。您可以使用其他方法。在這裡,我将實作讀取和寫入操作以進行資料通路。檢視下圖以了解應用程式的整個過程。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

軟體包安裝

在“HR.App.DAL.CQRS”項目中,我已經使用NuGet包管理器安裝了MediatR.Extensions.Microsoft.DependencyInjection,Microsoft.EntityFrameworkCore和Microsoft.EntityFrameworkCore.SqlServer。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

我需要MediatR來實作指令和查詢處理程式。我将為ASP.NET Core使用MediatR.Extensions來解決依賴關系。

讀取查詢處理程式的實作

為了從資料庫中擷取圖像,我添加了具有以下代碼的GetImageQuery.cs類:

using HR.App.DAL.CQRS.Models;
using HR.App.DAL.CQRS.ViewModel;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace HR.App.DAL.CQRS.Query
{
    public class GetImageQuery : IRequest<ImageResponse>
    {
        public int ImageId { get; set; }
    }

    public class GetImageQueryHandler : IRequestHandler<GetImageQuery, ImageResponse>
    {
        private readonly HrAppContext context;

        public GetImageQueryHandler(HrAppContext context)
        {
            this.context = context;
        }

        public async Task<ImageResponse> Handle(GetImageQuery request, CancellationToken cancellationToken)
        {
            ImageResponse imageResponse = new ImageResponse();

            try
            {
                UploadedImage uploadedImage = await context.UploadedImage.AsNoTracking()
                    .Where(x => x.ImageId == request.ImageId).SingleAsync();
                
                if (uploadedImage == null)
                {
                    imageResponse.Errors.Add("No Image found!");
                    return imageResponse;
                }

                imageResponse.UploadedImage = uploadedImage;
                imageResponse.IsSuccess = true;
            }
            catch (Exception exception)
            {
                imageResponse.Errors.Add(exception.Message);
            }

            return imageResponse;
        }
    }
}
           

GetImageQuery類繼承IRequest<ImageResponse>;  ImageResponse類型訓示的響應。另一方面,GetImageQueryHandler類繼承了IRequestHandler<GetImageQuery, ImageResponse>,其中GetImageQuery類型表示請求/消息,ImageResponse類型表示響應/輸出。此GetImageQueryHandler類實作傳回ImageResponse對象的Handle方法。

寫入指令處理程式

為了将圖像儲存到資料庫中,我添加了SaveImageCommand.cs類,其中包含以下代碼:

using HR.App.DAL.CQRS.Models;
using HR.App.DAL.CQRS.ViewModel;
using MediatR;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace HR.App.DAL.CQRS.Command
{
    public class SaveImageCommand : IRequest<ResponseResult>
    {
        public UploadedImage UploadedImage { get; set; }
    }

    public class SaveImageCommandHandler : IRequestHandler<SaveImageCommand, ResponseResult>
    {
        private readonly HrAppContext context;

        public SaveImageCommandHandler(HrAppContext context)
        {
            this.context = context;
        }

        public async Task<ResponseResult> Handle
               (SaveImageCommand request, CancellationToken cancellationToken)
        {
            using (var trans = context.Database.BeginTransaction())
            {
                ResponseResult response = new ResponseResult();

                try
                {
                    context.Add(request.UploadedImage);
                    await context.SaveChangesAsync();
                    trans.Commit();
                    response.IsSuccess = true;
                    response.ImageId = request.UploadedImage.ImageId;
                }
                catch (Exception exception)
                {
                    trans.Rollback();
                    response.Errors.Add(exception.Message);
                }

                return response;
            }
        }
    }
}
           

将MediatR與API控制器/控制器內建

在“HR.App.Web”項目中,我已經使用NuGet軟體包管理器安裝了MediatR.Extensions.Microsoft.DependencyInjection。它可能會請求安裝相關軟體包的許可(Microsoft.Extensions.DependencyInjection.Abstractions)。

配置MediatR

在ConfigureServices方法中将以下代碼添加到Startup.cs類中進行注冊MediatR:

services.AddMediatR(typeof(Startup));
           

如果您将所有處理程式類都放入ASP.NET Core MVC項目的同一程式集中(例如,“HR.App.Web”),則此配置效果很好。如果您在同一項目解決方案中使用不同的程式集(例如HR.App.DAL.CQRS),則必須轉義以上代碼,并需要添加以下代碼:

services.AddMediatR(typeof(GetImageQuery));
           

如果在同一項目解決方案中使用多個程式集(例如AssemblyA和AnotherAssemblyB),則需要添加所有類型的程式集:

services.AddMediatR(typeof(AssemblyAClassName), typeof(AnotherAssemblyBClassName));
           

将依賴項注入Web-API控制器/控制器

在HomeApiController.cs類中,我添加了“SubmitFile”和“GetImageId”操作,這些操作将使用MediatR發送指令和查詢對象。下面的代碼表明我已經在HomeApiController構造函數中注入了一個依賴Mediator對象。順便說一下,Web API控制器傳回Json/XML資料。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

控制器傳回視圖。在HomeController.cs中,我僅使用預設的Index操作傳回視圖。

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

如何發送指令/查詢請求

我們可以使用中介對象發送指令/查詢對象:mediator.Send(command/query Object);檢視下面的代碼:

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

完整的代碼如下:

using HR.App.DAL.CQRS.Command;
using HR.App.DAL.CQRS.Models;
using HR.App.DAL.CQRS.Query;
using HR.App.DAL.CQRS.ViewModel;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Threading.Tasks;

namespace HR.App.Web.Controllers
{
    [Route("api")]
    [ApiController]
    public class HomeApiController : Controller
    {
        private readonly IMediator mediator;

        public HomeApiController(IMediator mediator)
        {
            this.mediator = mediator;
        }

        [HttpPost("/HomeApi/SubmitFile")]
        public async Task<ActionResult> SubmitFile(IFormFile file)
        {
            try
            {
                #region Validation & BL
                if (file.Length == 0)
                {
                    return Json(new { status = "Error", message = "Image is not found!" });
                }

                if (!file.ContentType.Contains("image"))
                {
                    return Json
                     (new { status = "Error", message = "This is not an image file!" });
                }

                string fileName = file.FileName;

                if (file.FileName.Length > 50)
                {
                    fileName = string.Format($"{file.FileName.Substring(0, 45)}
                               {Path.GetExtension(file.FileName)}");
                }
                #endregion

                byte[] bytes = null;

                using (BinaryReader br = new BinaryReader(file.OpenReadStream()))
                {
                    bytes = br.ReadBytes((int)file.OpenReadStream().Length);
                }

                UploadedImage uploadedImage = new UploadedImage()
                {
                    ImageFileName= fileName,
                    FileContentType = file.ContentType,
                    ImageContent = bytes
                };

                SaveImageCommand saveImageCommand = new SaveImageCommand()
                {
                    UploadedImage = uploadedImage
                };

                ResponseResult responseResult = await mediator.Send(saveImageCommand);

                if (!responseResult.IsSuccess)
                {
                    return Json(new { status = "Error", 
                           message = string.Join("; ", responseResult.Errors) });
                }

                return Json(new { status = "OK", imageId= responseResult.ImageId });
            }
            catch (Exception ex)
            {
                return Json(new { status = "Error", message = ex.Message });
            }
        }

        [HttpGet("/HomeApi/GetImageById/{imageId:int}")]
        public async Task<ActionResult> GetImageById(int imageId)
        {
            try
            {
                ImageResponse imageResponse = await mediator.Send(new GetImageQuery()
                {
                    ImageId = imageId
                });

                UploadedImage uploadedImage = imageResponse.UploadedImage;

                if (!imageResponse.IsSuccess)
                {
                    return Json(new { status = "Error", 
                           message = string.Join("; ", imageResponse.Errors) });
                }
                if (uploadedImage.FileContentType == null || 
                            !uploadedImage.FileContentType.Contains("image"))
                {
                    return Json(new { status = "Error", 
                           message = string.Join("; ", imageResponse.Errors) });
                }

                string imgBase64Data = Convert.ToBase64String(uploadedImage.ImageContent);
                string imgDataURL = string.Format("data:{0};base64,{1}", 
                    uploadedImage.FileContentType, imgBase64Data);
                
                return Json(new { status = "OK", imgData = imgDataURL });
            }
            catch (Exception ex)
            {
                return Json(new { status = "Error", message = ex.Message });
            }
        }
    }
}
           

在上面的代碼中,“SubmitFile”動作将接收帶有IFormFile對象和圖像的HttpPost ,并将圖像轉換為位元組數組。最後,它使用中介器發送SaveImageCommand對象。

另一方面,GetImageById動作使用imageId接收HttpGet請求,并使用調解器發送查詢請求。最後,它處理從位元組數組到base64字元串的圖像内容,以将其發送到視圖。

無論如何,現在如果您運作項目,那麼您将看到以下螢幕來上傳和檢視圖像:

使用Vue.js和ASP.NET Core MVC實作CQRS模式介紹先決條件深入了解基本資訊應用解決方案結構圖像上傳和顯示應用MVC與JS架構之間的通信設計在SPA中,在表示層中添加UI和PLL用于資料讀取和寫入操作的資料通路層軟體包安裝讀取查詢處理程式的實作寫入指令處理程式将MediatR與API控制器/控制器內建配置MediatR将依賴項注入Web-API控制器/控制器如何發送指令/查詢請求

在很多情況下,我們需要讀寫操作才能完成一項任務。例如,我需要更新一個對象的幾個屬性。首先,我可以調用查詢操作來讀取對象,然後在放入所需的值之後,可以調用更新操作來存儲它。在這種情況下,切勿将讀取查詢和寫入指令混合到同一操作/處理程式中。然後将它們分開,以後将很容易修改。

繼續閱讀