OpusSolution.Tasken.Core
1.1.41
dotnet add package OpusSolution.Tasken.Core --version 1.1.41
NuGet\Install-Package OpusSolution.Tasken.Core -Version 1.1.41
<PackageReference Include="OpusSolution.Tasken.Core" Version="1.1.41" />
<PackageVersion Include="OpusSolution.Tasken.Core" Version="1.1.41" />
<PackageReference Include="OpusSolution.Tasken.Core" />
paket add OpusSolution.Tasken.Core --version 1.1.41
#r "nuget: OpusSolution.Tasken.Core, 1.1.41"
#:package OpusSolution.Tasken.Core@1.1.41
#addin nuget:?package=OpusSolution.Tasken.Core&version=1.1.41
#tool nuget:?package=OpusSolution.Tasken.Core&version=1.1.41
Tasken Platform
Repository này chứa các thành phần nền tảng chính của hệ thống Tasken trên .NET 8.
Hiện tại solution được tổ chức quanh hai project cốt lõi:
OPUS.Tasken.Gateway: public API host, gateway, reverse proxy, swagger, health checks, metricsOPUS.Tasken.Core: shared business platform package dùng chung cho các host và module services
Phiên bản package hiện tại của OPUS.Tasken.Core là 1.1.41.
Navigation
Phần 1. Hệ Thống Và Package
Tổng quan
Mục tiêu của repository này:
- chuẩn hóa business platform dùng chung cho hệ sinh thái Tasken
- cung cấp một public API host thống nhất cho external traffic
- hỗ trợ mô hình module service đứng sau gateway
- giữ dependency direction rõ ràng giữa host layer và core business layer
Kiến trúc hiện tại
Kiến trúc hiện tại theo mô hình:
OPUS.Tasken.Gatewaylà public entry pointOPUS.Tasken.Corelà shared business package- các module nghiệp vụ như
Tasken.Accountantcó thể chạy độc lập phía sau gateway
flowchart LR
Client["Client"] --> Api["OPUS.Tasken.Gateway"]
subgraph Gateway [OPUS.Tasken.Gateway]
STS["STS /auth/exchange"]
Proxy["YARP Reverse Proxy"]
end
STS --> Core["OPUS.Tasken.Core"]
Proxy --> Module["Tasken.Accountant / Modules"]
Module --> Core
Tài liệu chi tiết hơn có tại docs/01-System-Architecture.md.
Các project chính
OPUS.Tasken.Gateway
Vai trò:
- ASP.NET Core public host
- gateway và reverse proxy qua YARP
- common API endpoints
- Swagger/OpenAPI exposure
- health checks và Prometheus metrics
Tài liệu riêng: OPUS.Tasken.Gateway/README.md
OPUS.Tasken.Core
Vai trò:
- entities
- repositories
- services
- persistence
- shared infrastructure primitives
- email abstractions và implementations
- authentication, request tracking, logging support
Đây là package dùng lại giữa các host và module services.
OPUS.UnitTest
Chứa unit tests cho repository layer và các thành phần nền cần kiểm thử tự động.
Cấu trúc repository
Tasken.Core/
README.md
OPUS.Tasken.sln
compose.yaml
docs/
OPUS.Tasken.Gateway/
OPUS.Tasken.Core/
OPUS.UnitTest/
OPUS.Tasken.Core package
Metadata chính của package:
PackageId:OpusSolution.Tasken.CoreRoot Namespace:OPUS.Tasken.CoreVersion:1.1.41TargetFramework:net8.0Company:Opus Solution Company
Project file: OPUS.Tasken.Core.csproj
Capabilities
OPUS.Tasken.Core hiện cung cấp:
- shared entities
- EF Core persistence (với
IEntityTypeConfigurationchuẩn SRP) - repository abstractions và implementations
- batch-read repository APIs để lấy đúng tập bản ghi theo danh sách khóa ngay tại database, tránh tải toàn bộ dữ liệu rồi lọc trong RAM
- inventory/accounting repositories mới cho
InventoryBalance,InventoryBatch,InventorySerial,AccountingLedger,AccountingCostLayer,AccountingStockLedger,AccountingIssueCostAllocation, vàAccountingDebtLedger; bảngInventorycũ đã bị loại bỏ - service registration extensions
- email abstractions, options, validators, và implementations
- shared constants và helper infrastructure
- enterprise authentication: Azure AD, ADFS, Local JWT, Token Exchange, Stateless Refresh Token,
TaskenAuthorizeAttributehỗ trợ Constructor Injection - logging và request tracking: cấu hình logging tập trung, correlation ID, user context enrichment, client IP tracking, latency monitoring
ITaskenUnitOfWorkabstraction để transaction boundary nằm ở application layer
Install From NuGet
dotnet add package OpusSolution.Tasken.Core --version 1.1.41
dotnet restore
Install From GitHub Packages
Thêm source:
dotnet nuget add source "https://nuget.pkg.github.com/Opus-Solution/index.json" \
--name "github-opus-tasken" \
--username "<github-username>" \
--password "<github-token>" \
--store-password-in-clear-text
Cài package:
dotnet add package OpusSolution.Tasken.Core --version 1.1.41 --source "github-opus-tasken" --source "https://api.nuget.org/v3/index.json"
dotnet restore
Build And Test
Build entire solution
dotnet build OPUS.Tasken.sln
Run tests
dotnet test OPUS.Tasken.sln
Build individual projects
dotnet build OPUS.Tasken.Gateway/OPUS.Tasken.Gateway.csproj
dotnet build OPUS.Tasken.Core/OPUS.Tasken.Core.csproj
Pack And Publish
Pack OPUS.Tasken.Core
dotnet pack OPUS.Tasken.Core/OPUS.Tasken.Core.csproj -c Release
Package output dự kiến:
OPUS.Tasken.Core/bin/Release/OpusSolution.Tasken.Core.1.1.41.nupkgOPUS.Tasken.Core/bin/Release/OpusSolution.Tasken.Core.1.1.41.snupkg
Nếu GeneratePackageOnBuild đang bật trong project file, package cũng có thể được sinh trong quá trình build phù hợp.
Sử dụng OPUS.Tasken.Core trong project khác
Ví dụ đăng ký platform capability theo facade:
using OPUS.Tasken.Core.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseLogging();
builder.Services.AddTaskenCorePlatform(builder.Configuration, options =>
{
options.UsePersistence = true;
options.UseRepositories = true;
options.UseServices = true;
options.UseAuthentication = true;
options.UseEmail = true;
});
var app = builder.Build();
app.UseCorePlatformMiddleware(options =>
{
options.UseRequestTracking = true;
});
app.UseCorePlatformSecurity(options =>
{
options.UseAuthentication = true;
options.UseAuthorization = true;
});
Nếu cần granular registration:
using OPUS.Tasken.Core.Persistence.Extensions;
using OPUS.Tasken.Core.Repositories.Extensions;
using OPUS.Tasken.Core.Services.Extensions;
builder.Services.AddTaskenSqlServerPersistence(builder.Configuration);
builder.Services.AddTaskenRepositories();
builder.Services.AddTaskenServices();
builder.Services.AddTaskenEmail(builder.Configuration);
builder.Services.AddTaskenAuthentication(builder.Configuration);
Inventory And Accounting Data Model Update
Các luồng tồn kho hiện không còn dùng bảng Inventory. Thay vào đó, dữ liệu được tách theo trách nhiệm:
InventoryBalance: số dư tồn tức thời theo sản phẩm, kho, lô và serial, gồm số lượng/khối lượng khả dụng và phần đã giữ chỗ.InventoryBatch: quản lý lô hàng, ngày sản xuất, hạn sử dụng và trạng thái lô.InventorySerial: quản lý serial chi tiết theo sản phẩm/lô/kho, trạng thái và thông tin bảo hành.AccountingStockLedger: phát sinh sổ kho theo chứng từ, sản phẩm, kho, số lượng, khối lượng và giá vốn.AccountingCostLayer: lớp giá vốn tồn kho phục vụ FIFO, moving average hoặc specific cost.AccountingIssueCostAllocation: phân bổ giá vốn cho các dòng xuất kho, liên kết dòng xuất với cost layer được tiêu thụ.AccountingLedger: bút toán kế toán tài chính, debit/credit, đối tượng, thuế, audit và liên kết tùy chọn tới sổ kho.AccountingDebtLedger: sổ công nợ theo tenant, khách hàng và chứng từ nguồn; hỗ trợ theo dõi số tiền mở, hạn thanh toán, phân bổ thanh toán và soft-delete.
Các entity inventory/accounting sử dụng Medo.Uuid7 cho khóa và tham chiếu lô/serial/cost layer. EF Core được cấu hình converter toàn cục trong TaskenDbContext.ConfigureConventions để map Uuid7 và Uuid7? sang Guid/SQL Server UNIQUEIDENTIFIER, giúp các cột như AccountingCostLayer.BatchId, SerialId, StockLedgerId, ParentLayerId và các khóa Uuid7 khác được provider SQL Server hỗ trợ trực tiếp.
Repository inventory chi tiết xem tại docs/05.4-Repositories-Inventory.html.
InventoryBatchRepository hỗ trợ IncreaseAsync(...) để tăng tồn theo tenantId, productId, warehouseId, batchId, serialId, quantity và weight; repository sẽ tạo mới hoặc cập nhật InventoryBalance tương ứng và đồng bộ thông tin lô/serial liên quan.
Batch Read Repository APIs
Các repository chính đã hỗ trợ batch-read để service/module lấy đúng tập bản ghi cần dùng bằng một query database:
IFundsRepository.GetByAddressesAsync(...)choFund, vì khóa chính làAddress.GetByIdsAsync(...)choRequest,RequestDetail,RequestComment,Module,ModuleCategory,ApplicationUser,Attachment,Department,PaymentAllocation,InventoryBatch,InventorySerial,InventoryBalance, vàAccountingDebtLedger.
Ví dụ:
var funds = await _fundsRepository.GetByAddressesAsync(
tenantId,
fundAddresses,
isDeleted: false,
includes: FundInclude.Full,
cancellationToken: cancellationToken);
var requests = await _requestRepository.GetByIdsAsync(
tenantId,
requestIds,
includes: RequestInclude.ListDefault,
cancellationToken: cancellationToken);
Batch-read APIs loại bỏ khóa trùng, trả danh sách rỗng khi input không có khóa hợp lệ, dùng AsNoTracking() và áp dụng tenant/soft-delete/include theo contract của từng repository. Không dùng GetAll...Async() rồi lọc bằng Where(...Contains(...)) trong service khi repository đã có batch API tương ứng.
Hướng dẫn chi tiết xem tại docs/05.2-Repositories-Usage.md và docs/05.3-Repositories-Conventions.md.
Ví dụ dùng email services:
using OPUS.Tasken.Core.Email.Abstractions;
using OPUS.Tasken.Core.Email.Models;
public sealed class NotificationService
{
private readonly IEmailService _emailService;
public NotificationService(IEmailService emailService)
{
_emailService = emailService;
}
public async Task SendAsync()
{
await _emailService.SendAsync(new EmailMessage
{
Subject = "Hello from OPUS.Tasken.Core",
Body = "<b>Notification</b>",
IsHtml = true,
To = new List<string> { "user@company.com" }
});
}
}
Phần 2. Module Structure Standard
Phần này gộp nội dung chuẩn từ docs/02-Module-Structure-Standard.md để README có thể đóng vai trò tài liệu entry point cho team tạo module mới.
Purpose
Tài liệu này định nghĩa cấu trúc chuẩn cho các module service mới trong hệ sinh thái Tasken.
Baseline được rút ra từ cách tổ chức hiện tại của Tasken.Accountant tại C:\opus\Tasken.Accountant, sau đó chuẩn hóa lại thành rule dùng chung cho các module về sau như:
Tasken.HrTasken.ProcurementTasken.Sales- các module domain khác
Mục tiêu:
- thống nhất cấu trúc folder giữa các module
- giảm thời gian onboarding
- giữ boundary rõ giữa API, application logic, feature logic, và infrastructure wiring
- đảm bảo mọi module có thể dùng lại
OPUS.Tasken.Coretheo cùng một cách
Scope
Chuẩn này áp dụng cho các project:
- ASP.NET Core Web API
- module service chạy độc lập
- module service có thể đứng sau
OPUS.Tasken.Gatewaygateway
Chuẩn này không áp dụng cho:
OPUS.Tasken.Gatewaygateway hostOPUS.Tasken.Coreshared platform package- test project
Design Principles
1. Thin composition root
Program.cs chỉ làm các việc:
- đăng ký ASP.NET Core services
- nạp
OPUS.Tasken.Core - nạp DI của module
- cấu hình Swagger và middleware pipeline
Không để business logic trong Program.cs.
2. Feature-first at API boundary
Mọi HTTP capability nên được nhóm theo feature tại Features/<FeatureName>, thay vì dồn tất cả controller hoặc DTO vào một thư mục phẳng.
3. Shared stays small
Shared chỉ chứa thành phần thực sự dùng chung trong toàn module:
- API base class
- route constants
- response envelopes
- pagination mapping helpers
Không đẩy DTO đặc thù feature vào Shared.
4. Application coordinates use cases
Application là nơi điều phối use case ở mức module:
- orchestration
- mapping giữa API contract và platform models
- gọi repository/service từ
OPUS.Tasken.Core
Controller không gọi repository trực tiếp.
5. Infrastructure wires dependencies
Infrastructure là nơi đăng ký dependency injection, tích hợp adapter cục bộ của module, và giữ cho host startup sạch.
Standard Folder Structure
1. Visual Architecture Flow
flowchart TD
subgraph Module [Module Project: Tasken.ModuleName]
direction TB
F["Features<br/>(Controllers, Feature DTOs, Mappers)"]
A["Application<br/>(Orchestration Services, App Mappers)"]
I["Infrastructure<br/>(DI Setup, Local Adapters)"]
S["Shared<br/>(Constants, Base APIs, Responses)"]
end
Core["OPUS.Tasken.Core<br/>(Entities, Repositories, DbContext, UnitOfWork)"]
F -->|1. Nhận HTTP Request & Gọi Service| A
A -->|2. Orchestrate via UnitOfWork| Core
A -->|3. Gọi Repository| Core
F -.->|Dùng chung| S
A -.->|Dùng chung| S
I -.->|4. Đăng ký DI lúc Startup| A
classDef boundary fill:transparent,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5;
class Module boundary;
2. Directory Tree
Tasken.<ModuleName>/
Application/
Constants/
Contracts/
Mappers/
Services/
Interfaces/
Features/
Health/
Controllers/
<FeatureName>/
Contracts/
Controllers/
Mappers/
Infrastructure/
DependencyInjection/
Shared/
Api/
Constants/
Contracts/
Responses/
Mappers/
docs/
scripts/
Properties/
appsettings.json
Program.cs
README.md
Tasken.<ModuleName>.csproj
Tasken.<ModuleName>.sln
Folder Responsibilities
Application/
Chứa module-level application logic.
Subfolders chuẩn:
Constants/: constants nghiệp vụ ở mức applicationContracts/: DTO hoặc model dùng chung cho nhiều feature trong moduleMappers/: mapper giữa application DTO vàOPUS.Tasken.CoremodelsServices/: application servicesServices/Interfaces/: contracts cho application services
Được phép phụ thuộc vào:
OPUS.Tasken.CoreSharedFeatures/<FeatureName>/Contractskhi cần input/output đặc thù
Không nên chứa:
- HTTP concerns
- route attributes
- middleware setup
Features/
Chứa từng capability theo vertical slice.
Mỗi feature tối thiểu nên có:
Contracts/Controllers/Mappers/
Ví dụ:
Features/
PurchaseManagement/
Contracts/
Controllers/
Mappers/
Trách nhiệm:
- gom API surface của feature về một chỗ
- giảm việc tìm file rải rác
- giữ boundary rõ cho từng use case domain
Infrastructure/
Tối thiểu nên có:
DependencyInjection/ServiceCollectionExtensions.cs
Trách nhiệm:
- đăng ký application services của module
- đăng ký local adapters nếu module có thêm integration riêng
Ví dụ pattern:
public static class ServiceCollectionExtensions
{
public static IServiceCollection Add<ModuleName>Module(this IServiceCollection services)
{
services.AddScoped<IModuleService, ModuleService>();
return services;
}
}
Shared/
Chỉ chứa concern dùng chung toàn module.
Subfolders chuẩn:
Api/Constants/Contracts/Responses/Mappers/
Ví dụ thành phần hợp lệ:
BaseApiControllerRouteConstantsApiResponse<T>BaseResponse<T>- pagination mapper
docs/
Chứa tài liệu kiến trúc và tài liệu vận hành riêng cho module.
Tối thiểu nên có:
- overview kiến trúc module
- route summary
- dependency notes nếu module có integration đặc biệt
scripts/
Chứa script hỗ trợ local development hoặc benchmark.
Ví dụ:
- benchmark script
- local smoke test script
Program.cs Standard
Mọi module nên giữ Program.cs theo cùng pattern:
builder.Host.UseLogging()configurationAddControllers()- Swagger/OpenAPI registration
OPUS.Tasken.Coreregistration qua facade hoặc qua từng capability nhỏAddTaskenAuthentication()registration nếu không dùng facade- module DI registration
- build app
- development-only Swagger middleware
app.UseCorePlatformMiddleware(...)app.UseCorePlatformSecurity(...)- transport middleware cần thiết
MapControllers()
Pattern tham chiếu:
using OPUS.Tasken.Core.Extensions;
using Tasken.<ModuleName>.Infrastructure.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseLogging();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTaskenCorePlatform(builder.Configuration, options =>
{
options.UsePersistence = true;
options.UseRepositories = true;
options.UseServices = true;
options.UseAuthentication = true;
options.UseEmail = true;
});
builder.Services.Add<ModuleName>Module();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCorePlatformMiddleware(options =>
{
options.UseRequestTracking = true;
options.UseHttpsRedirection = true;
});
app.UseCorePlatformSecurity(options =>
{
options.UseAuthentication = true;
options.UseAuthorization = true;
});
app.MapControllers();
app.Run();
Nếu module cần chọn capability chi tiết hơn, vẫn được phép đăng ký theo từng nhóm riêng:
builder.Services.AddTaskenSqlServerPersistence(builder.Configuration);
builder.Services.AddTaskenRepositories();
builder.Services.AddTaskenServices();
builder.Services.AddTaskenEmail(builder.Configuration);
builder.Services.AddTaskenAuthentication(builder.Configuration);
Rule:
- dùng
AddTaskenCorePlatform(...)khi module đi theo platform mặc định - dùng granular registrations khi module chỉ cần một phần của
OPUS.Tasken.Core - không trộn facade và granular registrations cho cùng một capability trong cùng
Program.cs
Controller Standard
Controller phải:
- nằm trong
Features/<FeatureName>/Controllers - dùng route constants từ
Shared/Constants - dùng
[TaskenAuthorize]thay vì[Authorize]mặc định - chỉ xử lý transport concern
- gọi application service qua interface
- map response model bằng mapper
- trích xuất thông tin user qua
HttpContext.GetTaskenUser()nếu cần - nhận và truyền
CancellationToken
Ví dụ:
[ApiController]
[Route(RouteConstants.MyFeature)]
[TaskenAuthorize]
public class MyFeatureController : ControllerBase
{
private readonly IMyFeatureService _service;
private readonly ICurrentUserAccessor _currentUserAccessor;
public MyFeatureController(
IMyFeatureService service,
ICurrentUserAccessor currentUserAccessor)
{
_service = service;
_currentUserAccessor = currentUserAccessor;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id, CancellationToken cancellationToken)
{
var currentUser = _currentUserAccessor.GetCurrentUser();
if (currentUser == null) return Unauthorized();
// ASP.NET Core tự động inject cancellationToken của request hiện tại
var result = await _service.GetByIdAsync(id, cancellationToken);
if (result == null) return NotFound();
return Ok(result);
}
}
GetCurrentUser() trong ví dụ trên không tự xác thực token. Nó chỉ hoạt động đúng khi request đã đi qua UseAuthentication() và route đang được bảo vệ bởi [TaskenAuthorize] hoặc cơ chế authorize tương đương.
Controller không nên:
- gọi trực tiếp repository từ
OPUS.Tasken.Core - chứa business rules lớn
- serialize/deserialize dữ liệu nghiệp vụ phức tạp trong controller
Service Standard
Application service là nơi đặt:
- query orchestration
- command orchestration
- validation ở mức use case nếu không phải validation transport đơn giản
- interaction với
OPUS.Tasken.Corerepositories/services
Naming chuẩn:
I<ModuleName>Service<ModuleName>Service
Nếu module lớn dần, tách tiếp theo use case:
I<FeatureName>QueryServiceI<FeatureName>CommandService
Không để một service phình thành “god service” khi module mở rộng.
Transaction Management
Đối với các use case yêu cầu tính nguyên tố (atomicity) liên quan đến nhiều repository hoặc đơn giản là ghi dữ liệu chuẩn:
Cách tiếp cận khuyến nghị: Inject
ITaskenUnitOfWorkvào Application Service.Lý do sử dụng Unit of Work (Rationale):
- Tính nguyên tử (Atomicity): Đảm bảo mọi thay đổi trong một use case đều thành công hoặc thất bại cùng nhau.
- Tách biệt trách nhiệm (Separation of Concerns): Repository chỉ lo việc truy vấn và quản lý trạng thái entity trong bộ nhớ (
DbSet). Application Service quyết định khi nào các thay đổi đó thực sự được ghi xuống Database. - Hiệu năng (Performance): Gom nhiều thay đổi và gọi
SaveChangesAsyncmột lần giúp giảm số lượng round-trip tới SQL Server. - Tường minh (Explicitness): Code review dễ dàng hơn vì điểm commit dữ liệu hiển thị rõ ràng ở tầng Service thay vì bị ẩn sâu trong các phương thức của Repository.
Luồng xử lý chuẩn cho use case có transaction:
- Application Service gọi
_unitOfWork.ExecuteInTransactionAsync(...). - Bên trong callback, Service gọi các Repository cần thiết.
ITaskenUnitOfWorktự mở transaction, chạy trong EF Core execution strategy,SaveChangesAsync, rồi commit.- Nếu lỗi xảy ra, transaction sẽ rollback và toàn bộ use case được fail đúng boundary.
- Application Service gọi
Ví dụ:
public async Task UpdateRequestStatusAsync(int id, string status, CancellationToken cancellationToken = default)
{
await _unitOfWork.ExecuteInTransactionAsync(async ct =>
{
var request = await _requestRepository.GetByIdAsync(id, ct);
request.Status = status;
await _requestRepository.UpdateAsync(request, ct);
await _requestProgressRepository.AddAsync(new Progress(...), ct);
await _notificationRepository.AddAsync(new Notification(...), ct);
}, cancellationToken);
}
Execution Strategy và Retry-safe Transaction
Khi dùng SQL Server với EF Core retry strategy, không được tự mở transaction thủ công bằng BeginTransactionAsync(...) ở Application Service rồi chạy lệnh bên ngoài ExecutionStrategy.
Nếu làm như vậy, runtime có thể ném lỗi:
SqlServerRetryingExecutionStrategy does not support user-initiated transactions
Nguyên nhân:
- EF Core retry strategy cần quyền retry lại toàn bộ transaction unit nếu gặp lỗi transient.
- Nếu service tự mở transaction ở ngoài execution strategy, EF Core không thể đảm bảo retry boundary đúng cách.
Quy tắc kiến trúc bắt buộc:
- Application Service không inject
DbContextchỉ để gọiCreateExecutionStrategy(). DbContextphải ở lại tầng infrastructure / core persistence.- Mọi transaction retry-safe phải đi qua
ITaskenUnitOfWork.ExecuteInTransactionAsync(...).
Anti-pattern:
// Không expose API này cho application service.
// Đây là ví dụ về pattern cần tránh.
await _repositoryA.AddAsync(entityA, cancellationToken);
await _repositoryB.UpdateAsync(entityB, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
Pattern chuẩn:
await _unitOfWork.ExecuteInTransactionAsync(async ct =>
{
await _repositoryA.AddAsync(entityA, ct);
await _repositoryB.UpdateAsync(entityB, ct);
}, cancellationToken);
Khi nào dùng SaveChangesAsync trực tiếp
Không phải mọi use case ghi dữ liệu đều bắt buộc phải gọi ExecuteInTransactionAsync(...).
Với các use case đơn giản, chỉ cần:
- Thao tác lên repository.
- Gọi
_unitOfWork.SaveChangesAsync(cancellationToken).
Ví dụ:
public async Task UpdateNoteAsync(int id, string note, CancellationToken cancellationToken = default)
{
var request = await _requestRepository.GetByIdAsync(id, cancellationToken);
request.Note = note;
await _requestRepository.UpdateAsync(request, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
Nên dùng ExecuteInTransactionAsync(...) khi:
- use case thay đổi nhiều entity / nhiều repository
- cần commit như một business unit duy nhất
- có nguy cơ phát sinh lỗi giữa chừng và không được để dữ liệu dở dang
- logic đó về bản chất là một transaction boundary hoàn chỉnh
Trách nhiệm của ITaskenUnitOfWork
ITaskenUnitOfWork chuẩn nên cung cấp ít nhất:
SaveChangesAsync(...)ExecuteInTransactionAsync(...)ExecuteInTransactionAsync<T>(...)
Trong đó:
BeginTransactionAsync(...)không nên là API public của application layer abstraction.- Application Service chỉ nên thấy
SaveChangesAsync(...)vàExecuteInTransactionAsync(...). ExecuteInTransactionAsync(...)là nơi core chịu trách nhiệm:- tạo EF Core execution strategy
- mở transaction
- chạy callback
- gọi
SaveChangesAsync - commit / rollback
Điều này giữ cho service code sạch, không phải biết tới DbContext, và không lặp transaction boilerplate ở từng module.
Repository Persistence Pattern
Hệ thống tuân thủ mô hình Unit of Work, trong đó:
- Loại bỏ
saveChangesflag: Các phương thức command (AddAsync,UpdateAsync,DeleteAsync, ...) không bao giờ nhận tham số boolean để tự động lưu. - CancellationToken: Mọi phương thức bất đồng bộ phải nhận và truyền
CancellationToken. - Lý do sử dụng CancellationToken: Giúp quản lý tài nguyên hệ thống tốt hơn. Khi một request bị hủy, server có thể ngừng xử lý các lệnh DB đang chạy, giải phóng thread và kết nối database ngay lập tức.
- Trách nhiệm persistence: Repository chỉ chịu trách nhiệm thay đổi trạng thái entity trong
DbContext. Việc commit dữ liệu là trách nhiệm duy nhất của Application Service thông quaITaskenUnitOfWork.
Trường hợp chỉ dùng 1 Repository
Ngay cả khi use case chỉ tác động lên 1 repository duy nhất, Application Service vẫn phải gọi _unitOfWork.SaveChangesAsync(cancellationToken) một cách tường minh sau khi thao tác xong trên repository.
Lý do:
- Tính nhất quán: Toàn bộ hệ thống đi theo một pattern duy nhất, giúp code dễ đọc và dễ bảo trì.
- Khả năng mở rộng: Nếu sau này use case cần thêm các thao tác khác, bạn đã có sẵn cấu trúc để đảm bảo tính nguyên tử mà không cần refactor lại Repository.
Ngoại lệ: Tiến trình không được phép hủy (Critical Background Tasks)
Trong trường hợp một tiến trình nghiệp vụ cực kỳ quan trọng và bắt buộc phải hoàn tất ngay cả khi người dùng tắt trình duyệt hoặc hủy request, bạn không nên truyền cancellationToken từ Controller vào các hàm persistence cuối cùng.
Cách xử lý:
- Dùng
CancellationToken.None: Sử dụngCancellationToken.Nonethay cho token từ request khi gọiSaveChangesAsync. - Background Service: Đối với các tác vụ tốn thời gian, hãy đẩy công việc vào một
IBackgroundTaskQueuehoặc Background Service để xử lý độc lập với vòng đời của HTTP Request.
Ví dụ:
await _unitOfWork.SaveChangesAsync(CancellationToken.None);
Quy tắc cho module mới:
- Không mở transaction ở controller.
- Không để repository tự commit (
SaveChangesAsync) bên trong các phương thức command. - Mọi use case ghi dữ liệu, dù đơn giản hay phức tạp, transaction boundary phải nằm ở application service.
- Không inject
DbContextvào Application Service chỉ để lấy execution strategy hoặc điều phối transaction. - Với transaction có retry support, chỉ dùng
ExecuteInTransactionAsync(...)ở application layer. - Bắt buộc truyền
cancellationTokenxuyên suốt từ Controller → Service → Repository → EF Core.
Async Safety Rules với DbContext và ITaskenUnitOfWork
Do TaskenDbContext và ITaskenUnitOfWork đang được đăng ký theo scoped lifetime, mọi use case ghi dữ liệu phải tuân thủ các quy tắc sau:
- Mọi lời gọi async dùng repository,
DbContexthoặcITaskenUnitOfWorkđều phải đượcawaitđầy đủ. - Không được fire-and-forget các hàm async đang dùng scoped
DbContext. - Không được gọi một hàm async thứ hai mà không
awaitnếu hàm đó còn dùng chungDbContextvới flow hiện tại. - Không chạy song song nhiều thao tác EF Core trên cùng một
DbContextbằngTask.WhenAll(...),Parallel.ForEachAsync(...)hoặc pattern tương tự. - Bên trong
_unitOfWork.ExecuteInTransactionAsync(...), mọi thao tác phải hoàn tất và đượcawaitxong trước khi callback kết thúc. - Nếu cần background processing thật sự, phải tách sang background worker hoặc queue riêng và tạo scope mới cho
DbContext.
Rủi ro nếu vi phạm các rule trên:
DbContextbị dispose trước khi task chạy xong- transaction commit hoặc rollback trước khi tác vụ chưa-
awaithoàn tất - lỗi
A second operation was started on this context instance before a previous operation completed - lỗi commit thiếu dữ liệu, mất exception hoặc fail ngoài transaction boundary
Anti-pattern cần tránh:
await _service.Step1Async(cancellationToken);
_service.Step2Async(cancellationToken); // Khong await
await _unitOfWork.SaveChangesAsync(cancellationToken);
var task1 = _repositoryA.AddAsync(entityA, cancellationToken);
var task2 = _repositoryB.UpdateAsync(entityB, cancellationToken);
await Task.WhenAll(task1, task2); // Cung dung chung DbContext
Pattern chuẩn:
await _unitOfWork.ExecuteInTransactionAsync(async ct =>
{
await _service.Step1Async(ct);
await _service.Step2Async(ct);
await _service.Step3Async(ct);
}, cancellationToken);
Contract Standard
Shared contracts
Chỉ đặt trong Shared/Contracts nếu:
- được nhiều feature dùng chung
- là response envelope hoặc primitive dùng toàn module
Feature contracts
Đặt trong Features/<FeatureName>/Contracts nếu:
- chỉ phục vụ một feature
- là request/response model của endpoint feature đó
Application contracts
Đặt trong Application/Contracts nếu:
- dùng cho orchestration application layer
- là shape trung gian giữa feature DTO và
OPUS.Tasken.Coremodel
Mapping Standard
Tách mapper thành 3 mức rõ ràng:
- Feature mapper
- map request/response HTTP model của feature
- Application mapper
- map giữa application DTO và
OPUS.Tasken.Coreentity/model
- map giữa application DTO và
- Shared mapper
- map concern dùng chung như pagination response
Không để một mapper làm cả ba vai trò nếu có thể tách rõ.
Route Standard
Mọi module phải có Shared/Constants/RouteConstants.cs.
Route constants nên theo pattern:
public static class RouteConstants
{
public const string Route<ModuleName> = "<module-route-prefix>";
public const string <FeatureName> = "<feature-route>";
}
Ví dụ:
accountantpurchase-management
Điều này giữ route declaration nhất quán và tránh hardcode string rải rác trong controller.
Authentication Standard
Mọi module nghiệp vụ phải tuân thủ cơ chế xác thực tập trung của Tasken:
- Xác thực: Sử dụng
TaskenAuthorizeAttributeđể bảo vệ các endpoint. - Thông tin người dùng: Ưu tiên inject
ICurrentUserAccessorđể lấyTaskenUserInfo. Ở controller, có thể dùngHttpContext.GetTaskenUser()như một shortcut. - Cấu hình (Quan trọng): Dù các module chạy trên cùng một domain, mỗi Module Project riêng biệt bắt buộc phải có block
AuthSettingstrong fileappsettings.jsongiống hệt với Gateway. - Phân quyền: Mọi API nghiệp vụ nên được bảo vệ mặc định. Nếu một module muốn mở endpoint cho anonymous, phải sử dụng
[AllowAnonymous]một cách tường minh. - Policy mặc định: Endpoint dùng
[Authorize]mà không ghi rõ scheme sẽ đi theoDefaultPolicy. Với cấu hình chuẩn hiện tại,DefaultPolicydùngAzureScheme. - Fail-fast config validation:
AddTaskenAuthentication()sử dụngIValidateOptions<AuthSettings>vàValidateOnStart(). Nếu cấu hình thiếu hoặc sai cho scheme đang bật, module phải fail ngay từ startup. - Không hard-code text auth: Tên scheme, policy, và claim type phải dùng từ
AuthConstantscủaOPUS.Tasken.Core. - Fast user info: Flow chuẩn hiện tại chỉ đảm bảo lấy nhanh được
UserId,TenantId,UserName,Email. Trong đóUserNamevàEmailcó thểnull. Nếu cần profile đầy đủ hơn thì phải query thêm từ database hoặc profile service.
Ví dụ cấu hình:
"AuthSettings": {
"UseLocalTokenResponse": false,
"EnableAzureAd": true,
"EnableAdfs": true,
"EnableLocalJwt": true,
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "8dc6957b-4869-4877-a511-6563f990d59e",
"ClientId": "997db321-4c77-465c-a9a4-493b2c542b2d",
"Audience": "997db321-4c77-465c-a9a4-493b2c542b2d"
},
"Adfs": {
"MetadataAddress": "https://sjc.tasken.click/adfs/.well-known/openid-configuration",
"Audiences": "your-app-audience-id"
},
"Jwt": {
"SecretKey": "{SECRET_KEY_GIONG_GATEWAY}",
"Issuer": "TaskenCore",
"Audience": "TaskenApp",
"ExpiryMinutes": 1440
}
}
Rule cho AuthSettings:
- phải bật ít nhất một trong
EnableAzureAd,EnableAdfs,EnableLocalJwt UseLocalTokenResponse = trueyêu cầuEnableLocalJwt = true- nếu
EnableAzureAd = truethìAzureAd.Instance,TenantId,ClientIdlà bắt buộc - nếu
EnableAdfs = truethìAdfs.MetadataAddress,Audienceslà bắt buộc - nếu
EnableLocalJwt = truethìJwt.SecretKey,Issuer,Audience,ExpiryMinuteslà bắt buộc Jwt.SecretKeynên dài tối thiểu 32 ký tự
Ý nghĩa của UseLocalTokenResponse:
false: endpoint/common/auth/exchangesẽ trả lại chính bearer token hiện tại sau khi xác thực thành côngtrue: endpoint/common/auth/exchangesẽ sinh và trả về cặpTaskenlocal JWT (AccessToken+RefreshToken)- khi bật
UseLocalTokenResponse, các module phía sau phải có cùngJwt.SecretKey,Issuer,Audiencevới Gateway để validate được token nội bộ do Gateway phát hành
Quy ước policy:
[Authorize]: dùngDefaultPolicy, mặc định làAzureSchemenếu Azure đang bật[Authorize(Policy = AuthConstants.Policies.AnyEnabledScheme)]: chấp nhận mọi scheme đang bật[Authorize(Policy = AuthConstants.Policies.AzureOnly)]: chỉ chấp nhận Azure AD[Authorize(Policy = AuthConstants.Policies.LocalJwtOnly)]: chỉ chấp nhận Tasken local JWT[Authorize(Policy = AuthConstants.Policies.AdfsOnly)]: chỉ chấp nhận ADFS[Authorize(Policy = AuthConstants.Policies.InternalUsersOnly)]: yêu cầu user đã được normalize thành claimTaskenUserID
Chi tiết xem tại tài liệu: docs/08-Authentication-Architecture.md
Logging & Request Tracking
Mọi module nghiệp vụ nên kích hoạt hệ thống logging và middleware theo dõi request:
- Cấu hình Log Sink: Sử dụng
builder.Host.UseLogging()để cấu hình Serilog ghi log ra Console và File. - Cấu hình appsettings.json: Có thể tùy chỉnh đường dẫn file log qua khóa
Logging:FilePath. - Middleware: Sử dụng
app.UseCorePlatformMiddleware(...)để bật request tracking và các middleware nền dùng chung.
Tính năng cung cấp:
- Correlation ID
- LogContext enrichment với
UserId,TenantId,CorrelationId,ClientIp - latency monitoring
Lưu ý:
ClientIpưu tiên lấy từRemoteIpAddress- nếu
RemoteIpAddresskhông có, middleware sẽ fallback sangX-Forwarded-ForrồiX-Real-IP - nếu chạy sau reverse proxy hoặc load balancer, nên cấu hình
ForwardedHeaderstrướcapp.UseCorePlatformMiddleware(...)để IP phản ánh đúng client thật
Ví dụ:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseLogging();
// ... registration services
var app = builder.Build();
app.UseCorePlatformMiddleware(options =>
{
options.UseRequestTracking = true;
});
Database Configuration
Module sử dụng SQL Server làm database chính. Connection string đặt trong ConnectionStrings:
"ConnectionStrings": {
"ChivasDbContext": "Server={SERVER_IP};Initial Catalog={DB_NAME};User Id={USER};Password={PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=True;"
}
Lưu ý: tên DbContext mặc định là ChivasDbContext để tương thích với OPUS.Tasken.Core.
Email Configuration
Module hỗ trợ gửi mail qua Microsoft Graph hoặc SMTP. Cấu hình đặt trong section Email:
"Email": {
"Provider": "Graph",
"Graph": {
"TenantId": "{TENANT_ID}",
"ClientId": "{CLIENT_ID}",
"ClientSecret": "{CLIENT_SECRET}",
"UserId": "{SENDER_EMAIL_OR_ID}"
},
"Smtp": {
"Host": "smtp.office365.com",
"Port": 587,
"From": "noreply@company.com",
"Account": "noreply@company.com",
"Password": "{PASSWORD}",
"EnableSsl": true
}
}
Dependency Standard
Mọi module service chuẩn hiện tại được phép phụ thuộc trực tiếp vào:
OPUS.Tasken.Core- ASP.NET Core packages
- Swagger/OpenAPI packages
- các package integration thật sự cần cho module
Dependency direction bắt buộc:
Tasken.<Module>→OPUS.Tasken.Core- không có chiều ngược lại
Module không được đẩy logic đặc thù ngược vào OPUS.Tasken.Core nếu concern đó chưa thật sự generic.
Required Files For New Module
Khi tạo module mới, tối thiểu phải có:
Program.csTasken.<ModuleName>.csprojREADME.mdappsettings.jsonInfrastructure/DependencyInjection/ServiceCollectionExtensions.csShared/Constants/RouteConstants.csShared/Api/BaseApiController.cshoặc shared API base tương đươngFeatures/Health/Controllers/HealthController.cs- ít nhất một feature business đầu tiên theo chuẩn
Features/<FeatureName>/...
Recommended First Feature Skeleton
Features/
Health/
Controllers/
HealthController.cs
<FeatureName>/
Contracts/
<FeatureName>Dtos.cs
Controllers/
<FeatureName>Controller.cs
Mappers/
<FeatureName>Mapper.cs
Application/
Services/
Interfaces/
I<ModuleName>Service.cs
<ModuleName>Service.cs
Infrastructure/
DependencyInjection/
ServiceCollectionExtensions.cs
Shared/
Api/
BaseApiController.cs
Constants/
RouteConstants.cs
Contracts/
Responses/
ApiResponse.cs
BaseResponse.cs
Governance Rules
- Không tạo thư mục
Controllersở root project cho endpoint nghiệp vụ. - Controller luôn nằm dưới
Features/<FeatureName>/Controllers. - Không hardcode route string lặp lại trong controller nếu đã có
RouteConstants. - Không gọi repository trực tiếp từ controller.
Program.cskhông chứa business logic.Sharedphải được giữ nhỏ.- Mọi module mới phải có tài liệu kiến trúc riêng trong
docs/. - Unit of Work Standard: Repositories không được tự ý gọi
SaveChangesAsync(). Việc commit dữ liệu là trách nhiệm của Application Service thông quaITaskenUnitOfWork. - Async Pattern: Mọi phương thức bất đồng bộ (
async) bắt buộc phải nhận tham sốCancellationTokenđể đảm bảo khả năng hủy yêu cầu và quản lý tài nguyên hệ thống tối ưu.
Known Improvement Path
Chuẩn hiện tại được rút ra từ Tasken.Accountant, nên vẫn phản ánh một số trade-off của baseline đó:
BaseApiControllercòn nằm cục bộ theo module- application service có thể còn rộng trách nhiệm
- một số mapper có thể vẫn mang nhiều concern
Vì vậy, module mới nên xem cấu trúc này là baseline bắt buộc, nhưng vẫn ưu tiên:
- tách command/query service khi module lớn lên
- đưa API shared concern lên shared package khi nền tảng sẵn sàng
- giảm kích thước service orchestration quá lớn
Final Position
Từ thời điểm này, các module service mới trong hệ sinh thái Tasken nên được tạo theo cấu trúc của Tasken.Accountant đã chuẩn hóa trong tài liệu này, thay vì tự chọn layout riêng.
Nếu một module cần lệch chuẩn, lý do lệch chuẩn phải được ghi lại trong tài liệu kiến trúc của chính module đó.
Additional Documentation
Latest Changes
v1.1.41
- mở rộng
IRepository<T>/Repository<T>với query contract hiệu năng dùng chung cho toàn bộ specific repository - hỗ trợ batch-read khóa đơn/khóa ghép, read-only single, tracked single, filtered list, pagination, projection và exists
- các query chung sử dụng EF primary-key metadata,
AsNoTracking()cho read và predicate database-side để service áp tenant/soft-delete - bổ sung entity, SQL script và
IAccountingDebtLedgerRepository/AccountingDebtLedgerRepositorycho sổ công nợ kế toán - hỗ trợ query công nợ theo tenant, customer, chứng từ nguồn, trạng thái, ngày ghi sổ, hạn thanh toán, số tiền mở và overdue
- bổ sung
GetByIdsAsync(...),AddRangeAsync(...),UpdateRangeAsync(...),SoftDeleteAsync(...)vàSoftDeleteRangeAsync(...)choAccountingDebtLedger - hỗ trợ
AccountingDebtLedgerInclude.DetailDefaultđể eager-loadPaymentAllocations - các command repository công nợ chỉ thay đổi EF tracking state; application service vẫn phải commit qua
ITaskenUnitOfWork - bổ sung
IInventoryBatchRepository.IncreaseAsync(...)và implementation trongInventoryBatchRepository - hỗ trợ tăng tồn kho theo
tenantId,productId,warehouseId,batchId,serialId,quantityvàweight - bổ sung batch-read APIs cho nhóm repository request, fund, module, user, attachment, department, payment allocation và inventory
- batch-read lọc danh sách khóa tại database, bảo vệ tenant/soft-delete và tránh tải toàn bộ dữ liệu vào RAM
- tự tạo hoặc cập nhật
InventoryBalancetương ứng, đồng thời đồng bộBatchNo,LotNo,SerialNokhi có batch/serial - bổ sung unit tests cho tạo mới và tăng tồn kho hiện có theo batch/serial
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Azure.Identity (>= 1.12.0)
- Medo.Uuid7 (>= 3.2.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.0)
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (>= 8.0.0)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.EntityFrameworkCore.SqlServer (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore (>= 8.0.0)
- Microsoft.Extensions.Http (>= 8.0.0)
- Serilog.AspNetCore (>= 10.0.0)
- Serilog.Sinks.File (>= 8.0.0-nblumhardt-02322)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.41 | 46 | 6/4/2026 |
| 1.1.40 | 49 | 6/4/2026 |
| 1.1.39 | 89 | 6/2/2026 |
| 1.1.38 | 81 | 6/2/2026 |
| 1.1.37 | 81 | 6/2/2026 |
| 1.1.36 | 88 | 6/2/2026 |
| 1.1.35 | 92 | 6/1/2026 |
| 1.1.34 | 88 | 6/1/2026 |
| 1.1.33 | 95 | 5/27/2026 |
| 1.1.32 | 90 | 5/27/2026 |
| 1.1.30 | 96 | 5/27/2026 |
| 1.1.29 | 112 | 5/27/2026 |
| 1.1.28 | 89 | 5/25/2026 |
| 1.1.27 | 96 | 5/25/2026 |
| 1.1.26 | 88 | 5/21/2026 |
| 1.1.25 | 88 | 5/21/2026 |
| 1.1.24 | 96 | 5/21/2026 |
| 1.1.22 | 103 | 5/15/2026 |
| 1.1.21 | 96 | 5/14/2026 |