
2024年在線市場平臺的11大最佳支付解決方案
JSON Response (for GET requests):
{
[
{ "id": 1, "name": "Fruits and Vegetables" },
{ "id": 2, "name": "Breads" },
… // Other categories
]
}
API endpoint: /api/products
JSON Response (for GET requests):
{
[
{
"id": 1,
"name": "Sugar",
"quantityInPackage": 1,
"unitOfMeasurement": "KG"
"category": {
"id": 3,
"name": "Sugar"
}
},
… // Other products
]
}
讓我們開始編寫應用程序。
首先,我們必須為Web服務創建文件夾結構,然后我們必須使用.NET CLI工具來構建基本的Web API。打開終端或命令提示符(取決于您使用的操作系統),并依次鍵入以下命令:
mkdir src/Supermarket.API
cd src/Supermarket.API
dotnet new webapi
前兩個命令只是為API創建一個新目錄,然后將當前位置更改為新文件夾。最后一個遵循Web API模板生成一個新項目,這是我們正在開發的應用程序。您可以閱讀有關這些命令和其他項目模板的更多信息,并可以通過檢查此鏈接來生成其他項目模板。
現在,新目錄將具有以下結構:
項目結構
結構概述
ASP.NET Core應用程序由在類中配置的一組中間件(應用程序流水線中的小塊應用程序,用于處理請求和響應)組成Startup。如果您以前已經使用過Express.js之類的框架,那么這個概念對您來說并不是什么新鮮事物。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
當應用程序啟動時,將調用類中的Main** **方法Program。它使用啟動配置創建默認的Web主機,通過HTTP通過特定端口(默認情況下,HTTP為5000,HTTPS為5001)公開應用程序。
namespace Supermarket.API
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
看一下文件夾中的ValuesController類Controllers。它公開了API通過路由接收請求時將調用的方法/api/values。
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
如果您不了解此代碼的某些部分,請不要擔心。在開發必要的API端點時,我將詳細介紹每一個。現在,只需刪除此類,因為我們不會使用它。
我將應用一些設計概念,以使應用程序簡單易維護。
編寫可以由您自己理解和維護的代碼并不難,但是您必須牢記您將成為團隊的一部分。如果您不注意如何編寫代碼,那么結果將是一個龐然大物,這將使您和您的團隊成員頭痛不已。聽起來很極端吧?但是相信我,這就是事實。
衡量好代碼的標準是WTF的頻率。原圖來自smitty42,發表于filckr。該圖遵循CC-BY-2.0。
在Supermarket.API目錄中,創建一個名為的新文件夾Domain。在新的領域文件夾中,創建另一個名為的文件夾Models。我們必須添加到此文件夾的第一個模型是Category。最初,它將是一個簡單的Plain Old CLR Object(POCO)類。這意味著該類將僅具有描述其基本信息的屬性。
using System.Collections.Generic;
namespace Supermarket.API.Domain.Models
{
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Product> Products { get; set; } = new List<Product>();
}
}
該類具有一個Id** 屬性(用于標識類別)和一個Name屬性。以及一個Products 屬性。最后一個屬性將由Entity Framework Core使用**,大多數ASP.NET Core應用程序使用ORM將數據持久化到數據庫中,以映射類別和產品之間的關系。由于類別具有許多相關產品,因此在面向對象的編程方面也具有合理的思維能力。
我們還必須創建產品模型。在同一文件夾中,添加一個新Product類。
namespace Supermarket.API.Domain.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public short QuantityInPackage { get; set; }
public EUnitOfMeasurement UnitOfMeasurement { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
}
該產品還具有ID和名稱的屬性。屬性QuantityInPackage,它告訴我們一包中有多少個產品單位(請記住應用范圍的餅干示例)和一個UnitOfMeasurement** 屬性,這是表示一個枚舉類型,它表示可能的度量單位的枚舉。最后兩個屬性,CategoryId **和Category將由ORM用于映射的產品和類別之間的關系。它表明一種產品只有一個類別。
讓我們定義領域模型的最后一部分,EUnitOfMeasurement** **枚舉。
按照慣例,枚舉不需要在名稱前以“ E”開頭,但是在某些庫和框架中,您會發現此前綴是將枚舉與接口和類區分開的一種方式。
using System.ComponentModel;
namespace Supermarket.API.Domain.Models
{
public enum EUnitOfMeasurement : byte
{
[Description("UN")]
Unity = 1,
[Description("MG")]
Milligram = 2,
[Description("G")]
Gram = 3,
[Description("KG")]
Kilogram = 4,
[Description("L")]
Liter = 5
}
}
該代碼非常簡單。在這里,我們僅定義了幾種度量單位的可能性,但是,在實際的超市系統中,您可能具有許多其他度量單位,并且可能還有一個單獨的模型。
注意,【Description】特性應用于所有枚舉可能性。特性是一種在C#語言的類,接口,屬性和其他組件上定義元數據的方法。在這種情況下,我們將使用它來簡化產品API端點的響應,但是您現在不必關心它。我們待會再回到這里。
我們的基本模型已準備就緒,可以使用。現在,我們可以開始編寫將管理所有類別的API端點。
在Controllers文件夾中,添加一個名為的新類CategoriesController。
按照慣例,該文件夾中所有后綴為“ Controller”的類都將成為我們應用程序的控制器。這意味著他們將處理請求和響應。您必須從命名空間【Microsoft.AspNetCore.Mvc】繼承Controller。
命名空間由一組相關的類,接口,枚舉和結構組成。您可以將其視為類似于Java語言模塊或Java 程序包的東西。
新的控制器應通過路由/api/categories做出響應。我們通過Route** **在類名稱上方添加屬性,指定占位符來實現此目的,該占位符表示路由應按照慣例使用不帶控制器后綴的類名稱。
using Microsoft.AspNetCore.Mvc;
namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
}
}
讓我們開始處理GET請求。首先,當有人/api/categories通過GET動詞請求數據時,API需要返回所有類別。為此,我們可以創建類別服務。
從概念上講,服務基本上是定義用于處理某些業務邏輯的方法的類或接口。創建用于處理業務邏輯的服務是許多不同編程語言的一種常見做法,例如身份驗證和授權,付款,復雜的數據流,緩存和需要其他服務或模型之間進行某些交互的任務。
使用服務,我們可以將請求和響應處理與完成任務所需的真實邏輯隔離開來。
該服務,我們要創建將首先定義一個單獨的行為,或方法:一個list方法。我們希望該方法返回數據庫中所有現有的類別。
為簡單起見,在這篇博客中,我們將不處理數據分頁或過濾,(譯者注:基于RESTFul規范,提供了一套完整的分頁和過濾的規則)。將來,我將寫一篇文章,展示如何輕松處理這些功能。
為了定義C#(以及其他面向對象的語言,例如Java)中某事物的預期行為,我們定義一個interface。一個接口告訴某些事情應該如何工作,但是沒有實現行為的真實邏輯。邏輯在實現接口的類中實現。如果您不清楚此概念,請不要擔心。一段時間后您將了解它。
在Domain文件夾中,創建一個名為的新目錄Services。在此添加一個名為ICategoryService的接口。按照慣例,所有接口都應以C#中的大寫字母“ I”開頭。定義接口代碼,如下所示:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Domain.Services
{
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
}
}
該ListAsync方法的實現必須異步返回類別的可枚舉對象。
Task封裝返回的類表示異步。由于必須等待數據庫完成操作才能返回數據,因此我們需要考慮執行此過程可能需要一段時間,因此我們需要使用異步方法。另請注意“Async”后綴。這是一個約定,告訴我們的方法應異步執行。
我們有很多約定,對嗎?我個人喜歡它,因為它使應用程序易于閱讀,即使你在一家使用.NET技術的公司是新人。
“-好的,我們定義了此接口,但是它什么也沒做。有什么用?”
如果您來自Javascript或其他非強類型語言,則此概念可能看起來很奇怪。
接口使我們能夠從實際實現中抽象出所需的行為。使用稱為依賴注入的機制,我們可以實現這些接口并將它們與其他組件隔離。
基本上,當您使用依賴項注入時,您可以使用接口定義一些行為。然后,創建一個實現該接口的類。最后,將引用從接口綁定到您創建的類。
”-聽起來確實令人困惑。我們不能簡單地創建一個為我們做這些事情的類嗎?”
讓我們繼續實現我們的API,您將了解為什么使用這種方法。
更改CategoriesController代碼,如下所示:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
private readonly ICategoryService _categoryService;
public CategoriesController(ICategoryService categoryService)
{
_categoryService = categoryService;
}
[HttpGet]
public async Task<IEnumerable<Category>> GetAllAsync()
{
var categories = await _categoryService.ListAsync();
return categories;
}
}
}
我已經為控制器定義了一個構造函數(當創建一個類的新實例時會調用一個構造函數),并且它接收的實例ICategoryService。這意味著實例可以是任何實現服務接口的實例。我將此實例存儲在一個私有的只讀字段中_categoryService。我們將使用此字段訪問類別服務實現的方法。
順便說一下,下劃線前綴是表示字段的另一個通用約定。特別地,.NET的官方命名約定指南不建議使用此約定,但是這是一種非常普遍的做法,可以避免使用“ this”關鍵字來區分類字段和局部變量。我個人認為閱讀起來要干凈得多,并且許多框架和庫都使用此約定。
在構造函數下,我定義了用于處理請求的方法/api/categories。該HttpGet** **屬性告訴ASP.NET Core管道使用該屬性來處理GET請求(可以省略此屬性,但是最好編寫它以便于閱讀)。
該方法使用我們的CategoryService實例列出所有類別,然后將類別返回給客戶端。框架管道將數據序列化為JSON對象。IEnumerable類型告訴框架,我們想要返回一個類別的枚舉,而Task類型(使用async關鍵字修飾)告訴管道,這個方法應該異步執行。最后,當我們定義一個異步方法時,我們必須使用await關鍵字來處理需要一些時間的任務。
好的,我們定義了API的初始結構。現在,有必要真正實現類別服務。
在API的根文件夾(即Supermarket.API文件夾)中,創建一個名為的新文件夾Services。在這里,我們將放置所有服務實現。在新文件夾中,添加一個名為CategoryService的新類。更改代碼,如下所示:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
namespace Supermarket.API.Services
{
public class CategoryService : ICategoryService
{
public async Task<IEnumerable<Category>> ListAsync()
{
}
}
}
以上只是接口實現的基本代碼,我們暫時仍不處理任何邏輯。讓我們考慮一下列表方法應該如何實現。
我們需要訪問數據庫并返回所有類別,然后我們需要將此數據返回給客戶端。
服務類不是應該處理數據訪問的類。我們將使用一種稱為“倉儲模式”的設計模式,定義倉儲類,用于管理數據庫中的數據。
在使用倉儲模式時,我們定義了repository 類,該類基本上封裝了處理數據訪問的所有邏輯。這些倉儲類使方法可以列出,創建,編輯和刪除給定模型的對象,與操作集合的方式相同。在內部,這些方法與數據庫對話以執行CRUD操作,從而將數據庫訪問與應用程序的其余部分隔離開。
我們的服務需要調用類別倉儲,以獲取列表對象。
從概念上講,服務可以與一個或多個倉儲或其他服務“對話”以執行操作。
創建用于處理數據訪問邏輯的新定義似乎是多余的,但是您將在一段時間內看到將這種邏輯與服務類隔離是非常有利的。
讓我們創建一個倉儲,該倉儲負責與數據庫通信,作為持久化保存類別的一種方式。
在該Domain文件夾內,創建一個名為的新目錄Repositories。然后,添加一個名為的新接口ICategoryRespository。定義接口如下:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Domain.Repositories
{
public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
}
}
初始代碼基本上與服務接口的代碼相同。
定義了接口之后,我們可以返回服務類并使用的實例ICategoryRepository返回數據來完成實現list方法。
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Domain.Services;
namespace Supermarket.API.Services
{
public class CategoryService : ICategoryService
{
private readonly ICategoryRepository _categoryRepository;
public CategoryService(ICategoryRepository categoryRepository)
{
this._categoryRepository = categoryRepository;
}
public async Task<IEnumerable<Category>> ListAsync()
{
return await _categoryRepository.ListAsync();
}
}
}
現在,我們必須實現類別倉儲的真實邏輯。在這樣做之前,我們必須考慮如何訪問數據庫。
順便說一句,我們仍然沒有數據庫!
我們將使用Entity Framework Core(為簡單起見,我將其稱為EF Core)作為我們的數據庫ORM。該框架是ASP.NET Core的默認ORM,并公開了一個友好的API,該API使我們能夠將應用程序的類映射到數據庫表。
EF Core還允許我們先設計應用程序,然后根據我們在代碼中定義的內容生成數據庫。此技術稱為Code First。我們將使用Code First方法來生成數據庫(實際上,在此示例中,我將使用內存數據庫,但是您可以輕松地將其更改為像SQL Server或MySQL服務器這樣的實例數據庫)。
在API的根文件夾中,創建一個名為的新目錄Persistence。此目錄將包含我們訪問數據庫所需的所有內容,例如倉儲實現。
在新文件夾中,創建一個名為的新目錄Contexts,然后添加一個名為的新類AppDbContext。此類必須繼承DbContext,EF Core通過DBContext用來將您的模型映射到數據庫表的類。通過以下方式更改代碼:
using Microsoft.EntityFrameworkCore;
namespace Supermarket.API.Domain.Persistence.Contexts
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
}
}
我們添加到此類的構造函數負責通過依賴注入將數據庫配置傳遞給基類。稍后您將看到其工作原理。
現在,我們必須創建兩個DbSet屬性。這些屬性是將模型映射到數據庫表的集合(唯一對象的集合)。
另外,我們必須將模型的屬性映射到相應的列,指定哪些屬性是主鍵,哪些是外鍵,列類型等。我們可以使用稱為Fluent API的功能來覆蓋OnModelCreating方法,以指定數據庫映射。更改AppDbContext類,如下所示:
該代碼是如此直觀。
using Microsoft.EntityFrameworkCore;
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Persistence.Contexts
{
public class AppDbContext : DbContext
{
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Category>().ToTable("Categories");
builder.Entity<Category>().HasKey(p => p.Id);
builder.Entity<Category>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd();
builder.Entity<Category>().Property(p => p.Name).IsRequired().HasMaxLength(30);
builder.Entity<Category>().HasMany(p => p.Products).WithOne(p => p.Category).HasForeignKey(p => p.CategoryId);
builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" }, // Id set manually due to in-memory provider
new Category { Id = 101, Name = "Dairy" }
);
builder.Entity<Product>().ToTable("Products");
builder.Entity<Product>().HasKey(p => p.Id);
builder.Entity<Product>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd();
builder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(50);
builder.Entity<Product>().Property(p => p.QuantityInPackage).IsRequired();
builder.Entity<Product>().Property(p => p.UnitOfMeasurement).IsRequired();
}
}
}
我們指定我們的模型應映射到哪些表。此外,我們設置了主鍵,使用該方法HasKey,該表的列,使用Property方法,和一些限制,例如IsRequired,HasMaxLength,和ValueGeneratedOnAdd,這些都是使用FluentApi的方式基于Lamada 表達式語法實現的(鏈式語法)。
看一下下面的代碼:
builder.Entity<Category>()
.HasMany(p => p.Products)
.WithOne(p => p.Category)
.HasForeignKey(p => p.CategoryId);
在這里,我們指定表之間的關系。我們說一個類別有很多產品,我們設置了將映射此關系的屬性(Products,來自Category類,和Category,來自Product類)。我們還設置了外鍵(CategoryId)。
如果您想學習如何使用EF Core配置一對一和多對多關系,以及如何完整的使用它,請看一下本教程。
還有一種用于通過HasData方法配置種子數據的方法:
builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" },
new Category { Id = 101, Name = "Dairy" }
);
默認情況下,在這里我們僅添加兩個示例類別。這對我們完成后進行API的測試來說是非常有必要的。
注意:我們在Id這里手動設置屬性,因為內存提供程序的工作機制需要。我將標識符設置為大數字,以避免自動生成的標識符和種子數據之間發生沖突。
真正的關系數據庫提供程序中不存在此限制,因此,例如,如果要使用SQL Server等數據庫,則不必指定這些標識符。如果您想了解此行為,請檢查此Github問題。
在實現數據庫上下文類之后,我們可以實現類別倉儲。添加一個名為新的文件夾Repositories里面Persistence的文件夾,然后添加一個名為新類BaseRepository。
using Supermarket.API.Persistence.Contexts;
namespace Supermarket.API.Persistence.Repositories
{
public abstract class BaseRepository
{
protected readonly AppDbContext _context;
public BaseRepository(AppDbContext context)
{
_context = context;
}
}
}
此類只是我們所有倉儲都將繼承的抽象類。抽象類是沒有直接實例的類。您必須創建直接類來創建實例。
在BaseRepository接受我們的實例,AppDbContext通過依賴注入暴露了一個受保護的屬性稱為(只能是由子類訪問一個屬性)_context,即可以訪問我們需要處理數據庫操作的所有方法。
在相同文件夾中添加一個新類CategoryRepository。現在,我們將真正實現倉儲邏輯:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Persistence.Contexts;
namespace Supermarket.API.Persistence.Repositories
{
public class CategoryRepository : BaseRepository, ICategoryRepository
{
public CategoryRepository(AppDbContext context) : base(context)
{
}
public async Task<IEnumerable<Category>> ListAsync()
{
return await _context.Categories.ToListAsync();
}
}
}
倉儲繼承BaseRepository和實現ICategoryRepository。
注意實現list方法是很簡單的。我們使用Categories數據庫集訪問類別表,然后調用擴展方法ToListAsync,該方法負責將查詢結果轉換為類別的集合。
EF Core 將我們的方法調用轉換為SQL查詢,這是最有效的方法。這種方式僅當您調用將數據轉換為集合的方法或使用方法獲取特定數據時才執行查詢。
現在,我們有了類別控制器,服務和倉儲庫的代碼實現。
我們將關注點分離開來,創建了只執行應做的事情的類。
測試應用程序之前的最后一步是使用ASP.NET Core依賴項注入機制將我們的接口綁定到相應的類。
現在是時候讓您最終了解此概念的工作原理了。
在應用程序的根文件夾中,打開Startup類。此類負責在應用程序啟動時配置各種配置。
該ConfigureServices和Configure方法通過框架管道在運行時調用來配置應用程序應該如何工作,必須使用哪些組件。
打開ConfigureServices方法。在這里,我們只有一行配置應用程序以使用MVC管道,這基本上意味著該應用程序將使用控制器類來處理請求和響應(在這段代碼背后發生了很多事情,但目前您僅需要知道這些)。
我們可以使用ConfigureServices訪問services參數的方法來配置我們的依賴項綁定。清理類代碼,刪除所有注釋并按如下所示更改代碼:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Domain.Services;
using Supermarket.API.Persistence.Contexts;
using Supermarket.API.Persistence.Repositories;
using Supermarket.API.Services;
namespace Supermarket.API
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<AppDbContext>(options => {
options.UseInMemoryDatabase("supermarket-api-in-memory");
});
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<ICategoryService, CategoryService>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}
看一下這段代碼:
services.AddDbContext<AppDbContext>(options => {
options.UseInMemoryDatabase("supermarket-api-in-memory");
});
在這里,我們配置數據庫上下文。我們告訴ASP.NET Core將其AppDbContext與內存數據庫實現一起使用,該實現由作為參數傳遞給我們方法的字符串標識。通常,在編寫集成測試時才會使用內存數據庫,但是為了簡單起見,我在這里使用了內存數據庫。這樣,我們無需連接到真實的數據庫即可測試應用程序。
這些代碼行在內部配置我們的數據庫上下文,以便使用確定作用域的生存周期進行依賴注入。
scoped生存周期告訴ASP.NET Core管道,每當它需要解析接收AppDbContext作為構造函數參數的實例的類時,都應使用該類的相同實例。如果內存中沒有實例,則管道將創建一個新實例,并在給定請求期間在需要它的所有類中重用它。這樣,您無需在需要使用時手動創建類實例。
如果你想了解其他有關生命周期的知識,可以閱讀官方文檔。
依賴注入技術為我們提供了許多優勢,例如:
配置數據庫上下文之后,我們還將我們的服務和倉儲綁定到相應的類。
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<ICategoryService, CategoryService>();
在這里,我們還使用了scoped生存周期,因為這些類在內部必須使用數據庫上下文類。在這種情況下,指定相同的范圍是有意義的。
現在我們配置了依賴綁定,我們必須在Program類上進行一些小的更改,以便數據庫正確地初始化種子數據。此步驟僅在使用內存數據庫提供程序時才需要執行(請參閱此Github問題以了解原因)。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Supermarket.API.Persistence.Contexts;
namespace Supermarket.API
{
public class Program
{
public static void Main(string[] args)
{
var host = BuildWebHost(args);
using(var scope = host.Services.CreateScope())
using(var context = scope.ServiceProvider.GetService<AppDbContext>())
{
context.Database.EnsureCreated();
}
host.Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
由于我們使用的是內存提供程序,因此有必要更改Main方法 添加“ context.Database.EnsureCreated();”代碼以確保在應用程序啟動時將“創建”數據庫。沒有此更改,將不會創建我們想要的初始化種子數據。
實現了所有基本功能后,就該測試我們的API端點了。
在API根文件夾中打開終端或命令提示符,然后鍵入以下命令:
dotnet run
上面的命令啟動應用程序。控制臺將顯示類似于以下內容的輸出:
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
Entity Framework Core 2.2.0-rtm-35687 initialized ‘AppDbContext’ using provider ‘Microsoft.EntityFrameworkCore.InMemory’ with options: StoreName=supermarket-api-in-memory
info: Microsoft.EntityFrameworkCore.Update[30100]
Saved 2 entities to in-memory store.
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using ‘C:\Users\evgomes\AppData\Local\ASP.NET\DataProtection-Keys’ as key repository and Windows DPAPI to encrypt keys at rest.
Hosting environment: Development
Content root path: C:\Users\evgomes\Desktop\Tutorials\src\Supermarket.API
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
您可以看到調用了EF Core來初始化數據庫。最后幾行顯示應用程序在哪個端口上運行。
打開瀏覽器,然后導航到 http://localhost:5000/api/categories (或控制臺輸出上顯示的URL)。如果您發現由于HTTPS導致的安全錯誤,則只需為應用程序添加一個例外。
瀏覽器將顯示以下JSON數據作為輸出:
[
{
"id": 100,
"name": "Fruits and Vegetables",
"products": []
},
{
"id": 101,
"name": "Dairy",
"products": []
}
]
在這里,我們看到配置數據庫上下文時添加到數據庫的數據。此輸出確認我們的代碼正在運行。
您使用很少的代碼行創建了GET API端點,并且由于當前API項目的架構模式,您的代碼結構確實很容易更改。
現在,該向您展示在由于業務需要而不得不對其進行更改時,更改此代碼有多么容易。
如果您還記得API端點的規范,您會注意到我們的實際JSON響應還有一個額外的屬性:products數組。看一下所需響應的示例:
{
[
{ "id": 1, "name": "Fruits and Vegetables" },
{ "id": 2, "name": "Breads" },
… // Other categories
]
}
產品數組出現在我們當前的JSON響應中,因為我們的Category模型具有Products,EF Core需要的屬性,以正確映射給定類別的產品。
我們不希望在響應中使用此屬性,但是不能更改模型類以排除此屬性。當我們嘗試管理類別數據時,這將導致EF Core引發錯誤,并且也將破壞我們的領域模型設計,因為沒有產品的產品類別沒有意義。
要返回僅包含超級市場類別的標識符和名稱的JSON數據,我們必須創建一個資源類。
資源類是一種包含將客戶端應用程序和API端點之間進行交換的類型,通常以JSON數據的形式出現,以表示一些特定信息的類。
來自API端點的所有響應都必須返回資源。
將真實模型表示形式作為響應返回是一種不好的做法,因為它可能包含客戶端應用程序不需要或沒有其權限的信息(例如,用戶模型可以返回用戶密碼的信息) ,這將是一個很大的安全問題)。
我們需要一種資源來僅代表我們的類別,而沒有產品。
現在您知道什么是資源,讓我們實現它。首先,在命令行中按Ctrl + C停止正在運行的應用程序。在應用程序的根文件夾中,創建一個名為Resources的新文件夾。在其中添加一個名為的新類CategoryResource。
namespace Supermarket.API.Resources
{
public class CategoryResource
{
public int Id { get; set; }
public string Name { get; set; }
}
}
我們必須將類別服務提供的類別模型集合映射到類別資源集合。
我們將使用一個名為AutoMapper的庫來處理對象之間的映射。AutoMapper是.NET世界中非常流行的庫,并且在許多商業和開源項目中使用。
在命令行中輸入以下命令,以將AutoMapper添加到我們的應用程序中:
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
要使用AutoMapper,我們必須做兩件事:
首先,打開Startup課程。在該ConfigureServices方法的最后一行之后,添加以下代碼:
services.AddAutoMapper();
此行處理AutoMapper的所有必需配置,例如注冊它以進行依賴項注入以及在啟動過程中掃描應用程序以配置映射配置文件。
現在,在根目錄中,添加一個名為的新文件夾Mapping,然后添加一個名為的類ModelToResourceProfile。通過以下方式更改代碼:
using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Resources;
namespace Supermarket.API.Mapping
{
public class ModelToResourceProfile : Profile
{
public ModelToResourceProfile()
{
CreateMap<Category, CategoryResource>();
}
}
}
該類繼承Profile了AutoMapper用于檢查我們的映射如何工作的類類型。在構造函數上,我們在Category模型類和CategoryResource類之間創建一個映射。由于類的屬性具有相同的名稱和類型,因此我們不必為其使用任何特殊的配置。
最后一步包括更改類別控制器以使用AutoMapper處理我們的對象映射。
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
using Supermarket.API.Resources;
namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
private readonly ICategoryService _categoryService;
private readonly IMapper _mapper;
public CategoriesController(ICategoryService categoryService, IMapper mapper)
{
_categoryService = categoryService;
_mapper = mapper;
}
[HttpGet]
public async Task<IEnumerable<CategoryResource>> GetAllAsync()
{
var categories = await _categoryService.ListAsync();
var resources = _mapper.Map<IEnumerable<Category>, IEnumerable<CategoryResource>>(categories);
return resources;
}
}
}
我更改了構造函數以接收IMapper實現的實例。您可以使用這些接口方法來使用AutoMapper映射方法。
我還更改了GetAllAsync使用Map方法將類別枚舉映射到資源枚舉的方法。此方法接收我們要映射的類或集合的實例,并通過通用類型定義定義必須映射到什么類型的類或集合。
注意,我們只需將新的依賴項(IMapper)注入構造函數,就可以輕松地更改實現,而不必修改服務類或倉儲。
依賴注入使您的應用程序可維護且易于更改,因為您不必中斷所有代碼實現即可添加或刪除功能。
您可能意識到,不僅控制器類,而且所有接收依賴項的類(包括依賴項本身)都會根據綁定配置自動解析為接收正確的類。
依賴注入如此的Amazing,不是嗎?
現在,使用dotnet run命令再次啟動API,然后轉到http://localhost:5000/api/categories以查看新的JSON響應。
這是您應該看到的響應數據
我們已經有了GET端點。現在,讓我們為POST(創建)類別創建一個新端點。
在處理資源創建時,我們必須關心很多事情,例如:
在本教程中,我不會顯示如何處理身份驗證和授權,但是您可以閱讀JSON Web令牌身份驗證教程,了解如何輕松實現這些功能。
另外,有一個非常流行的框架稱為ASP.NET Identity,該框架提供了有關安全性和用戶注冊的內置解決方案,您可以在應用程序中使用它們。它包括與EF Core配合使用的提供程序,例如IdentityDbContext可以使用的內置程序。您可以在此處了解更多信息。
讓我們編寫一個HTTP POST端點,該端點將涵蓋其他場景(日志記錄除外,它可以根據不同的范圍和工具進行更改)。
在創建新端點之前,我們需要一個新資源。此資源會將客戶端應用程序發送到此端點的數據(在本例中為類別名稱)映射到我們應用程序的類。
由于我們正在創建一個新類別,因此我們還沒有ID,這意味著我們需要一種資源來表示僅包含其名稱的類別。
在Resources文件夾中,添加一個新類SaveCategoryResource:
using System.ComponentModel.DataAnnotations;
namespace Supermarket.API.Resources
{
public class SaveCategoryResource
{
[Required]
[MaxLength(30)]
public string Name { get; set; }
}
}
注意Name屬性上的Required和MaxLength特性。這些屬性稱為數據注釋。ASP.NET Core管道使用此元數據來驗證請求和響應。顧名思義,類別名稱是必填項,最大長度為30個字符。
現在,讓我們定義新API端點的形狀。將以下代碼添加到類別控制器:
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
}
我們使用HttpPost特性告訴框架這是一個HTTP POST端點。
注意此方法的響應類型Task。控制器類中存在的方法稱為動作,它們具有此簽名,因為在應用程序執行動作之后,我們可以返回一個以上的可能結果。
在這種情況下,如果類別名稱無效或出現問題,我們必須返回400代碼(錯誤請求)響應,該響應通常包含一條錯誤消息,客戶端應用程序可以使用該錯誤消息來解決該問題,或者我們可以如果一切正常,則對數據進行200次響應(成功)。
可以將多種類型的操作類型用作響應,但是通常,我們可以使用此接口,并且ASP.NET Core將為此使用默認類。
該FromBody屬性告訴ASP.NET Core將請求正文數據解析為我們的新資源類。這意味著當包含類別名稱的JSON發送到我們的應用程序時,框架將自動將其解析為我們的新類。
現在,讓我們實現路由邏輯。我們必須遵循一些步驟才能成功創建新類別:
這似乎很復雜,但是使用為API構建的服務架構來實現此邏輯確實很容易。
讓我們開始驗證傳入的請求。
ASP.NET Core控制器具有名為ModelState的屬性。在執行我們的操作之前,該屬性在請求執行期間填充。它是ModelStateDictionary的實例,該類包含諸如請求是否有效以及潛在的驗證錯誤消息之類的信息。
如下更改端點代碼:
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());
}
這段代碼檢查模型狀態(在這種情況下為請求正文中發送的數據)是否無效,并檢查我們的數據注釋。如果不是,則API返回錯誤的請求(狀態代碼400),以及我們的注釋元數據提供的默認錯誤消息。
該ModelState.GetErrorMessages()方法尚未實現。這是一種擴展方法(一種擴展現有類或接口功能的方法),我將實現該方法將驗證錯誤轉換為簡單的字符串以返回給客戶端。
Extensions在我們的API的根目錄中添加一個新文件夾,然后添加一個新類ModelStateExtensions。
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Supermarket.API.Extensions
{
public static class ModelStateExtensions
{
public static List<string> GetErrorMessages(this ModelStateDictionary dictionary)
{
return dictionary.SelectMany(m => m.Value.Errors)
.Select(m => m.ErrorMessage)
.ToList();
}
}
}
所有擴展方法以及聲明它們的類都應該是靜態的。** **這意味著它們不處理特定的實例數據,并且在應用程序啟動時僅被加載一次。
this參數聲明前面的關鍵字告訴C#編譯器將其視為擴展方法。結果是我們可以像此類的常規方法一樣調用它,因為我們在要使用擴展的地方包含的特定的using代碼。
該擴展使用LINQ查詢,這是.NET的非常有用的功能,它使我們能夠使用鏈式語法來查詢和轉換數據。此處的表達式將驗證錯誤方法轉換為包含錯誤消息的字符串列表。
Supermarket.API.Extensions在進行下一步之前,將名稱空間導入Categories控制器。
using Supermarket.API.Extensions;
讓我們通過將新資源映射到類別模型類來繼續實現端點邏輯。
我們已經定義了映射配置文件,可以將模型轉換為資源。現在,我們需要一個與之相反的新配置項。
ResourceToModelProfile在Mapping文件夾中添加一個新類:
using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Resources;
namespace Supermarket.API.Mapping
{
public class ResourceToModelProfile : Profile
{
public ResourceToModelProfile()
{
CreateMap<SaveCategoryResource, Category>();
}
}
}
這里沒有新內容。由于依賴注入的魔力,AutoMapper將在應用程序啟動時自動注冊此配置文件,而我們無需更改任何其他位置即可使用它。
現在,我們可以將新資源映射到相應的模型類:
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());
var category = _mapper.Map<SaveCategoryResource, Category>(resource);
}
現在我們必須實現最有趣的邏輯:保存一個新類別。我們希望我們的服務能夠做到。
由于連接到數據庫時出現問題,或者由于任何內部業務規則使我們的數據無效,因此保存邏輯可能會失敗。
如果出現問題,我們不能簡單地拋出一個錯誤,因為它可能會停止API,并且客戶端應用程序也不知道如何處理該問題。另外,我們可能會有某種日志記錄機制來記錄錯誤。
保存方法的約定(即方法的簽名和響應類型)需要指示我們是否正確執行了該過程。如果處理正常,我們將接收類別數據。如果沒有,我們至少必須收到一條錯誤消息,告訴您該過程失敗的原因。
我們可以通過應用request-response模式來實現此功能。這種企業設計模式將我們的請求和響應參數封裝到類中,以封裝我們的服務將用于處理某些任務并將信息返回給正在使用該服務的類的信息。
這種模式為我們提供了一些優勢,例如:
讓我們為處理數據更改的服務方法創建一個標準響應類型。對于這種類型的每個請求,我們都想知道該請求是否被正確執行。如果失敗,我們要向客戶端返回錯誤消息。
在Domain文件夾的內部Services,添加一個名為的新目錄Communication。在此處添加一個名為的新類BaseResponse。
namespace Supermarket.API.Domain.Services.Communication
{
public abstract class BaseResponse
{
public bool Success { get; protected set; }
public string Message { get; protected set; }
public BaseResponse(bool success, string message)
{
Success = success;
Message = message;
}
}
}
那是我們的響應類型將繼承的抽象類。
抽象定義了一個Success屬性和一個Message屬性,該屬性將告知請求是否已成功完成,如果失敗,該屬性將顯示錯誤消息。
請注意,這些屬性是必需的,只有繼承的類才能設置此數據,因為子類必須通過構造函數傳遞此信息。
提示:為所有內容定義基類不是一個好習慣,因為基類會耦合您的代碼并阻止您輕松對其進行修改。優先使用組合而不是繼承。
在此API的范圍內,使用基類并不是真正的問題,因為我們的服務不會增長太多。如果您意識到服務或應用程序會經常增長和更改,請避免使用基類。
現在,在同一文件夾中,添加一個名為的新類SaveCategoryResponse。
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Domain.Services.Communication
{
public class SaveCategoryResponse : BaseResponse
{
public Category Category { get; private set; }
private SaveCategoryResponse(bool success, string message, Category category) : base(success, message)
{
Category = category;
}
/// <summary>
/// Creates a success response.
/// </summary>
/// <param name="category">Saved category.</param>
/// <returns>Response.</returns>
public SaveCategoryResponse(Category category) : this(true, string.Empty, category)
{ }
/// <summary>
/// Creates am error response.
/// </summary>
/// <param name="message">Error message.</param>
/// <returns>Response.</returns>
public SaveCategoryResponse(string message) : this(false, message, null)
{ }
}
}
響應類型還設置了一個Category屬性,如果請求成功完成,該屬性將包含我們的類別數據。
請注意,我為此類定義了三種不同的構造函數:
因為C#支持多個構造函數,所以我們僅通過使用不同的構造函數就簡化了響應的創建過程,而無需定義其他方法來處理此問題。
現在,我們可以更改服務界面以添加新的保存方法合同。
更改ICategoryService接口,如下所示:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services.Communication;
namespace Supermarket.API.Domain.Services
{
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<SaveCategoryResponse> SaveAsync(Category category);
}
}
我們只需將類別傳遞給此方法,它將處理保存模型數據,編排倉儲和其他必要服務所需的所有邏輯。
請注意,由于我們不需要任何其他參數來執行此任務,因此我不在此處創建特定的請求類。計算機編程中有一個名為KISS的概念 —Keep It Simple,Stupid的簡稱。基本上,它說您應該使您的應用程序盡可能簡單。
設計應用程序時請記住這一點:僅應用解決問題所需的內容。不要過度設計您的應用程序。
現在我們可以完成端點邏輯:
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());
var category = _mapper.Map<SaveCategoryResource, Category>(resource);
var result = await _categoryService.SaveAsync(category);
if (!result.Success)
return BadRequest(result.Message);
var categoryResource = _mapper.Map<Category, CategoryResource>(result.Category);
return Ok(categoryResource);
}
在驗證請求數據并將資源映射到我們的模型之后,我們將其傳遞給我們的服務以保留數據。
如果失敗,則API返回錯誤的請求。如果沒有,API會將新類別(現在包括諸如new的數據Id)映射到我們先前創建的類別CategoryResource,并將其發送給客戶端。
現在,讓我們為服務實現真正的邏輯。
由于我們要將數據持久化到數據庫中,因此我們需要在倉儲中使用一種新方法。
向ICategoryRepository接口添加AddAsync新方法:
public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
Task AddAsync(Category category);
}
現在,讓我們在真正的倉儲類中實現此方法:
public class CategoryRepository : BaseRepository, ICategoryRepository
{
public CategoryRepository(AppDbContext context) : base(context)
{ }
public async Task<IEnumerable<Category>> ListAsync()
{
return await _context.Categories.ToListAsync();
}
public async Task AddAsync(Category category)
{
await _context.Categories.AddAsync(category);
}
}
在這里,我們只是在集合中添加一個新類別。
當我們向中添加類時DBSet<>,EF Core將開始跟蹤模型發生的所有更改,并在當前狀態下使用此數據生成將插入,更新或刪除模型的查詢。
當前的實現只是將模型添加到我們的集合中,但是我們的數據仍然不會保存。
在上下文類中提供了SaveChanges的方法,我們必須調用該方法才能真正將查詢執行到數據庫中。我之所以沒有在這里調用它,是因為倉儲不應該持久化數據,它只是一種內存集合對象。
即使在經驗豐富的.NET開發人員之間,該主題也引起很大爭議,但是讓我向您解釋為什么您不應該在倉儲類中調用SaveChanges方法。
我們可以從概念上將倉儲像.NET框架中存在的任何其他集合一樣。在.NET(和許多其他編程語言,例如Javascript和Java)中處理集合時,通常可以:
想一想現實世界中的清單。想象一下,您正在編寫一份購物清單以在超市購買東西(巧合,不是嗎?)。
在列表中,寫下您需要購買的所有水果。您可以將水果添加到此列表中,如果放棄購買就刪除水果,也可以替換水果的名稱。但是您無法將水果保存到列表中。用簡單的英語說這樣的話是沒有意義的。
提示:在使用面向對象的編程語言設計類和接口時,請嘗試使用自然語言來檢查您所做的工作是否正確。
例如,說人實現了person的接口是有道理的,但是說一個人實現了一個帳戶卻沒有道理。
如果您要“保存”水果清單(在這種情況下,要購買所有水果),請付款,然后超市會處理庫存數據以檢查他們是否必須從供應商處購買更多水果。
編程時可以應用相同的邏輯。倉儲不應保存,更新或刪除數據。相反,他們應該將其委托給其他類來處理此邏輯。
將數據直接保存到倉儲中時,還有另一個問題:您不能使用transaction。
想象一下,我們的應用程序具有一種日志記錄機制,該機制存儲一些用戶名,并且每次對API數據進行更改時都會執行操作。
現在想象一下,由于某種原因,您調用了一個更新用戶名的服務(這是不常見的情況,但讓我們考慮一下)。
您同意要更改虛擬用戶表中的用戶名,首先必須更新所有日志以正確告訴誰執行了該操作,對嗎?
現在想象我們已經為用戶和不同倉儲中的日志實現了update方法,它們都調用了SaveChanges。如果這些方法之一在更新過程中失敗,會發生什么?最終會導致數據不一致。
只有在一切完成之后,我們才應該將更改保存到數據庫中。為此,我們必須使用transaction,這基本上是大多數數據庫實現的功能,只有在完成復雜的操作后才能保存數據。
“-好的,所以如果我們不能在這里保存東西,我們應該在哪里做?”
處理此問題的常見模式是工作單元模式。此模式包含一個類,該類將我們的AppDbContext實例作為依賴項接收,并公開用于開始,完成或中止事務的方法。
在這里,我們將使用工作單元的簡單實現來解決我們的問題。
Repositories在Domain層的倉儲文件夾Repositories內添加一個新接口IUnitOfWork:
using System.Threading.Tasks;
namespace Supermarket.API.Domain.Repositories
{
public interface IUnitOfWork
{
Task CompleteAsync();
}
}
如您所見,它僅公開一種將異步完成數據管理操作的方法。
現在讓我們添加實際的實現。
在Persistence層RepositoriesRepositories文件夾中的添加一個名為的UnitOfWork的新類:
using System.Threading.Tasks;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Persistence.Contexts;
namespace Supermarket.API.Persistence.Repositories
{
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public UnitOfWork(AppDbContext context)
{
_context = context;
}
public async Task CompleteAsync()
{
await _context.SaveChangesAsync();
}
}
}
這是一個簡單,干凈的實現,僅在使用倉儲修改完所有更改后,才將所有更改保存到數據庫中。
如果研究工作單元模式的實現,則會發現實現回滾操作的更復雜的模式。
由于EF Core已經在后臺實現了倉儲模式和工作單元,因此我們不必在意回滾方法。
“ – 什么?那么為什么我們必須創建所有這些接口和類?”
將持久性邏輯與業務規則分開在代碼可重用性和維護方面具有許多優勢。如果直接使用EF Core,我們最終將擁有更復雜的類,這些類將很難更改。
想象一下,將來您決定將ORM框架更改為其他框架,例如Dapper,或者由于性能而必須實施純SQL查詢。如果將查詢邏輯與服務耦合在一起,將很難更改該邏輯,因為您必須在許多類中進行此操作。
使用倉儲模式,您可以簡單地實現一個新的倉儲類并使用依賴注入將其綁定。
因此,基本上,如果您直接在服務中使用EF Core,并且必須進行一些更改,那么您將獲得:
就像我說的那樣,EF Core在后臺實現了工作單元和倉儲模式。我們可以將DbSet<>屬性視為倉儲。而且,SaveChanges僅在所有數據庫操作成功的情況下才保留數據。
現在,您知道什么是工作單元以及為什么將其與倉儲一起使用,讓我們實現真實服務的邏輯。
public class CategoryService : ICategoryService
{
private readonly ICategoryRepository _categoryRepository;
private readonly IUnitOfWork _unitOfWork;
public CategoryService(ICategoryRepository categoryRepository, IUnitOfWork unitOfWork)
{
_categoryRepository = categoryRepository;
_unitOfWork = unitOfWork;
}
public async Task<IEnumerable<Category>> ListAsync()
{
return await _categoryRepository.ListAsync();
}
public async Task<SaveCategoryResponse> SaveAsync(Category category)
{
try
{
await _categoryRepository.AddAsync(category);
await _unitOfWork.CompleteAsync();
return new SaveCategoryResponse(category);
}
catch (Exception ex)
{
// Do some logging stuff
return new SaveCategoryResponse($"An error occurred when saving the category: {ex.Message}");
}
}
}
多虧了我們的解耦架構,我們可以簡單地將實例UnitOfWork作為此類的依賴傳遞。
我們的業務邏輯非常簡單。
首先,我們嘗試將新類別添加到數據庫中,然后API嘗試保存新類別,將所有內容包裝在try-catch塊中。
如果失敗,則API會調用一些虛構的日志記錄服務,并返回指示失敗的響應。
如果該過程順利完成,則應用程序將返回成功響應,并發送我們的類別數據。簡單吧?
提示:在現實世界的應用程序中,您不應將所有內容包裝在通用的try-catch塊中,而應分別處理所有可能的錯誤。
簡單地添加一個try-catch塊并不能解決大多數可能的失敗情況。請確保正確實現錯誤處理。
測試我們的API之前的最后一步是將工作單元接口綁定到其各自的類。
將此新行添加到類的ConfigureServices方法中Startup:
services.AddScoped<IUnitOfWork, UnitOfWork>();
現在讓我們測試一下!
重新啟動我們的應用程序dotnet run。
我們無法使用瀏覽器測試POST端點。讓我們使用Postman測試我們的端點。這是測試RESTful API的非常有用的工具。
打開Postman,然后關閉介紹性消息。您會看到這樣的屏幕:
屏幕顯示測試端點的選項
GET默認情況下,將所選內容更改為選擇框POST。
在Enter request URL字段中輸入API地址。
我們必須提供請求正文數據以發送到我們的API。單擊Body菜單項,然后將其下方顯示的選項更改為raw。
Postman將在右側顯示一個Text選項,將其更改為JSON (application/json)并粘貼以下JSON數據:
{
"name": ""
}
發送請求前的屏幕
如您所見,我們將向我們的新端點發送一個空的名稱字符串。
點擊Send按鈕。您將收到如下輸出:
如您所見,我們的驗證邏輯有效!
您還記得我們為端點創建的驗證邏輯嗎?此輸出是它起作用的證明!
還要注意右側顯示的400狀態代碼。該BadRequest結果自動將此狀態碼的響應。
現在,讓我們將JSON數據更改為有效數據,以查看新的響應:
最后,我們期望得到的結果
API正確創建了我們的新資源。
到目前為止,我們的API可以列出和創建類別。您學到了很多有關C#語言,ASP.NET Core框架以及構造API的通用設計方法的知識。
讓我們繼續我們的類別API,創建用于更新類別的端點。
從現在開始,由于我向您解釋了大多數概念,因此我將加快解釋速度,并專注于新主題,以免浪費您的時間。Let’s go!
要更新類別,我們需要一個HTTP PUT端點。
我們必須編寫的邏輯與POST邏輯非常相似:
讓我們將新PutAsync方法添加到控制器類中:
[HttpPut("{id}")]
public async Task<IActionResult> PutAsync(int id, [FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());
var category = _mapper.Map<SaveCategoryResource, Category>(resource);
var result = await _categoryService.UpdateAsync(id, category);
if (!result.Success)
return BadRequest(result.Message);
var categoryResource = _mapper.Map<Category, CategoryResource>(result.Category);
return Ok(categoryResource);
}
如果將其與POST邏輯進行比較,您會注意到這里只有一個區別:HttPut屬性指定給定路由應接收的參數。
我們將調用此端點,將類別指定Id 為最后一個URL片段,例如/api/categories/1。ASP.NET Core管道將此片段解析為相同名稱的參數。
現在我們必須UpdateAsync在ICategoryService接口中定義方法簽名:
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<SaveCategoryResponse> SaveAsync(Category category);
Task<SaveCategoryResponse> UpdateAsync(int id, Category category);
}
現在讓我們轉向真正的邏輯。
首先,要更新類別,我們需要從數據庫中返回當前數據(如果存在)。我們還需要將其更新到我們的中DBSet<>。
讓我們在ICategoryService界面中添加兩個新的方法約定:
public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
Task AddAsync(Category category);
Task<Category> FindByIdAsync(int id);
void Update(Category category);
}
我們已經定義了FindByIdAsync方法,該方法將從數據庫中異步返回一個類別,以及該Update方法。請注意,該Update方法不是異步的,因為EF Core API不需要異步方法來更新模型。
現在,讓我們在CategoryRepository類中實現真正的邏輯:
public async Task<Category> FindByIdAsync(int id) { return await _context.Categories.FindAsync(id); } public void Update(Category category) { _context.Categories.Update(category); }
最后,我們可以對服務邏輯進行編碼:
public async Task<SaveCategoryResponse> UpdateAsync(int id, Category category)
{
var existingCategory = await _categoryRepository.FindByIdAsync(id);
if (existingCategory == null)
return new SaveCategoryResponse("Category not found.");
existingCategory.Name = category.Name;
try
{
_categoryRepository.Update(existingCategory);
await _unitOfWork.CompleteAsync();
return new SaveCategoryResponse(existingCategory);
}
catch (Exception ex)
{
// Do some logging stuff
return new SaveCategoryResponse($"An error occurred when updating the category: {ex.Message}");
}
}
API嘗試從數據庫中獲取類別。如果結果為null,我們將返回一個響應,告知該類別不存在。如果類別存在,我們需要設置其新名稱。
然后,API會嘗試保存更改,例如創建新類別時。如果該過程完成,則該服務將返回成功響應。如果不是,則執行日志記錄邏輯,并且端點接收包含錯誤消息的響應。
現在讓我們對其進行測試。首先,讓我們添加一個新類別Id以使用有效類別。我們可以使用播種到數據庫中的類別的標識符,但是我想通過這種方式向您展示我們的API將更新正確的資源。
再次運行該應用程序,然后使用Postman將新類別發布到數據庫中:
添加新類別以供日后更新
使用一個可用的數據Id,將POST 選項更改PUT為選擇框,然后在URL的末尾添加ID值。將name屬性更改為其他名稱,然后發送請求以檢查結果:
類別數據已成功更新
您可以將GET請求發送到API端點,以確保您正確編輯了類別名稱:
那是現在GET請求的結果
我們必須對類別執行的最后一項操作是排除類別。讓我們創建一個HTTP Delete端點。
刪除類別的邏輯確實很容易實現,因為我們所需的大多數方法都是先前構建的。
這些是我們工作路線的必要步驟:
讓我們開始添加新的端點邏輯:
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync(int id)
{
var result = await _categoryService.DeleteAsync(id);
if (!result.Success)
return BadRequest(result.Message);
var categoryResource = _mapper.Map<Category, CategoryResource>(result.Category);
return Ok(categoryResource);
}
該HttpDelete屬性還定義了一個id 模板。
在將DeleteAsync簽名添加到我們的ICategoryService接口之前,我們需要做一些小的重構。
新的服務方法必須返回包含類別數據的響應,就像對PostAsyncand UpdateAsync方法所做的一樣。我們可以SaveCategoryResponse為此目的重用,但在這種情況下我們不會保存數據。
為了避免創建具有相同形狀的新類來滿足此要求,我們可以將我們重命名SaveCategoryResponse為CategoryResponse。
如果您使用的是Visual Studio Code,則可以打開SaveCategoryResponse類,將鼠標光標放在類名上方,然后使用選項Change All Occurrences* *來重命名該類:
確保也重命名文件名。
讓我們將DeleteAsync方法簽名添加到ICategoryService 接口中:
public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<CategoryResponse> SaveAsync(Category category);
Task<CategoryResponse> UpdateAsync(int id, Category category);
Task<CategoryResponse> DeleteAsync(int id);
}
在實施刪除邏輯之前,我們需要在倉儲中使用一種新方法。
將Remove方法簽名添加到ICategoryRepository接口:
void Remove(Category category);
現在,在倉儲類上添加真正的實現:
public void Remove(Category category)
{
_context.Categories.Remove(category);
}
EF Core要求將模型的實例傳遞給Remove方法,以正確了解我們要刪除的模型,而不是簡單地傳遞Id。
最后,讓我們在CategoryService類上實現邏輯:
public async Task<CategoryResponse> DeleteAsync(int id)
{
var existingCategory = await _categoryRepository.FindByIdAsync(id);
if (existingCategory == null)
return new CategoryResponse("Category not found.");
try
{
_categoryRepository.Remove(existingCategory);
await _unitOfWork.CompleteAsync();
return new CategoryResponse(existingCategory);
}
catch (Exception ex)
{
// Do some logging stuff
return new CategoryResponse($"An error occurred when deleting the category: {ex.Message}");
}
}
這里沒有新內容。該服務嘗試通過ID查找類別,然后調用我們的倉儲以刪除類別。最后,工作單元完成將實際操作執行到數據庫中的事務。
“-嘿,但是每個類別的產品呢?為避免出現錯誤,您是否不需要先創建倉儲并刪除產品?”
答案是否定的。借助EF Core跟蹤機制,當我們從數據庫中加載模型時,框架便知道了該模型具有哪些關系。如果我們刪除它,EF Core知道它應該首先遞歸刪除所有相關模型。
在將類映射到數據庫表時,我們可以禁用此功能,但這在本教程的范圍之外。如果您想了解此功能,請看這里。
現在是時候測試我們的新端點了。再次運行該應用程序,并使用Postman發送DELETE請求,如下所示:
如您所見,API毫無問題地刪除了現有類別
我們可以通過發送GET請求來檢查我們的API是否正常工作:
我們已經完成了類別API。現在是時候轉向產品API。
到目前為止,您已經學習了如何實現所有基本的HTTP動詞來使用ASP.NET Core處理CRUD操作。讓我們進入實現產品API的下一個層次。
我將不再詳細介紹所有HTTP動詞,因為這將是詳盡無遺的。在本教程的最后一部分,我將僅介紹GET請求,以向您展示在從數據庫查詢數據時如何包括相關實體,以及如何使用Description我們為EUnitOfMeasurement 枚舉值定義的屬性。
將新控制器ProductsController添加到名為Controllers的文件夾中。
在這里編寫任何代碼之前,我們必須創建產品資源。
讓我刷新您的記憶,再次顯示我們的資源應如何:
{
[
{
"id": 1,
"name": "Sugar",
"quantityInPackage": 1,
"unitOfMeasurement": "KG"
"category": {
"id": 3,
"name": "Sugar"
}
},
… // Other products
]
}
我們想要一個包含數據庫中所有產品的JSON數組。
JSON數據與產品模型有兩點不同:
為了表示度量單位,我們可以使用簡單的字符串屬性代替枚舉類型(順便說一下,我們沒有JSON數據的默認枚舉類型,因此我們必須將其轉換為其他類型)。
現在,我們現在要塑造新資源,讓我們創建它。ProductResource在Resources文件夾中添加一個新類:
namespace Supermarket.API.Resources
{
public class ProductResource
{
public int Id { get; set; }
public string Name { get; set; }
public int QuantityInPackage { get; set; }
public string UnitOfMeasurement { get; set; }
public CategoryResource Category {get;set;}
}
}
現在,我們必須配置模型類和新資源類之間的映射。
映射配置將與用于其他映射的配置幾乎相同,但是在這里,我們必須處理將EUnitOfMeasurement枚舉轉換為字符串的操作。
您還記得StringValue應用于枚舉類型的屬性嗎?現在,我將向您展示如何使用.NET框架的強大功能:反射 API提取此信息。
反射 API是一組強大的資源工具集,可讓我們提取和操作元數據。許多框架和庫(包括ASP.NET Core本身)都利用這些資源來處理許多后臺工作。
現在讓我們看看它在實踐中是如何工作的。將新類添加到Extensions名為的文件夾中EnumExtensions。
using System.ComponentModel;
using System.Reflection;
namespace Supermarket.API.Extensions
{
public static class EnumExtensions
{
public static string ToDescriptionString<TEnum>(this TEnum @enum)
{
FieldInfo info = @enum.GetType().GetField(@enum.ToString());
var attributes = (DescriptionAttribute[])info.GetCustomAttributes(typeof(DescriptionAttribute), false);
return attributes?[0].Description ?? @enum.ToString();
}
}
}
第一次看代碼可能會讓人感到恐懼,但這并不復雜。讓我們分解代碼定義以了解其工作原理。
首先,我們定義了一種通用方法(一種方法,該方法可以接收不止一種類型的參數,在這種情況下,該方法由TEnum聲明表示),該方法接收給定的枚舉作為參數。
由于enum是C#中的保留關鍵字,因此我們在參數名稱前面添加了@,以使其成為有效名稱。
該方法的第一步是使用該方法獲取參數的類型信息(類,接口,枚舉或結構定義)GetType。
然后,該方法使用來獲取特定的枚舉值(例如Kilogram)GetField(@enum.ToString())。
下一行找到Description應用于枚舉值的所有屬性,并將其數據存儲到數組中(在某些情況下,我們可以為同一屬性指定多個屬性)。
最后一行使用較短的語法來檢查我們是否至少有一個枚舉類型的描述屬性。如果有,我們將返回Description此屬性提供的值。如果不是,我們使用默認的強制類型轉換將枚舉作為字符串返回。
?.操作者(零條件運算)檢查該值是否null訪問其屬性之前。
??運算符(空合并運算符)告訴應用程序在左邊的返回值,如果它不為空,或者在正確的,否則價值。
現在我們有了擴展方法來提取描述,讓我們配置模型和資源之間的映射。多虧了AutoMapper,我們只需要多一行就可以做到這一點。
打開ModelToResourceProfile類并通過以下方式更改代碼:
using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Extensions;
using Supermarket.API.Resources;
namespace Supermarket.API.Mapping
{
public class ModelToResourceProfile : Profile
{
public ModelToResourceProfile()
{
CreateMap<Category, CategoryResource>();
CreateMap<Product, ProductResource>()
.ForMember(src => src.UnitOfMeasurement,
opt => opt.MapFrom(src => src.UnitOfMeasurement.ToDescriptionString()));
}
}
此語法告訴AutoMapper使用新的擴展方法將我們的EUnitOfMeasurement值轉換為包含其描述的字符串。簡單吧?您可以閱讀官方文檔以了解完整語法。
注意,我們尚未為category屬性定義任何映射配置。因為我們之前為類別配置了映射,并且由于產品模型具有相同類型和名稱的category屬性,所以AutoMapper隱式知道應該使用各自的配置來映射它。
現在,我們添加端點代碼。更改ProductsController代碼:
using System.Collections.Generic;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Services;
using Supermarket.API.Resources;
namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class ProductsController : Controller
{
private readonly IProductService _productService;
private readonly IMapper _mapper;
public ProductsController(IProductService productService, IMapper mapper)
{
_productService = productService;
_mapper = mapper;
}
[HttpGet]
public async Task<IEnumerable<ProductResource>> ListAsync()
{
var products = await _productService.ListAsync();
var resources = _mapper.Map<IEnumerable<Product>, IEnumerable<ProductResource>>(products);
return resources;
}
}
}
基本上,為類別控制器定義的結構相同。
讓我們進入服務部分。將一個新IProductService接口添加到Domain層中的Services文件夾中:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Domain.Services
{
public interface IProductService
{
Task<IEnumerable<Product>> ListAsync();
}
}
您應該已經意識到,在真正實現新服務之前,我們需要一個倉儲。
IProductRepository在相應的文件夾中添加一個名為的新接口:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
namespace Supermarket.API.Domain.Repositories
{
public interface IProductRepository
{
Task<IEnumerable<Product>> ListAsync();
}
}
現在,我們實現倉儲。除了必須在查詢數據時返回每個產品的相應類別數據外,我們幾乎必須像對類別倉儲一樣實現。
默認情況下,EF Core在查詢數據時不包括與模型相關的實體,因為它可能非常慢(想象一個具有十個相關實體的模型,所有相關實體都有自己的關系)。
要包括類別數據,我們只需要多一行:
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Persistence.Contexts;
namespace Supermarket.API.Persistence.Repositories
{
public class ProductRepository : BaseRepository, IProductRepository
{
public ProductRepository(AppDbContext context) : base(context)
{
}
public async Task<IEnumerable<Product>> ListAsync()
{
return await _context.Products.Include(p => p.Category)
.ToListAsync();
}
}
}
請注意對的調用Include(p => p.Category)。我們可以鏈接此語法,以在查詢數據時包含盡可能多的實體。執行選擇時,EF Core會將其轉換為聯接。
現在,我們可以ProductService像處理類別一樣實現類:
using System.Collections.Generic;
using System.Threading.Tasks;
using Supermarket.API.Domain.Models;
using Supermarket.API.Domain.Repositories;
using Supermarket.API.Domain.Services;
namespace Supermarket.API.Services
{
public class ProductService : IProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<IEnumerable<Product>> ListAsync()
{
return await _productRepository.ListAsync();
}
}
}
讓我們綁定更改Startup類的新依賴項:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<AppDbContext>(options =>
{
options.UseInMemoryDatabase("supermarket-api-in-memory");
});
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<ICategoryService, CategoryService>();
services.AddScoped<IProductService, ProductService>();
services.AddAutoMapper();
}
最后,在測試API之前,讓我們AppDbContext在初始化應用程序時更改類以包括一些產品,以便我們看到結果:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Category>().ToTable("Categories");
builder.Entity<Category>().HasKey(p => p.Id);
builder.Entity<Category>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd().HasValueGenerator<InMemoryIntegerValueGenerator<int>>();
builder.Entity<Category>().Property(p => p.Name).IsRequired().HasMaxLength(30);
builder.Entity<Category>().HasMany(p => p.Products).WithOne(p => p.Category).HasForeignKey(p => p.CategoryId);
builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" }, // Id set manually due to in-memory provider
new Category { Id = 101, Name = "Dairy" }
);
builder.Entity<Product>().ToTable("Products");
builder.Entity<Product>().HasKey(p => p.Id);
builder.Entity<Product>().Property(p => p.Id).IsRequired().ValueGeneratedOnAdd();
builder.Entity<Product>().Property(p => p.Name).IsRequired().HasMaxLength(50);
builder.Entity<Product>().Property(p => p.QuantityInPackage).IsRequired();
builder.Entity<Product>().Property(p => p.UnitOfMeasurement).IsRequired();
builder.Entity<Product>().HasData
(
new Product
{
Id = 100,
Name = "Apple",
QuantityInPackage = 1,
UnitOfMeasurement = EUnitOfMeasurement.Unity,
CategoryId = 100
},
new Product
{
Id = 101,
Name = "Milk",
QuantityInPackage = 2,
UnitOfMeasurement = EUnitOfMeasurement.Liter,
CategoryId = 101,
}
);
}
我添加了兩個虛構產品,將它們與初始化應用程序時我們播種的類別相關聯。
該測試了!再次運行API并發送GET請求以/api/products使用Postman:
就是這樣!恭喜你!
現在,您將了解如何使用解耦的代碼架構使用ASP.NET Core構建RESTful API。您了解了.NET Core框架的許多知識,如何使用C#,EF Core和AutoMapper的基礎知識以及在設計應用程序時要使用的許多有用的模式。
您可以檢查API的完整實現,包括產品的其他HTTP動詞,并檢查Github倉儲:
evgomes / supermarket-api
使用ASP.NET Core 2.2構建的簡單RESTful API,展示了如何使用分離的,可維護的……創建RESTful服務。github.com
ASP.NET Core是創建Web應用程序時使用的出色框架。它帶有許多有用的API,可用于構建干凈,可維護的應用程序。創建專業應用程序時,可以將其視為一種選擇。
文章轉自微信公眾號@DotNET技術圈