# 1. Cấu trúc Project
> Tham khảo:
> - Nguyên lý [SOLID](https://toidicodedao.com/2015/03/24/solid-la-gi-ap-dung-cac-nguyen-ly-solid-de-tro-thanh-lap-trinh-vien-code-cung/)
> - Thiết kế [Domain Driven Design](https://www.microservicesvn.com/docs/arch/ddd.html)
## 1.1. Monolithic
Sơ đồ tổ chức của một Project thông thường:

Cấu trúc tham khảo:
### 1.1.1. Domain
```
- Company.[Project].Domain
- [Module]
- [Entities]
- [Module].cs
- ...
- [Repositories]
- I[Module]Repo
```
### 1.1.2. Domain.Shared
```
- Company.[Project].Domain.Shared
- [Module]
- [Module]Type
- ...
```
### 1.1.3. Infrastructure
```
- Company.[Project].[Infra/EntityFrameworkCore]
- [Module]
- [Child-Module]
- [Repositories]
- [Module]Repo.cs
- ...
- [Extensions]
- [Module]Extension.cs
- ...
- [Helpers]
- [Module]Helper.cs
- Extensions
- Helpers
```
### 1.1.4. Application
```
- Company.[Project].[Application]
- [Module]
- [Child-Module]
- [Dtos]
- [Module]CreateDto.cs
- ...
- [Mappers]
- [Module]Profile.cs
- ...
- [Validators]
- [Module]CreateValidator.cs
- [Services]
- [IModule]Service.cs
- Impl
- [Module]Service.cs
```
### 1.1.5. WebApi
```
- Company.[Project].[WebApi]
- [Controllers]
- [Module]Controller.cs
- ...
- [Middlewares]
- [Extensions]
```
### Một vài quy tắc trong cách sử dụng:
> - Entity: Mapping class với table
> - Dto: Transfer dữ liệu từ Controller <-> Service, cần cấu hình Mapper để chuyển đổi qua lại với Entity
> - Repository: Cung cấp các giải pháp thao tác dữ liệu. Repository chỉ truyền và trả về Entity
> - Controller: Mapping URL với phương thức xử lý, gọi các Service liên quan
> - Service: Thực hiện các chức năng nghiệp vụ, call validation, call mapper DTO <-> Entity, Transaction. Service truyền và trả về DTO.
### Một vài lỗi sai về cách tổ chức trong hình sau:
> - Tổ chức không theo thứ tự: [Tên Module] -> [Tên Entity] -> [Dto, Mapper, Service...] -> [Class]
> - Sử dụng dấu "." cho tên file.
Sửa lại: Bỏ dấu "." và cho vào thư mục (nếu cần). Ví dụ: Document.CreateRequest.cs -> DocumentCreateDto.cs
> - Các class thực thi cùng cấp với interface.
Sửa lại: Cho vào thư mục Impl cùng cấp
### Ưu, nhược điểm của kiến trúc nguyên khối:
- Ưu:
- Đơn giản, dễ phát triển trong thời gian đầu
- Thích hợp các dự án nhỏ, team size nhỏ
- Dễ test (postman for API), deploy (copy single app to server)
- Nhược:
- Khi chức năng phần mềm trở nên khá nhiều, phức tạp, thì khó để một/vài người nắm hết tất cả nghiệp vụ.
- Khó khăn cho việc bảo trì, nâng cấp. Đặc biệt khi team size trở nên lớn, việc phân chia chức năng cho từng team trở nên phức tạp (bị conflit, thay đổi database, schema...)
- Dự án ngày càng lớn, việc build và run ứng dụng trở nên chậm hơn vì các module ngày càng phồng lên, gây trở ngại cho việc phát triển.
- Một lỗi trong Lock database của một Module có thể làm cho cả ứng dụng bị treo.
- Chọn công nghệ lúc đầu cần phải cân nhắc, vì khi đã phát triển không thể chọn lại, hoặc dự án phát triển quá lâu bị outdate. Nâng cấp phiên bản của Framework hiện tại cũng là vấn đề gây đau đầu.
## 1.2. Microservices
Sơ đồ cơ bản định hình cho Microservices:

### Ưu, nhược điểm của kiến trúc microservices
- Ưu:
- Cho phép [Continuous Delivery/Deployment](https://continuousdelivery.com/) trong các ứng dụng lớn, phức tạp: giảm chi phí, nhanh chóng đưa sản phẩm lên thị trường cũng như các bản vá lỗi, nhận phản hồi nhanh chóng từ người dùng.
- Testability: mỗi service được chia nhỏ, phát triển độc lập => Dễ dàng automation test cho từng service.
- Deployability: tương tự, từng service có thể deploy độc lập, mỗi team tương ứng có thể tự deploy service của team mình.
- Tăng tốc độ development vì mỗi team phát triển một chức năng riêng biệt.
- Service được chia nhỏ, dễ phát triển, nâng cấp chức năng.
- Service được phát triển và triển khai độc lập lên các server có cấu hình yêu cầu khác nhau. Một service chết không ảnh hưởng các service khác.
- Các service khác nhau có thể sử dụng các nền tảng khác nhau. Ví dụ: Ứng dụng cần một máy AI trực và trả lời các câu hỏi cơ bản về nghiệp vụ. Ta có thể tách sang AiService để phát triển bằng python...

- Nhược:
- Chia nhỏ ứng dụng thành các Service là một thách thức, đòi hỏi phân tích, thiết kế hệ thống kĩ càng.
- Hệ thống phân tán khá phức tạp, đòi hỏi nhiều kinh nghiệm trong việc phát triển, vận hành.
# 2. Quy tắc đặt tên
- Tổng quan: Có 3 kiểu đặt tên thông dụng nhất:
- PascalCase: Chữ cái đầu tiên trong từ định danh và chữ cái đầu tiên của mỗi từ nối theo sau phải được viết hoa. Sử dụng Pascal Case để đặt tên cho một tên có từ 3 ký tự trở lên. Sử dụng cho tên Class, File, Namespace, Method, và tên các public Member.
- camelCase: Chữ cái đầu tiên trong từ định danh là chữ thường và chữ cái đầu tiên của mối từ nối theo sau phải được viết hoa. Sử dụng cho tên các Member không phải public hoặc các Parameters, thêm kí tự _ cho các private Member.
- UPPER_CASE: Tất cả các ký tự trong từ định danh phải được viết hoa, nếu có nhiều từ thì ngăn cách bằng kí tự gạch dưới. Sử dụng quy tắc này đối với tên định danh có từ 2 ký tự trở xuống hoặc các hằng số const.
- Chi tiết
| Loại | Kiểu | Ví dụ |
| ----------- | ----------- | ----------- |
| Tên biến | camelCase | variable |
| Tên hằng | UPPER_CASE | USER_LIMIT |
| Tên namespace | PascalCase | Company.Dms.Domain.Entities |
| Tên file | PascalCase | UserService.cs |
| Tên class, enum | PascalCase | UserManagement |
| Tên tham số (Parameter) | camelCase | userCreateDto |
| Tên field/member private | [_]camelCase | _userService |
| Tên thuộc tính | PascalCase | UserName |
| Tên phương thức | PascalCase | GetCurrentUser() |
| Tên interface | [I]PascalCase | IUser |
> Chú ý: Tên file không có dấu "." để phân tách Category với chức năng (Ví dụ: Document.CreateDto.cs). Trường hợp tên quá dài, có thể dùng tiếng Việt viết tắt để đặt tên file. Ví dụ: LegalDocumentManagement[...]Service.cs có thể viết tắt thành VbplService.cs và chú thích rõ trong class.
{.is-warning}
```csharp=
///
/// Service cung cấp các thao tác thêm, sửa, xóa, ký, duyệt trên văn bản pháp luật
///
public class VbplService {
//...
}
```
# 3. Common
## 3.1. Sử dụng var cho các khai báo bên trong Method
- Bad
```csharp=
public ActionResult GetEmployeeInfo([FromQuery] int? EmployeeId, string Type) {
string SQL = "[HR].[sp_" + Type + "] @Activity = 'GetDatabyEmpId', @EmployeeId = " + EmployeeId.ToString();
DataTable data = _Execute.GetDataTable(SQL);
return Ok(new { success = true, result = data });
}
```
- Good
```csharp=
public ActionResult GetEmployeeInfo([FromQuery] int? employeeId, string type) {
var sql = "[HR].[sp_" + type + "] @Activity = 'GetDatabyEmpId', @EmployeeId = " + employeeId.ToString();
var data = _execute.GetDataTable(sql);
return Ok(new { success = true, result = data });
}
```
## 3.2. Sử dụng Using cho các Class có implement interface IDisposable
- Bad
```csharp=
public class FileService {
public override async Task<UserDetailDto> UploadAsync(FileCreateDto inputDto)
{
var fileStream = File.Create(inputDto.FileName);
// File processing...
fileStream.Close();
...
}
}
```
- Good
```csharp=
public class FileService {
public override async Task<UserDetailDto> UploadAsync(FileCreateDto inputDto)
{
using (var fileStream = File.Create(inputDto.FileName)) {
// File processing...
}
...
}
}
```
## 3.3. Sử dụng kiểu dữ liệu tường minh, DTO thay vì Dictionary, Dynamic, Object... trong trường hợp truyền Parameters
- Bad
```csharp=
public override async Task<UserDetailDto> UpdateAsync(object id, object input)
{
var user = JsonConvert.DeserializeObject<User>(input);
...
}
```
- Good
```csharp=
public class UserUpdateDto
{
public string UserName { get; set; }
}
```
```csharp=
public override async Task<UserDetailDto> UpdateAsync(string id, UserUpdateDto inputDto)
{
...
}
```
## 3.4. Luôn sử dụng Access Modifier (private, protected, public) cho các Member.
> Phần này sử dụng file .editorconfig của Project để check
{.is-info}
- Bad
```csharp=
// User
namespace idtek_authentication_user {
public class user {
public const int SuperId = 1;
int id;
public string userName { get; set; }
public boolean ignoreAuditSuperUser() {
...
var abc = "super";
...
UserUtil.IgnoreAudit(abc);
return true;
}
}
}
```
- Good
```csharp=
// UserConstants
namespace CompanyAuthenticationUser {
public class UserConstants {
public const int SUPER_ID = 1;
public const string SUPER_USERNAME = "super";
}
}
```
```csharp=
// User
namespace CompanyAuthenticationUser {
public class User {
private int _id;
public string UserName { get; set; }
public boolean IgnoreAuditSuperUser() {
...
UserUtil.IgnoreAudit(UserConstants.SUPER_USERNAME);
return true;
}
}
}
```
## 3.5. Comment trên Class, Method, mô tả ngắn gọn mục đích, cách sử dụng bằng tiếng Anh/ tiếng Việt. Xóa bỏ code thừa (vì muốn tìm lại code đã có Git)
- Bad
```csharp=
class CurrentUser {
public Claim FindClaim(string claimType)
{
// (_principalAccessor.FindAll(claimType) as Claim[]).First();
return _principalAccessor.FindFirst(claimType);
}
}
```
- Good
```csharp=
/// <summary>
/// Define current user for context
/// </summary>
class CurrentUser {
/// <summary>
/// Find first claim on principal by type
/// How to use: ...
/// </summary>
/// <param name="claimType">Claim type string</param>
/// <returns>Claim object {Id, Name}</returns>
public Claim FindClaim(string claimType)
{
return _principalAccessor.FindFirst(claimType);
}
}
```
## 3.6. Tránh việc Duplicate code, khi cần tái sử dụng lại Method nên bỏ vào Helper, tái sử dụng Service thì nên bỏ vào Middleware hoặc Action Filter
- Bad
```csharp=
// UserService
[HttpPost("create")]
public async Task<ApiResponse<UserDetailDto>> CreateAsync([FromForm] UserCreateDto inputDto, CancellationToken cancellationToken)
{
if (!await _authorizationService.IsAllow(new[] {UserPermission.Create})) {
throw new AccessDeniedException("Create user permission is required");
}
.... // Do something
}
```
- Good
```csharp=
// Program
builder.Services.AddScoped<AuthorizationFilter>();
// AuthorizationFilter
public class AuthorizationFilter : ActionFilterAttribute
{
private readonly string _right;
public AuthorizationFilter(string right)
{
_right = right;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// Check right here
}
}
// UserService
[HttpPost("create")]
[AuthorizationFilter(UserPermission.Create)]
public async Task<ApiResponse<UserDetailDto>> CreateAsync([FromForm] UserCreateDto inputDto, CancellationToken cancellationToken)
{
.... // Do something
}
```
## 3.7. Input Parameter chỉ đọc, không được đổi dữ liệu tham chiếu (Anti-Pattern). Khi cần thao tác, chỉnh sửa thì Clone Object.
- Bad
```csharp=
// UserService
[HttpPost("create")]
public async Task<ApiResponse<UserDetailDto>> ([FromForm] UserCreateDto inputDto)
{
// Wrong, inputDto is readonly
inputDto.CreatedAt = DateTime.Now;
.... // Do something
// Used on another funtion
ProcessUser(inputDto);
}
```
- Good
```csharp=
// UserService
[HttpPost("create")]
public async Task<ApiResponse<UserDetailDto>> ([FromForm] UserCreateDto inputDto)
{
// Create entity using Mapper
var entity = Mapper.Map<UserCreateDto, User>(inputDto);
entity.CreatedAt = DateTime.Now;
.... // Do something
}
```
## 3.8. Null Pointer Check cho các Input Parameter hoặc các Response
- Bad
```csharp=
public void ShowPerson(Person p)
{
if (p == null) return;
string firstName = p.FirstName;
//...
var client = new HttpClient();
var response = await client.GetAsync(uri, cancellationToken);
var resData = await response.Content.ReadFromJsonAsync<PersonResponseDto>(cancellationToken: cancellationToken);
var userName = resData.UserName;
}
```
- Good
```csharp=
public void ShowPerson(Person p)
{
string firstName = p?.FirstName ?? string.Empty;
//...
var client = new HttpClient();
var response = await client.GetAsync(uri, cancellationToken);
var resData = await response.Content.ReadFromJsonAsync<PersonResponseDto>(cancellationToken: cancellationToken);
var userName = resData?.UserName;
}
```
## 3.9. Sử dụng cancellation token cho các method query
- Ngoại trừ các action create/update/delete thì khuyến khích dùng cancellation token cho các async method query data như ToListAsync, FirstOrDefaultAsync, CountAsync,...
- Bad
```csharp=
public async Task<ICollection<Person>> GetListWibuPersons(){
return await _context.Set<Wibu>().Where(x=>x.IsWibu).ToListAsync();
}
```
- Good
```csharp=
public async Task<ICollection<Person>> GetListWibuPersons(CancellationToken cancelToken = default){
return await _context.Set<Wibu>().Where(x=>x.IsWibu).ToListAsync(cancelToken);
}
```
# 4. Logging
## 4.1. Sử dụng Logger để Tracking Log phục vụ cho Debug lỗi
- Bad
```csharp=
// UserService Method
try
{
... // Do something
}
catch(Exception)
{
Console.WriteLine("User login: " + user.GetInfo());
}
```
- Good
```csharp=
// UserService Member
private readonly ILogger<UserController> _logger;
// UserService Method
try
{
... // Do something
}
catch(Exception)
{
_logger.LogInformation("User login: ");
_logger.LogInformation(JsonConvert.SerializeObject(user.GetInfo()));
}
```
# 5. Validation
## 5.1. Validate InputDto là bắt buộc, validate trên Front-End chỉ mang tính chất tăng trải nghiệm UI/UX
- Bad
```csharp=
public override async Task<UserDetailDto> CreateAsync(UserCreateDto inputDto)
{
var entity = Mapper.Map<UserCreateDto, User>(inputDto);
await EntityRepository.AddAsync(entity);
await UnitOfWork.SaveChangesAsync();
...
}
```
- Good
```csharp=
public class ValidateUserCreateDto : AbstractValidator<UserCreateDto>
{
public ValidateUserCreateDto()
{
RuleFor(x => x.UserName).NotEmpty().NotNull();
...
}
}
```
```csharp=
public override async Task<UserDetailDto> CreateAsync(UserCreateDto inputDto)
{
if (!DeferValidator.Validate(inputDto, out var resValid)) {
throw new InvalidBusinessException(resValid);
}
var entity = Mapper.Map<UserCreateDto, User>(inputDto);
await EntityRepository.AddAsync(entity);
await UnitOfWork.SaveChangesAsync();
...
}
```
# 6. Context, LINQ, Transaction
## 6.1. Filter với Where trước khi lấy toàn bộ dữ liệu bằng ToList hoặc ToListAsync
- Bad
```csharp=
var entities = await _dbContext.Queryable().ToListAsync();
entities.Where(x => x.Code == 'abc_code').ToList()
```
- Good
```csharp=
var entities = await _dbContext.Queryable()
.Where(x => x.Code == 'abc_code')
.ToListAsync();
```
## 6.2. Count - Count() - Any()
- Đối với các ICollection<TEntity> thì sử dụng property **Count** thay vì Count()
- Sử dụng Any() thay vì Count() để check empty collection
- Bad
```csharp=
if(queryable.Count() > 0){ // sẽ đếm cả collection rồi mới so sánh
// do something if collection has item
}
```
- Good
```csharp=
if(queryable.Any()){ // -> trả về true nếu tồn tại ít nhất 1 item
// do something if collection has item
}
```
## 6.2. Sử dụng Transaction khi Write dữ liệu xuống database
- Bad
```csharp=
foreach(var item in items) {
_dbContext.Add(item);
await _dbContext.SaveChanges();
}
```
- Good
```csharp=
using(var transaction = await _unitOfWork.BeginTransactionAsync())
{
try {
foreach(var item in items) {
_dbContext.Add(item);
await _dbContext.SaveChanges();
_dbContext.Detach(item);
}
await transaction.CompleteAsync();
}
catch(Exception ex)
{
_logger.LogInformation($"Server error: {ex.Message}");
await transaction.RollbackAsync();
}
}
```
Prefer use FreeSql for Bulk Insert or Bulk Update
Compiled Command SQL:
```csharp=
var t2 = fsql.Insert(items).ExecuteAffrows();
//INSERT INTO `Topic`(`Clicks`, `Title`, `CreateTime`)
//VALUES(@Clicks0, @Title0, @CreateTime0), (@Clicks1, @Title1, @CreateTime1),
//(@Clicks2, @Title2, @CreateTime2), (@Clicks3, @Title3, @CreateTime3),
//(@Clicks4, @Title4, @CreateTime4), (@Clicks5, @Title5, @CreateTime5),
//(@Clicks6, @Title6, @CreateTime6), (@Clicks7, @Title7, @CreateTime7),
//(@Clicks8, @Title8, @CreateTime8), (@Clicks9, @Title9, @CreateTime9)
```
# 7. Handle Exception
## 7.1. Không sử dụng Exception chung chung mà phải định nghĩa rõ Exception loại gì, kèm theo Message tương ứng
- Bad
```csharp=
public override async Task<UserDetailDto> UpdateAsync(int id, UserUpdateDto inputDto)
{
var entity = UserRepository.FindById(id);
if (entity == null) {
throw new Exception($"Not found: (UserId: {id})");
}
...
}
```
- Good
```csharp=
// ErrorMesssage
public class ErrorMessage {
public const string E404 = "Not found :({0}: {1})";
}
```
```csharp=
// UserService
public override async Task<UserDetailDto> UpdateAsync(int id, UserUpdateDto inputDto)
{
var entity = UserRepository.FindById(id);
if (entity == null) {
throw new NotFoundException(string.Format(ErrorMessage.E404, "UserId", id));
}
...
}
```
> Chú ý: Tham khảo [Error Code and Message](/tech-docs/net-core/error-code-and-message) để chọn loại Exception cho phù hợp
## 7.2. Throw exception trong try catch
- Bad
```csharp=
[HttpGet("check")]
public async Task<ApiResponse<object>> GetHealthStatus()
{
try
{
throw new Exception("Test");
}
catch (Exception ex)
{
throw ex; // -> SẼ HIDE STACKTRACE, KHÔNG BIẾT EXCEPTION Ở DÒNG NÀO
}
return ApiResponse<object>.Ok();
}
```
- Good
```csharp=
[HttpGet("check")]
public async Task<ApiResponse<object>> GetHealthStatus()
{
try
{
throw new Exception("Test");
}
catch (Exception ex)
{
throw; // -> KHÔNG HIDE STACKTRACE, NẾU KHÔNG CÓ NHU CẦU THÌ BỎ LUÔN TRY CATCH
}
return ApiResponse<object>.Ok();
}
```
```csharp=
[HttpGet("check")]
public async Task<ApiResponse<object>> GetHealthStatus()
{
try
{
throw new Exception("Test");
}
catch (Exception ex)
{
_logger.LogError(ex.Message); // -> phải có logger ghi nhận lại lỗi để trace bug
_logger.LogError(ex.Stacktrace);
return ApiResponse<object>.Fail("Error !");
}
return ApiResponse<object>.Ok();
}
```