ASP.NET Core Web API With EF Core

2024-06-20

筆記 Web API 專案搭配 EF Core 使用的注意要點。

logo

Basic Entity

[Route("RPS")]
[HttpGet]
public async Task<ActionResult<string>> GetOneRPS(string user)
{
    return GetRPSResult(user);
}

Collection

// GET: api/One/RPSMultitimes
[Route("RPSMultitimes")]
[HttpPost]
public async Task<ActionResult<List<string>>> GetOneRPSMultitimes(string[] users)
{
    var repsonses = new List<string> { };
    foreach (string user in users)
    {
        string user_copy = user;
        repsonses.Add(GetRPSResult(user).Value);
    }
    return repsonses;
}

One to Many

Model (One to Many)

Customer 有零個到多個 Order 且 Order 必須要有 1 個 Customer

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public ICollection<Order>? Orders { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public string Product { get; set; }
    public int CustomerId { get; set; }
    public Customer? Customer { get; set; }
}

GET Series

預設的 Get 方式不會將 Dependent Entity 一起載入,並需要主動調整處理邏輯,加入 include 才能夠 eager loading 預先載入 Dependenty Entity。

[HttpGet]
public async Task<ActionResult<IEnumerable<Customer>>> GetCustomer()
{
    return await _context.Customer.Include(c => c.Orders).ToListAsync();
}
GET https://localhost:7001/api/Customers

POST Series

_context.Customers.Add(customer);
await _context.SaveChangesAsync();

一對多新增單一實體

POST https://localhost:7052/api/Customers
Content-Type: application/json

{
  "name": "Webber"
}

一對多新增所有實體

POST https://localhost:7052/api/Customers
Content-Type: application/json

{
  "name": "Webber",
  "orders": [
    {
      "product": "Pear"
    },
    {
      "product": "Lemon"
    },
    {
      "product": "Orange"
    }
  ]
}

PUT Series

Scaffold 預設提供的處理邏輯只能夠針對主體 Entity 如果要對於 Navigation Property (Many 的 Entity Order) 也進行作業,需要加上額外的處理邏輯。

_context.Entry(customer).State = EntityState.Modified;

// check if customer has orders
if (customer.Orders != null)
{
    foreach (var order in customer.Orders)
    {
        if (order.OrderId == 0)
        {
            // 新增額外關聯實體
            _context.Orders.Add(order);
        }
        else
        {
            // 更新所有實體
            _context.Entry(order).State = EntityState.Modified;
        }
    }
}

新增關聯實體的方式 (Order)

PUT https://localhost:7052/api/Customers/18
Content-Type: application/json

{
  "customerId": 18,
  "name": "Webber",
  "orders": [
    {
      "product": "Corn"
    }
  ]
}

更新所有實體的方式 (Customer & Order)

PUT https://localhost:7052/api/Customers/1
Content-Type: application/json

{
  "customerId": 1,
  "name": "Webber",
  "orders": [
    {
      "OrderId": 3,
      "product": "Pear"
    }
  ]
}

PUT 預設的操作無法刪除,但如果是要藉由 PUT 更新關聯實體的時候先刪除所有的關係,再加入新關聯實體,可以將處理邏輯進行以下處理:

_context.Entry(customer).State = EntityState.Modified;

// check if customer has orders
if (customer.Orders != null)
{
    _context.Orders.RemoveRange(_context.Orders.Where(o => o.CustomerId == id));
    // Remove 後要將異動儲存回資料庫,同步 Entity 與 資料庫的一致
    await _context.SaveChangesAsync();

    foreach (var order in customer.Orders)
    {
        if (order.OrderId == 0)
        {
            _context.Orders.Add(order);
        }
        else
        {
            _context.Entry(order).State = EntityState.Modified;
        }
    }
}

DELETE Series

如果刪除主 Entity 因為有 CASCADE ON DELETE 會自動刪除關聯實體相當方便。

One to Many (Self Reference)

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? ManagerId { get; set; }
    public Employee? Manager { get; set; }
    public ICollection<Employee> Members { get; } = new List<Employee>();
}

POST Series

同時新增 Entity 與參照的 Entity

POST https://localhost:7052/api/Employees
Content-Type: application/json

{
  "name": "John",
  "manager": {
    "name": "Boss"
  }
}

新增 Entity 並指定已存在的 Entity 為參照

POST https://localhost:7052/api/Employees
Content-Type: application/json

{
  "name": "Allen",
  "managerId": 6
}

PUT Series

PUT https://localhost:7052/api/Employees/5
Content-Type: application/json

{
  "id": 5,
  "name": "Webber",
  "managerId": 6
}

DELETE Series

刪除的情境,如果直接刪除被參照的 Entity,會受到 Foreign Key 的 REFERENCE constraint。

必須依序刪除,可以使用簡單的 DFS 方式來進行處理,但處理邏輯仍是先將負責標註為 Deleted,最後透過 SaveChangesAsync 才交由 EF Core 來判斷刪除順序。

var entityToDelete = new Stack<Employee>();
entityToDelete.Push(employee);

while (entityToDelete.Count > 0)
{
    var current = entityToDelete.Pop();

    await _context.Entry(current).Collection(c => c.Members).LoadAsync();
    foreach (var child in current.Members)
    {
        entityToDelete.Push(child);
    }

    _context.Employees.Remove(current);
}

await _context.SaveChangesAsync();

Many to Many

Model (Many to Many)

public class Student
{
    public int StudentId { get; set; }
    public string Name { get; set; }
    public ICollection<StudentCourse>? StudentCourses { get; set; }
}

public class Course
{
    public int CourseId { get; set; }
    public string Title { get; set; }
    public ICollection<StudentCourse>? StudentCourses { get; set; }
}

public class StudentCourse
{
    public int StudentId { get; set; }
    public Student? Student { get; set; }
    public int CourseId { get; set; }
    public Course? Course { get; set; }
    public string? Grade { get; set; }
}

Post Series

POST Method

[HttpPost]
public async Task<ActionResult<Student>> PostStudent(Student student)
{
    _context.Student.Add(student);
    await _context.SaveChangesAsync();

    return CreatedAtAction("GetStudent", new { id = student.StudentId }, student);
}

多對多新增單一實體

新增單一實體 (Student)

POST https://localhost:7052/api/Students/
Content-Type: application/json

{
  "name": "Lynn"
}

多對多新增單一實體與使用既有實體

新增單一實體 (Student) 與使用既有實體 (Coruse)

POST https://localhost:7052/api/Students/
Content-Type: application/json

{
  "name": "Marx",
  "studentCourses": [
    {
      "courseId": 3,
      "grade": "A"
    }
  ]
}

多對多直接新增所有實體

POST https://localhost:7052/api/Students/
Content-Type: application/json

{
  "name": "Marx",
  "studentCourses": [
    {
      "course": {
        "title": "Python"
      },
      "grade": "A"
    }
  ]
}

PUT Series

Scaffold 提供的方法是透過 EntityState.Modified 來達到更新的效果,但僅限於 Student Entity,Navigation Property 如 StudentCourse 並不會隨著被修改。

_context.Entry(student).State = EntityState.Modified;

await _context.SaveChangesAsync();

如果要修改 Navigation Property 必須主動進行相關的程式碼處理:

_context.Entry(student).State = EntityState.Modified;

foreach (var studentCourse in student.StudentCourses)
{
    // Add Entity Situation
    if (studentCourse.CourseId == 0)
    {
        _context.Course.Add(studentCourse.Course);
        _context.Entry(studentCourse).State = EntityState.Added;
    }
    // Modify Entity Situation
    else
    {
        _context.Entry(studentCourse).State = EntityState.Modified;
    }
}

http request 達成調整 Join Table 的 Property

PUT https://localhost:7052/api/Students/3
Content-Type: application/json

{
  "studentId": 3,
  "name": "Webber",
  "studentCourses": [
    {
      "studentId": 3,
      "courseId": 1,
      "grade": "D"
    }
  ]
}

第二個 http request 達成新增 Course Entity 及 Join Table Entity

###
PUT https://localhost:7052/api/Students/3
Content-Type: application/json

{
  "studentId": 3,
  "name": "Webber",
  "studentCourses": [
    {
      "course": {
        "title": "Chemistry"
      },
      "grade": "E"
    }
  ]
}

DELETE Series

Scaffold 預設提供的刪除方式。

public async Task<IActionResult> DeleteStudent(int id)
{
    var student = await _context.Student.FindAsync(id);
    if (student == null)
    {
        return NotFound();
    }

    _context.Student.Remove(student);
    await _context.SaveChangesAsync();

    return NoContent();
}
DELETE https://localhost:7052/api/Students/3

預設只支援單一 Entity 刪除,如果想要全部刪除,可以調整 route 以及當沒有輸入 id 情況下的處理邏輯為以下方式:

[HttpDelete("{id?}")]
public async Task<IActionResult> DeleteStudent(int id)
{
    if (id == 0)
    {
        _context.Student.RemoveRange(_context.Student);
        await _context.SaveChangesAsync();
        return NoContent();
    }

    ...
}

參考資源

Glossary of relationship terms | learn.microsoft