Link Search Menu Expand Document

.NET Web API - に

Nội dung

  1. AWS CloudWatch
    1. Cài đặt Serilog
    2. Sử dụng Serilog
    3. Kiểm tra Logging
  2. Data Models
  3. Unit of Work

Chúng ta tiếp tục phát triển dịch vụ RemindersManagement với những chức năng:

  • Tích hợp AWS CloudWatch qua Serilog Framework
  • Cập nhật mô hình nghiệp vụ với Reminders và List model
  • Bổ sung Unit of Work patter cho các repositories

AWS CloudWatch

AWS CloudWatch là dịch vụ theo dõi và giám sát tình trạng hoạt động của các tài nguyên trên môi trường AWS. CloudWatch có khả năng thu thập và thống kê số liệu từ đó đưa ra báo cáo về tình trạng hoạt động của các tài nguyên thông qua những Metrics, đồng thời gửi đi cảnh báo theo những qui tắc xác định.

Hướng dẫn

Metric: là biểu đồ biểu diễn lượng sử dụng một nguồn tài nguyên nào đó trên hệ thống (RAM, CPU, Network Bandwith).

Môi trường .NET cung cấp một số frameworks cho phép ứng dụng tích hợp chức năng logging với dịch vụ CloudWatch, bao gồm:

  • NLog
  • Apache log4net
  • ASP.NET Core Logging
  • Serilog

Trong dịch vụ RemindersManagement, chúng ta sẽ cài đặt và sử dụng Serilog.

Cài đặt Serilog

  • Trong folder RemindersManagement.API, cài đặt package:
dotnet add package Serilog.AspNetCore
dotnet add package AWS.Logger.SeriLog

Hướng dẫn

Sink: Serilog sử dụng Sinks để gửi dữ liệu log đến các storages provider: console, files, AWS / Azure services…Trong trường hợp Amazon CloudWatch, chúng ta có thể sử dụng một trong các Sinks:

  • Serilog: Serilog.Sinks.AwsCloudWatch
  • AWS: AWS.Logger.SeriLog
  • Cập nhật cấu hình Serilog trong setting files

appSettings.json

{
  "Serilog": {
    "Using": [
      "AWS.Logger.SeriLog",
      "Serilog.Sinks.Console"
    ],
    "LogGroup": "friendReminders/RemindersManagement",
    "Region": "ap-southeast-2",
    "MinimumLevel": "Information",
    "WriteTo": [
      {
        "Name": "AWSSeriLog"
      }
    ],
    "Enrich": [
      "FromLogContext"
    ]
  },
  "AllowedHosts": "*"
}

appsettings.Development.json

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "AWS.Logger.SeriLog"
    ],
    "LogGroup": "friendReminders/RemindersManagement",
    "Region": "ap-southeast-2",
    "MinimumLevel": {
      "Default": "Information"
    },
    "WriteTo": [
      {
        "Name": "Console"
      },
      {
        "Name": "AWSSeriLog"
      }
    ],
    "Enrich": [
      "FromLogContext"
    ]
  },
  "AllowedHosts": "*"  
}

Với cấu hình của appsettings, trong môi trường phát triển, ứng dụng gửi thông tin log ra màn hình Terminal và AWS CloudWatch.

Hướng dẫn

Để Serilog gửi được thông tin log đến AWS CloudWatch, ứng dụng cần phải được cung cấp AWS Credentials cho phép quyền ghi dữ liệu vào một Log Groups trên CloudWtach.

.NET Framework trợ giúp ứng dụng tìm kiếm thông tin AWS Credentials theo thứ tự:

  • Default AWS Profile
  • Environment Variables
  • ECS Task Credentials / EC2 Instance Credential

Tham khảo: Configuring AWS CredentialsCredential Loading

Trong ví dụ này, AWS Credentials được cung cấp bởi default AWS Profile

  • Trong file Program.cs
    • Khởi tạo Logger: Log.Logger
    • Khai báo Log Provider: UseSerilog()

Nội dung thay đổi:

using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;

namespace RemindersManagement.API
{
    public class Program
    {
        public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: false)
            .AddEnvironmentVariables()
            .Build();

        public static int Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(Configuration)                  
                .CreateLogger();

            try
            {
                var host = CreateHostBuilder(args).Build();
                using (var scope =host.Services.CreateScope())
                {
                    var context = scope.ServiceProvider.GetService<RemindersDbContext>();
                    context.Database.EnsureCreated();
                }

                Log.Information("RemindersManagement is starting.......");
                host.Run();

                return 0;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly");
                return 1;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseStartup<Startup>()
                        .UseUrls("http://*:80");
                })
                .UseSerilog();
    }
}
  • Trong file Startup.cs, sử dụng middleware UseSerilogRequestLogging để ghi lại thông tin chi tiết khi mỗi yêu cầu gửi đến dịch vụ
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RemindersManagement.API.Domain.Interfaces;
using RemindersManagement.API.Domain.Services;
using RemindersManagement.API.Infrastructure.Data;
using RemindersManagement.API.Infrastructure.Repositories;
using Serilog;

namespace RemindersManagement.API
{
    public class Startup
    {
        ...
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseHttpsRedirection();
            app.UseSerilogRequestLogging();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

Sử dụng Serilog

RemindersController.cs sử dụng Serilog để lưu lại thông tin khi controller method gặp lỗi hoặc không tìm thấy thông tin yêu cầu.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using RemindersManagement.API.Domain.Models;
using RemindersManagement.API.Domain.Services;

namespace RemindersManagement.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class RemindersController : ControllerBase
    {
        private readonly ILogger<RemindersController> _logger;
        private readonly IRemindersService _remindersService;

        public RemindersController(ILogger<RemindersController> logger, IRemindersService remindersService)
        {
            _logger = logger;
            _remindersService = remindersService;
        }

        // GET: api/reminders
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Reminder>>> GetReminders()
        {
            var reminders = await _remindersService.GetRemindersAsync();
            return Ok(reminders);
        }

        // GET: api/reminder/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Reminder>> GetReminder(Guid id)
        {
            var reminder = await _remindersService.GetReminderAsync(id);
            if (reminder == null)
            {
                _logger.LogWarning($"Could not found {id}.", id);
                return NotFound();
            }
            return reminder;
        }

        // PUT: api/reminders/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutReminder(Guid id, Reminder reminder)
        {
            if (id != reminder.Id)
            {
                return BadRequest();
            }
            try
            {
                await _remindersService.PutReminderAsync(id, reminder);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error when update reminder {ex}", ex.Message);
                return StatusCode(500, "A problem happened while handling your request.");
            }
            return NoContent();
        }

        // POST: api/reminders
        [HttpPost]
        public async Task<ActionResult<Reminder>> PostReminder(Reminder reminder)
        {
            await _remindersService.PutReminderAsync(reminder);
            return CreatedAtAction("GetReminder", new { id = reminder.Id }, reminder);
        }

        // DELETE: api/reminders/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<Reminder>> DeleteReminder(Guid id)
        {
            var reminder = await _remindersService.GetReminderAsync(id);
            if (reminder == null)
            {
                _logger.LogWarning($"Could not found {id} to delete.", id);
                return NotFound();
            }
            try
            {
                await _remindersService.DeleteReminder(id);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error when delete reminder {ex}", ex.Message);
                return StatusCode(500, "A problem happened while handling your request.");
            }
            return reminder;
        }
    }
}

Kiểm tra Logging

  • Thực thi với chế độ Development (sử dụng appsettings.Development.json)

Trong folder FriendReminders:

dotnet run --urls="http://localhost:8000/" --project Services/RemindersManagement/RemindersManagement.API/RemindersManagement.API.csproj

Output của Serilog trên Terminal:

Serilog Local Ouput Output của Serilog trên Terminal

  • Thực thi với chế độ Production (sử dụng appsettings.json)

Trong folder FriendReminders, thực hiện các lệnh

dotnet build
dotnet publish
cd Services/RemindersManagement/RemindersManagement.API/bin/Debug/netcoreapp3.1/publish
dotnet RemindersManagement.dll

Chú ý

Khi thực thi ứng dụng trong chế độ Production, để các cấu hình từ appsettings được thiết lập chính xác, chúng ta cần thực thi lệnh dotnet [service].dll trong folder chứa file dll đó.

  • Output của Serilog trên AWS - friendReminders/RemindersManagement:

Serilog Product Log Ouput AWS Log Groups

Serilog Product Log Insight Ouput Logs Insight


Data Models

Để quản lý hiệu quả, ứng dụng FriendReminders cho phép nhóm Reminders có cùng một mục đích thành các Categories khác nhau. Mỗi Category bao gồm các thuộc tính:

  • Name: tên category
  • Icon: biểu tượng ý nghĩa
  • HexaColor: màu sắc hiển thị
  • Reminders: danh sách reminders

Với yêu cầu này, chúng ta cần thay đổi Models của FriendReminders theo các bước sau:

  • Cập nhật Data Model

Trong folder Domain\Models, tạo file Category.cs:

using System;
using System.Collections.Generic;

namespace RemindersManagement.API.Domain.Models
{
    public class Category
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Icon { get; set; }
        public string HexaColor { get; set; }
        public List<Reminder> Reminders { get; set; }
    }
}

Thay đổi trong file Reminder.cs:

using System;

namespace RemindersManagement.API.Domain.Models
{
    public enum ReminderStatus
    {
        Doing,
        Finished,
    }
    public enum Priority
    {
        Low,
        Medium,
        Hight
    }
    public class Reminder
    {
        public Guid Id { get; set; }
        public string Description { get; set; }
        public ReminderStatus Status { get; set; }
        public Priority Priority { get; set; } = Priority.Medium;
        public DateTime? RemiderTime { get; set; }
        public Guid CategoryId { get; set; }
        public Category Category { get; set; }
    }
}
  • Cập nhật DbContext trong file RemindersDbContext.cs:
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RemindersManagement.API.Domain.Models;

namespace RemindersManagement.API.Infrastructure.Data
{
    public class RemindersDbContext : DbContext
    {
        const string DEFAULT_SCHEMA = "reminders";
        public DbSet<Reminder> Reminders { get; set; }
        public DbSet<Category> Categories { get; set; }

        public RemindersDbContext(DbContextOptions<RemindersDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Fluent API
            modelBuilder.Entity<Reminder>(ConfigureReminders);
            modelBuilder.Entity<Category>(ConfigureCategories);
            // Relation Define
            modelBuilder.Entity<Reminder>()
                .HasOne(r => r.Category)
                .WithMany(l => l.Reminders)
                .HasForeignKey(r => r.CategoryId);                
            // Data Seed
            Category defaultCategory = new Category()
            {
                Id = Guid.NewGuid(),
                Name = "Default"
            };
            modelBuilder.Entity<Category>().HasData(
                defaultCategory
            );
            modelBuilder.Entity<Reminder>().HasData(
                new Reminder
                {
                    Id = Guid.NewGuid(),
                    Description = "Learning Microservices",
                    Status = ReminderStatus.Finished,
                    CategoryId = defaultCategory.Id
                },
                new Reminder
                {
                    Id = Guid.NewGuid(),
                    Description = "Writing Blog",
                    Status = ReminderStatus.Doing,
                    CategoryId = defaultCategory.Id
                },
                new Reminder
                {
                    Id = Guid.NewGuid(),
                    Description = "Presentation prepare",
                    Status = ReminderStatus.Doing,
                    CategoryId = defaultCategory.Id
                }
            );
        }
        void ConfigureReminders(EntityTypeBuilder<Reminder> reminderConfiguration)
        {
            reminderConfiguration.ToTable("Reminders", DEFAULT_SCHEMA);
            reminderConfiguration.HasKey(r => r.Id);
            reminderConfiguration.Property(r => r.Description).IsRequired();
            reminderConfiguration.Property(r => r.Status).HasColumnType("varchar(50)").IsRequired();
        }
        void ConfigureCategories(EntityTypeBuilder<Category> categoryConfiguration)
        {
            categoryConfiguration.ToTable("Categories", DEFAULT_SCHEMA);
            categoryConfiguration.HasKey(l => l.Id);
            categoryConfiguration.Property(l => l.Name).HasColumnType("varchar(50)").IsRequired();
        }
    }
}
  • Trong folder RemindersManagement.API, tạo Migration
dotnet ef migrations add UpdateRemindersDb --context RemindersDbContext

Hướng dẫn

Việc tạo Migration có thể gặp lỗi do các xung đột với Seed Data trong table Reminders. Trong trường hợp đó, chúng ta có thể xoá database FriendReminders.db và thực hiện lại quá trình migration.

  • Thực thi Migration
dotnet ef database update --context RemindersDbContext

Unit of Work

Unit Of Work là một kĩ thuật thiết kế trong lập trình cho phép chúng ta thực hiện các thao tác dữ liệu liên quan trọng cùng một giao dịch - transaction. Thiết kế này đảm bảo các dữ liệu được cập nhật đồng bộ, hoặc có thể cùng huỷ bỏ khi một trong các hoạt động của giao dịch thất bại.

Trong phần này, chúng ta sẽ xây dựng một đối tượng Unit Of Work đóng gói DbContext và các Repositories. Các hoạt động thay đổi dữ liệu sẽ được thực hiện thông qua qua đối tượng này.

Các bước thực hiện:

Bước 1: Xây dựng UnitOfWork

  • Trong folder Domain/Interfaces, tạo file IUnitOfWork.cs:
using System.Threading.Tasks;

namespace RemindersManagement.API.Domain.Interfaces
{
    public interface IUnitOfWork
    {
        IRemindersRepository RemindersRepository { get; }
        ICategoriesRepository CategoriesRepository { get;  }
        Task CommitAsync();
        Task RollbackAsync();
    }
}
  • Trong folder Infrastructure/Data, tạo file UnitOfWork.cs:
using System.Threading.Tasks;
using RemindersManagement.API.Domain.Interfaces;
using RemindersManagement.API.Infrastructure.Repositories;

namespace RemindersManagement.API.Infrastructure.Data
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly RemindersDbContext _context;
        private IRemindersRepository _remindersRepository;
        private ICategoriesRepository _categoriesRepository;

        public UnitOfWork(RemindersDbContext context)
        {
            _context = context;
        }

        public IRemindersRepository RemindersRepository
        {
            get
            {
                return _remindersRepository = _remindersRepository ?? new RemindersRepository(_context);
            }
        }

        public ICategoriesRepository CategoriesRepository
        {
            get
            {
                return _categoriesRepository = _categoriesRepository ?? new CategoriesRepository(_context);
            }
        }

        public async Task CommitAsync()
        {
            await _context.SaveChangesAsync();
        }

        public async Task RollbackAsync()
        {
            await _context.DisposeAsync();
        }
    }
}
  • Khai báo Depedency Injection cho IUnitOfWork và repositories trong Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddScoped<IRemindersRepository, RemindersRepository>();
    // TODO: Let implement category by yourself ;)
    services.AddScoped<ICategoriesRepository, CategoriesRepository>();
    services.AddScoped<IRemindersService, RemindersService>();

    services.AddDbContext<RemindersDbContext>(options => {
        options.UseSqlite("Data Source=FriendReminders.db");
    });
}

Bước 2: Thay đổi Service Layer

  • Service RemindersService sử dụng IUnitOfWork cho hoạt động thay đổi dữ liệu thay vì repositories.

Nội dung thay đổi của RemindersService.cs:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using RemindersManagement.API.Domain.Exceptions;
using RemindersManagement.API.Domain.Interfaces;
using RemindersManagement.API.Domain.Models;

namespace RemindersManagement.API.Domain.Services
{
    public interface IRemindersService
    {
        Task<IEnumerable<Reminder>> GetRemindersAsync();
        Task<Reminder> GetReminderAsync(Guid id);
        Task<Reminder> PutReminderAsync(Reminder reminder);
        Task<Reminder> DeleteReminder(Guid id);
        Task<Reminder> PutReminderAsync(Guid id, Reminder reminder);
    }

    public class RemindersService : IRemindersService
    {
        private readonly IUnitOfWork _unitOfWork;
        private readonly ILogger<RemindersService> _logger;

        public RemindersService(IUnitOfWork unitOfWork, ILogger<RemindersService> logger)
        {
            _logger = logger;
            _unitOfWork = unitOfWork;
        }

        public async Task<IEnumerable<Reminder>> GetRemindersAsync()
        {
            return await _unitOfWork.RemindersRepository.ListAsync();
        }

        public async Task<Reminder> GetReminderAsync(Guid id)
        {
            return await _unitOfWork.RemindersRepository.FindAsync(id);
        }

        public async Task<Reminder> PutReminderAsync(Reminder reminder)
        {
            try
            {
                var result = await _unitOfWork.RemindersRepository.AddAsync(reminder);
                await _unitOfWork.CommitAsync();

                return result;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error when create reminder {ex}", ex.Message);
                throw ex;
            }
        }

        public async Task<Reminder> PutReminderAsync(Guid id, Reminder reminder)
        {
            var existingReminder = await _unitOfWork.RemindersRepository.FindAsync(id);
            if (existingReminder == null)
            {
                throw new ReminderNotFoundException($"Reminder {id} is not found.");
            }
            existingReminder.Description = reminder.Description;
            try
            {
                _unitOfWork.RemindersRepository.Update(existingReminder);
                await _unitOfWork.CommitAsync();

                return existingReminder;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error when update reminder {ex}", ex.Message);
                throw ex;
            }
        }

        public async Task<Reminder> DeleteReminder(Guid id)
        {
            var existingReminder = await _unitOfWork.RemindersRepository.FindAsync(id);
            if (existingReminder == null)
            {
                throw new ReminderNotFoundException($"Reminder {id} is not found.");
            }
            try
            {
                _unitOfWork.RemindersRepository.Remove(existingReminder);
                await _unitOfWork.CommitAsync();

                return existingReminder;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error when delete reminder {ex}", ex.Message);
                throw ex;
            }
        }
    }
}

Trong logic của RemindersService, khi yêu cầu xoá hoặc cập nhật một Reminder không tồn tại trên hệ thống, phương thức sẽ thông báo cho phía Controller thông qua ReminderNotFoundException exception.

  • Trong folder Domain\Exceptions, nội dung của file ReminderNotFoundException.cs:
using System;

namespace RemindersManagement.API.Domain.Exceptions
{
    public class ReminderNotFoundException : Exception
    {
        public ReminderNotFoundException()
        {
        }

        public ReminderNotFoundException(string message)
            : base(message)
        {
        }

        public ReminderNotFoundException(string message, Exception inner)
            : base(message, inner)
        {
        }
    }
}

Trong bài viết tiếp theo Web API - P.3, chúng ta sẽ hoàn thiện dịch vụ RemindersManagement bằng cách bổ sung thêm Unit Test Cases cho thành phần Controllers và Services. Việc bổ sung Unit Tests không chỉ đảm bảo các chức năng thành phần hoạt động đúng như mong muốn, đồng thời giúp các kĩ sư liên tục kiểm tra và cải thiện chất lượng source code trong suốt quá trình phát triển sản phẩm.


Tài liệu tham khảo


Copyright © 2019-2022 Tuan Anh Le.