06. Tìm hiểu về Options Pattern và triển khai trong ứng dụng .NET Core
Đọc config là một yêu cầu rất phổ biến mà bạn có thể gặp trong bất kỳ ứng dụng nào. Chúng ta thường đặt các giá trị config trong một file cấu hình, trong ứng dụng .NET thường là file appsettings.json.
Đọc config ứng dụng thông qua IConfiguration
Chúng ta có thể đọc config trực tiếp thông qua những phương thức có sẵn của IConfiguration như sau:
Giả sử tôi có config kết nối cơ sở dữ liệu MySQL trong Entity Framework Core ở trong file appsetting.json như sau:
Một Controller có một API lấy ra danh sách những config (API này chỉ để test demo trong bài viết):
Kết quả của API sẽ là:
Chúng ta đã đọc được config từ appsetting, Tuy nhiên đọc config như trên có một số nhược điểm:
- Không validation: các giá trị trong file cấu hình sẽ không được validation, nếu muốn chúng được validation thì chúng ta phải kiểm tra mỗi khi lấy giá trị config rồi kiểm tra trong quá trình runtime.
- Không chỉ định rõ ràng loại của giá trị từng config: các giá trị trong file cấu hình cũng không chỉ định rõ ràng type là gì, và cũng không thể truy cập thông qua type của class, nên có thể gây lỗi tiềm ẩn trong quá trình phát triển.
- Không có giá trị mặc định: không có giá trị default, chúng ta có thể gọi hàm lấy config rồi truyền giá trị default theo từng key. Nhưng nếu muốn chỉ cần một lần đặt giá trị default mặc định để dùng cho toàn bộ ứng dụng thì sao?
Trong bài viết này tôi sẽ giới thiệu một phương pháp là Options Pattern sẽ giải quyết những vấn đề trên.
Bắt đầu với Options Pattern
Options Pattern là một mẫu thiết kế phổ biến trong phát triển phần mềm, đặc biệt là trong ứng dụng .NET. Mẫu thiết kế này cho phép chúng ta định nghĩa các lớp đại diện cho các config của ứng dụng, và sau đó sử dụng các đối tượng được cấu hình này trong ứng dụng. Các lớp đại diện này được gọi là Options hoặc Configuration classes.
Mục tiêu của mẫu thiết kế này là cung cấp một cách linh hoạt và dễ bảo trì để quản lý cấu hình của ứng dụng. Nó cung cấp một type rõ ràng, minh bạch cho các config trong toàn bộ ứng dụng, validation giá trị, gán giá trị mặc định và reload lấy giá trị config mới mà không cần chạy lại hay restart ứng dụng...
Bây giờ chúng ta sẽ tìm hiểu các sử dụng Options Pattern:
- Chúng ta sẽ tạo một lớp Options ở trong bài viết này là DatabaseOptions đại diện cho những cấu hình để kết nối database như đã đề cập trong phần trên, trong lớp này sẽ chứa những config trong section DatabaseMysqlOptions và một hằng số DatabaseMysqlOptions là tên của section DatabaseMysqlOptions của file appsetting.json.
- Các thuộc tính ở trong class này đều phải để public.
Sau khi tạo lớp DatabaseOptions thành công, chúng ta có thể đọc config bằng một trong hai cách sau:
Sử dụng Bind(object)
Sử dụng Get<T>
Bằng việc sử dụng lớp Options, config DatabaseMysqlOptions trong file cấu hình đã được bind tương ứng vào từng thuộc tính trong lớp DatabaseOptions. Trong hai API trên, thì việc sử dụng Get<T> thuận tiện hơn việc sử dụng Bind(object).
Một cách tiếp cận khác là chúng ta có thể đăng ký vào service container và bind các giá trị từ appsetting.json vào lớp DatabaseOptions chúng ta vừa tạo. Sau đó chúng ta có thể sử dụng lớp DatabaseOptions này trong toàn bộ ứng dụng bằng cách inject vào contructor bằng cách sử dụng DI.
Chúng ta sẽ tạo một lớp static nơi đó sẽ chứa những extension method để đăng ký với services, tên lớp đó sẽ đặt theo quy tắc sau: {Type}Extensions trong đó {Type} là loại đối tượng bạn cần mở rộng. Ở đây tôi sẽ tạo một lớp ServiceExtensions.
Trong ServiceExtensions sẽ có một extension method mở rộng việc đăng ký Options Config cho services container, quy tắc đặt tên method sẽ như sau Add{Service}, trong đó {Service} là tên dịch vụ mở rộng. Vì vậy tôi sẽ đặt tên là AddOptionsService:
Gọi exensions method trong file program.cs để đăng ký;
Như vậy chúng ta đã đọc được config dựa vào IOptions bằng cách đăng ký vào OptionsController sử dụng DI.
Ở đây, chúng ta inject IOptions<T> vào trong contructor của OptionsController. Việc inject sẽ chỉ inject intanse của IOptions<T> . Việc lấy giá trị của DatabaseOptions được trì hoãn cho đến khi gọi .Value, điều này có thể dẫn đến cải thiện hiệu suất và giảm sử dụng bộ nhớ, đặc biệt là đối với các cấu hình không luôn được sử dụng.
Kết quả như sau:
Vậy là chúng ta đã lấy được giá trị config bằng các sử dụng DI, tiếp theo chúng ta sẽ tìm hiểu hiểu về cách validation dữ liệu config.
Validation dữ liệu config
Validation giá trị đầu vào là một yêu cầu quan trọng, nó đảm bảo tính đúng đắn của dữ liệu. Vậy để, validation giá trị của config. Options Pattern cũng cung cấp cách cho chúng ta validation dữ liệu bằng cách sử dụng Data Annotations.
Giả sử chúng ta phải áp dụng những quy tắc validation sau với những config ở trên:
- ConnectionString: không được null hoặc rỗng.
- CommandTimeout: phải lớn hơn 10 và nhỏ hơn 50.
- ...
Để làm được điều đó, hãy mở file DatabaseOptions và thêm các thuộc tính Data Annotations như sau:
Sau khi hoàn tất, chúng ta sẽ vào nơi đăng ký service options, và gọi thêm extension method là ValidateDataAnnotations để kích hoạt validation bằng cách sử dụng DataAnnotations.
Khi đã đăng ký xong, chúng ta sửa ConnectionString thành rỗng và chạy lại ứng dụng sau đó gọi lại API lấy config ở OptionsController. Kết quả sẽ văng một exception như sau:
Để sửa lỗi này bạn cần sửa lại đúng giá trị hợp lệ trong config ConnectionString. Lỗi vừa rồi được thông báo trong quá trình runtime, khi bạn gọi vào API lấy dữ liệu. Vậy để lỗi văng trong quá trình complie time thì ta sẽ gọi thêm một extension method ValidateOnStart ngay dưới ValidateDataAnnotations.
Ta sẽ chạy lại và lỗi sẽ được văng ra ngay lập tức:
Reload config, các interface Options
Trong quá trình phát triển ở phía Development, việc lấy giá trị config mới nhất hay việc sửa đổi config không là vấn đề lớn. Nhưng trong môi trường triển khai như Production, khi chúng ta sửa một cấu hình, chúng ta sẽ cần phải restart hay chạy lại ứng dụng. Vậy trong trường hợp chúng ta muốn ứng dụng luôn đọc được config mới nhất khi chúng ta sửa mà không cần chạy lại ứng dụng thì sao? Với mục đích này, chúng ta có hai interface là IOptionsSnapshot và IOptionsMonitor. Sau đây sẽ là cách sử dụng của hai interface này và so sánh với IOptions ở phía trên:
IOptions:
- Đăng ký là một Singleton, có thể inject ở bất kì service lifetime nào.
- Không thể đọc config thay đổi khi ứng dụng đã chạy mà không restart lại ứng dụng.
IOptionsSnapshot
- Đăng ký là một Scoped, không thể inject vào Singleton service.
- Khi IOptionsSnapshot<T> được khởi tạo nó sẽ chụp nhanh lại giá trị của config và duy trình nó trong phạm vi scope hay là trong một vòng đời của request.
IOptionsMonitor
- Đăng ký là một Singleton, có thể inject ở bất kì service lifetime nào.
- Đọc sự thay đổi của config bất kì thời điểm nào khi nó có sự thay đổi.
Ở ví dụ trên, chúng ta sẽ inject thêm IOptionsSnapshot và IOptionsMonitor vào contructor của OptionsController:
Chúng ta viết thêm một API lấy cấu hình theo 3 loại IOptions, IOptionsSnapshot và IOptionsMonitor như sau:
Chi tiết xem ở souce code.
Gọi thử API trên chúng ta có kết quả như sau: Sau đó, thay đổi key config ChangeMe thành 2, không chạy lại ứng dụng. Chúng ta gọi API lại một lần nữa. Giá trị config lấy từ IOptions không thay đổi khi thay đổi config và chạy lại ứng dụng, còn giá trị config lấy từ IOptionsSnapshot và IOptionsMonitor đều đã thay đổi bằng giá trị mới nhất là 2.
Vậy khác biệt giữa IOptionsSnapshot và IOptionsMonitor là gì? Để làm rõ điều này chúng ta sẽ đi đến API tiếp theo:
Ở đây tôi sẽ lấy giá những giá trị cũ và mới của config bằng hai interface đề cập trên. Ứng dụng của chúng ta vẫn chưa chạy lại một lần nào, chúng ta đã sửa config từ 1 sang 2.
Bây giờ giá trị config đang là 2. Chúng ta sẽ gọi API ở phía trên, và ngay sau đó nó sẽ bị delay một thời gian để minh họa một quá trình xử lý trong ứng dụng thật, ngay lập tức chúng ta sẽ sửa config thành 3. Kết quả là:
Đối với những request thì mọi giá trị khi sử dụng IOptionsSnapshot sẽ được chụp lại là 2 khi nó được khởi tạo và nó sẽ giữ nguyên giá trị trong toàn bộ vòng đời của request. Nên trong toàn bộ request của API vừa rồi giá trị vẫn là 2.
Còn IOptionsMonitor thì nó sẽ lấy giá trị mới nhất dù bất cứ khi nào, không bị ràng buộc bởi một phạm vi của request, nên ngay lập tức nó đã nhận được giá trị mới nhất là 3.
Qua ví dụ trên, có lẽ bạn sẽ hiểu được về cách dùng của IOptions, IOptionsSnapshot và IOptiosnMonitor. Bằng việc hiểu chúng, ta sẽ có thể áp dụng được vào những tình huống thực tế trong quá trình xây dựng dự án.
Những API trên chỉ là API demo tìm hiểu rõ hơn về Options Pattern. Để cấu hình EFCore kết nối với Database Mysql sau khi đọc config sẽ như sau:
Tổng kết:
Như vậy chúng ta đã tìm hiểu cơ bản về Options Pattern, cách áp dụng trong một dự án. Hy vọng bài viết này hữu ích với mọi người.
Nếu mọi người thấy hay có thể đăng ký kênh của mình nhé. Cảm ơn mọi người đã dành thời gian đọc bài viết của mình!!!
Mã nguồn bài viết: Tại đây.
Tài liệu tham khảo:
https://codewithmukesh.com/blog/options-pattern-in-aspnet-core/
All rights reserved