Link Search Menu Expand Document

EF Core Configuration

Bài thực hành EF Core & PostgreSQL sử dụng EF Core để tự động tạo ra database schema (table, relation) từ các domain classes theo những qui ước sẵn có - conventions.

Ví dụ:

  • Thuộc tính AccountId trong Account tự động tạo ra khoá chính - primary key AccountId trong bảng Accounts
  • Thuộc tính AccountId trong Payment tự động tạo ra khoá ngoài - foreign key AcocuntId trong bảng Payments

Trong software frameworks, phương pháp sử dụng các qui ước cấu hình mặc định thay vì yêu cầu người dùng thực hiện các cấu hình không thực sự cần thiết còn gọi là convention over configuration.

Tuy vậy, vẫn có trường hợp cần những tuỳ biến khác với cấu hình mặc định. Trong phần thực hành trước, các thuộc tính kiểu string trong domain classes tự động ánh xạ sang dữ liểu kiểu text trên database. Điều này không cần thiết do các thuộc tính như FirstName, LastName thường chỉ có độ dài tối thiểu và chúng ta nên áp dụng kiểu varchar cho những thuộc tính này.

EF Core cung cấp khả năng cấu hình qua hai cách thức:

  1. Data Annotation
  2. Fluent API

Data Annotation

Trong EF Core, Data Annotation có thể áp dụng cho những chức năng:

  • Thay đổi cấu hình mặc định của việc ánh xạ
  • Xác nhận tính hợp lệ của dữ liệu trước khi đưa vào cơ sở dữ liệu

Để minh hoạ cho những chức năng này, chúng ta cập nhật nội dung các file Model.csProgram.cs trong ví dụ trước.

Thay đổi Entity class Accout trong file Model.cs

public class Account
{
    [Key]
    public Guid AccountId { get; set; }

    [Required, Column(TypeName = "varchar(50)")]
    public string FirstName { get; set; }

    [Required, Column(TypeName = "varchar(50)")]
    public string LastName { get; set; }

    [Required, Column(TypeName = "varchar(50)")]
    public string MiddleName { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [Phone]
    public string PhoneNumber { get; set; }

    public Address Address { get; set; }
    public List<Payment> Payments { get; set; }
}

Trong thay đổi này, chúng ta sử dụng annotations cho các mục đích

  • [Key] - khoá chính với column PaymentId
  • [Require] - yêu cầu Not null với columns LastName, MiddleName, FirstName
  • [Column(TypeName)] - chỉ định kiểu dữ liệu của column tương ứng
  • [Phone] - giá trị theo định dạng phone number
  • [EmailAddress] - giá trị theo định dạng email

Với thay đổi của Model.cs, chúng ta tạo ra migration mới và cập nhật migration sang database:

dotnet ef migrations add UpdateDbAnnotation
dotnet ef database update

Xác nhận các thay đổi đã được cập nhật với kiểu dữ liệu trong các columns:

Annotation Ouput

Chú ý

Thực thi lệnh dotnet run, chúng ta thấy dữ liệu vẫn được cập nhật trong cơ sở dữ liệu mặc dù giá trị PhoneNumberEmail không chính xác. Để validate dữ liệu trước khi đưa vào cơ sở dữ liệu, chúng ta cần sử dụng ValidationContext.

Xác nhận tính hợp lệ của dữ liệu (validate) trong class AccountContext:

public override int SaveChanges()
{
    bool isValid = true;

    var entities = from e in ChangeTracker.Entries()
                    where e.State == EntityState.Added || e.State == EntityState.Modified
                    select e.Entity;

    foreach (var entity in entities)
    {
        ValidationContext context = new ValidationContext(entity, null, null);
        List<ValidationResult> validationResults = new List<ValidationResult>();
        isValid = isValid && Validator.TryValidateObject(entity, context, validationResults, true);
        if (!isValid)
        {
            foreach (ValidationResult validationResult in validationResults)
            {
                Console.WriteLine("{0}", validationResult.ErrorMessage);
            }
        }
    }

    if (isValid)
    {
        return base.SaveChanges();
    }
    else
    {
        Console.WriteLine("Save Changes is failured");
        return 0;
    }
}

Khi thực thi lại lệnh dotnet run, dữ liệu trong đối tượng customer sẽ không lưu được trong table do giá trị PhoneNumberEmail không chính xác.

Applying migrations
Writing a new record
The Email field is not a valid e-mail address.
The PhoneNumber field is not a valid phone number.
...

Cập nhật giá trị Email và PhoneNumber theo đúng định dạng yêu cầu, thực thi chương trình và xác nhận lại kết quả.

Applying migrations
Writing a new record
Reading a record
{
  "AccountId": "4ccd579c-e099-4887-99a9-64098ce924e2",
  "FirstName": "Minh",
  "LastName": "Le",
  "MiddleName": "Duc",
  "Email": "anhlt@gmail.com",
  "PhoneNumber": "0423564482",
  "Address": {
    "AddressId": "94b02de8-f6a4-450f-8895-b224b3103d71",
    "StreetName": "Mincha",
    "City": "Melbourne",
    "ZipCode": 3055,
    "State": "Victoria",
    "Country": "Australia"
  },
  "Payments": [
    {
      "PaymentId": "574db459-a44e-43eb-b9d9-5e83d6398329",
      "PaymentType": "Visa",
      "IsDefault": true,
      "CardNumber": "4000 0082 6000 0000",
      "ExpireMonth": 10,
      "ExpiredYear": 2020,
      "AccountId": "4ccd579c-e099-4887-99a9-64098ce924e2"
    }
  ]
}
Clean up database

Fluent API

Việc thay đổi cấu hình quan hệ giữa Entitiy classes và tables dựa trên Fluent API có mức độ ưu tiên cao nhất so với các phương pháp khác. Để sử dụng Fluent API, chúng ta cần override phương thức OnModelCreating của lớp DbContext và sử dụng ModelBuilder API để cấu hình model trong đó.

Ví dụ sau đây sử dụng Fluent API để cấu hình các thuộc tính trong Account entity và thiết lập mối quan hệ giữa Account với các entity khác.

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

Bước 1: Tạo Entitiy classes

Chuyển Entity classes trong file Models.cs sang các files trong một folder mới với tên Entities

  • File Account.cs
using System;
using System.Collections.Generic;

namespace Accounts.Domain.Entities
{
    public class Account
    {
        public Guid AccountId { get; set; }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string MiddleName { get; set; }

        public string Email { get; set; }
        public string PhoneNumber { get; set; }

        public Address Address { get; set; }
        public List<Payment> Payments { get; set; }
    }
}
  • File Address.cs
using System;
namespace Accounts.Domain.Entities
{
    public class Address
    {
        public Guid AddressId { get; set; }

        public string StreetName { get; set; }
        public string City { get; set; }
        public int ZipCode { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
    }
}
  • File Payment.cs
using System;
using System.Text.Json.Serialization;

namespace Accounts.Domain.Entities
{
    public class Payment
    {
        public Guid PaymentId { get; set; }

        public string PaymentType { get; set; }
        public bool IsDefault { get; set; }

        public string CardNumber { get; set; }
        public int ExpireMonth { get; set; }
        public int ExpiredYear { get; set; }

        public Guid AccountId { get; set; }

        [JsonIgnore]
        public Account Account { get; set; }
    }
}

Bước 2: Xây dựng DbContext

Đổi tên file Models.cs sang AccountsContext.cs do mục đích file này chỉ chứa thông tin về DbContext class. Method OnModelCreating sử dụng modelBuilder để chỉ định quan hệ giữa các Entity classes, đồng thời định nghĩa ràng buộc với các thuộc tính bên trong mỗi Entity (khoá chính, kiểu dữ liệu etc…)

Nội dung AccountsContext.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Accounts.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace Accounts.Domain
{
    public class AccountsContext : DbContext
    {
        public DbSet<Account> Accounts { get; set; }
        public DbSet<Payment> Payments { get; set; }
        public DbSet<Address> Addresses { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            _ = options.UseNpgsql("Host=localhost;Database=Uber;Username=postgres;Password=password");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Account>(account =>
            {
                account.HasKey(k => k.AccountId);
                account.Property(p => p.FirstName).HasColumnType("varchar(50)").IsRequired();
                account.Property(p => p.MiddleName).HasColumnType("varchar(50)").IsRequired();
                account.Property(p => p.LastName).HasColumnType("varchar(50)").IsRequired();
                account.Property(p => p.Email).IsRequired();
                account.Property(p => p.PhoneNumber).IsRequired();
            });

            modelBuilder.Entity<Payment>(payment =>
            {
                payment.HasKey(k => k.PaymentId);
                payment.Property(p => p.CardNumber).IsRequired();
                payment.Property(p => p.ExpireMonth).IsRequired();
                payment.Property(p => p.ExpiredYear).IsRequired();
                payment.Property(p => p.PaymentType).HasColumnType("varchar(20)").IsRequired();
            });

            modelBuilder.Entity<Address>(address =>
            {
                address.HasKey(k => k.AddressId);
                address.Property(p => p.StreetName).IsRequired();
                address.Property(p => p.City).IsRequired();
                address.Property(p => p.State).IsRequired();
                address.Property(p => p.Country).IsRequired();
                address.Property(p => p.ZipCode).IsRequired();
            });

            modelBuilder.Entity<Account>().HasOne(p => p.Address);
            modelBuilder.Entity<Payment>().HasOne(a => a.Account).WithMany(p => p.Payments);
        }

        public override int SaveChanges()
        {
            bool isValid = true;

            var entities = from e in ChangeTracker.Entries()
                           where e.State == EntityState.Added
                               || e.State == EntityState.Modified
                           select e.Entity;

            foreach (var entity in entities)
            {
                ValidationContext context = new ValidationContext(entity, null, null);
                List<ValidationResult> validationResults = new List<ValidationResult>();
                isValid = isValid && Validator.TryValidateObject(entity, context, validationResults, true);
                if (!isValid)
                {
                    foreach (ValidationResult validationResult in validationResults)
                    {
                        Console.WriteLine("{0}", validationResult.ErrorMessage);
                    }
                }
            }

            if (isValid)
            {
                return base.SaveChanges();
            }
            else
            {
                Console.WriteLine("Save Changes is failured");
                return 0;
            }
        }
    }
}

Bước 3: Thao tác dữ liệu

Sử dụng dotnet run và xác nhận kết quả khi câu lệnh kết thúc:

Applying migrations
Writing a new record
Reading a record
{
  "AccountId": "e8175a4a-33ea-4c83-b9b3-5881c599f80e",
  "FirstName": "Minh",
  "LastName": "Le",
  "MiddleName": "Duc",
  "Email": "anhlt@gmail.com",
  "PhoneNumber": "0423564482",
  "Address": {
    "AddressId": "5e9fe731-a7cf-4fc8-b648-97ae393bdd31",
    "StreetName": "Mincha",
    "City": "Melbourne",
    "ZipCode": 3055,
    "State": "Victoria",
    "Country": "Australia"
  },
  "Payments": [
    {
      "PaymentId": "60b3c11e-9883-4088-8108-8ead68720666",
      "PaymentType": "Visa",
      "IsDefault": true,
      "CardNumber": "4000 0082 6000 0000",
      "ExpireMonth": 10,
      "ExpiredYear": 2020,
      "AccountId": "e8175a4a-33ea-4c83-b9b3-5881c599f80e"
    }
  ]
}
Clean up database

Copyright © 2019-2022 Tuan Anh Le.