Introduction
If you’re a software professional, then you’re familiar with the Software enhancement and maintenance work. This is the part of the software development life cycle, so you can correct the faults, and delete or enhance existing features. The software maintenance cost can be minimized if you use a software architectural pattern, choose the right technologies, and are aware of the industry trends for the future. It also helps to consider resource reliability/availability for now and the future, use a design pattern/principle in your code, re-use your code and keep your options open for future extensions, 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
- Configure 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
Drilldown the Basic Shorty
-
CQRS Pattern: In a 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 an impact on the code where Mediator is used when you need centralized control and communication between multiple classes/objects. For example, Facebook Messenger is a mediator to send messages to 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, and the Controller acts as a mediator between View & Model.
Application Solution Structure
The main goal of this project is to explain the CQRS architectural pattern. I’m planning to implement a tiny Single-Page-Application (SPA) project. The choice of technology is important, and you should choose it according to your requirements. For the User Interface (UI) and Presentation Logic Layer (PLL), I chose ASP.NET Core MVC and Vue.js (JavaScript framework). For the data access, I chose the Entity Framework (EF) Core Code First Approach. 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 “HR.App.DAL.CQRS” and another is the ASP.NET Core Web Application project which is named “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 update 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 a sample project, then recommended reading: "
Integrating/Configuring Vue.js in ASP.NET Core 3.1 MVC"
In SPA, Adding the UI and PLL in the Presentation Layer
In “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 reading 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">
- <!-- 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>
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 “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 code:
- 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; the ImageResponse type indicates the response. On the other hand, the GetImageQueryHandler class inherits the IRequestHandler 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 code:
- 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 permission to install the dependent package (Microsoft.Extensions.DependencyInjection.Abstractions).
Configuring MediatR
Add the following code 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) in 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 assembly 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 code indicates 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 codes below.
The whole 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 a HttpGet request with an imageId and sends a query request using a 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 the image:
There are many cases where we need to 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 the future.