© 2020, Developed by Hieu Dev

Thêm CRUD, sorting, filtering và paging trong .NET 6 Web API

Chào bạn, khi mới bắt đầu tiếp xúc với công nghệ mới, ngôn ngữ mới, theo tuần tự chúng ta thường tìm hiểu cách thực hiện CRUD trước tiên. Việc làm này sẽ làm hiểu hơn về cách vận hành hay mô hình của một công nghệ mới mà bạn mới tiếp cận để học.

Thêm CRUD, sorting, filtering và paging trong .NET 6 Web API

Trong project, chúng ta sẽ tiến hành phân tầng và tổ chức code theo repository pattern, tiến hành crud với 2 thực thể category và product, và sau cùng là tiến hành thêm sorting, filtering và paging cho từng API. Đầu tiên, ta sẽ tìm hiểu repository pattern là gì?

Repository Pattern

Repository pattern là một cách tổ chức source code trong ASP.NET, là lớp trung gian giữa tầng Data Access và Business Logic, đóng vai trò là một lớp kết nối giữa tầng Business và Model của ứng dụng, giúp cho việc truy cập dữ liệu chặt chẽ và bảo mật hơn.

Sau cùng, việc sử dụng repository pattern có những lý do chung quy sau:
  • Một nơi duy nhất để thay đổi quyền truy cập dữ liệu cũng như xử lý dữ liệu.
  • Tăng tính bảo mật và rõ ràng cho code.
  • Một nơi duy nhất chịu trách nhiệm cho việc mapping các bảng vào object.
  • Rất dễ dàng để thay thế một Repository với một implementation giả cho việc testing, vì vậy không cần chuẩn bị một cơ sở dữ liệu có sẵn.

Tiến hành 

Trước khi triển khai project, bạn cần cài đặt hay chuẩn bị sẵn những gì sau đây hoặc có thể update ở các version cao hơn:
  • Microsoft Visual Studio 2022
  • .Net 6 và SDK
  • SQL Server

1. Tạo các thực thể với Code First

Như đã đề cập trước đó, trong ví dụ sau mình sẽ tạo ra 2 entities là Category và Product. Cụ thể các bước tuần tự như sau:

Bước 1: Tạo mới một Class Library với tên ProductAPI.Data từ solution. Sau đó cài đặt các NuGet Package sau:
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Bước 2: Tạo như mục Entities để thực hiện chứa các class entity. 

Thêm CRUD, sorting, filtering và paging trong .NET 6 Web API

Bước 3: Trong thư mục Entities, tạo ra các file Product.cs và Category.cs với code sau:

Entities/Category.cs:

public class Category
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public Guid? ParentId { get; set; }
        public DateTime CreatedDate { get; set; }
        public bool Status { get; set; }

        public virtual IEnumerable<Product> Products { get; set; }

    }


Entities/Product.cs:

public class Product
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string? Content { get; set; }
        public string? UrlImage { get; set; }
        public DateTime CreatedDate { get; set; }
        public bool Status { get; set; }
        public int ViewCount { get; set; }
        public Guid CategoryId { get; set; }
        public virtual Category Category { get; set; }
        public virtual IEnumerable<Comment> Comments { get; set; }
    }


Bước 4: Ta sẽ tiến hành tạo ra các file để thực hiện validation cho các field ở các thực thể. Cụ thể ta tạo ra thư mục Configurations với các file bên trong sau:

Configurations/CategoryConfiguration.cs:

public class CategoryConfiguration : IEntityTypeConfiguration<Category>
    {
        public void Configure(EntityTypeBuilder<Category> builder)
        {
            builder.ToTable("Categories");
            builder.HasKey(x => x.Id);
            builder.Property(x => x.Name).IsRequired();
            builder.Property(x => x.Status).HasDefaultValue(true);

        }
    }


Configurations/ProductConfiguration.cs:

public class ProductConfiguration : IEntityTypeConfiguration<Product>
    {
        public void Configure(EntityTypeBuilder<Product> builder)
        {
            builder.ToTable("Products");
            builder.HasKey(x => x.Id);
            builder.Property(x => x.Name).IsRequired();
            builder.Property(x => x.Price).IsRequired();
            builder.Property(x => x.Status).HasDefaultValue(true);

        }
    }


Bước 5: Bây giờ, ta sẽ thực hiện seed ra các dữ liệu mẫu, các dữ liệu mẫu này sẽ được sinh ra khi chúng ta thực hiện migration. Cụ thể, ta sẽ tạo ra thư mục Extensions, và tạo file ModelBuilderExtensions.cs bên trong với code sau: 

Extensions/ModelBuilderExtensions.cs:

public static class ModelBuilderExtensions
    {
        public static void Seed(this ModelBuilder modelBuilder)
        {
            var shoeCategoryId = new Guid("1b60fd43-a1b5-4214-9ccc-d239f0f4c97b");
            var clothingCategoryId = new Guid("1b60fd43-a1b5-4214-9ccc-d239f0f4c97c");
            var productId = new Guid("1b60fd43-a1b5-4214-9ccc-d239f0f4c97f");

            modelBuilder.Entity<Category>().HasData(
                new Category()
                {
                    Id = shoeCategoryId,
                    Name = "Shoes",
                    ParentId = null,
                    Status = true,
                },
                 new Category()
                 {
                     Id = clothingCategoryId,
                     Name = "Clothing",
                     ParentId = null,
                     Status = true,
                 });

            modelBuilder.Entity<Product>().HasData(
                new Product()
                {
                    Id = productId,
                    Name = "Nike",
                    Content = "New fashion 2021",
                    Price = 120000,
                    Status = true,
                    CreatedDate = DateTime.Now,
                    UrlImage = null,
                    CategoryId = shoeCategoryId
                }
                );
        }
    }


Bước 6: Tiếp đến ta cần tạo ra Db Context, cụ thể bạn cần tạo thư mục EF và chứa 2 file lần lượt như sau:

EF/DataDbContext.cs:

public class DataDbContext : DbContext
    {
        public DataDbContext(DbContextOptions options) : base(options)
        {
        }
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                //Configure using Fluent API
                modelBuilder.ApplyConfiguration(new CategoryConfiguration());
                modelBuilder.ApplyConfiguration(new ProductConfiguration());
                modelBuilder.ApplyConfiguration(new CommentConfiguration());


            //Data seeding
            modelBuilder.Seed();
                //base.OnModelCreating(modelBuilder);
            }

        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
        public DbSet<Comment> Comments { get; set; }
    }


EF/DataDbContextFactory.cs:
public class DataDbContextFactory : IDesignTimeDbContextFactory<DataDbContext>
    {
        public DataDbContext CreateDbContext(string[] args)
        {
            IConfigurationRoot configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build();

            var connectionString = configuration.GetConnectionString("ConnectionString");

            var optionsBuilder = new DbContextOptionsBuilder<DataDbContext>();
            optionsBuilder.UseSqlServer(connectionString);

            return new DataDbContext(optionsBuilder.Options);
        }
    }


Bước 7: Bây giờ bạn cần tạo project ASP.NET Core Web API từ solution với tên ProductAPI. WebApplication, sau đó cài đặt các Nuget Package như bước 1

Sau đấy, bạn cần cấu hình chuỗi kết nối tới SQL Server trong appsetting.json:

"ConnectionStrings": {
    "ConnectionString": "Server=HIUPC;Database=Dbdotnet6_productAPI;Trusted_Connection=True;MultipleActiveResultSets=true"
  },


Và bạn cần thay đổi server name, database name cho phù hợp với cấu hình của bạn.

Bước 8: Ta vào Program.cs và thêm đoạn code sau bên trong:

builder.Services.AddDbContext<DataDbContext>(options => options.UseSqlServer(
                            builder.Configuration.GetConnectionString("ConnectionString")));

Bước 9: Vào Tools > NuGet Package Manager > NuGet Package Console và thực thi lần lượt 2 command sau:
  • Add-Migration InitialShopCore
  • Update-database

Bước 10: Sau khi thực hiện bước 9, database của bạn đã được tự động sinh ra, và bước cuối cùng là các bạn vào SQL Server kiểm tra database đã được sinh ra hay chưa. 

2. Thêm repository pattern

Trước tiên ta cần tạo ra các interfaces repository, và các interface của các thực thể mình sẽ tổ chức bên trong một tầng riêng biệt để dễ quản lý. 

Bước 1: Tạo mới project với Class Library với tên ProductAPI.Infrastructure từ solution để thực hiện chứa các interfaces repository:

Thêm CRUD, sorting, filtering và paging trong .NET 6 Web API

Bước 2: Ta tiến hành tạo lần lượt các interface với code sau:

ICategoryRepository.cs:

public interface ICategoryRepository
    {
        Task<IEnumerable<Category>> GetAll();
        Task<Pagination<Category>> GetAllPaging(string? filter, int pageIndex, int pageSize);
        Task<Category> GetById(Guid? id);
        Task<RepositoryResponse> Create(CreateCategoryViewModel model);
        Task<RepositoryResponse> Update(Guid id, UpdateCategoryViewModel model);
        Task<int> Delete(Guid id);
    }


IProductRepository.cs:

public interface IProductRepository
    {
        Task<IEnumerable<Product>> GetAll();
        Task<Pagination<ProductQuickViewModel>> GetAllPaging(string? filter, Guid? categoryId, int pageIndex, int pageSize);
        Task<Product> GetById(Guid? id);
        Task<RepositoryResponse> Create(CreateProductViewModel model);
        Task<RepositoryResponse> Update(Guid id, UpdateProductViewModel model);
        Task<int> Delete(Guid id);
        Task<int> UpdateViewCount(Guid id);
    }


Như bước trên, ta tạo ra 2 file interface repository, với bản chất 2 file rất tương tự nhau. Và các phương thức này được sử dụng với bất đồng bộ (asynchronous), cũng là một best practice khi bạn mới làm quen với code. 

Code trên sẽ xãy ra lỗi, vì chúng ta có gọi các view model bên trong, nhưng các file chưa được khởi tạo, vì thế bây giờ ta lần lượt tạo các file như bước dưới đây:

Bước 3: Quay lại project ProductAPI.Data, ta thực hiện tạo mới thư mục ViewModel, và lần lượt tạo các file view model sau:

ViewModel/CreateCategoryViewModel.cs:

public class CreateCategoryViewModel
    {
        public string Name { get; set; }
        public Guid? ParentId { get; set; }
        public bool Status { get; set; }
    }


ViewModel/CreateProductViewModel.cs:

public class CreateProductViewModel
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string? Content { get; set; }
        public string? UrlImage { get; set; }
        public bool Status { get; set; }
        public Guid CategoryId { get; set; }
    }


ViewModel/UpdateCategoryViewModel.cs:

public class UpdateCategoryViewModel
    {
        public string Name { get; set; }
        public Guid? ParentId { get; set; }
        public bool Status { get; set; }
    }


ViewModel/UpdateProductViewModel.cs:

public class UpdateProductViewModel
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string? Content { get; set; }
        public string? UrlImage { get; set; }
        public bool Status { get; set; }
        public Guid CategoryId { get; set; }
    }


ViewModel/ProductQuickViewModel.cs:

 public class ProductQuickViewModel
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string? Content { get; set; }
        public string? UrlImage { get; set; }
        public DateTime CreatedDate { get; set; }
        public int ViewCount { get; set; }
        public bool Status { get; set; }
        public Guid CategoryId { get; set; }
    }


ViewModel/RepositoryResponse.cs:
public class RepositoryResponse
    {
        public int Result { get; set; }
        public Guid Id { get; set; }
    }


ViewModel/PaginationBase.cs:

public class PaginationBase
    {
        public int PageIndex { get; set; }

        public int PageSize { get; set; }

        public int TotalRecords { get; set; }

        public int PageCount
        {
            get
            {
                var pageCount = (double)TotalRecords / PageSize;
                return (int)Math.Ceiling(pageCount);
            }
        }
    }


ViewModel/Pagination.cs:
public class Pagination<T> : PaginationBase where T : class
    {
        public List<T> Items { get; set; }
    }


Bước này khá nhiều file view model, nó đảm nhận các vai trò như tạo view model để định nghĩa các field khi ta thực hiện các request, file RepositoryResponse để định nghĩa reponse body, và paging.

Sau khi đã định nghĩa các interface repository, bây giờ bạn cần định nghĩa ra các repository kế thừa từ các interface này. Để dễ dàng trong việc quản lý, chúng ta cũng sẽ định nghĩa ra một tầng riêng.

Bước 4: Tạo project với Class Library với tên ProductAPI.Business từ solution để thực hiện chứa các repository sau đây:

CategoryRepository.cs:

public class CategoryRepository : ICategoryRepository
    {
        private readonly DataDbContext _context;

        public CategoryRepository(DataDbContext context)
        {
            _context = context;
        }

        public async Task<IEnumerable<Category>> GetAll()
        {
            return await _context.Categories
                            .OrderByDescending(p => p.CreatedDate)
                            .ToListAsync();
        }

        public async Task<Pagination<Category>> GetAllPaging(string? filter, int pageIndex, int pageSize)
        {
            var query = _context.Categories.AsQueryable();

            if (!string.IsNullOrEmpty(filter))
            {
                query = query.Where(x => x.Name.Contains(filter)
                || x.Name.Contains(filter));
            }
            var totalRecords = await query.CountAsync();

            var items = await query.Skip((pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();

            var pagination = new Pagination<Category>
            {
                Items = items,
                TotalRecords = totalRecords,
                PageIndex = pageIndex,
                PageSize = pageSize,
            };

            return pagination;
        }

        public async Task<Category?> GetById(Guid? id)
        {
            var item = await _context.Categories
                            .OrderByDescending(p => p.CreatedDate)
                            .DefaultIfEmpty()
                            .FirstOrDefaultAsync(p => p.Id == id);

            return item;

        }

        public async Task<RepositoryResponse> Create(CreateCategoryViewModel model)
        {
            Category item = new Category()
            {
                Name = model.Name,
                ParentId = model.ParentId,
                CreatedDate = DateTime.Now,
                Status = model.Status,
            };

            _context.Categories.Add(item);
            var result = await _context.SaveChangesAsync();

            return new RepositoryResponse()
            {
                Result = result,
                Id = item.Id
            };
        }

        public async Task<RepositoryResponse> Update(Guid id, UpdateCategoryViewModel model)
        {
            var item = await _context.Categories.FindAsync(id);
            item.Name = model.Name;
            item.ParentId = model.ParentId;
            item.CreatedDate = DateTime.Now;
            item.Status = model.Status;

            _context.Categories.Update(item);
            var result = await _context.SaveChangesAsync();

            return new RepositoryResponse()
            {
                Result = result,
                Id = id
            };

        }

        public async Task<int> Delete(Guid id)
        {
            var item = await _context.Categories.FindAsync(id);

            _context.Categories.Remove(item);
            var result = await _context.SaveChangesAsync();

            return result;
        }


    }


ProductRepository.cs:

public class ProductRepository : IProductRepository
    {
        private readonly DataDbContext _context;

        public ProductRepository(DataDbContext context)
        {
            _context = context;
        }

        public async Task<IEnumerable<Product>> GetAll()
        {
            return await _context.Products
                            .OrderByDescending(p => p.CreatedDate)
                            .ToListAsync();
        }

        public async Task<Pagination<ProductQuickViewModel>> GetAllPaging(string? filter, Guid? categoryId, int pageIndex, int pageSize)
        {
            var query = from pr in _context.Products
                        join c in _context.Categories on pr.CategoryId equals c.Id
                        select new { pr, c };

            if (!string.IsNullOrEmpty(filter))
            {
                query = query.Where(x => x.pr.Name.Contains(filter)
                || x.pr.Name.Contains(filter));
            }

            if (categoryId.HasValue)
            {
                query = query.Where(x => x.pr.CategoryId == categoryId.Value);
            }

            var totalRecords = await query.CountAsync();

            var items = await query.Skip((pageIndex - 1) * pageSize)
                .Take(pageSize)
                .Select(u => new ProductQuickViewModel()
                {
                    Id = u.pr.Id,
                    Name = u.pr.Name,
                    CategoryId = u.pr.CategoryId,
                    Content = u.pr.Content,
                    Price = u.pr.Price,
                    UrlImage = u.pr.UrlImage,
                    ViewCount = u.pr.ViewCount,
                    CreatedDate = u.pr.CreatedDate,
                    Status = u.pr.Status,

                })
                .ToListAsync();

            var pagination = new Pagination<ProductQuickViewModel>
            {
                Items = items,
                TotalRecords = totalRecords,
                PageIndex = pageIndex,
                PageSize = pageSize,
            };

            return pagination;
        }

        public async Task<Product?> GetById(Guid? id)
        {
            var item = await _context.Products
                            .OrderByDescending(p => p.CreatedDate)
                            .DefaultIfEmpty()
                            .FirstOrDefaultAsync(p => p.Id == id);

            return item;

        }

        public async Task<RepositoryResponse> Create(CreateProductViewModel model)
        {
            Product item = new Product()
            {
                Name = model.Name,
                CategoryId = model.CategoryId,
                Content = model.Content,
                Price = model.Price,
                UrlImage = model.UrlImage,
                CreatedDate = DateTime.Now,
                Status = model.Status,
            };

            _context.Products.Add(item);
            var result = await _context.SaveChangesAsync();

            return new RepositoryResponse()
            {
                Result = result,
                Id = item.Id
            };
        }

        public async Task<RepositoryResponse> Update(Guid id, UpdateProductViewModel model)
        {
            var item = await _context.Products.FindAsync(id);
            item.Name = model.Name;
            item.CategoryId = model.CategoryId;
            item.Content = model.Content;
            item.Price = model.Price;
            item.UrlImage = model.UrlImage;
            item.CreatedDate = DateTime.Now;
            item.Status = model.Status;

            _context.Products.Update(item);
            var result = await _context.SaveChangesAsync();

            return new RepositoryResponse()
            {
                Result = result,
                Id = id
            };

        }


        public async Task<int> UpdateViewCount(Guid id)
        {
            var item = await _context.Products.FindAsync(id);

            if (item.ViewCount == null)
                item.ViewCount = 0;

            item.ViewCount += 1;
            _context.Products.Update(item);
            var result = await _context.SaveChangesAsync();

            return result;
        }

        public async Task<int> Delete(Guid id)
        {
            var item = await _context.Products.FindAsync(id);

            _context.Products.Remove(item);
            var result = await _context.SaveChangesAsync();

            return result;
        }


    }


Như vậy, ta đã có 2 files repository rất đơn giản, bạn cần add scope cho 2 files này.

Bước 5: Thêm dòng code sau trong Program.cs:

builder.Services.AddScoped(typeof(ICategoryRepository), typeof(CategoryRepository));
builder.Services.AddScoped(typeof(IProductRepository), typeof(ProductRepository));


3. Tạo controller thực hiện CRUD, filtering và paging

Như ở bước 1.7, chúng ta đã tạo project ProductAPI.WebApplication, và ở tầng này, bây giờ ta thực hiên thêm các controller, thực hiện CRUD, filter và pagination.

Bước 1: Trong Controller thêm các code controller sau:

Controller/CategoryController.cs:

[Route("api/category")]
    [ApiController]
    public class CategoryController : ControllerBase
    {
        private readonly ICategoryRepository _repo;

        public CategoryController(ICategoryRepository repo)
        {
            _repo = repo;
        }

        [HttpGet("")]
        public async Task<ActionResult> GetAll()
        {
            return Ok(await _repo.GetAll());
        }

        [HttpGet("filter")]
        public async Task<ActionResult> GetAllPaging(string? filter, int pageIndex, int pageSize)
        {
            return Ok(await _repo.GetAllPaging(filter, pageIndex, pageSize));
        }

        [HttpGet("{id}")]
        public async Task<ActionResult> Get(Guid id)
        {

            var item = await _repo.GetById(id);

            if (item == null)
            {
                return NotFound(new ApiNotFoundResponse($"Category with id: {id} is not found"));
            }

            return Ok(item);
        }

        [HttpPost("create")]
        public async Task<IActionResult> Post(CreateCategoryViewModel model)
        {
            var result = await _repo.Create(model);

            if(result.Result > 0)
            {
                return RedirectToAction(nameof(Get), new { id = result.Id });
            } else
            {
                return BadRequest(new ApiBadRequestResponse("Create category failed"));
            }
        }

        [HttpPut("update/{id}")]
        public async Task<IActionResult> Put(UpdateCategoryViewModel model, Guid id)
        {
            var item = await _repo.GetById(id);
            if(item == null)
                return NotFound(new ApiNotFoundResponse($"Category with id: {id} is not found"));

            if (id == model.ParentId)
            {
                return BadRequest(new ApiBadRequestResponse("Category cannot be a child itself."));
            }


            var result = await _repo.Update(id, model);

            if (result.Result > 0)
            {
                return Ok();
            }
            else
            {
                return BadRequest(new ApiBadRequestResponse("Update category failed"));
            }
        }

        [HttpDelete("delete/{id}")]
        public async Task<IActionResult> Delete (Guid id)
        {
            var item = _repo.GetById(id);

            if(item == null)
                return NotFound(new ApiNotFoundResponse($"Category with id: {id} is not found"));

            var result = await _repo.Delete(id);

            if (result > 0)
            {
                return Ok();
            } else
            {
                return BadRequest(new ApiBadRequestResponse("Delete category failed"));
            }
                
        }

    }


Controller/ProductController.cs:
[Route("api/product")]
    [ApiController]
    public class ProductController : ControllerBase
    {
        private readonly IProductRepository _repo;

        public ProductController(IProductRepository repo)
        {
            _repo = repo;
        }

        [HttpGet("")]
        public async Task<ActionResult> GetAll()
        {
            return Ok(await _repo.GetAll());
        }

        [HttpGet("filter")]
        public async Task<ActionResult> GetAllPaging(string? filter, Guid? categoryId, int pageIndex, int pageSize)
        {
            return Ok(await _repo.GetAllPaging(filter, categoryId, pageIndex, pageSize));
        }

        [HttpGet("{id}")]
        public async Task<ActionResult> Get(Guid id)
        {

            var item = await _repo.GetById(id);

            if (item == null)
            {
                return NotFound(new ApiNotFoundResponse($"Product with id: {id} is not found"));
            }

            return Ok(item);
        }

        [HttpPost("create")]
        public async Task<IActionResult> Post(CreateProductViewModel model)
        {
            var result = await _repo.Create(model);

            if (result.Result > 0)
            {
                return RedirectToAction(nameof(Get), new { id = result.Id });
            }
            else
            {
                return BadRequest(new ApiBadRequestResponse("Create product failed"));
            }
        }

        [HttpPut("update/{id}")]
        public async Task<IActionResult> Put(UpdateProductViewModel model, Guid id)
        {
            var item = await _repo.GetById(id);
            if (item == null)
                return NotFound(new ApiNotFoundResponse($"Product with id: {id} is not found"));


            var result = await _repo.Update(id, model);

            if (result.Result > 0)
            {
                return Ok();
            }
            else
            {
                return BadRequest(new ApiBadRequestResponse("Update product failed"));
            }
        }

        [HttpDelete("delete/{id}")]
        public async Task<IActionResult> Delete(Guid id)
        {
            var item = _repo.GetById(id);

            if (item == null)
                return NotFound(new ApiNotFoundResponse($"Product with id: {id} is not found"));

            var result = await _repo.Delete(id);

            if (result > 0)
            {
                return Ok();
            }
            else
            {
                return BadRequest(new ApiBadRequestResponse("Delete product failed"));
            }

        }

        [HttpPut("{id}/view-count")]
        public async Task<IActionResult> UpdateViewCount(Guid id)
        {
            var item = _repo.GetById(id);

            if (item == null)
                return NotFound(new ApiNotFoundResponse($"Product with id: {id} is not found"));

            var result = await _repo.UpdateViewCount(id);

            if (result > 0)
            {
                return Ok();
            }
            else
            {
                return BadRequest(new ApiBadRequestResponse("Increase view count failed"));
            }
        }


Sau khi thực hiện bước trên, bạn có thể thấy controller có config các response để dùng chung, bây giờ các file này mình sẽ để về một tầng khác để dùng chung và dễ quản lý. Cụ thể như bước dưới đây:

Bước 2: Tạo project Class Library mới từ solution với tên ProductAPI.Common và thêm các file sau:

ApiResponse.cs:


public class ApiResponse
    {
        public int StatusCode { get; }

        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
        public string Message { get; }

        public ApiResponse(int statusCode, string message = null)
        {
            StatusCode = statusCode;
            Message = message ?? GetDefaultMessageForStatusCode(statusCode);
        }

        private static string GetDefaultMessageForStatusCode(int statusCode)
        {
            switch (statusCode)
            {
                case 404:
                    return "Resource not found";

                case 500:
                    return "An unhandled error occurred";

                default:
                    return null;
            }
        }
    }


ApiBadRequestResponse.cs:
public class ApiBadRequestResponse : ApiResponse
    {
        public IEnumerable<string> Errors { get; }

        public ApiBadRequestResponse(ModelStateDictionary modelState)
            : base(400)
        {
            if (modelState.IsValid)
            {
                throw new ArgumentException("ModelState must be invalid", nameof(modelState));
            }

            Errors = modelState.SelectMany(x => x.Value.Errors)
                .Select(x => x.ErrorMessage).ToArray();
        }

        public ApiBadRequestResponse(IdentityResult identityResult)
           : base(400)
        {
            Errors = identityResult.Errors
                .Select(x => x.Code + " - " + x.Description).ToArray();
        }

        public ApiBadRequestResponse(string message)
           : base(400, message)
        {
        }
    }

ApiNotFoundResponse.cs:
public class ApiNotFoundResponse : ApiResponse
    {
        public ApiNotFoundResponse(string message)
           : base(404, message)
        {
        }
    }


Và đó cũng là bước cuối cùng, khi bạn tạo mới project .NET 6 Web API, thì mặc định nó đã tích hợp sẵn Swagger cho bạn, bây giờ bạn chỉ chạy project lên và trải nghiệm.

Lời kết

Như ở các bài viết trước, bạn không cần phải phân tầng phức tạp như vậy, chỉ vài thao tác đơn giản cũng có thể thực hiện CRUD rồi, nhưng mình đang muốn bạn hướng tới một structure thực tế hơn, kết hợp các kiến thức đã học vào project.

Tùy vào mỗi dự án thì cách phân tầng sẽ khác nhau, nhưng cơ bản khi bạn làm việc với repository pattern thì có lẽ đây là kiến chung quy dễ tiếp cận nhất với bạn.

Project triển khai phân tầng phức tạp, nên mình có push source lên github, các bạn có thể tham khảo tại đường dẫn bên dưới:


Trong nhưng bài viết sắp tới, vẫn CRUD operation, nhưng sẽ là vẫn dụng triển khai với Generic repository pattern và Specification pattern. Mong các bạn luôn theo dõi và ủng hộ mình.

Chúc các bạn thành công.

Hieu Ho.

2 Nhận xét

Mới hơn Cũ hơn