การสร้าง ASP.NET Web API ด้วย ASP.NET Core

เผยแพร่แล้ว: 2022-03-11

บทนำ

เมื่อหลายปีก่อน ฉันได้รับหนังสือ “Pro ASP.NET Web API” บทความนี้เป็นส่วนเสริมของแนวคิดจากหนังสือเล่มนี้ ซึ่งเป็น CQRS เล็กน้อย และประสบการณ์ของผมในการพัฒนาระบบไคลเอนต์-เซิร์ฟเวอร์

ในบทความนี้ฉันจะกล่าวถึง:

  • วิธีสร้าง REST API ตั้งแต่เริ่มต้นโดยใช้ .NET Core, EF Core, AutoMapper และ XUnit
  • จะแน่ใจได้อย่างไรว่า API ทำงานหลังจากการเปลี่ยนแปลง
  • วิธีทำให้การพัฒนาและสนับสนุนระบบ REST API ง่ายขึ้นมากที่สุด

ทำไมต้อง ASP.NET Core?

ASP.NET Core มีการปรับปรุงมากมายบน ASP.NET MVC/Web API อย่างแรกคือตอนนี้เป็นเฟรมเวิร์กเดียวไม่ใช่สองเฟรม ฉันชอบมันมากเพราะมันสะดวกและไม่สับสน ประการที่สอง เรามีการบันทึกและคอนเทนเนอร์ DI โดยไม่มีไลบรารีเพิ่มเติม ซึ่งช่วยให้ฉันประหยัดเวลาและช่วยให้ฉันมีสมาธิกับการเขียนโค้ดที่ดีขึ้น แทนที่จะเลือกและวิเคราะห์ไลบรารีที่ดีที่สุด

ตัวประมวลผลแบบสอบถามคืออะไร?

ตัวประมวลผลแบบสอบถามเป็นแนวทางเมื่อตรรกะทางธุรกิจทั้งหมดที่เกี่ยวข้องกับเอนทิตีหนึ่งของระบบถูกห่อหุ้มไว้ในบริการเดียว และการเข้าถึงหรือการดำเนินการใดๆ กับเอนทิตีนี้ดำเนินการผ่านบริการนี้ บริการนี้มักจะเรียกว่า {EntityPluralName}QueryProcessor หากจำเป็น ตัวประมวลผลแบบสอบถามจะรวมเมธอด CRUD (สร้าง อ่าน อัปเดต ลบ) สำหรับเอนทิตีนี้ อาจใช้วิธีการบางอย่างไม่ได้ ทั้งนี้ขึ้นอยู่กับข้อกำหนด มาดูตัวอย่างกันที่ ChangePassword หากวิธีการของตัวประมวลผลการสืบค้นต้องการข้อมูลที่ป้อนเข้าไป ก็ควรระบุเฉพาะข้อมูลที่จำเป็นเท่านั้น โดยปกติ สำหรับแต่ละเมธอด คลาสเคียวรีแยกต่างหากจะถูกสร้างขึ้น และในกรณีธรรมดา เป็นไปได้ (แต่ไม่ต้องการ) ที่จะนำคลาสเคียวรีมาใช้ซ้ำ

จุดมุ่งหมายของเรา

ในบทความนี้ ฉันจะแสดงวิธีสร้าง API สำหรับระบบการจัดการต้นทุนขนาดเล็ก รวมถึงการตั้งค่าพื้นฐานสำหรับการตรวจสอบสิทธิ์และการควบคุมการเข้าถึง แต่ฉันจะไม่เข้าสู่ระบบย่อยการตรวจสอบสิทธิ์ ฉันจะครอบคลุมตรรกะทางธุรกิจทั้งหมดของระบบด้วยการทดสอบแบบแยกส่วน และสร้างการทดสอบการรวมอย่างน้อยหนึ่งรายการสำหรับแต่ละวิธี API ในตัวอย่างของเอนทิตีหนึ่งรายการ

ข้อกำหนดสำหรับระบบที่พัฒนาขึ้น: ผู้ใช้สามารถเพิ่ม แก้ไข ลบค่าใช้จ่าย และสามารถดูเฉพาะรายจ่ายได้

รหัสทั้งหมดของระบบนี้มีอยู่ที่ Github

ดังนั้น มาเริ่มออกแบบระบบเล็กๆ แต่มีประโยชน์มากกัน

เลเยอร์ API

ไดอะแกรมแสดงเลเยอร์ API

แผนภาพแสดงให้เห็นว่าระบบจะมีสี่ชั้น:

  • ฐานข้อมูล - ที่นี่เราจัดเก็บข้อมูลและไม่มีอะไรมากไปกว่านี้ ไม่มีตรรกะ
  • DAL - ในการเข้าถึงข้อมูล เราใช้รูปแบบหน่วยของงาน และในการนำไปใช้งาน เราใช้ ORM EF Core พร้อมโค้ดก่อนและรูปแบบการย้ายข้อมูล
  • ตรรกะทางธุรกิจ - เพื่อสรุปตรรกะทางธุรกิจ เราใช้ตัวประมวลผลการสืบค้น เฉพาะเลเยอร์นี้เท่านั้นที่ประมวลผลตรรกะทางธุรกิจ ข้อยกเว้นคือการตรวจสอบความถูกต้องที่ง่ายที่สุด เช่น ฟิลด์บังคับ ซึ่งจะดำเนินการโดยใช้ตัวกรองใน API
  • REST API - อินเทอร์เฟซจริงที่ลูกค้าสามารถทำงานกับ API ของเราได้จะถูกใช้งานผ่าน ASP.NET Core การกำหนดค่าเส้นทางถูกกำหนดโดยแอตทริบิวต์

นอกจากเลเยอร์ที่อธิบายแล้ว เรามีแนวคิดที่สำคัญหลายประการ ประการแรกคือการแยกตัวแบบข้อมูล โมเดลข้อมูลไคลเอ็นต์ส่วนใหญ่จะใช้ในเลเยอร์ REST API โดยจะแปลงการสืบค้นเป็นแบบจำลองโดเมนและในทางกลับกันจากแบบจำลองโดเมนเป็นแบบจำลองข้อมูลไคลเอ็นต์ แต่แบบจำลองการสืบค้นยังสามารถใช้ในตัวประมวลผลการสืบค้นได้อีกด้วย การแปลงทำได้โดยใช้ AutoMapper

โครงสร้างโครงการ

ฉันใช้ VS 2017 Professional เพื่อสร้างโครงการ ฉันมักจะแชร์ซอร์สโค้ดและการทดสอบในโฟลเดอร์ต่างๆ สะดวกสบาย ดูดี การทดสอบใน CI ทำงานสะดวก และดูเหมือนว่า Microsoft แนะนำให้ทำเช่นนี้:

โครงสร้างโฟลเดอร์ใน VS 2017 Professional

รายละเอียดโครงการ:

โครงการ คำอธิบาย
ค่าใช้จ่าย โครงการสำหรับผู้ควบคุม การแมประหว่างโมเดลโดเมนและโมเดล API การกำหนดค่า API
Expense.Api.Common ณ จุดนี้ มีคลาสข้อยกเว้นที่รวบรวมไว้ซึ่งถูกแปลความหมายโดยตัวกรองเพื่อส่งคืนรหัส HTTP ที่ถูกต้องพร้อมข้อผิดพลาดไปยังผู้ใช้
Expenses.Api.Models โครงการสำหรับโมเดล API
Expense.Data.Access โครงการส่วนต่อประสานและการดำเนินการตามรูปแบบหน่วยงาน
Expense.Data.Model โครงการสำหรับโมเดลโดเมน
ค่าใช้จ่ายแบบสอบถาม โครงการสำหรับตัวประมวลผลแบบสอบถามและคลาสเฉพาะแบบสอบถาม
ค่าใช้จ่าย ความปลอดภัย โครงการสำหรับอินเทอร์เฟซและการใช้งานบริบทความปลอดภัยของผู้ใช้ปัจจุบัน

ข้อมูลอ้างอิงระหว่างโครงการ:

แผนภาพแสดงข้อมูลอ้างอิงระหว่างโครงการ

ค่าใช้จ่ายที่สร้างจากเทมเพลต:

รายการค่าใช้จ่ายที่สร้างจากเทมเพลต

โครงการอื่นๆ ในโฟลเดอร์ src ตามเทมเพลต:

รายการโปรเจ็กต์อื่นๆ ในโฟลเดอร์ src ตามเทมเพลต

โครงการทั้งหมดในโฟลเดอร์การทดสอบตามเทมเพลต:

รายชื่อโครงการในโฟลเดอร์การทดสอบตามเทมเพลต

การดำเนินการ

บทความนี้จะไม่อธิบายส่วนที่เกี่ยวข้องกับ UI แม้ว่าจะมีการใช้งานแล้วก็ตาม

ขั้นตอนแรกคือการพัฒนาโมเดลข้อมูลที่อยู่ในแอสเซมบลี Expenses.Data.Model :

แผนภาพความสัมพันธ์ระหว่างบทบาท

คลาส Expense ประกอบด้วยแอตทริบิวต์ต่อไปนี้:

 public class Expense { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public virtual User User { get; set; } public bool IsDeleted { get; set; } }

คลาสนี้รองรับ "การลบแบบนุ่มนวล" โดยใช้แอตทริบิวต์ IsDeleted และมีข้อมูลทั้งหมดสำหรับค่าใช้จ่ายเพียงครั้งเดียวของผู้ใช้รายใดรายหนึ่งซึ่งจะเป็นประโยชน์ต่อเราในอนาคต

คลาส User , Role และ UserRole อ้างถึงระบบย่อยการเข้าถึง ระบบนี้ไม่ได้อ้างว่าเป็นระบบแห่งปี และคำอธิบายของระบบย่อยนี้ไม่ใช่จุดประสงค์ของบทความนี้ ดังนั้นแบบจำลองข้อมูลและรายละเอียดบางประการของการดำเนินการจะถูกละเว้น ระบบการเข้าถึงองค์กรสามารถถูกแทนที่ด้วยระบบที่สมบูรณ์แบบยิ่งขึ้นโดยไม่ต้องเปลี่ยนตรรกะทางธุรกิจ

ถัดไป เทมเพลต Unit of Work ถูกนำไปใช้ในแอสเซมบลี Expenses.Data.Access โครงสร้างของโครงการนี้จะปรากฏขึ้น:

โครงสร้างโครงการ Expense.Data.Access

ไลบรารีต่อไปนี้จำเป็นสำหรับการประกอบ:

  • Microsoft.EntityFrameworkCore.SqlServer

จำเป็นต้องใช้บริบท EF ที่จะค้นหาการแมปในโฟลเดอร์เฉพาะโดยอัตโนมัติ:

 public class MainDbContext : DbContext { public MainDbContext(DbContextOptions<MainDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var mappings = MappingsHelper.GetMainMappings(); foreach (var mapping in mappings) { mapping.Visit(modelBuilder); } } }

การทำแผนที่ทำได้ผ่านคลาส MappingsHelper :

 public static class MappingsHelper { public static IEnumerable<IMap> GetMainMappings() { var assemblyTypes = typeof(UserMap).GetTypeInfo().Assembly.DefinedTypes; var mappings = assemblyTypes // ReSharper disable once AssignNullToNotNullAttribute .Where(t => t.Namespace != null && t.Namespace.Contains(typeof(UserMap).Namespace)) .Where(t => typeof(IMap).GetTypeInfo().IsAssignableFrom(t)); mappings = mappings.Where(x => !x.IsAbstract); return mappings.Select(m => (IMap) Activator.CreateInstance(m.AsType())).ToArray(); } }

การแมปไปยังคลาสอยู่ในโฟลเดอร์ Maps และการแมปสำหรับ Expenses :

 public class ExpenseMap : IMap { public void Visit(ModelBuilder builder) { builder.Entity<Expense>() .ToTable("Expenses") .HasKey(x => x.Id); } }

อินเทอร์เฟซ IUnitOfWork :

 public interface IUnitOfWork : IDisposable { ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot); void Add<T>(T obj) where T: class ; void Update<T>(T obj) where T : class; void Remove<T>(T obj) where T : class; IQueryable<T> Query<T>() where T : class; void Commit(); Task CommitAsync(); void Attach<T>(T obj) where T : class; }

การนำไปใช้นั้นเป็นเสื้อคลุมสำหรับ EF DbContext :

 public class EFUnitOfWork : IUnitOfWork { private DbContext _context; public EFUnitOfWork(DbContext context) { _context = context; } public DbContext Context => _context; public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot) { return new DbTransaction(_context.Database.BeginTransaction(isolationLevel)); } public void Add<T>(T obj) where T : class { var set = _context.Set<T>(); set.Add(obj); } public void Update<T>(T obj) where T : class { var set = _context.Set<T>(); set.Attach(obj); _context.Entry(obj).State = EntityState.Modified; } void IUnitOfWork.Remove<T>(T obj) { var set = _context.Set<T>(); set.Remove(obj); } public IQueryable<T> Query<T>() where T : class { return _context.Set<T>(); } public void Commit() { _context.SaveChanges(); } public async Task CommitAsync() { await _context.SaveChangesAsync(); } public void Attach<T>(T newUser) where T : class { var set = _context.Set<T>(); set.Attach(newUser); } public void Dispose() { _context = null; } }

อินเทอร์เฟซ ITransaction ใช้ในแอปพลิเคชันนี้จะไม่ถูกใช้งาน:

 public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

การใช้งานเพียงแค่ตัดธุรกรรม EF :

 public class DbTransaction : ITransaction { private readonly IDbContextTransaction _efTransaction; public DbTransaction(IDbContextTransaction efTransaction) { _efTransaction = efTransaction; } public void Commit() { _efTransaction.Commit(); } public void Rollback() { _efTransaction.Rollback(); } public void Dispose() { _efTransaction.Dispose(); } }

นอกจากนี้ ในขั้นตอนนี้ สำหรับการทดสอบหน่วย จำเป็นต้องมีอินเทอร์เฟซ ISecurityContext ซึ่งกำหนดผู้ใช้ปัจจุบันของ API (โครงการคือ Expenses.Security ):

 public interface ISecurityContext { User User { get; } bool IsAdministrator { get; } }

ถัดไป คุณต้องกำหนดอินเทอร์เฟซและการใช้งานตัวประมวลผลการสืบค้น ซึ่งจะประกอบด้วยตรรกะทางธุรกิจทั้งหมดสำหรับการทำงานกับต้นทุน ในกรณีของเรา IExpensesQueryProcessor และ ExpensesQueryProcessor :

 public interface IExpensesQueryProcessor { IQueryable<Expense> Get(); Expense Get(int id); Task<Expense> Create(CreateExpenseModel model); Task<Expense> Update(int id, UpdateExpenseModel model); Task Delete(int id); } public class ExpensesQueryProcessor : IExpensesQueryProcessor { public IQueryable<Expense> Get() { throw new NotImplementedException(); } public Expense Get(int id) { throw new NotImplementedException(); } public Task<Expense> Create(CreateExpenseModel model) { throw new NotImplementedException(); } public Task<Expense> Update(int id, UpdateExpenseModel model) { throw new NotImplementedException(); } public Task Delete(int id) { throw new NotImplementedException(); } }

ขั้นตอนต่อไปคือการกำหนดค่าแอสเซมบลี Expenses.Queries.Tests ฉันติดตั้งไลบรารีต่อไปนี้:

  • ขั้นต่ำ
  • FluentAssertions

จากนั้นในชุด Expenses.Queries.Tests เรากำหนดอุปกรณ์จับยึดสำหรับการทดสอบหน่วยและอธิบายการทดสอบหน่วยของเรา:

 public class ExpensesQueryProcessorTests { private Mock<IUnitOfWork> _uow; private List<Expense> _expenseList; private IExpensesQueryProcessor _query; private Random _random; private User _currentUser; private Mock<ISecurityContext> _securityContext; public ExpensesQueryProcessorTests() { _random = new Random(); _uow = new Mock<IUnitOfWork>(); _expenseList = new List<Expense>(); _uow.Setup(x => x.Query<Expense>()).Returns(() => _expenseList.AsQueryable()); _currentUser = new User{Id = _random.Next()}; _securityContext = new Mock<ISecurityContext>(MockBehavior.Strict); _securityContext.Setup(x => x.User).Returns(_currentUser); _securityContext.Setup(x => x.IsAdministrator).Returns(false); _query = new ExpensesQueryProcessor(_uow.Object, _securityContext.Object); } [Fact] public void GetShouldReturnAll() { _expenseList.Add(new Expense{UserId = _currentUser.Id}); var result = _query.Get().ToList(); result.Count.Should().Be(1); } [Fact] public void GetShouldReturnOnlyUserExpenses() { _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get().ToList(); result.Count().Should().Be(1); result[0].UserId.Should().Be(_currentUser.Id); } [Fact] public void GetShouldReturnAllExpensesForAdministrator() { _securityContext.Setup(x => x.IsAdministrator).Returns(true); _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get(); result.Count().Should().Be(2); } [Fact] public void GetShouldReturnAllExceptDeleted() { _expenseList.Add(new Expense { UserId = _currentUser.Id }); _expenseList.Add(new Expense { UserId = _currentUser.Id, IsDeleted = true}); var result = _query.Get(); result.Count().Should().Be(1); } [Fact] public void GetShouldReturnById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); var result = _query.Get(expense.Id); result.Should().Be(expense); } [Fact] public void GetShouldThrowExceptionIfExpenseOfOtherUser() { var expense = new Expense { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow<NotFoundException>(); } [Fact] public void GetShouldThrowExceptionIfItemIsNotFoundById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); Action get = () => { _query.Get(_random.Next()); }; get.ShouldThrow<NotFoundException>(); } [Fact] public void GetShouldThrowExceptionIfUserIsDeleted() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id, IsDeleted = true}; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow<NotFoundException>(); } [Fact] public async Task CreateShouldSaveNew() { var model = new CreateExpenseModel { Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now }; var result = await _query.Create(model); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); result.UserId.Should().Be(_currentUser.Id); _uow.Verify(x => x.Add(result)); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task UpdateShouldUpdateFields() { var user = new Expense {Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); var model = new UpdateExpenseModel { Comment = _random.Next().ToString(), Description = _random.Next().ToString(), Amount = _random.Next(), Date = DateTime.Now }; var result = await _query.Update(user.Id, model); result.Should().Be(user); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); _uow.Verify(x => x.CommitAsync()); } [Fact] public void UpdateShoudlThrowExceptionIfItemIsNotFound() { Action create = () => { var result = _query.Update(_random.Next(), new UpdateExpenseModel()).Result; }; create.ShouldThrow<NotFoundException>(); } [Fact] public async Task DeleteShouldMarkAsDeleted() { var user = new Expense() { Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); await _query.Delete(user.Id); user.IsDeleted.Should().BeTrue(); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task DeleteShoudlThrowExceptionIfItemIsNotBelongTheUser() { var expense = new Expense() { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action execute = () => { _query.Delete(expense.Id).Wait(); }; execute.ShouldThrow<NotFoundException>(); } [Fact] public void DeleteShoudlThrowExceptionIfItemIsNotFound() { Action execute = () => { _query.Delete(_random.Next()).Wait(); }; execute.ShouldThrow<NotFoundException>(); }

หลังจากอธิบายการทดสอบหน่วยแล้ว การใช้งานตัวประมวลผลการสืบค้นจะได้รับการอธิบาย:

 public class ExpensesQueryProcessor : IExpensesQueryProcessor { private readonly IUnitOfWork _uow; private readonly ISecurityContext _securityContext; public ExpensesQueryProcessor(IUnitOfWork uow, ISecurityContext securityContext) { _uow = uow; _securityContext = securityContext; } public IQueryable<Expense> Get() { var query = GetQuery(); return query; } private IQueryable<Expense> GetQuery() { var q = _uow.Query<Expense>() .Where(x => !x.IsDeleted); if (!_securityContext.IsAdministrator) { var userId = _securityContext.User.Id; q = q.Where(x => x.UserId == userId); } return q; } public Expense Get(int id) { var user = GetQuery().FirstOrDefault(x => x.Id == id); if (user == null) { throw new NotFoundException("Expense is not found"); } return user; } public async Task<Expense> Create(CreateExpenseModel model) { var item = new Expense { UserId = _securityContext.User.Id, Amount = model.Amount, Comment = model.Comment, Date = model.Date, Description = model.Description, }; _uow.Add(item); await _uow.CommitAsync(); return item; } public async Task<Expense> Update(int id, UpdateExpenseModel model) { var expense = GetQuery().FirstOrDefault(x => x.Id == id); if (expense == null) { throw new NotFoundException("Expense is not found"); } expense.Amount = model.Amount; expense.Comment = model.Comment; expense.Description = model.Description; expense.Date = model.Date; await _uow.CommitAsync(); return expense; } public async Task Delete(int id) { var user = GetQuery().FirstOrDefault(u => u.Id == id); if (user == null) { throw new NotFoundException("Expense is not found"); } if (user.IsDeleted) return; user.IsDeleted = true; await _uow.CommitAsync(); } }

เมื่อตรรกะทางธุรกิจพร้อมแล้ว ฉันจะเริ่มเขียนการทดสอบการรวม API เพื่อกำหนดสัญญา API

ขั้นตอนแรกคือการเตรียมโครงการ Expenses.Api.IntegrationTests

  1. ติดตั้งแพ็คเกจ nuget:
    • FluentAssertions
    • ขั้นต่ำ
    • Microsoft.AspNetCore.TestHost
  2. กำหนดโครงสร้างโครงการ
    โครงสร้างโครงการ
  3. สร้าง CollectionDefinition ด้วยความช่วยเหลือซึ่งเราจะกำหนดทรัพยากรที่จะสร้างขึ้นเมื่อเริ่มต้นการทดสอบแต่ละครั้ง และจะถูกทำลายเมื่อสิ้นสุดการทดสอบแต่ละครั้ง
 [CollectionDefinition("ApiCollection")] public class DbCollection : ICollectionFixture<ApiServer> { } ~~~ And define our test server and the client to it with the already authenticated user by default:

ApiServer คลาสสาธารณะ: IDisposable { สตริง const สาธารณะ Username = “admin”; สตริง const สาธารณะ รหัสผ่าน = “ผู้ดูแลระบบ”;

 private IConfigurationRoot _config; public ApiServer() { _config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); Server = new TestServer(new WebHostBuilder().UseStartup<Startup>()); Client = GetAuthenticatedClient(Username, Password); } public HttpClient GetAuthenticatedClient(string username, string password) { var client = Server.CreateClient(); var response = client.PostAsync("/api/Login/Authenticate", new JsonContent(new LoginModel {Password = password, Username = username})).Result; response.EnsureSuccessStatusCode(); var data = JsonConvert.DeserializeObject<UserWithTokenModel>(response.Content.ReadAsStringAsync().Result); client.DefaultRequestHeaders.Add("Authorization", "Bearer " + data.Token); return client; } public HttpClient Client { get; private set; } public TestServer Server { get; private set; } public void Dispose() { if (Client != null) { Client.Dispose(); Client = null; } if (Server != null) { Server.Dispose(); Server = null; } } } ~~~

เพื่อความสะดวกในการทำงานกับคำขอ HTTP ในการทดสอบการรวม ฉันได้เขียนผู้ช่วย:

 public class HttpClientWrapper { private readonly HttpClient _client; public HttpClientWrapper(HttpClient client) { _client = client; } public HttpClient Client => _client; public async Task<T> PostAsync<T>(string url, object body) { var response = await _client.PostAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); var respnoseText = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject<T>(respnoseText); return data; } public async Task PostAsync(string url, object body) { var response = await _client.PostAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); } public async Task<T> PutAsync<T>(string url, object body) { var response = await _client.PutAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); var respnoseText = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject<T>(respnoseText); return data; } }

ในขั้นตอนนี้ ฉันต้องกำหนดสัญญา REST API สำหรับแต่ละเอนทิตี ฉันจะเขียนมันสำหรับค่าใช้จ่าย REST API:

URL วิธี ประเภทของร่างกาย ประเภทผลลัพธ์ คำอธิบาย
ค่าใช้จ่าย รับ - DataResult<ExpenseModel> รับค่าใช้จ่ายทั้งหมดด้วยการใช้ตัวกรองและตัวเรียงลำดับที่เป็นไปได้ในพารามิเตอร์การค้นหา "คำสั่ง"
ค่าใช้จ่าย/{id} รับ - รุ่นค่าใช้จ่าย รับค่าใช้จ่ายโดย id
ค่าใช้จ่าย โพสต์ CreateExpenseModel รุ่นค่าใช้จ่าย สร้างบันทึกค่าใช้จ่ายใหม่
ค่าใช้จ่าย/{id} ใส่ UpdateExpenseModel รุ่นค่าใช้จ่าย อัพเดทค่าใช้จ่ายที่มีอยู่

เมื่อคุณขอรายการค่าใช้จ่าย คุณสามารถใช้คำสั่งการกรองและการเรียงลำดับต่างๆ ได้โดยใช้ไลบรารี AutoQueryable ตัวอย่างแบบสอบถามที่มีการกรองและการเรียงลำดับ:

/expenses?commands=take=25%26amount%3E=12%26orderbydesc=date

ค่าพารามิเตอร์คำสั่งถอดรหัสคือ take=25&amount>=12&orderbydesc=date ดังนั้นเราจึงสามารถค้นหาเพจ การกรอง และการเรียงลำดับส่วนต่างๆ ในแบบสอบถามได้ ตัวเลือกแบบสอบถามทั้งหมดคล้ายกับไวยากรณ์ OData มาก แต่น่าเสียดายที่ OData ยังไม่พร้อมสำหรับ .NET Core ดังนั้นฉันจึงใช้ไลบรารีอื่นที่เป็นประโยชน์

ด้านล่างแสดงโมเดลทั้งหมดที่ใช้ใน API นี้:

 public class DataResult<T> { public T[] Data { get; set; } public int Total { get; set; } } public class ExpenseModel { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public string Username { get; set; } } public class CreateExpenseModel { [Required] public DateTime Date { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, int.MaxValue)] public decimal Amount { get; set; } [Required] public string Comment { get; set; } } public class UpdateExpenseModel { [Required] public DateTime Date { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, int.MaxValue)] public decimal Amount { get; set; } [Required] public string Comment { get; set; } }

โมเดล CreateExpenseModel และ UpdateExpenseModel ใช้แอ็ตทริบิวต์คำอธิบายประกอบข้อมูล เพื่อดำเนินการตรวจสอบอย่างง่ายที่ระดับ REST API ผ่านแอ็ตทริบิวต์

ถัดไป สำหรับแต่ละวิธี HTTP โฟลเดอร์แยกต่างหากจะถูกสร้างขึ้นในโปรเจ็กต์ และไฟล์ในนั้นจะถูกสร้างโดยฟิกซ์เจอร์สำหรับเมธอด HTTP แต่ละวิธีที่ได้รับการสนับสนุนโดยทรัพยากร:

โครงสร้างโฟลเดอร์ค่าใช้จ่าย

การดำเนินการทดสอบการรวมเพื่อรับรายการค่าใช้จ่าย:

 [Collection("ApiCollection")] public class GetListShould { private readonly ApiServer _server; private readonly HttpClient _client; public GetListShould(ApiServer server) { _server = server; _client = server.Client; } public static async Task<DataResult<ExpenseModel>> Get(HttpClient client) { var response = await client.GetAsync($"api/Expenses"); response.EnsureSuccessStatusCode(); var responseText = await response.Content.ReadAsStringAsync(); var items = JsonConvert.DeserializeObject<DataResult<ExpenseModel>>(responseText); return items; } [Fact] public async Task ReturnAnyList() { var items = await Get(_client); items.Should().NotBeNull(); } }

การดำเนินการทดสอบการรวมเพื่อรับข้อมูลค่าใช้จ่ายโดย id:

 [Collection("ApiCollection")] public class GetItemShould { private readonly ApiServer _server; private readonly HttpClient _client; private Random _random; public GetItemShould(ApiServer server) { _server = server; _client = _server.Client; _random = new Random(); } [Fact] public async Task ReturnItemById() { var item = await new PostShould(_server).CreateNew(); var result = await GetById(_client, item.Id); result.Should().NotBeNull(); } public static async Task<ExpenseModel> GetById(HttpClient client, int id) { var response = await client.GetAsync(new Uri($"api/Expenses/{id}", UriKind.Relative)); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<ExpenseModel>(result); } [Fact] public async Task ShouldReturn404StatusIfNotFound() { var response = await _client.GetAsync(new Uri($"api/Expenses/-1", UriKind.Relative)); response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.NotFound); } }

การดำเนินการทดสอบการรวมเพื่อสร้างค่าใช้จ่าย:

 [Collection("ApiCollection")] public class PostShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private Random _random; public PostShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task<ExpenseModel> CreateNew() { var requestItem = new CreateExpenseModel() { Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now.AddMinutes(-15), Description = _random.Next().ToString() }; var createdItem = await _client.PostAsync<ExpenseModel>("api/Expenses", requestItem); createdItem.Id.Should().BeGreaterThan(0); createdItem.Amount.Should().Be(requestItem.Amount); createdItem.Comment.Should().Be(requestItem.Comment); createdItem.Date.Should().Be(requestItem.Date); createdItem.Description.Should().Be(requestItem.Description); createdItem.Username.Should().Be("admin admin"); return createdItem; } }

การดำเนินการทดสอบการรวมเพื่อเปลี่ยนแปลงค่าใช้จ่าย:

 [Collection("ApiCollection")] public class PutShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private readonly Random _random; public PutShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task UpdateExistingItem() { var item = await new PostShould(_server).CreateNew(); var requestItem = new UpdateExpenseModel { Date = DateTime.Now, Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString() }; await _client.PutAsync<ExpenseModel>($"api/Expenses/{item.Id}", requestItem); var updatedItem = await GetItemShould.GetById(_client.Client, item.Id); updatedItem.Date.Should().Be(requestItem.Date); updatedItem.Description.Should().Be(requestItem.Description); updatedItem.Amount.Should().Be(requestItem.Amount); updatedItem.Comment.Should().Contain(requestItem.Comment); } }

การดำเนินการทดสอบการรวมเพื่อลบค่าใช้จ่าย:

 [Collection("ApiCollection")] public class DeleteShould { private readonly ApiServer _server; private readonly HttpClient _client; public DeleteShould(ApiServer server) { _server = server; _client = server.Client; } [Fact] public async Task DeleteExistingItem() { var item = await new PostShould(_server).CreateNew(); var response = await _client.DeleteAsync(new Uri($"api/Expenses/{item.Id}", UriKind.Relative)); response.EnsureSuccessStatusCode(); } }

ณ จุดนี้ เราได้กำหนดสัญญา REST API อย่างสมบูรณ์แล้ว และตอนนี้ฉันสามารถเริ่มใช้งานโดยใช้ ASP.NET Core ได้

การใช้งาน API

จัดทำโครงการ ค่าใช้จ่าย. สำหรับสิ่งนี้ ฉันต้องติดตั้งไลบรารีต่อไปนี้:

  • AutoMapper
  • AutoQueryable.AspNetCore.Filter
  • Microsoft.ApplicationInsights.AspNetCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.SqlServer.Design
  • Microsoft.EntityFrameworkCore.Tools
  • Swashbuckle.AspNetCore

หลังจากนั้น คุณต้องเริ่มสร้างการโยกย้ายเริ่มต้นสำหรับฐานข้อมูลโดยเปิด Package Manager Console เปลี่ยนเป็นโปรเจ็กต์ Expenses.Data.Access (เนื่องจากบริบท EF อยู่ที่นั่น) และรันคำสั่ง Add-Migration InitialCreate :

คอนโซลตัวจัดการแพ็คเกจ

ในขั้นตอนต่อไป ให้เตรียมไฟล์คอนฟิกูเรชัน appsettings.json ไว้ล่วงหน้า ซึ่งหลังจากการจัดเตรียมจะยังคงต้องถูกคัดลอกไปยังโปรเจ็กต์ Expenses.Api.IntegrationTests เพราะจากที่นั่น เราจะรัน API ของอินสแตนซ์การทดสอบ

 { "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "Data": { "main": "Data Source=.; Initial Catalog=expenses.main; Integrated Security=true; Max Pool Size=1000; Min Pool Size=12; Pooling=True;" }, "ApplicationInsights": { "InstrumentationKey": "Your ApplicationInsights key" } }

ส่วนการบันทึกจะถูกสร้างขึ้นโดยอัตโนมัติ ฉันเพิ่มส่วน Data เพื่อจัดเก็บสตริงการเชื่อมต่อไปยังฐานข้อมูลและคีย์ ApplicationInsights ของฉัน

การกำหนดค่าแอปพลิเคชัน

คุณต้องกำหนดค่าบริการต่าง ๆ ที่มีอยู่ในแอปพลิเคชันของเรา:

การเปิด ApplicationInsights : services.AddApplicationInsightsTelemetry(Configuration);

ลงทะเบียนบริการของคุณผ่านการโทร: ContainerSetup.Setup(services, Configuration);

ContainerSetup เป็นคลาสที่สร้างขึ้น ดังนั้นเราจึงไม่ต้องจัดเก็บการลงทะเบียนบริการทั้งหมดในคลาส Startup คลาสนี้อยู่ในโฟลเดอร์ IoC ของโปรเจ็กต์ Expenses:

 public static class ContainerSetup { public static void Setup(IServiceCollection services, IConfigurationRoot configuration) { AddUow(services, configuration); AddQueries(services); ConfigureAutoMapper(services); ConfigureAuth(services); } private static void ConfigureAuth(IServiceCollection services) { services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<ITokenBuilder, TokenBuilder>(); services.AddScoped<ISecurityContext, SecurityContext>(); } private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient<IAutoMapper, AutoMapperAdapter>(); } private static void AddUow(IServiceCollection services, IConfigurationRoot configuration) { var connectionString = configuration["Data:main"]; services.AddEntityFrameworkSqlServer(); services.AddDbContext<MainDbContext>(options => options.UseSqlServer(connectionString)); services.AddScoped<IUnitOfWork>(ctx => new EFUnitOfWork(ctx.GetRequiredService<MainDbContext>())); services.AddScoped<IActionTransactionHelper, ActionTransactionHelper>(); services.AddScoped<UnitOfWorkFilterAttribute>(); } private static void AddQueries(IServiceCollection services) { var exampleProcessorType = typeof(UsersQueryProcessor); var types = (from t in exampleProcessorType.GetTypeInfo().Assembly.GetTypes() where t.Namespace == exampleProcessorType.Namespace && t.GetTypeInfo().IsClass && t.GetTypeInfo().GetCustomAttribute<CompilerGeneratedAttribute>() == null select t).ToArray(); foreach (var type in types) { var interfaceQ = type.GetTypeInfo().GetInterfaces().First(); services.AddScoped(interfaceQ, type); } } }

โค้ดเกือบทั้งหมดในคลาสนี้อธิบายได้ด้วยตัวของมันเอง แต่ฉันอยากจะพูดถึงเมธอด ConfigureAutoMapper อีกสักหน่อย

 private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient<IAutoMapper, AutoMapperAdapter>(); }

เมธอดนี้ใช้คลาสตัวช่วยเพื่อค้นหาการแมปทั้งหมดระหว่างโมเดลและเอนทิตี และในทางกลับกัน และรับอินเทอร์เฟซ IMapper เพื่อสร้าง wrapper IAutoMapper ที่จะใช้ในคอนโทรลเลอร์ ไม่มีอะไรพิเศษเกี่ยวกับ Wrapper นี้—แต่มีอินเทอร์เฟซที่สะดวกสำหรับวิธีการ AutoMapper

 public class AutoMapperAdapter : IAutoMapper { private readonly IMapper _mapper; public AutoMapperAdapter(IMapper mapper) { _mapper = mapper; } public IConfigurationProvider Configuration => _mapper.ConfigurationProvider; public T Map<T>(object objectToMap) { return _mapper.Map<T>(objectToMap); } public TResult[] Map<TSource, TResult>(IEnumerable<TSource> sourceQuery) { return sourceQuery.Select(x => _mapper.Map<TResult>(x)).ToArray(); } public IQueryable<TResult> Map<TSource, TResult>(IQueryable<TSource> sourceQuery) { return sourceQuery.ProjectTo<TResult>(_mapper.ConfigurationProvider); } public void Map<TSource, TDestination>(TSource source, TDestination destination) { _mapper.Map(source, destination); } }

ในการกำหนดค่า AutoMapper จะมีการใช้คลาสตัวช่วย ซึ่งมีหน้าที่ค้นหาการแมปสำหรับคลาสเนมสเปซเฉพาะ การแมปทั้งหมดจะอยู่ในโฟลเดอร์ Expenses/Maps:

 public static class AutoMapperConfigurator { private static readonly object Lock = new object(); private static MapperConfiguration _configuration; public static MapperConfiguration Configure() { lock (Lock) { if (_configuration != null) return _configuration; var thisType = typeof(AutoMapperConfigurator); var configInterfaceType = typeof(IAutoMapperTypeConfigurator); var configurators = thisType.GetTypeInfo().Assembly.GetTypes() .Where(x => !string.IsNullOrWhiteSpace(x.Namespace)) // ReSharper disable once AssignNullToNotNullAttribute .Where(x => x.Namespace.Contains(thisType.Namespace)) .Where(x => x.GetTypeInfo().GetInterface(configInterfaceType.Name) != null) .Select(x => (IAutoMapperTypeConfigurator)Activator.CreateInstance(x)) .ToArray(); void AggregatedConfigurator(IMapperConfigurationExpression config) { foreach (var configurator in configurators) { configurator.Configure(config); } } _configuration = new MapperConfiguration(AggregatedConfigurator); return _configuration; } } }

All mappings must implement a specific interface:

 public interface IAutoMapperTypeConfigurator { void Configure(IMapperConfigurationExpression configuration); }

An example of mapping from entity to model:

 public class ExpenseMap : IAutoMapperTypeConfigurator { public void Configure(IMapperConfigurationExpression configuration) { var map = configuration.CreateMap<Expense, ExpenseModel>(); map.ForMember(x => x.Username, x => x.MapFrom(y => y.User.FirstName + " " + y.User.LastName)); } }

Also, in the Startup.ConfigureServices method, authentication through JWT Bearer tokens is configured:

 services.AddAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser().Build()); });

And the services registered the implementation of ISecurityContext , which will actually be used to determine the current user:

 public class SecurityContext : ISecurityContext { private readonly IHttpContextAccessor _contextAccessor; private readonly IUnitOfWork _uow; private User _user; public SecurityContext(IHttpContextAccessor contextAccessor, IUnitOfWork uow) { _contextAccessor = contextAccessor; _uow = uow; } public User User { get { if (_user != null) return _user; var username = _contextAccessor.HttpContext.User.Identity.Name; _user = _uow.Query<User>() .Where(x => x.Username == username) .Include(x => x.Roles) .ThenInclude(x => x.Role) .FirstOrDefault(); if (_user == null) { throw new UnauthorizedAccessException("User is not found"); } return _user; } } public bool IsAdministrator { get { return User.Roles.Any(x => x.Role.Name == Roles.Administrator); } } }

Also, we changed the default MVC registration a little in order to use a custom error filter to convert exceptions to the right error codes:

services.AddMvc(options => { options.Filters.Add(new ApiExceptionFilter()); });

Implementing the ApiExceptionFilter filter:

public class ApiExceptionFilter : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { if (context.Exception is NotFoundException) { // handle explicit 'known' API errors var ex = context.Exception as NotFoundException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } else if (context.Exception is BadRequestException) { // handle explicit 'known' API errors var ex = context.Exception as BadRequestException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } else if (context.Exception is UnauthorizedAccessException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else if (context.Exception is ForbiddenException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } base.OnException(context); } }

สิ่งสำคัญคือต้องไม่ลืม Swagger เพื่อให้ได้คำอธิบาย API ที่ยอดเยี่ยมสำหรับนักพัฒนา ASP.net รายอื่น:

 services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info {Title = "Expenses", Version = "v1"}); c.OperationFilter<AuthorizationHeaderParameterOperationFilter>(); }); 

เอกสาร API

เมธอด Startup.Configure เพิ่มการเรียกไปยังเมธอด InitDatabase ซึ่งจะย้ายฐานข้อมูลโดยอัตโนมัติจนกว่าจะมีการโยกย้ายครั้งล่าสุด:

 private void InitDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var context = serviceScope.ServiceProvider.GetService<MainDbContext>(); context.Database.Migrate(); } }

Swagger จะเปิดขึ้นก็ต่อเมื่อแอปพลิเคชันทำงานในสภาพแวดล้อมการพัฒนาและไม่ต้องการการตรวจสอบสิทธิ์เพื่อเข้าถึง:

 app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); });

ต่อไป เราเชื่อมต่อการตรวจสอบ (รายละเอียดสามารถพบได้ในที่เก็บ):

ConfigureAuthentication(app);

ณ จุดนี้ คุณสามารถเรียกใช้การทดสอบการรวมและตรวจดูให้แน่ใจว่าคอมไพล์ทุกอย่างแล้ว แต่ไม่มีอะไรทำงาน และไปที่ตัวควบคุม ExpensesController

หมายเหตุ: ตัวควบคุมทั้งหมดจะอยู่ในโฟลเดอร์ Expenses/Server และแบ่งออกเป็นสองโฟลเดอร์ตามเงื่อนไข: Controllers และ RestApi ในโฟลเดอร์ ตัวควบคุมคือตัวควบคุมที่ทำงานเป็นตัวควบคุมใน MVC รุ่นเก่า—เช่น ส่งคืนมาร์กอัป และใน RestApi ตัวควบคุม REST

คุณต้องสร้างคลาส Expenses/Server/RestApi/ExpensesController และสืบทอดจากคลาส Controller:

 public class ExpensesController : Controller { }

ถัดไป กำหนดค่าการกำหนดเส้นทางของประเภท ~ / api / Expenses โดยทำเครื่องหมายคลาสด้วยแอตทริบิวต์ [Route ("api / [controller]")]

ในการเข้าถึงตรรกะทางธุรกิจและผู้ทำแผนที่ คุณต้องใส่บริการต่อไปนี้:

 private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; }

ในขั้นตอนนี้ คุณสามารถเริ่มใช้วิธีการได้ วิธีแรกคือการรับรายการค่าใช้จ่าย:

 [HttpGet] [QueryaCollectionDefinitionbleResult] public IQueryable<ExpenseModel> Get() { var result = _query.Get(); var models = _mapper.Map<Expense, ExpenseModel>(result); return models; }

การนำวิธีการไปใช้นั้นง่ายมาก เราได้รับแบบสอบถามไปยังฐานข้อมูลซึ่งถูกแมปใน IQueryable <ExpenseModel> จาก ExpensesQueryProcessor ซึ่งจะส่งคืนผลลัพธ์

แอตทริบิวต์ที่กำหนดเองที่นี่คือ QueryableResult ซึ่งใช้ไลบรารี AutoQueryable เพื่อจัดการเพจ การกรอง และการเรียงลำดับทางฝั่งเซิร์ฟเวอร์ แอตทริบิวต์อยู่ในโฟลเดอร์ Expenses/Filters ด้วยเหตุนี้ ตัวกรองนี้จึงส่งคืนข้อมูลประเภท DataResult <ExpenseModel> ไปยังไคลเอ็นต์ API

 public class QueryableResult : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Exception != null) return; dynamic query = ((ObjectResult)context.Result).Value; if (query == null) throw new Exception("Unable to retreive value of IQueryable from context result."); Type entityType = query.GetType().GenericTypeArguments[0]; var commands = context.HttpContext.Request.Query.ContainsKey("commands") ? context.HttpContext.Request.Query["commands"] : new StringValues(); var data = QueryableHelper.GetAutoQuery(commands, entityType, query, new AutoQueryableProfile {UnselectableProperties = new string[0]}); var total = System.Linq.Queryable.Count(query); context.Result = new OkObjectResult(new DataResult{Data = data, Total = total}); } }

นอกจากนี้ มาดูการนำวิธีการ Post ไปใช้สร้างโฟลว์:

 [HttpPost] [ValidateModel] public async Task<ExpenseModel> Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map<ExpenseModel>(item); return model; }

ที่นี่ คุณควรให้ความสนใจกับแอตทริบิวต์ ValidateModel ซึ่งทำการตรวจสอบความถูกต้องของข้อมูลที่ป้อนอย่างง่ายตามแอตทริบิวต์คำอธิบายประกอบข้อมูล และทำผ่านการตรวจสอบ MVC ในตัว

 public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }

รหัสแบบเต็มของ ExpensesController :

 [Route("api/[controller]")] public class ExpensesController : Controller { private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; } [HttpGet] [QueryableResult] public IQueryable<ExpenseModel> Get() { var result = _query.Get(); var models = _mapper.Map<Expense, ExpenseModel>(result); return models; } [HttpGet("{id}")] public ExpenseModel Get(int id) { var item = _query.Get(id); var model = _mapper.Map<ExpenseModel>(item); return model; } [HttpPost] [ValidateModel] public async Task<ExpenseModel> Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map<ExpenseModel>(item); return model; } [HttpPut("{id}")] [ValidateModel] public async Task<ExpenseModel> Put(int id, [FromBody]UpdateExpenseModel requestModel) { var item = await _query.Update(id, requestModel); var model = _mapper.Map<ExpenseModel>(item); return model; } [HttpDelete("{id}")] public async Task Delete(int id) { await _query.Delete(id); } }

บทสรุป

ฉันจะเริ่มต้นด้วยปัญหา: ปัญหาหลักคือความซับซ้อนของการกำหนดค่าเริ่มต้นของโซลูชันและการทำความเข้าใจเลเยอร์ของแอปพลิเคชัน แต่ด้วยความซับซ้อนที่เพิ่มขึ้นของแอปพลิเคชันความซับซ้อนของระบบเกือบจะไม่เปลี่ยนแปลงซึ่งเป็นเรื่องใหญ่ บวกเมื่อมาพร้อมกับระบบดังกล่าว และเป็นสิ่งสำคัญมากที่เรามี API ซึ่งมีชุดการทดสอบการรวมและชุดการทดสอบหน่วยที่สมบูรณ์สำหรับตรรกะทางธุรกิจ ตรรกะทางธุรกิจแยกออกจากเทคโนโลยีเซิร์ฟเวอร์ที่ใช้อย่างสมบูรณ์และสามารถทดสอบได้อย่างสมบูรณ์ โซลูชันนี้เหมาะอย่างยิ่งสำหรับระบบที่มี API ที่ซับซ้อนและตรรกะทางธุรกิจที่ซับซ้อน

หากคุณต้องการสร้างแอป Angular ที่ใช้ API ของคุณ ลองดู Angular 5 และ ASP.NET Core โดย Toptaler Pablo Albella