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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OpusSolution.Tasken.Core" Version="1.1.41" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OpusSolution.Tasken.Core" Version="1.1.41" />
                    
Directory.Packages.props
<PackageReference Include="OpusSolution.Tasken.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OpusSolution.Tasken.Core --version 1.1.41
                    
#r "nuget: OpusSolution.Tasken.Core, 1.1.41"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OpusSolution.Tasken.Core@1.1.41
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OpusSolution.Tasken.Core&version=1.1.41
                    
Install as a Cake Addin
#tool nuget:?package=OpusSolution.Tasken.Core&version=1.1.41
                    
Install as a Cake Tool

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, metrics
  • OPUS.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.Core1.1.41.

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.Gateway là public entry point
  • OPUS.Tasken.Core là shared business package
  • các module nghiệp vụ như Tasken.Accountant có 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.Core
  • Root Namespace: OPUS.Tasken.Core
  • Version: 1.1.41
  • TargetFramework: net8.0
  • Company: 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 IEntityTypeConfiguration chuẩ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ảng Inventory cũ đã 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, TaskenAuthorizeAttribute hỗ 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
  • ITaskenUnitOfWork abstraction để 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.nupkg
  • OPUS.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 Uuid7Uuid7? 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, quantityweight; 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(...) cho Fund, vì khóa chính là Address.
  • GetByIdsAsync(...) cho Request, 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.mddocs/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.Hr
  • Tasken.Procurement
  • Tasken.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.Core theo 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.Gateway gateway

Chuẩn này không áp dụng cho:

  • OPUS.Tasken.Gateway gateway host
  • OPUS.Tasken.Core shared 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 application
  • Contracts/: DTO hoặc model dùng chung cho nhiều feature trong module
  • Mappers/: mapper giữa application DTO và OPUS.Tasken.Core models
  • Services/: application services
  • Services/Interfaces/: contracts cho application services

Được phép phụ thuộc vào:

  • OPUS.Tasken.Core
  • Shared
  • Features/<FeatureName>/Contracts khi 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ệ:

  • BaseApiController
  • RouteConstants
  • ApiResponse<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:

  1. builder.Host.UseLogging() configuration
  2. AddControllers()
  3. Swagger/OpenAPI registration
  4. OPUS.Tasken.Core registration qua facade hoặc qua từng capability nhỏ
  5. AddTaskenAuthentication() registration nếu không dùng facade
  6. module DI registration
  7. build app
  8. development-only Swagger middleware
  9. app.UseCorePlatformMiddleware(...)
  10. app.UseCorePlatformSecurity(...)
  11. transport middleware cần thiết
  12. 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.Core repositories/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>QueryService
  • I<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 ITaskenUnitOfWork vào Application Service.

  • Lý do sử dụng Unit of Work (Rationale):

    1. 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.
    2. 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.
    3. Hiệu năng (Performance): Gom nhiều thay đổi và gọi SaveChangesAsync một lần giúp giảm số lượng round-trip tới SQL Server.
    4. 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:

    1. Application Service gọi _unitOfWork.ExecuteInTransactionAsync(...).
    2. Bên trong callback, Service gọi các Repository cần thiết.
    3. ITaskenUnitOfWork tự mở transaction, chạy trong EF Core execution strategy, SaveChangesAsync, rồi commit.
    4. Nếu lỗi xảy ra, transaction sẽ rollback và toàn bộ use case được fail đúng boundary.

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 DbContext chỉ để gọi CreateExecutionStrategy().
  • DbContext phả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:

  1. Thao tác lên repository.
  2. 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(...)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ỏ saveChanges flag: 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 qua ITaskenUnitOfWork.
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:

  1. 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ì.
  2. 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ý:

  1. Dùng CancellationToken.None: Sử dụng CancellationToken.None thay cho token từ request khi gọi SaveChangesAsync.
  2. 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 IBackgroundTaskQueue hoặ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 DbContext và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 cancellationToken xuyên suốt từ Controller → Service → Repository → EF Core.
Async Safety Rules với DbContextITaskenUnitOfWork

Do TaskenDbContextITaskenUnitOfWork đ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, DbContext hoặc ITaskenUnitOfWork đều phải được await đầ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 await nếu hàm đó còn dùng chung DbContext vớ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 DbContext bằng Task.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à được await xong 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:

  • DbContext bị dispose trước khi task chạy xong
  • transaction commit hoặc rollback trước khi tác vụ chưa-await hoà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.Core model

Mapping Standard

Tách mapper thành 3 mức rõ ràng:

  1. Feature mapper
    • map request/response HTTP model của feature
  2. Application mapper
    • map giữa application DTO và OPUS.Tasken.Core entity/model
  3. 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ụ:

  • accountant
  • purchase-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:

  1. Xác thực: Sử dụng TaskenAuthorizeAttribute để bảo vệ các endpoint.
  2. Thông tin người dùng: Ưu tiên inject ICurrentUserAccessor để lấy TaskenUserInfo. Ở controller, có thể dùng HttpContext.GetTaskenUser() như một shortcut.
  3. 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 AuthSettings trong file appsettings.json giống hệt với Gateway.
  4. 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.
  5. Policy mặc định: Endpoint dùng [Authorize] mà không ghi rõ scheme sẽ đi theo DefaultPolicy. Với cấu hình chuẩn hiện tại, DefaultPolicy dùng AzureScheme.
  6. Fail-fast config validation: AddTaskenAuthentication() sử dụng IValidateOptions<AuthSettings>ValidateOnStart(). Nếu cấu hình thiếu hoặc sai cho scheme đang bật, module phải fail ngay từ startup.
  7. Không hard-code text auth: Tên scheme, policy, và claim type phải dùng từ AuthConstants của OPUS.Tasken.Core.
  8. Fast user info: Flow chuẩn hiện tại chỉ đảm bảo lấy nhanh được UserId, TenantId, UserName, Email. Trong đó UserNameEmail có 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 = true yêu cầu EnableLocalJwt = true
  • nếu EnableAzureAd = true thì AzureAd.Instance, TenantId, ClientId là bắt buộc
  • nếu EnableAdfs = true thì Adfs.MetadataAddress, Audiences là bắt buộc
  • nếu EnableLocalJwt = true thì Jwt.SecretKey, Issuer, Audience, ExpiryMinutes là bắt buộc
  • Jwt.SecretKey nên dài tối thiểu 32 ký tự

Ý nghĩa của UseLocalTokenResponse:

  • false: endpoint /common/auth/exchange sẽ trả lại chính bearer token hiện tại sau khi xác thực thành công
  • true: endpoint /common/auth/exchange sẽ sinh và trả về cặp Tasken local JWT (AccessToken + RefreshToken)
  • khi bật UseLocalTokenResponse, các module phía sau phải có cùng Jwt.SecretKey, Issuer, Audience với Gateway để validate được token nội bộ do Gateway phát hành

Quy ước policy:

  • [Authorize]: dùng DefaultPolicy, mặc định là AzureScheme nế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 claim TaskenUserID

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:

  1. Cấu hình Log Sink: Sử dụng builder.Host.UseLogging() để cấu hình Serilog ghi log ra Console và File.
  2. Cấu hình appsettings.json: Có thể tùy chỉnh đường dẫn file log qua khóa Logging:FilePath.
  3. 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 RemoteIpAddress không có, middleware sẽ fallback sang X-Forwarded-For rồi X-Real-IP
  • nếu chạy sau reverse proxy hoặc load balancer, nên cấu hình ForwardedHeaders trước app.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.cs
  • Tasken.<ModuleName>.csproj
  • README.md
  • appsettings.json
  • Infrastructure/DependencyInjection/ServiceCollectionExtensions.cs
  • Shared/Constants/RouteConstants.cs
  • Shared/Api/BaseApiController.cs hoặc shared API base tương đương
  • Features/Health/Controllers/HealthController.cs
  • ít nhất một feature business đầu tiên theo chuẩn Features/<FeatureName>/...
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

  1. Không tạo thư mục Controllers ở root project cho endpoint nghiệp vụ.
  2. Controller luôn nằm dưới Features/<FeatureName>/Controllers.
  3. Không hardcode route string lặp lại trong controller nếu đã có RouteConstants.
  4. Không gọi repository trực tiếp từ controller.
  5. Program.cs không chứa business logic.
  6. Shared phải được giữ nhỏ.
  7. Mọi module mới phải có tài liệu kiến trúc riêng trong docs/.
  8. 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 qua ITaskenUnitOfWork.
  9. 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 đó:

  • BaseApiController cò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 / AccountingDebtLedgerRepository cho 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(...)SoftDeleteRangeAsync(...) cho AccountingDebtLedger
  • hỗ trợ AccountingDebtLedgerInclude.DetailDefault để eager-load PaymentAllocations
  • 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 trong InventoryBatchRepository
  • hỗ trợ tăng tồn kho theo tenantId, productId, warehouseId, batchId, serialId, quantityweight
  • 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 InventoryBalance tương ứng, đồng thời đồng bộ BatchNo, LotNo, SerialNo khi 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed