Thử Nghiệm Với .Net - 05.Xây dựng Domain project với Domain Driven Design

Posted on:2 tháng 1, 2025 at 14:50

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

Value Object

Các Domain Entity chính của ứng dụng rút gọn link bao gồm:

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.

Abstractions/Entity.cs
1
namespace Ziply.Domain.Abstractions;
2
public abstract class Entity
3
{
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.

Links/Link.cs
1
using Ziply.Domain.Abstractions;
2
3
namespace Ziply.Domain.Links;
4
public sealed class Link : Entity
5
{
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ọn
15
/// </summary>
16
public string ShortId { get; set; }
17
/// <summary>
18
/// Tiêu đề của link, dùng để hiển thị và tìm kiếm
19
/// </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ội
23
/// </summary>
24
public object? LinkMetaData { get; set; }
25
/// <summary>
26
/// Số lượt click vào link
27
/// </summary>
28
public int ClickCount { get; set; }
29
/// <summary>
30
/// Id Người tạo link
31
/// </summary>
32
public Guid UserId { get; set; }
33
/// <summary>
34
/// Thời gian chỉnh sửa
35
/// </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 = 0
48
};
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

Links/ClickCount.cs
1
namespace Ziply.Domain.Links;
2
3
public record ClickCount(int Value);
Links/LinkMetaData.cs
1
namespace Ziply.Domain.Links;
2
3
public record LinkMetaData(string? SiteName = null, string? Title = null, string? Description = null, string? OgTitle = null, string? OgDescription = null, string? OgImage = null, string? OgType = null);
Links/OriginalUrl.cs
1
namespace Ziply.Domain.Links;
2
public record OriginalUrl(string Value);
Links/ShortId.cs
1
namespace Ziply.Domain.Links;
2
3
public record ShortId(string Value);
Links/Title.cs
1
namespace Ziply.Domain.Links;
2
3
public record Title(string Value);

Sửa lại Link Entity

Links/Link.cs
1
using Ziply.Domain.Abstractions;
2
3
namespace Ziply.Domain.Links;
4
public sealed class Link : Entity
5
{
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ọn
15
/// </summary>
16
public ShortId ShortId { get; set; }
17
/// <summary>
18
/// Tiêu đề của link, dùng để hiển thị và tìm kiếm
19
/// </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ội
23
/// </summary>
24
public LinkMetaData? LinkMetaData { get; set; }
25
/// <summary>
26
/// Số lượt click vào link
27
/// </summary>
28
public ClickCount ClickCount { get; set; }
29
/// <summary>
30
/// Id Người tạo link
31
/// </summary>
32
public Guid UserId { get; set; }
33
/// <summary>
34
/// Thời gian chỉnh sửa
35
/// </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

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

MediatR

Tạo một interface IDomainEvent implement interface INotification của thư viện MediatR

Abstractions/IDomainEvent.cs
1
using MediatR;
2
3
namespace Ziply.Domain.Abstractions;
4
public interface IDomainEvent : INotification
5
{
6
}

Đối với Link Entity, mình sẽ tạo ra 3 events là

1
title="Links/Events/LinkCreatedDomainEvent.cs" using
2
Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public
3
record LinkCreatedDomainEvent(Guid LinkId) : IDomainEvent;
1
c# title="Links/Events/LinkClickedDomainEvent.cs" using
2
Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public
3
record LinkClickedDomainEvent(Guid LinkId) : IDomainEvent;
1
title="Links/Events/LinkScanedDomainEvent.cs" using
2
Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public
3
record LinkScanedDomainEvent(Guid LinkId) : IDomainEvent;
1
title="Links/Events/LinkOriginUrlChangedDomainEvent.cs" using
2
Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public
3
record LinkOriginUrlChangedDomainEvent(Guid LinkId) : IDomainEvent;
1
title="Links/Events/LinkRemovedDomainEvent.cs" using
2
Ziply.Domain.Abstractions; namespace Ziply.Domain.Links.Events; public
3
record LinkRemovedDomainEvent(Guid LinkId) : IDomainEvent;

Chỉnh sửa lại class Entity

Abstractions/Entity.cs
1
namespace Ziply.Domain.Abstractions;
2
public abstract class Entity
3
{
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

1
using Ziply.Domain.Abstractions;
2
using Ziply.Domain.Links.Events;
3
4
namespace Ziply.Domain.Links;
5
public sealed class Link : Entity
6
{
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ọn
16
/// </summary>
17
public ShortId ShortId { get; set; }
18
/// <summary>
19
/// Tiêu đề của link, dùng để hiển thị và tìm kiếm
20
/// </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ội
24
/// </summary>
25
public LinkMetaData? LinkMetaData { get; set; }
26
/// <summary>
27
/// Số lượt click vào link
28
/// </summary>
29
public ClickCount ClickCount { get; set; }
30
/// <summary>
31
/// Id Người tạo link
32
/// </summary>
33
public Guid UserId { get; set; }
34
/// <summary>
35
/// Thời gian chỉnh sửa
36
/// </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

Abstractions/IUnitOfWork.cs
1
namespace Ziply.Domain.Abstractions;
2
public interface IUnitOfWork
3
{
4
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
5
}
Links/ILinkRepository.cs
1
namespace Ziply.Domain.Links;
2
public interface ILinkRepository
3
{
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ày

Bà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