目錄
介紹
如果您是軟件專業人員,那麼您將熟悉軟件增強和維護工作。這是軟件開發生命週期的一部分;這樣,您就可以糾正故障,刪除/增強現有功能。軟件維護成本可以最小化如果使用軟件體系結構模式,選擇合適的技術和了解行業趨勢的未來,考慮資源可靠性/可用性爲現在和未來,在代碼中使用設計模式/原則,重用代碼,保持打開你選擇未來的擴展,等等。無論如何,如果您在應用程序中使用任何已知的軟件架構模式,那麼其他人將很容易理解您的應用程序的結構/組件設計。我將使用ASP.Net Core MVC中的MediatR和Vue.js根據CQRS模式解釋示例項目實現。
先決條件
- 需要.NET Core 3.1的Visual Studio 2019
- 安裝Node.js,NPM和@Vue/Cli
- 需要WebPack和NPM任務運行器
深入瞭解基本信息
- 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應用程序項目。
MVC與JS框架之間的通信設計
在此階段,我要指出UI/PLL以及它們如何相互通信。看下面的圖。我將JS框架放在View和Web API Controller之間。
根據上圖,ASP.NET MVC控制器呈現視圖。JS將來自視圖的HTTP請求(GET
/PUT
/POST
/DELETE
)傳遞到Web API控制器,並將Web API控制器的響應數據(JSON/XML)更新到視圖。
注意:我正在猜測,如何在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”方法用於寫入請求。界面看起來像:
用於數據讀取和寫入操作的數據訪問層
我猜,您知道EF核心代碼優先方法,並且您具有域模型和上下文類。您可以使用其他方法。在這裏,我將實現讀取和寫入操作以進行數據訪問。查看下圖以瞭解應用程序的整個過程。
軟件包安裝
在“HR.App.DAL.CQRS”項目中,我已經使用NuGet包管理器安裝了MediatR.Extensions.Microsoft.DependencyInjection,Microsoft.EntityFrameworkCore和Microsoft.EntityFrameworkCore.SqlServer。
我需要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數據。
控制器返回視圖。在HomeController.cs中,我僅使用默認的Index操作返回視圖。
如何發送命令/查詢請求
我們可以使用中介對象發送命令/查詢對象:mediator.Send(command/query Object);查看下面的代碼:
完整的代碼如下:
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字符串的圖像內容,以將其發送到視圖。
無論如何,現在如果您運行項目,那麼您將看到以下屏幕來上傳和查看圖像:
在很多情況下,我們需要讀寫操作才能完成一項任務。例如,我需要更新一個對象的幾個屬性。首先,我可以調用查詢操作來讀取對象,然後在放入所需的值之後,可以調用更新操作來存儲它。在這種情況下,切勿將讀取查詢和寫入命令混合到同一操作/處理程序中。然後將它們分開,以後將很容易修改。