Table of contents
Open Table of contents
Mở đầu
Trong bài viết trước, chúng ta đã cùng nhau khám phá cách xây dựng cấu trúc thư mục cho dự án rút gọn link. Việc thiết lập một cấu trúc thư mục hợp lý là bước khởi đầu quan trọng để đảm bảo dự án của chúng ta có tính tổ chức và dễ dàng trong việc phát triển sau này.
Domain được xem là “linh hồn” của ứng dụng, nơi chứa đựng những quy tắc nghiệp vụ cốt lõi (business logic). Nó độc lập hoàn toàn với các yếu tố bên ngoài như database hay giao diện người dùng. Đây là nơi bạn định nghĩa các entities (đối tượng nghiệp vụ) và value objects (đối tượng giá trị), những thứ không thay đổi dù công nghệ bên ngoài có biến động thế nào.
Bài viết này, chúng ta sẽ đi sâu vào việc xây dựng Domain Project bằng phương pháp tiếp cận Domain Driven Design (DDD).
Giới Thiệu Về Domain Driven Design
Domain Driven Design được phát triển bởi Eric Evans trong cuốn sách cùng tên, với mục tiêu giúp các nhà phát triển tạo ra các hệ thống phức tạp một cách hiệu quả hơn. DDD tập trung vào việc xây dựng một mô hình miền (domain model) chính xác và phản ánh đúng yêu cầu của doanh nghiệp. Điều này bao gồm các khái niệm như Entity, Value Object, Aggregate, và Domain Event, giúp chúng ta quản lý sự phức tạp trong ứng dụng một cách có tổ chức hơn.
Với bài toán rút gọn link, mình tạm xác định các bounded context như sau
Auth Service: Quản lý xác thực và người dùng.
Link Management Service: Quản lý việc tạo, cập nhật và xóa link rút gọn.
Redirect Service: Xử lý việc chuyển hướng từ link rút gọn đến link gốc.
Histories Service: Lưu trữ và phân tích lịch sử truy cập link.
Chúng ta sẽ áp dụng một phần các nguyên lý của DDD để xây dựng ứng dụng, có thể không phải tất cả các service đều áp dụng.
Tạo các Domain Entity và Value Object của ứng dụng
Entity
- Là một đối tượng có danh tính duy nhất, thường được xác định bởi một khóa (Id).
- Có thể thay đổi theo thời gian
- Các thuộc tính của nó có thể thay đổi nhưng danh tính vẫn giữ nguyên.
Value Object
- Là một đối tượng không có danh tính duy nhất, được xác định bởi các thuộc tính của nó.
- Thường là không thay đổi (immutable). Khi giá trị thay đổi, một đối tượng mới sẽ được tạo ra.
Các Domain Entity chính của ứng dụng rút gọn link bao gồm:
- Link: đại diện cho một liên kết rút gọn
- User: đại diện cho người dùng tạo ra liên kết rút gọn, sẽ được đề cập khi xây dựng Auth Service.
- History: đại diện cho dữ liệu lịch sử truy cập của liên kết rút gọn, sẽ được đề cập khi xây dựng Analystic Service
Vì các Domain Entity đều có một Id định danh duy nhất nên chúng ta sẽ tạo một abstract class có một property là Id dùng chung cho tất cả các Domain Entity cũng như tương tác với các Domain Events.
Hệ thống rút gọn link không phải là một hệ thống quá phức tạp để áp dụng đầy đủ các nguyên lý của DDD nên ở đây mình sẽ bỏ qua việc xây dựng Aggregate.
Tạo Entity classs nằm trong thư mục Abstractions bên trong Ziply.Domain project.
1namespace Ziply.Domain.Abstractions;2public abstract class Entity3{4 public Guid Id { get; }5
6 protected Entity(Guid id)7 {8 Id = id;9 }10}
Tiếp đến chúng ta sẽ xây dựng Entity Link và các Value Object liên quan đến Entity này.
Tạo một class Link trong folder Links kế thừa từ Entity class.
1using Ziply.Domain.Abstractions;2
3namespace Ziply.Domain.Links;4public sealed class Link : Entity5{6 protected Link(Guid id) : base(id)7 {8 }9 /// <summary>10 /// Url thực tế11 /// </summary>12 public string OriginalUrl { get; set; }13 /// <summary>14 /// Url rút gọn15 /// </summary>16 public string ShortId { get; set; }17 /// <summary>18 /// Tiêu đề của link, dùng để hiển thị và tìm kiếm19 /// </summary>20 public string? Title { get; set; }21 /// <summary>22 /// Các meta data dùng cho việc hiển thị link trên các trang mạng xã hội23 /// </summary>24 public object? LinkMetaData { get; set; }25 /// <summary>26 /// Số lượt click vào link27 /// </summary>28 public int ClickCount { get; set; }29 /// <summary>30 /// Id Người tạo link31 /// </summary>32 public Guid UserId { get; set; }33 /// <summary>34 /// Thời gian chỉnh sửa35 /// </summary>36 public DateTime LastUpdate { get; set; } = DateTime.Now;37
38 public static Link Create(string originalUrl, string shortId, string? title, object? metaData, Guid userId)39 {40 return new Link(Guid.NewGuid())41 {42 OriginalUrl = originalUrl,43 ShortId = shortId,44 Title = title,45 LinkMetaData = metaData,46 UserId = userId,47 ClickCount = 048 };49 }50}
Thay thế các kiêu dữ liệu của Entity bằng các Value Object.
Sử dụng record type để tạo các Value Object
1namespace Ziply.Domain.Links;2
3public record ClickCount(int Value);
1namespace Ziply.Domain.Links;2
3public record LinkMetaData(string? SiteName = null, string? Title = null, string? Description = null, string? OgTitle = null, string? OgDescription = null, string? OgImage = null, string? OgType = null);
1namespace Ziply.Domain.Links;2public record OriginalUrl(string Value);
1namespace Ziply.Domain.Links;2
3public record ShortId(string Value);
1namespace Ziply.Domain.Links;2
3public record Title(string Value);
Sửa lại Link Entity
1using Ziply.Domain.Abstractions;2
3namespace Ziply.Domain.Links;4public sealed class Link : Entity5{6 protected Link(Guid id) : base(id)7 {8 }9 /// <summary>10 /// Url thực tế11 /// </summary>12 public OriginalUrl OriginalUrl { get; set; }13 /// <summary>14 /// Url rút gọn15 /// </summary>16 public ShortId ShortId { get; set; }17 /// <summary>18 /// Tiêu đề của link, dùng để hiển thị và tìm kiếm19 /// </summary>20 public Title? Title { get; set; }21 /// <summary>22 /// Các meta data dùng cho việc hiển thị link trên các trang mạng xã hội23 /// </summary>24 public LinkMetaData? LinkMetaData { get; set; }25 /// <summary>26 /// Số lượt click vào link27 /// </summary>28 public ClickCount ClickCount { get; set; }29 /// <summary>30 /// Id Người tạo link31 /// </summary>32 public Guid UserId { get; set; }33 /// <summary>34 /// Thời gian chỉnh sửa35 /// </summary>36 public DateTime LastUpdate { get; set; } = DateTime.Now;37
38 public static Link Create(Guid userId, string originalUrl, string shortId, string? title)39 {40 var link = new Link(Guid.NewGuid())41 {42 OriginalUrl = new OriginalUrl(originalUrl),43 ShortId = new ShortId(shortId),44 Title = new Title(title),45 UserId = userId,46 };47 return link;48 }49}
Tại sao lại dùng Value Object
Có một câu hỏi đặt ra là tại sao lại dùng value object, dùng các kiểu dữ liệu nguyên thủy chẳng phải nhanh gọn hơn?
Có một thuật ngữ tong code smell được gọi là Primitive Obsession ban có thể đọc thêm ở đây
Và tất nhiên mọi thứ đều phải có sự đánh đổi, khi ta sử dụng value object, các thuộc tính trong entity sẽ có độ rõ ràng về mặt ngữ nghĩa, tránh sự lặp lại, có độ trừu tượng cao hơn tuy nhiên sẽ phải đánh đôi về mặt hiệu suất cũng như độ phức tạp.
Domain event
Domain event là một trong những khái niệm trong DDD ùng để mô tả một sự kiện quan trọng đã xảy ra trong miền ứng dụng (application’s domain).
Đây là một cách để ghi lại những thay đổi trong trạng thái của hệ thống, những điều này có thể ảnh hưởng đến các đối tượng khác hoặc các phần khác của ứng dụng.
Xây dựng các Domain Event
Sử dụng thư viện MediatR
MediatR là một thư viện nhẹ giúp quản lý giao tiếp giữa các thành phần trong ứng dụng .NET thông qua Mediator Design Pattern.
Một số lợi ích khi áp dụng Mediator Design Pattern
- Giảm sự phụ thuộc giữa các thành phần
- Tăng tính linh hoạt và dễ dàng thay đổi
- Tách biệt logic của hệ thống
Chuột phải vào Dependencies 👉 Manage NuGet Packages… 👉 Chọn tab Browse 👉 Search MediatR 👉 Chọn package đầu tiền 👉 Nhấn Install

Tạo một interface IDomainEvent implement interface INotification của thư viện MediatR
1using MediatR;2
3namespace Ziply.Domain.Abstractions;4public interface IDomainEvent : INotification5{6}
Đối với Link Entity, mình sẽ tạo ra 3 events là
- LinkCreatedDomainEvent: Khi một link được tạo mới
- LinkClickedDomainEvent: Khi link được truy cập
- LinkScanedDomainEvent: Khi link được truy cập nhưng mà bằng quét mã QR
- LinkOriginUrlChangedDomainEvent: Khi link cập nhật lại đường dẫn gốc
- LinkRemovedDomainEvent: Khi link được xóa khỏi hệ thống
1title="Links/Events/LinkCreatedDomainEvent.cs" using2Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public3record LinkCreatedDomainEvent(Guid LinkId) : IDomainEvent;
1c# title="Links/Events/LinkClickedDomainEvent.cs" using2Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public3record LinkClickedDomainEvent(Guid LinkId) : IDomainEvent;
1title="Links/Events/LinkScanedDomainEvent.cs" using2Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public3record LinkScanedDomainEvent(Guid LinkId) : IDomainEvent;
1title="Links/Events/LinkOriginUrlChangedDomainEvent.cs" using2Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public3record LinkOriginUrlChangedDomainEvent(Guid LinkId) : IDomainEvent;
1title="Links/Events/LinkRemovedDomainEvent.cs" using2Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public3record LinkRemovedDomainEvent(Guid LinkId) : IDomainEvent;
Chỉnh sửa lại class Entity
1namespace Ziply.Domain.Abstractions;2public abstract class Entity3{4 private readonly List<IDomainEvent> _domainEvents = new();5
6 public Guid Id { get; }7
8 protected Entity(Guid id)9 {10 Id = id;11 }12
13 public IReadOnlyList<IDomainEvent> GetDomainEvents()14 {15 return _domainEvents.ToList();16 }17
18 public void ClearDomainEvents()19 {20 _domainEvents.Clear();21 }22
23 protected void RaiseDomainEvent(IDomainEvent domainEvent)24 {25 _domainEvents.Add(domainEvent);26 }27}
Chỉnh sửa lại Link Entity
1using Ziply.Domain.Abstractions;2using Ziply.Domain.Links.Events;3
4namespace Ziply.Domain.Links;5public sealed class Link : Entity6{7 private Link(Guid id) : base(id)8 {9 }10 /// <summary>11 /// Url thực tế12 /// </summary>13 public OriginalUrl OriginalUrl { get; set; }14 /// <summary>15 /// Url rút gọn16 /// </summary>17 public ShortId ShortId { get; set; }18 /// <summary>19 /// Tiêu đề của link, dùng để hiển thị và tìm kiếm20 /// </summary>21 public Title? Title { get; set; }22 /// <summary>23 /// Các meta data dùng cho việc hiển thị link trên các trang mạng xã hội24 /// </summary>25 public LinkMetaData? LinkMetaData { get; set; }26 /// <summary>27 /// Số lượt click vào link28 /// </summary>29 public ClickCount ClickCount { get; set; }30 /// <summary>31 /// Id Người tạo link32 /// </summary>33 public Guid UserId { get; set; }34 /// <summary>35 /// Thời gian chỉnh sửa36 /// </summary>37 public DateTime LastUpdate { get; set; } = DateTime.Now;38
39 public static Link Create(Guid userId, string originalUrl, string shortId, string? title)40 {41 var link = new Link(Guid.NewGuid())42 {43 OriginalUrl = new OriginalUrl(originalUrl),44 ShortId = new ShortId(shortId),45 Title = new Title(title),46 UserId = userId,47 };48 link.RaiseDomainEvent(new LinkCreatedDomainEvent(link.Id));49 return link;50 }51
52 public Link Update(string originalUrl, string shortId, string? title)53 {54 if(OriginalUrl.Value != originalUrl)55 {56 RaiseDomainEvent(new LinkOriginUrlChangedDomainEvent(Id));57 }58 OriginalUrl = new OriginalUrl(originalUrl);59 Title = new Title(title);60 ShortId = new ShortId(shortId);61 LastUpdate = DateTime.Now;62 return this;63 }64
65 public void Remove()66 {67 RaiseDomainEvent(new LinkRemovedDomainEvent(Id));68 }69}
Repository Pattern và Unit of Work
Repository Pattern là gì? Một Repository Pattern là một design pattern có thể hiểu đơn giản là nó làm trung gian giữa tầng Domain và Data Access (như Dapper hoặc Entity Framework). Mọi thứ liên quan đến ORM được xử lý ở bên trong repository layer, nó giúp rõ ràng project hơn theo nguyên lý SEPARATION OF CONCERNS (chia để trị), mẫu thiết kế được sử dụng nhiều để xây dựng solution một cách clean hơn.
Unit of work pattern Quản lý các transaction và thay đôi trên nhiều đối tượng trong một phiên làm việc duy nhất đảm bảo transaction nhất quán và không bị mất dữ liệu.
Bài viết tham khảo về Repository Pattern và Unit of Work
Tạo các abstraction IUnitOfWork và ILinkRepository
1namespace Ziply.Domain.Abstractions;2public interface IUnitOfWork3{4 Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);5}
1namespace Ziply.Domain.Links;2public interface ILinkRepository3{4 Task<Link?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);5 Task<IEnumerable<Link>> GetListAsync(Guid userId, CancellationToken cancellationToken = default);6 void Add(Link link);7 void Update(Link link);8 void Remove(Link link);9 Task<bool> IsShortIdUniqueAsync(Guid? linkId, string shortId, CancellationToken cancellationToken = default);10
11}
Năm mới chúc các bạn nhiều sức khỏe, hạnh phúc và thành công!
Source Code
Source code của bài nàyBài viết trong series
Bài 1 - Implement hệ thống rút gọn link giống tiny url, bitly
Bài 2 - Setup môi trường, cài các công cụ và extension cần thiết để tiến hành xây dựng ứng dụng rút gọn link
Bài 3 - Tạo dự án và xây dựng cấu trúc thư mục backend với .Net 8 bằng Visual Studio.
Bài 4 - Tạo dự án và xây dựng cấu trúc thư mục backend với .Net 8 bằng .NetCLI
Bài 5 -Xây dựng Domain project với Domain Driven Design