ASP.NET Core Web API With EF Core


  1. Basic Entity
    1. Collection
  2. One to Many
    1. GET Series
    2. POST Series
      1. 一對多新增單一實體
      2. 一對多新增所有實體
    3. PUT Series
    4. DELETE Series
  3. One to Many (Self Reference)
    1. POST Series
    2. PUT Series
    3. DELETE Series
  4. Many to Many
    1. Post Series
      1. 多對多新增單一實體
      2. 多對多新增單一實體與使用既有實體
      3. 多對多直接新增所有實體
    2. PUT Series
    3. DELETE Series
  5. 參考資源

筆記 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