In this artice, you will see a sample project implementation according to the CQRS pattern using MediatR in ASP.NET Core MVC with vue.js.
Introduction
If you’re a software professional, then you’re familiar with the Software enhancement and maintenance work. This is the part of software development life cycle; so that, you can correct the faults, delete/ enhance the existing features. The software maintenance cost can be minimized if you use software architectural pattern, choosing right technologies and be aware of the industry trends for the future, consider resource reliability/availability for now and future, use design pattern/principle in your code, re-use your code and keep open your option for future extension, etc. Anyway, if you use any known software architectural pattern in your application, then it will be easy for others to understand the structure/component design of your application. I’ll explain a sample project implementation according to the CQRS pattern using MediatR in ASP.NET Core MVC with vue.js.
Coverage Topics
- Implementation of CQRS Pattern
- Configuring MediatR Package for ASP.NET Core
- Data Read/Write Handler Implantation using MediatR
- Integrating MediatR with API Controller/Controller
- Passing Command/Query Object using MediatR
- Converting image to byte array/byte array to base64 string
Prerequisites
Drilldown the Basic Shorty
- CQRS Pattern: In short, Command – Query Separation Responsibility (CQRS) pattern separates the READ query operation which returns the data without changing the database/system and WRITE command (insert/update/delete) operation which changes the data into the database/system. NEVER mix the read and write operation together.
- Mediator Pattern: This is a design pattern that has impact on the code where Mediator is used when you need centralize control and communication between multiple classes/objects. For example, Facebook Messenger is a mediator to send messages to the multiple users.
- MVC Pattern: This is an architectural pattern for the application where Model, View and Controller are separated by their responsibility. Model is the data of an object; View presents data to the user and handles user interaction; Controller acts like a mediator between View & Model.
Application Solution Structure
The main goal of this project is to explain the CQRS architectural pattern. I’m plaining to implement a tiny Single-Page-Application (SPA) project. The choice of the technology is important, and you should choose it according to your requirements. For the User Interface (UI) and Presentation Logic Layer (PLL), I choose ASP.NET Core MVC and Vue.js (JavaScript framework). For the data access, I choose Entity Framework (EF) Core Code First Approach and it will be implemented into the Data-Access-Layer (DAL). Intentionally, I’m avoiding a separate Business Logic Layer (BLL) and other layers to minimize the length of this article.
Image Upload & Display Application
In this project, considering the CQRS pattern, at first, I will upload the image file to save it into the database; it will explain the write command operation. Secondly, I will read the data from the database to display the image; it will explain the read query operation.
I’ve added two separate projects in the same solution. One is the ClassLibrary
(.NET Core) project which is named as “HR.App.DAL.CQRS
” and another is the ASP.NET Core Web Application project which is named as “HR.App.Web
”.
Communication Design Between MVC & JS Framework
In this stage, I’m pointing the UI/PLL and how they will talk to each other. Look at the diagram below. I’m placing the JS framework in between the View and Web API Controller.
According to the above diagram, ASP.NET MVC Controller renders the View. JS passes the HTTP request (GET
/PUT
/POST
/DELETE
) from the view to the Web API Controller as well as it updates the response data (JSON/XML) from Web API Controller to the View.
Note: I’m guessing, you know, how to configure the Vue.js in ASP.NET Core MVC project. If you need the step by step instructions to configure the Vue.js in the ASP.NET Core with sample project, then recommended to read: Integrating/Configuring Vue.js in ASP.NET Core 3.1 MVC
In SPA, Adding the UI and PLL in the Presentation Layer
In the “HR.App.Web
” project, add the Index.cshtml view and Index.cshtml.js file. I add the following HTML scripts for the Image upload and Image view tag/control into the Index.cshtml. These are associated with the read and write actions.
@{
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">
<!--
<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>
Add the following Vue.js script for the HTTP GET
and POST
request into the Index.cshtml.js file:
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/*",
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 = "";
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));
}
}
});
});
In this JS file, the “viewImageById
” method is used for the read request and “onUploaded
” method is used for the write request. The screen looks like:
Data Access Layer for Data READ & WRITE Operations
I’m guessing, you know the EF Core Code First Approach and you have the domain models and context class. You may use a different approach. Here, I’ll implement the Read and Write operations for data access. Look at the diagram below to understand the whole process of the application.
Packages Installation
In the “HR.App.DAL.CQRS
” project, I’ve installed MediatR.Extensions.Microsoft.DependencyInjection
, Microsoft.EntityFrameworkCore
and Microsoft.EntityFrameworkCore.SqlServer
“ using NuGet Package Manager.
I need the MediatR to implement the Command and Query handlers. I’ll use the MediatR.Extensions
for ASP.NET Core to resolve the dependency.
Read Query Handler Implementation
To get an image from the database, I’ve added the GetImageQuery.cs class with the following codes:
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;
}
}
}
The GetImageQuery
class inherits IRequest<ImageResponse>
; the ImageResponse
type indicates the response. On the other hand, the GetImageQueryHandler
class inherits the IRequestHandler<GetImageQuery, ImageResponse>
where the GetImageQuery
type indicates the request/message and the ImageResponse
type indicates the response/output. This GetImageQueryHandler
class implements the Handle
method which returns the ImageResponse
object.
WRITE Command Handler
To save an image into the database, I’ve added the SaveImageCommand.cs class which contains the following codes:
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;
}
}
}
}
Integrating MediatR with API Controller/Controller
In the “HR.App.Web
” project, I’ve installed MediatR.Extensions.Microsoft.DependencyInjection
, using NuGet Package Manager. It may ask for the permission to install the dependent package (Microsoft.Extensions.DependencyInjection.Abstractions
).
Configuring MediatR
Add the following codes in the ConfigureServices
method into the Startup.cs class to register MediatR
:
services.AddMediatR(typeof(Startup));
This configuration works well, if you have all the handler classes into the same assembly of the ASP.NET Core MVC project (say, “HR.App.Web
”). If you use a different assembly (say, HR.App.DAL.CQRS
) into the same project solution, then you have to escape the above code and need to add the following code:
services.AddMediatR(typeof(GetImageQuery));
If you use multiple assemblies (say, AssemblyA
and AnotherAssemblyB
) in the same project solution, then you need to add all the types of the assemblies:
services.AddMediatR(typeof(AssemblyAClassName), typeof(AnotherAssemblyBClassName));
Injecting Dependency into Web-API Controller/Controller
In the HomeApiController.cs class, I’ve added “SubmitFile
” and “GetImageId
” actions and these actions will send the command and query objects using MediatR
. The below codes indicate that I’ve injected a dependency Mediator object in the HomeApiController
constructor. By the way, the web API controller returns the Json/XML data.
The Controller returns the view. In the HomeController.cs, I just use the default Index action to return the view.
How to Send Command/Query Request
We can send the command/query object using the mediator object: mediator.Send(command/query Object);
Look at the code below:
The entire code is given below:
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 });
}
}
}
}
In the above codes into the “SubmitFile”
action receives the HttpPost with IFormFile
object with an image and it Converts image to byte array. Finally, it sends the SaveImageCommand
object using mediator.
On the other hand, the GetImageById
action receives HttpGet
request with an imageId
and send a query request using mediator. Finally, it processes the image content from byte array to base64 string to send it to the view.
Anyway, now if you run the project, then you will see the following screen to upload and view image:
There’re many cases where we need read and write operations to complete a single task. For example, say, I need to update few properties of an object. First, I can call a query operation to read the object and then, after putting the required values, I can call an update Operation to store it. In this case, NEVER mix-up the read query and write command into the same operation/handler. Keep them separate then, it will be easy to modify in future.
History
- 16th March, 2020: Initial version