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
]
}

讓我們開始編寫應(yīng)用程序。

第1步-創(chuàng)建API

首先,我們必須為Web服務(wù)創(chuàng)建文件夾結(jié)構(gòu),然后我們必須使用.NET CLI工具來構(gòu)建基本的Web API。打開終端或命令提示符(取決于您使用的操作系統(tǒng)),并依次鍵入以下命令:

mkdir src/Supermarket.API

cd src/Supermarket.API

dotnet new webapi

前兩個(gè)命令只是為API創(chuàng)建一個(gè)新目錄,然后將當(dāng)前位置更改為新文件夾。最后一個(gè)遵循Web API模板生成一個(gè)新項(xiàng)目,這是我們正在開發(fā)的應(yīng)用程序。您可以閱讀有關(guān)這些命令和其他項(xiàng)目模板的更多信息,并可以通過檢查此鏈接來生成其他項(xiàng)目模板。
現(xiàn)在,新目錄將具有以下結(jié)構(gòu):

項(xiàng)目結(jié)構(gòu)

結(jié)構(gòu)概述

ASP.NET Core應(yīng)用程序由在類中配置的一組中間件(應(yīng)用程序流水線中的小塊應(yīng)用程序,用于處理請(qǐng)求和響應(yīng))組成Startup。如果您以前已經(jīng)使用過Express.js之類的框架,那么這個(gè)概念對(duì)您來說并不是什么新鮮事物。

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();
}
}

當(dāng)應(yīng)用程序啟動(dòng)時(shí),將調(diào)用類中的Main** **方法Program。它使用啟動(dòng)配置創(chuàng)建默認(rèn)的Web主機(jī),通過HTTP通過特定端口(默認(rèn)情況下,HTTP為5000,HTTPS為5001)公開應(yīng)用程序。

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通過路由接收請(qǐng)求時(shí)將調(diào)用的方法/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)
{
}
}

如果您不了解此代碼的某些部分,請(qǐng)不要擔(dān)心。在開發(fā)必要的API端點(diǎn)時(shí),我將詳細(xì)介紹每一個(gè)。現(xiàn)在,只需刪除此類,因?yàn)槲覀儾粫?huì)使用它。

第2步-創(chuàng)建領(lǐng)域模型

我將應(yīng)用一些設(shè)計(jì)概念,以使應(yīng)用程序簡(jiǎn)單易維護(hù)。

編寫可以由您自己理解和維護(hù)的代碼并不難,但是您必須牢記您將成為團(tuán)隊(duì)的一部分。如果您不注意如何編寫代碼,那么結(jié)果將是一個(gè)龐然大物,這將使您和您的團(tuán)隊(duì)成員頭痛不已。聽起來很極端吧?但是相信我,這就是事實(shí)。

衡量好代碼的標(biāo)準(zhǔn)是WTF的頻率。原圖來自smitty42,發(fā)表于filckr。該圖遵循CC-BY-2.0。

在Supermarket.API目錄中,創(chuàng)建一個(gè)名為的新文件夾Domain。在新的領(lǐng)域文件夾中,創(chuàng)建另一個(gè)名為的文件夾Models。我們必須添加到此文件夾的第一個(gè)模型是Category。最初,它將是一個(gè)簡(jiǎn)單的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>();
}
}

該類具有一個(gè)Id** 屬性(用于標(biāo)識(shí)類別)和一個(gè)Name屬性。以及一個(gè)Products 屬性。最后一個(gè)屬性將由Entity Framework Core使用**,大多數(shù)ASP.NET Core應(yīng)用程序使用ORM將數(shù)據(jù)持久化到數(shù)據(jù)庫中,以映射類別和產(chǎn)品之間的關(guān)系。由于類別具有許多相關(guān)產(chǎn)品,因此在面向?qū)ο蟮木幊谭矫嬉簿哂泻侠淼乃季S能力。
我們還必須創(chuàng)建產(chǎn)品模型。在同一文件夾中,添加一個(gè)新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; }
}
}

該產(chǎn)品還具有ID和名稱的屬性。屬性QuantityInPackage,它告訴我們一包中有多少個(gè)產(chǎn)品單位(請(qǐng)記住應(yīng)用范圍的餅干示例)和一個(gè)UnitOfMeasurement** 屬性,這是表示一個(gè)枚舉類型,它表示可能的度量單位的枚舉。最后兩個(gè)屬性,CategoryId **和Category將由ORM用于映射的產(chǎn)品和類別之間的關(guān)系。它表明一種產(chǎn)品只有一個(gè)類別。

讓我們定義領(lǐng)域模型的最后一部分,EUnitOfMeasurement** **枚舉。

按照慣例,枚舉不需要在名稱前以“ E”開頭,但是在某些庫和框架中,您會(huì)發(fā)現(xiàn)此前綴是將枚舉與接口和類區(qū)分開的一種方式。

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
}
}

該代碼非常簡(jiǎn)單。在這里,我們僅定義了幾種度量單位的可能性,但是,在實(shí)際的超市系統(tǒng)中,您可能具有許多其他度量單位,并且可能還有一個(gè)單獨(dú)的模型。
注意,【Description】特性應(yīng)用于所有枚舉可能性。特性是一種在C#語言的類,接口,屬性和其他組件上定義元數(shù)據(jù)的方法。在這種情況下,我們將使用它來簡(jiǎn)化產(chǎn)品API端點(diǎn)的響應(yīng),但是您現(xiàn)在不必關(guān)心它。我們待會(huì)再回到這里。

我們的基本模型已準(zhǔn)備就緒,可以使用。現(xiàn)在,我們可以開始編寫將管理所有類別的API端點(diǎn)。

第3步-類別API

在Controllers文件夾中,添加一個(gè)名為的新類CategoriesController。

按照慣例,該文件夾中所有后綴為“ Controller”的類都將成為我們應(yīng)用程序的控制器。這意味著他們將處理請(qǐng)求和響應(yīng)。您必須從命名空間【Microsoft.AspNetCore.Mvc】繼承Controller。

命名空間由一組相關(guān)的類,接口,枚舉和結(jié)構(gòu)組成。您可以將其視為類似于Java語言模塊或Java 程序包的東西。

新的控制器應(yīng)通過路由/api/categories做出響應(yīng)。我們通過Route** **在類名稱上方添加屬性,指定占位符來實(shí)現(xiàn)此目的,該占位符表示路由應(yīng)按照慣例使用不帶控制器后綴的類名稱。

using Microsoft.AspNetCore.Mvc;

namespace Supermarket.API.Controllers
{
[Route("/api/[controller]")]
public class CategoriesController : Controller
{
}
}

讓我們開始處理GET請(qǐng)求。首先,當(dāng)有人/api/categories通過GET動(dòng)詞請(qǐng)求數(shù)據(jù)時(shí),API需要返回所有類別。為此,我們可以創(chuàng)建類別服務(wù)。
從概念上講,服務(wù)基本上是定義用于處理某些業(yè)務(wù)邏輯的方法的類或接口。創(chuàng)建用于處理業(yè)務(wù)邏輯的服務(wù)是許多不同編程語言的一種常見做法,例如身份驗(yàn)證和授權(quán),付款,復(fù)雜的數(shù)據(jù)流,緩存和需要其他服務(wù)或模型之間進(jìn)行某些交互的任務(wù)。

使用服務(wù),我們可以將請(qǐng)求和響應(yīng)處理與完成任務(wù)所需的真實(shí)邏輯隔離開來。

該服務(wù),我們要?jiǎng)?chuàng)建將首先定義一個(gè)單獨(dú)的行為,或方法:一個(gè)list方法。我們希望該方法返回?cái)?shù)據(jù)庫中所有現(xiàn)有的類別。

為簡(jiǎn)單起見,在這篇博客中,我們將不處理數(shù)據(jù)分頁或過濾,(譯者注:基于RESTFul規(guī)范,提供了一套完整的分頁和過濾的規(guī)則)。將來,我將寫一篇文章,展示如何輕松處理這些功能。

為了定義C#(以及其他面向?qū)ο蟮恼Z言,例如Java)中某事物的預(yù)期行為,我們定義一個(gè)interface。一個(gè)接口告訴某些事情應(yīng)該如何工作,但是沒有實(shí)現(xiàn)行為的真實(shí)邏輯。邏輯在實(shí)現(xiàn)接口的類中實(shí)現(xiàn)。如果您不清楚此概念,請(qǐng)不要擔(dān)心。一段時(shí)間后您將了解它。

在Domain文件夾中,創(chuàng)建一個(gè)名為的新目錄Services。在此添加一個(gè)名為ICategoryService的接口。按照慣例,所有接口都應(yīng)以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方法的實(shí)現(xiàn)必須異步返回類別的可枚舉對(duì)象。
Task封裝返回的類表示異步。由于必須等待數(shù)據(jù)庫完成操作才能返回?cái)?shù)據(jù),因此我們需要考慮執(zhí)行此過程可能需要一段時(shí)間,因此我們需要使用異步方法。另請(qǐng)注意“Async”后綴。這是一個(gè)約定,告訴我們的方法應(yīng)異步執(zhí)行。

我們有很多約定,對(duì)嗎?我個(gè)人喜歡它,因?yàn)樗箲?yīng)用程序易于閱讀,即使你在一家使用.NET技術(shù)的公司是新人。

“-好的,我們定義了此接口,但是它什么也沒做。有什么用?”

如果您來自Javascript或其他非強(qiáng)類型語言,則此概念可能看起來很奇怪。

接口使我們能夠從實(shí)際實(shí)現(xiàn)中抽象出所需的行為。使用稱為依賴注入的機(jī)制,我們可以實(shí)現(xiàn)這些接口并將它們與其他組件隔離。

基本上,當(dāng)您使用依賴項(xiàng)注入時(shí),您可以使用接口定義一些行為。然后,創(chuàng)建一個(gè)實(shí)現(xiàn)該接口的類。最后,將引用從接口綁定到您創(chuàng)建的類。

”-聽起來確實(shí)令人困惑。我們不能簡(jiǎn)單地創(chuàng)建一個(gè)為我們做這些事情的類嗎?”

讓我們繼續(xù)實(shí)現(xiàn)我們的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;
}
}
}

我已經(jīng)為控制器定義了一個(gè)構(gòu)造函數(shù)(當(dāng)創(chuàng)建一個(gè)類的新實(shí)例時(shí)會(huì)調(diào)用一個(gè)構(gòu)造函數(shù)),并且它接收的實(shí)例ICategoryService。這意味著實(shí)例可以是任何實(shí)現(xiàn)服務(wù)接口的實(shí)例。我將此實(shí)例存儲(chǔ)在一個(gè)私有的只讀字段中_categoryService。我們將使用此字段訪問類別服務(wù)實(shí)現(xiàn)的方法。
順便說一下,下劃線前綴是表示字段的另一個(gè)通用約定。特別地,.NET的官方命名約定指南不建議使用此約定,但是這是一種非常普遍的做法,可以避免使用“ this”關(guān)鍵字來區(qū)分類字段和局部變量。我個(gè)人認(rèn)為閱讀起來要干凈得多,并且許多框架和庫都使用此約定。

在構(gòu)造函數(shù)下,我定義了用于處理請(qǐng)求的方法/api/categories。該HttpGet** **屬性告訴ASP.NET Core管道使用該屬性來處理GET請(qǐng)求(可以省略此屬性,但是最好編寫它以便于閱讀)。

該方法使用我們的CategoryService實(shí)例列出所有類別,然后將類別返回給客戶端。框架管道將數(shù)據(jù)序列化為JSON對(duì)象。IEnumerable類型告訴框架,我們想要返回一個(gè)類別的枚舉,而Task類型(使用async關(guān)鍵字修飾)告訴管道,這個(gè)方法應(yīng)該異步執(zhí)行。最后,當(dāng)我們定義一個(gè)異步方法時(shí),我們必須使用await關(guān)鍵字來處理需要一些時(shí)間的任務(wù)。

好的,我們定義了API的初始結(jié)構(gòu)。現(xiàn)在,有必要真正實(shí)現(xiàn)類別服務(wù)。

步驟4-實(shí)現(xiàn)類別服務(wù)

在API的根文件夾(即Supermarket.API文件夾)中,創(chuàng)建一個(gè)名為的新文件夾Services。在這里,我們將放置所有服務(wù)實(shí)現(xiàn)。在新文件夾中,添加一個(gè)名為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()
{
}
}
}

以上只是接口實(shí)現(xiàn)的基本代碼,我們暫時(shí)仍不處理任何邏輯。讓我們考慮一下列表方法應(yīng)該如何實(shí)現(xiàn)。
我們需要訪問數(shù)據(jù)庫并返回所有類別,然后我們需要將此數(shù)據(jù)返回給客戶端。

服務(wù)類不是應(yīng)該處理數(shù)據(jù)訪問的類。我們將使用一種稱為“倉(cāng)儲(chǔ)模式”的設(shè)計(jì)模式,定義倉(cāng)儲(chǔ)類,用于管理數(shù)據(jù)庫中的數(shù)據(jù)。

在使用倉(cāng)儲(chǔ)模式時(shí),我們定義了repository 類,該類基本上封裝了處理數(shù)據(jù)訪問的所有邏輯。這些倉(cāng)儲(chǔ)類使方法可以列出,創(chuàng)建,編輯和刪除給定模型的對(duì)象,與操作集合的方式相同。在內(nèi)部,這些方法與數(shù)據(jù)庫對(duì)話以執(zhí)行CRUD操作,從而將數(shù)據(jù)庫訪問與應(yīng)用程序的其余部分隔離開。

我們的服務(wù)需要調(diào)用類別倉(cāng)儲(chǔ),以獲取列表對(duì)象。

從概念上講,服務(wù)可以與一個(gè)或多個(gè)倉(cāng)儲(chǔ)或其他服務(wù)“對(duì)話”以執(zhí)行操作。

創(chuàng)建用于處理數(shù)據(jù)訪問邏輯的新定義似乎是多余的,但是您將在一段時(shí)間內(nèi)看到將這種邏輯與服務(wù)類隔離是非常有利的。

讓我們創(chuàng)建一個(gè)倉(cāng)儲(chǔ),該倉(cāng)儲(chǔ)負(fù)責(zé)與數(shù)據(jù)庫通信,作為持久化保存類別的一種方式。

步驟5-類別倉(cāng)儲(chǔ)和持久層

在該Domain文件夾內(nèi),創(chuàng)建一個(gè)名為的新目錄Repositories。然后,添加一個(gè)名為的新接口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();
}
}

初始代碼基本上與服務(wù)接口的代碼相同。
定義了接口之后,我們可以返回服務(wù)類并使用的實(shí)例ICategoryRepository返回?cái)?shù)據(jù)來完成實(shí)現(xiàn)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();
}
}
}

現(xiàn)在,我們必須實(shí)現(xiàn)類別倉(cāng)儲(chǔ)的真實(shí)邏輯。在這樣做之前,我們必須考慮如何訪問數(shù)據(jù)庫。
順便說一句,我們?nèi)匀粵]有數(shù)據(jù)庫!

我們將使用Entity Framework Core(為簡(jiǎn)單起見,我將其稱為EF Core)作為我們的數(shù)據(jù)庫ORM。該框架是ASP.NET Core的默認(rèn)ORM,并公開了一個(gè)友好的API,該API使我們能夠?qū)?yīng)用程序的類映射到數(shù)據(jù)庫表。

EF Core還允許我們先設(shè)計(jì)應(yīng)用程序,然后根據(jù)我們?cè)诖a中定義的內(nèi)容生成數(shù)據(jù)庫。此技術(shù)稱為Code First。我們將使用Code First方法來生成數(shù)據(jù)庫(實(shí)際上,在此示例中,我將使用內(nèi)存數(shù)據(jù)庫,但是您可以輕松地將其更改為像SQL Server或MySQL服務(wù)器這樣的實(shí)例數(shù)據(jù)庫)。

API的根文件夾中,創(chuàng)建一個(gè)名為的新目錄Persistence。此目錄將包含我們?cè)L問數(shù)據(jù)庫所需的所有內(nèi)容,例如倉(cāng)儲(chǔ)實(shí)現(xiàn)。

在新文件夾中,創(chuàng)建一個(gè)名為的新目錄Contexts,然后添加一個(gè)名為的新類AppDbContext。此類必須繼承DbContext,EF Core通過DBContext用來將您的模型映射到數(shù)據(jù)庫表的類。通過以下方式更改代碼:

using Microsoft.EntityFrameworkCore;

namespace Supermarket.API.Domain.Persistence.Contexts
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
}
}

我們添加到此類的構(gòu)造函數(shù)負(fù)責(zé)通過依賴注入將數(shù)據(jù)庫配置傳遞給基類。稍后您將看到其工作原理。
現(xiàn)在,我們必須創(chuàng)建兩個(gè)DbSet屬性。這些屬性是將模型映射到數(shù)據(jù)庫表的集合(唯一對(duì)象的集合)。

另外,我們必須將模型的屬性映射到相應(yīng)的列,指定哪些屬性是主鍵,哪些是外鍵,列類型等。我們可以使用稱為Fluent API的功能來覆蓋OnModelCreating方法,以指定數(shù)據(jù)庫映射。更改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();
}
}
}

我們指定我們的模型應(yīng)映射到哪些表。此外,我們?cè)O(shè)置了主鍵,使用該方法HasKey,該表的列,使用Property方法,和一些限制,例如IsRequired,HasMaxLength,和ValueGeneratedOnAdd,這些都是使用FluentApi的方式基于Lamada 表達(dá)式語法實(shí)現(xiàn)的(鏈?zhǔn)秸Z法)。
看一下下面的代碼:

builder.Entity<Category>()
.HasMany(p => p.Products)
.WithOne(p => p.Category)
.HasForeignKey(p => p.CategoryId);

在這里,我們指定表之間的關(guān)系。我們說一個(gè)類別有很多產(chǎn)品,我們?cè)O(shè)置了將映射此關(guān)系的屬性(Products,來自Category類,和Category,來自Product類)。我們還設(shè)置了外鍵(CategoryId)。
如果您想學(xué)習(xí)如何使用EF Core配置一對(duì)一和多對(duì)多關(guān)系,以及如何完整的使用它,請(qǐng)看一下本教程。

還有一種用于通過HasData方法配置種子數(shù)據(jù)的方法:

builder.Entity<Category>().HasData
(
new Category { Id = 100, Name = "Fruits and Vegetables" },
new Category { Id = 101, Name = "Dairy" }
);

默認(rèn)情況下,在這里我們僅添加兩個(gè)示例類別。這對(duì)我們完成后進(jìn)行API的測(cè)試來說是非常有必要的。

注意:我們?cè)贗d這里手動(dòng)設(shè)置屬性,因?yàn)閮?nèi)存提供程序的工作機(jī)制需要。我將標(biāo)識(shí)符設(shè)置為大數(shù)字,以避免自動(dòng)生成的標(biāo)識(shí)符和種子數(shù)據(jù)之間發(fā)生沖突。

真正的關(guān)系數(shù)據(jù)庫提供程序中不存在此限制,因此,例如,如果要使用SQL Server等數(shù)據(jù)庫,則不必指定這些標(biāo)識(shí)符。如果您想了解此行為,請(qǐng)檢查此Github問題。

在實(shí)現(xiàn)數(shù)據(jù)庫上下文類之后,我們可以實(shí)現(xiàn)類別倉(cāng)儲(chǔ)。添加一個(gè)名為新的文件夾Repositories里面Persistence的文件夾,然后添加一個(gè)名為新類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;
}
}
}

此類只是我們所有倉(cāng)儲(chǔ)都將繼承的抽象類。抽象類是沒有直接實(shí)例的類。您必須創(chuàng)建直接類來創(chuàng)建實(shí)例。
在BaseRepository接受我們的實(shí)例,AppDbContext通過依賴注入暴露了一個(gè)受保護(hù)的屬性稱為(只能是由子類訪問一個(gè)屬性)_context,即可以訪問我們需要處理數(shù)據(jù)庫操作的所有方法。

在相同文件夾中添加一個(gè)新類CategoryRepository。現(xiàn)在,我們將真正實(shí)現(xiàn)倉(cāng)儲(chǔ)邏輯:

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();
}
}
}

倉(cāng)儲(chǔ)繼承BaseRepository和實(shí)現(xiàn)ICategoryRepository。
注意實(shí)現(xiàn)list方法是很簡(jiǎn)單的。我們使用Categories數(shù)據(jù)庫集訪問類別表,然后調(diào)用擴(kuò)展方法ToListAsync,該方法負(fù)責(zé)將查詢結(jié)果轉(zhuǎn)換為類別的集合。

EF Core 將我們的方法調(diào)用轉(zhuǎn)換為SQL查詢,這是最有效的方法。這種方式僅當(dāng)您調(diào)用將數(shù)據(jù)轉(zhuǎn)換為集合的方法或使用方法獲取特定數(shù)據(jù)時(shí)才執(zhí)行查詢。

現(xiàn)在,我們有了類別控制器,服務(wù)和倉(cāng)儲(chǔ)庫的代碼實(shí)現(xiàn)。

我們將關(guān)注點(diǎn)分離開來,創(chuàng)建了只執(zhí)行應(yīng)做的事情的類。

測(cè)試應(yīng)用程序之前的最后一步是使用ASP.NET Core依賴項(xiàng)注入機(jī)制將我們的接口綁定到相應(yīng)的類。

第6步-配置依賴注入

現(xiàn)在是時(shí)候讓您最終了解此概念的工作原理了。

在應(yīng)用程序的根文件夾中,打開Startup類。此類負(fù)責(zé)在應(yīng)用程序啟動(dòng)時(shí)配置各種配置。

該ConfigureServices和Configure方法通過框架管道在運(yùn)行時(shí)調(diào)用來配置應(yīng)用程序應(yīng)該如何工作,必須使用哪些組件。

打開ConfigureServices方法。在這里,我們只有一行配置應(yīng)用程序以使用MVC管道,這基本上意味著該應(yīng)用程序?qū)⑹褂每刂破黝悂硖幚碚?qǐng)求和響應(yīng)(在這段代碼背后發(fā)生了很多事情,但目前您僅需要知道這些)。

我們可以使用ConfigureServices訪問services參數(shù)的方法來配置我們的依賴項(xiàng)綁定。清理類代碼,刪除所有注釋并按如下所示更改代碼:

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");

});

在這里,我們配置數(shù)據(jù)庫上下文。我們告訴ASP.NET Core將其AppDbContext與內(nèi)存數(shù)據(jù)庫實(shí)現(xiàn)一起使用,該實(shí)現(xiàn)由作為參數(shù)傳遞給我們方法的字符串標(biāo)識(shí)。通常,在編寫集成測(cè)試時(shí)才會(huì)使用內(nèi)存數(shù)據(jù)庫,但是為了簡(jiǎn)單起見,我在這里使用了內(nèi)存數(shù)據(jù)庫。這樣,我們無需連接到真實(shí)的數(shù)據(jù)庫即可測(cè)試應(yīng)用程序。
這些代碼行在內(nèi)部配置我們的數(shù)據(jù)庫上下文,以便使用確定作用域的生存周期進(jìn)行依賴注入。

scoped生存周期告訴ASP.NET Core管道,每當(dāng)它需要解析接收AppDbContext作為構(gòu)造函數(shù)參數(shù)的實(shí)例的類時(shí),都應(yīng)使用該類的相同實(shí)例。如果內(nèi)存中沒有實(shí)例,則管道將創(chuàng)建一個(gè)新實(shí)例,并在給定請(qǐng)求期間在需要它的所有類中重用它。這樣,您無需在需要使用時(shí)手動(dòng)創(chuàng)建類實(shí)例。

如果你想了解其他有關(guān)生命周期的知識(shí),可以閱讀官方文檔。

依賴注入技術(shù)為我們提供了許多優(yōu)勢(shì),例如:

配置數(shù)據(jù)庫上下文之后,我們還將我們的服務(wù)和倉(cāng)儲(chǔ)綁定到相應(yīng)的類。

services.AddScoped<ICategoryRepository, CategoryRepository>();

services.AddScoped<ICategoryService, CategoryService>();

在這里,我們還使用了scoped生存周期,因?yàn)檫@些類在內(nèi)部必須使用數(shù)據(jù)庫上下文類。在這種情況下,指定相同的范圍是有意義的。
現(xiàn)在我們配置了依賴綁定,我們必須在Program類上進(jìn)行一些小的更改,以便數(shù)據(jù)庫正確地初始化種子數(shù)據(jù)。此步驟僅在使用內(nèi)存數(shù)據(jù)庫提供程序時(shí)才需要執(zhí)行(請(qǐng)參閱此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();
}
}

由于我們使用的是內(nèi)存提供程序,因此有必要更改Main方法 添加“ context.Database.EnsureCreated();”代碼以確保在應(yīng)用程序啟動(dòng)時(shí)將“創(chuàng)建”數(shù)據(jù)庫。沒有此更改,將不會(huì)創(chuàng)建我們想要的初始化種子數(shù)據(jù)。
實(shí)現(xiàn)了所有基本功能后,就該測(cè)試我們的API端點(diǎn)了。

第7步-測(cè)試類別

API根文件夾中打開終端或命令提示符,然后鍵入以下命令:

dotnet run

上面的命令啟動(dòng)應(yīng)用程序。控制臺(tái)將顯示類似于以下內(nèi)容的輸出:

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.

您可以看到調(diào)用了EF Core來初始化數(shù)據(jù)庫。最后幾行顯示應(yīng)用程序在哪個(gè)端口上運(yùn)行。
打開瀏覽器,然后導(dǎo)航到 http://localhost:5000/api/categories (或控制臺(tái)輸出上顯示的URL)。如果您發(fā)現(xiàn)由于HTTPS導(dǎo)致的安全錯(cuò)誤,則只需為應(yīng)用程序添加一個(gè)例外。

瀏覽器將顯示以下JSON數(shù)據(jù)作為輸出:

[
{
"id": 100,
"name": "Fruits and Vegetables",
"products": []
},
{
"id": 101,
"name": "Dairy",
"products": []
}
]

在這里,我們看到配置數(shù)據(jù)庫上下文時(shí)添加到數(shù)據(jù)庫的數(shù)據(jù)。此輸出確認(rèn)我們的代碼正在運(yùn)行。
您使用很少的代碼行創(chuàng)建了GET API端點(diǎn),并且由于當(dāng)前API項(xiàng)目的架構(gòu)模式,您的代碼結(jié)構(gòu)確實(shí)很容易更改。

現(xiàn)在,該向您展示在由于業(yè)務(wù)需要而不得不對(duì)其進(jìn)行更改時(shí),更改此代碼有多么容易。

步驟8-創(chuàng)建類別資源

如果您還記得API端點(diǎn)的規(guī)范,您會(huì)注意到我們的實(shí)際JSON響應(yīng)還有一個(gè)額外的屬性:products數(shù)組。看一下所需響應(yīng)的示例:

{
[
{ "id": 1, "name": "Fruits and Vegetables" },
{ "id": 2, "name": "Breads" },
… // Other categories
]
}

產(chǎn)品數(shù)組出現(xiàn)在我們當(dāng)前的JSON響應(yīng)中,因?yàn)槲覀兊腃ategory模型具有Products,EF Core需要的屬性,以正確映射給定類別的產(chǎn)品。
我們不希望在響應(yīng)中使用此屬性,但是不能更改模型類以排除此屬性。當(dāng)我們嘗試管理類別數(shù)據(jù)時(shí),這將導(dǎo)致EF Core引發(fā)錯(cuò)誤,并且也將破壞我們的領(lǐng)域模型設(shè)計(jì),因?yàn)闆]有產(chǎn)品的產(chǎn)品類別沒有意義。

要返回僅包含超級(jí)市場(chǎng)類別的標(biāo)識(shí)符和名稱的JSON數(shù)據(jù),我們必須創(chuàng)建一個(gè)資源類。

資源類是一種包含將客戶端應(yīng)用程序和API端點(diǎn)之間進(jìn)行交換的類型,通常以JSON數(shù)據(jù)的形式出現(xiàn),以表示一些特定信息的類。

來自API端點(diǎn)的所有響應(yīng)都必須返回資源。

將真實(shí)模型表示形式作為響應(yīng)返回是一種不好的做法,因?yàn)樗赡馨蛻舳藨?yīng)用程序不需要或沒有其權(quán)限的信息(例如,用戶模型可以返回用戶密碼的信息) ,這將是一個(gè)很大的安全問題)。

我們需要一種資源來僅代表我們的類別,而沒有產(chǎn)品。

現(xiàn)在您知道什么是資源,讓我們實(shí)現(xiàn)它。首先,在命令行中按Ctrl + C停止正在運(yùn)行的應(yīng)用程序。在應(yīng)用程序的根文件夾中,創(chuàng)建一個(gè)名為Resources的新文件夾。在其中添加一個(gè)名為的新類CategoryResource。

namespace Supermarket.API.Resources
{
public class CategoryResource
{
public int Id { get; set; }
public string Name { get; set; }
}
}

我們必須將類別服務(wù)提供的類別模型集合映射到類別資源集合。
我們將使用一個(gè)名為AutoMapper的庫來處理對(duì)象之間的映射。AutoMapper是.NET世界中非常流行的庫,并且在許多商業(yè)和開源項(xiàng)目中使用。

在命令行中輸入以下命令,以將AutoMapper添加到我們的應(yīng)用程序中:

dotnet add package AutoMapper

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

要使用AutoMapper,我們必須做兩件事:

首先,打開Startup課程。在該ConfigureServices方法的最后一行之后,添加以下代碼:

services.AddAutoMapper();

此行處理AutoMapper的所有必需配置,例如注冊(cè)它以進(jìn)行依賴項(xiàng)注入以及在啟動(dòng)過程中掃描應(yīng)用程序以配置映射配置文件。
現(xiàn)在,在根目錄中,添加一個(gè)名為的新文件夾Mapping,然后添加一個(gè)名為的類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用于檢查我們的映射如何工作的類類型。在構(gòu)造函數(shù)上,我們?cè)贑ategory模型類和CategoryResource類之間創(chuàng)建一個(gè)映射。由于類的屬性具有相同的名稱和類型,因此我們不必為其使用任何特殊的配置。
最后一步包括更改類別控制器以使用AutoMapper處理我們的對(duì)象映射。

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;
}
}
}

我更改了構(gòu)造函數(shù)以接收IMapper實(shí)現(xiàn)的實(shí)例。您可以使用這些接口方法來使用AutoMapper映射方法。
我還更改了GetAllAsync使用Map方法將類別枚舉映射到資源枚舉的方法。此方法接收我們要映射的類或集合的實(shí)例,并通過通用類型定義定義必須映射到什么類型的類或集合。

注意,我們只需將新的依賴項(xiàng)(IMapper)注入構(gòu)造函數(shù),就可以輕松地更改實(shí)現(xiàn),而不必修改服務(wù)類或倉(cāng)儲(chǔ)。

依賴注入使您的應(yīng)用程序可維護(hù)且易于更改,因?yàn)槟槐刂袛嗨写a實(shí)現(xiàn)即可添加或刪除功能。

您可能意識(shí)到,不僅控制器類,而且所有接收依賴項(xiàng)的類(包括依賴項(xiàng)本身)都會(huì)根據(jù)綁定配置自動(dòng)解析為接收正確的類。

依賴注入如此的Amazing,不是嗎?

現(xiàn)在,使用dotnet run命令再次啟動(dòng)API,然后轉(zhuǎn)到http://localhost:5000/api/categories以查看新的JSON響應(yīng)。

這是您應(yīng)該看到的響應(yīng)數(shù)據(jù)

我們已經(jīng)有了GET端點(diǎn)。現(xiàn)在,讓我們?yōu)镻OST(創(chuàng)建)類別創(chuàng)建一個(gè)新端點(diǎn)。

第9步-創(chuàng)建新類別

在處理資源創(chuàng)建時(shí),我們必須關(guān)心很多事情,例如:

在本教程中,我不會(huì)顯示如何處理身份驗(yàn)證和授權(quán),但是您可以閱讀JSON Web令牌身份驗(yàn)證教程,了解如何輕松實(shí)現(xiàn)這些功能。

另外,有一個(gè)非常流行的框架稱為ASP.NET Identity,該框架提供了有關(guān)安全性和用戶注冊(cè)的內(nèi)置解決方案,您可以在應(yīng)用程序中使用它們。它包括與EF Core配合使用的提供程序,例如IdentityDbContext可以使用的內(nèi)置程序。您可以在此處了解更多信息。

讓我們編寫一個(gè)HTTP POST端點(diǎn),該端點(diǎn)將涵蓋其他場(chǎng)景(日志記錄除外,它可以根據(jù)不同的范圍和工具進(jìn)行更改)。

在創(chuàng)建新端點(diǎn)之前,我們需要一個(gè)新資源。此資源會(huì)將客戶端應(yīng)用程序發(fā)送到此端點(diǎn)的數(shù)據(jù)(在本例中為類別名稱)映射到我們應(yīng)用程序的類。

由于我們正在創(chuàng)建一個(gè)新類別,因此我們還沒有ID,這意味著我們需要一種資源來表示僅包含其名稱的類別。

在Resources文件夾中,添加一個(gè)新類SaveCategoryResource:

using System.ComponentModel.DataAnnotations;

namespace Supermarket.API.Resources
{
public class SaveCategoryResource
{
[Required]
[MaxLength(30)]
public string Name { get; set; }
}
}

注意Name屬性上的Required和MaxLength特性。這些屬性稱為數(shù)據(jù)注釋。ASP.NET Core管道使用此元數(shù)據(jù)來驗(yàn)證請(qǐng)求和響應(yīng)。顧名思義,類別名稱是必填項(xiàng),最大長(zhǎng)度為30個(gè)字符。
現(xiàn)在,讓我們定義新API端點(diǎn)的形狀。將以下代碼添加到類別控制器:

[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
}

我們使用HttpPost特性告訴框架這是一個(gè)HTTP POST端點(diǎn)。
注意此方法的響應(yīng)類型Task。控制器類中存在的方法稱為動(dòng)作,它們具有此簽名,因?yàn)樵趹?yīng)用程序執(zhí)行動(dòng)作之后,我們可以返回一個(gè)以上的可能結(jié)果。

在這種情況下,如果類別名稱無效或出現(xiàn)問題,我們必須返回400代碼(錯(cuò)誤請(qǐng)求)響應(yīng),該響應(yīng)通常包含一條錯(cuò)誤消息,客戶端應(yīng)用程序可以使用該錯(cuò)誤消息來解決該問題,或者我們可以如果一切正常,則對(duì)數(shù)據(jù)進(jìn)行200次響應(yīng)(成功)。

可以將多種類型的操作類型用作響應(yīng),但是通常,我們可以使用此接口,并且ASP.NET Core將為此使用默認(rèn)類。

該FromBody屬性告訴ASP.NET Core將請(qǐng)求正文數(shù)據(jù)解析為我們的新資源類。這意味著當(dāng)包含類別名稱的JSON發(fā)送到我們的應(yīng)用程序時(shí),框架將自動(dòng)將其解析為我們的新類。

現(xiàn)在,讓我們實(shí)現(xiàn)路由邏輯。我們必須遵循一些步驟才能成功創(chuàng)建新類別:

這似乎很復(fù)雜,但是使用為API構(gòu)建的服務(wù)架構(gòu)來實(shí)現(xiàn)此邏輯確實(shí)很容易。

讓我們開始驗(yàn)證傳入的請(qǐng)求。

步驟10-使用模型狀態(tài)驗(yàn)證請(qǐng)求主體

ASP.NET Core控制器具有名為ModelState的屬性。在執(zhí)行我們的操作之前,該屬性在請(qǐng)求執(zhí)行期間填充。它是ModelStateDictionary的實(shí)例,該類包含諸如請(qǐng)求是否有效以及潛在的驗(yàn)證錯(cuò)誤消息之類的信息。

如下更改端點(diǎn)代碼:

[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());
}

這段代碼檢查模型狀態(tài)(在這種情況下為請(qǐng)求正文中發(fā)送的數(shù)據(jù))是否無效,并檢查我們的數(shù)據(jù)注釋。如果不是,則API返回錯(cuò)誤的請(qǐng)求(狀態(tài)代碼400),以及我們的注釋元數(shù)據(jù)提供的默認(rèn)錯(cuò)誤消息。
該ModelState.GetErrorMessages()方法尚未實(shí)現(xiàn)。這是一種擴(kuò)展方法(一種擴(kuò)展現(xiàn)有類或接口功能的方法),我將實(shí)現(xiàn)該方法將驗(yàn)證錯(cuò)誤轉(zhuǎn)換為簡(jiǎn)單的字符串以返回給客戶端。

Extensions在我們的API的根目錄中添加一個(gè)新文件夾,然后添加一個(gè)新類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();
}
}
}

所有擴(kuò)展方法以及聲明它們的類都應(yīng)該是靜態(tài)的。** **這意味著它們不處理特定的實(shí)例數(shù)據(jù),并且在應(yīng)用程序啟動(dòng)時(shí)僅被加載一次。
this參數(shù)聲明前面的關(guān)鍵字告訴C#編譯器將其視為擴(kuò)展方法。結(jié)果是我們可以像此類的常規(guī)方法一樣調(diào)用它,因?yàn)槲覀冊(cè)谝褂脭U(kuò)展的地方包含的特定的using代碼。

該擴(kuò)展使用LINQ查詢,這是.NET的非常有用的功能,它使我們能夠使用鏈?zhǔn)秸Z法來查詢和轉(zhuǎn)換數(shù)據(jù)。此處的表達(dá)式將驗(yàn)證錯(cuò)誤方法轉(zhuǎn)換為包含錯(cuò)誤消息的字符串列表。

Supermarket.API.Extensions在進(jìn)行下一步之前,將名稱空間導(dǎo)入Categories控制器。

using Supermarket.API.Extensions;

讓我們通過將新資源映射到類別模型類來繼續(xù)實(shí)現(xiàn)端點(diǎn)邏輯。

步驟11-映射新資源

我們已經(jīng)定義了映射配置文件,可以將模型轉(zhuǎn)換為資源。現(xiàn)在,我們需要一個(gè)與之相反的新配置項(xiàng)。

ResourceToModelProfile在Mapping文件夾中添加一個(gè)新類:

using AutoMapper;
using Supermarket.API.Domain.Models;
using Supermarket.API.Resources;

namespace Supermarket.API.Mapping
{
public class ResourceToModelProfile : Profile
{
public ResourceToModelProfile()
{
CreateMap<SaveCategoryResource, Category>();
}
}
}

這里沒有新內(nèi)容。由于依賴注入的魔力,AutoMapper將在應(yīng)用程序啟動(dòng)時(shí)自動(dòng)注冊(cè)此配置文件,而我們無需更改任何其他位置即可使用它。
現(xiàn)在,我們可以將新資源映射到相應(yīng)的模型類:

[HttpPost]
public async Task<IActionResult> PostAsync([FromBody] SaveCategoryResource resource)
{
if (!ModelState.IsValid)
return BadRequest(ModelState.GetErrorMessages());

var category = _mapper.Map<SaveCategoryResource, Category>(resource);
}

第12步-應(yīng)用請(qǐng)求-響應(yīng)模式來處理保存邏輯

現(xiàn)在我們必須實(shí)現(xiàn)最有趣的邏輯:保存一個(gè)新類別。我們希望我們的服務(wù)能夠做到。

由于連接到數(shù)據(jù)庫時(shí)出現(xiàn)問題,或者由于任何內(nèi)部業(yè)務(wù)規(guī)則使我們的數(shù)據(jù)無效,因此保存邏輯可能會(huì)失敗。

如果出現(xiàn)問題,我們不能簡(jiǎn)單地拋出一個(gè)錯(cuò)誤,因?yàn)樗赡軙?huì)停止API,并且客戶端應(yīng)用程序也不知道如何處理該問題。另外,我們可能會(huì)有某種日志記錄機(jī)制來記錄錯(cuò)誤。

保存方法的約定(即方法的簽名和響應(yīng)類型)需要指示我們是否正確執(zhí)行了該過程。如果處理正常,我們將接收類別數(shù)據(jù)。如果沒有,我們至少必須收到一條錯(cuò)誤消息,告訴您該過程失敗的原因。

我們可以通過應(yīng)用request-response模式來實(shí)現(xiàn)此功能。這種企業(yè)設(shè)計(jì)模式將我們的請(qǐng)求和響應(yīng)參數(shù)封裝到類中,以封裝我們的服務(wù)將用于處理某些任務(wù)并將信息返回給正在使用該服務(wù)的類的信息。

這種模式為我們提供了一些優(yōu)勢(shì),例如:

讓我們?yōu)樘幚頂?shù)據(jù)更改的服務(wù)方法創(chuàng)建一個(gè)標(biāo)準(zhǔn)響應(yīng)類型。對(duì)于這種類型的每個(gè)請(qǐng)求,我們都想知道該請(qǐng)求是否被正確執(zhí)行。如果失敗,我們要向客戶端返回錯(cuò)誤消息。

在Domain文件夾的內(nèi)部Services,添加一個(gè)名為的新目錄Communication。在此處添加一個(gè)名為的新類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;
}
}
}

那是我們的響應(yīng)類型將繼承的抽象類。
抽象定義了一個(gè)Success屬性和一個(gè)Message屬性,該屬性將告知請(qǐng)求是否已成功完成,如果失敗,該屬性將顯示錯(cuò)誤消息。

請(qǐng)注意,這些屬性是必需的,只有繼承的類才能設(shè)置此數(shù)據(jù),因?yàn)樽宇惐仨毻ㄟ^構(gòu)造函數(shù)傳遞此信息。

提示:為所有內(nèi)容定義基類不是一個(gè)好習(xí)慣,因?yàn)榛悤?huì)耦合您的代碼并阻止您輕松對(duì)其進(jìn)行修改。優(yōu)先使用組合而不是繼承。

在此API的范圍內(nèi),使用基類并不是真正的問題,因?yàn)槲覀兊姆?wù)不會(huì)增長(zhǎng)太多。如果您意識(shí)到服務(wù)或應(yīng)用程序會(huì)經(jīng)常增長(zhǎng)和更改,請(qǐng)避免使用基類。

現(xiàn)在,在同一文件夾中,添加一個(gè)名為的新類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)
{ }
}
}

響應(yīng)類型還設(shè)置了一個(gè)Category屬性,如果請(qǐng)求成功完成,該屬性將包含我們的類別數(shù)據(jù)。
請(qǐng)注意,我為此類定義了三種不同的構(gòu)造函數(shù):

因?yàn)镃#支持多個(gè)構(gòu)造函數(shù),所以我們僅通過使用不同的構(gòu)造函數(shù)就簡(jiǎn)化了響應(yīng)的創(chuàng)建過程,而無需定義其他方法來處理此問題。

現(xiàn)在,我們可以更改服務(wù)界面以添加新的保存方法合同。

更改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);
}
}

我們只需將類別傳遞給此方法,它將處理保存模型數(shù)據(jù),編排倉(cāng)儲(chǔ)和其他必要服務(wù)所需的所有邏輯。
請(qǐng)注意,由于我們不需要任何其他參數(shù)來執(zhí)行此任務(wù),因此我不在此處創(chuàng)建特定的請(qǐng)求類。計(jì)算機(jī)編程中有一個(gè)名為KISS的概念 —Keep It Simple,Stupid的簡(jiǎn)稱。基本上,它說您應(yīng)該使您的應(yīng)用程序盡可能簡(jiǎn)單。

設(shè)計(jì)應(yīng)用程序時(shí)請(qǐng)記住這一點(diǎn):僅應(yīng)用解決問題所需的內(nèi)容。不要過度設(shè)計(jì)您的應(yīng)用程序。

現(xiàn)在我們可以完成端點(diǎn)邏輯:

[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);
}

在驗(yàn)證請(qǐng)求數(shù)據(jù)并將資源映射到我們的模型之后,我們將其傳遞給我們的服務(wù)以保留數(shù)據(jù)。
如果失敗,則API返回錯(cuò)誤的請(qǐng)求。如果沒有,API會(huì)將新類別(現(xiàn)在包括諸如new的數(shù)據(jù)Id)映射到我們先前創(chuàng)建的類別CategoryResource,并將其發(fā)送給客戶端。

現(xiàn)在,讓我們?yōu)榉?wù)實(shí)現(xiàn)真正的邏輯。

第13步—數(shù)據(jù)庫邏輯和工作單元模式

由于我們要將數(shù)據(jù)持久化到數(shù)據(jù)庫中,因此我們需要在倉(cāng)儲(chǔ)中使用一種新方法。

向ICategoryRepository接口添加AddAsync新方法:

public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
Task AddAsync(Category category);
}

現(xiàn)在,讓我們?cè)谡嬲膫}(cāng)儲(chǔ)類中實(shí)現(xiàn)此方法:

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);
}
}

在這里,我們只是在集合中添加一個(gè)新類別。
當(dāng)我們向中添加類時(shí)DBSet<>,EF Core將開始跟蹤模型發(fā)生的所有更改,并在當(dāng)前狀態(tài)下使用此數(shù)據(jù)生成將插入,更新或刪除模型的查詢。

當(dāng)前的實(shí)現(xiàn)只是將模型添加到我們的集合中,但是我們的數(shù)據(jù)仍然不會(huì)保存。

在上下文類中提供了SaveChanges的方法,我們必須調(diào)用該方法才能真正將查詢執(zhí)行到數(shù)據(jù)庫中。我之所以沒有在這里調(diào)用它,是因?yàn)閭}(cāng)儲(chǔ)不應(yīng)該持久化數(shù)據(jù),它只是一種內(nèi)存集合對(duì)象。

即使在經(jīng)驗(yàn)豐富的.NET開發(fā)人員之間,該主題也引起很大爭(zhēng)議,但是讓我向您解釋為什么您不應(yīng)該在倉(cāng)儲(chǔ)類中調(diào)用SaveChanges方法。

我們可以從概念上將倉(cāng)儲(chǔ)像.NET框架中存在的任何其他集合一樣。在.NET(和許多其他編程語言,例如Javascript和Java)中處理集合時(shí),通常可以:

想一想現(xiàn)實(shí)世界中的清單。想象一下,您正在編寫一份購(gòu)物清單以在超市購(gòu)買東西(巧合,不是嗎?)。

在列表中,寫下您需要購(gòu)買的所有水果。您可以將水果添加到此列表中,如果放棄購(gòu)買就刪除水果,也可以替換水果的名稱。但是您無法將水果保存到列表中。用簡(jiǎn)單的英語說這樣的話是沒有意義的。

提示:在使用面向?qū)ο蟮木幊陶Z言設(shè)計(jì)類和接口時(shí),請(qǐng)嘗試使用自然語言來檢查您所做的工作是否正確。

例如,說人實(shí)現(xiàn)了person的接口是有道理的,但是說一個(gè)人實(shí)現(xiàn)了一個(gè)帳戶卻沒有道理。

如果您要“保存”水果清單(在這種情況下,要購(gòu)買所有水果),請(qǐng)付款,然后超市會(huì)處理庫存數(shù)據(jù)以檢查他們是否必須從供應(yīng)商處購(gòu)買更多水果。

編程時(shí)可以應(yīng)用相同的邏輯。倉(cāng)儲(chǔ)不應(yīng)保存,更新或刪除數(shù)據(jù)。相反,他們應(yīng)該將其委托給其他類來處理此邏輯。

將數(shù)據(jù)直接保存到倉(cāng)儲(chǔ)中時(shí),還有另一個(gè)問題:您不能使用transaction。

想象一下,我們的應(yīng)用程序具有一種日志記錄機(jī)制,該機(jī)制存儲(chǔ)一些用戶名,并且每次對(duì)API數(shù)據(jù)進(jìn)行更改時(shí)都會(huì)執(zhí)行操作。

現(xiàn)在想象一下,由于某種原因,您調(diào)用了一個(gè)更新用戶名的服務(wù)(這是不常見的情況,但讓我們考慮一下)。

您同意要更改虛擬用戶表中的用戶名,首先必須更新所有日志以正確告訴誰執(zhí)行了該操作,對(duì)嗎?

現(xiàn)在想象我們已經(jīng)為用戶和不同倉(cāng)儲(chǔ)中的日志實(shí)現(xiàn)了update方法,它們都調(diào)用了SaveChanges。如果這些方法之一在更新過程中失敗,會(huì)發(fā)生什么?最終會(huì)導(dǎo)致數(shù)據(jù)不一致。

只有在一切完成之后,我們才應(yīng)該將更改保存到數(shù)據(jù)庫中。為此,我們必須使用transaction,這基本上是大多數(shù)數(shù)據(jù)庫實(shí)現(xiàn)的功能,只有在完成復(fù)雜的操作后才能保存數(shù)據(jù)。

“-好的,所以如果我們不能在這里保存東西,我們應(yīng)該在哪里做?”

處理此問題的常見模式是工作單元模式。此模式包含一個(gè)類,該類將我們的AppDbContext實(shí)例作為依賴項(xiàng)接收,并公開用于開始,完成或中止事務(wù)的方法。

在這里,我們將使用工作單元的簡(jiǎn)單實(shí)現(xiàn)來解決我們的問題。

Repositories在Domain層的倉(cāng)儲(chǔ)文件夾Repositories內(nèi)添加一個(gè)新接口IUnitOfWork:

using System.Threading.Tasks;

namespace Supermarket.API.Domain.Repositories
{
public interface IUnitOfWork
{
Task CompleteAsync();
}
}

如您所見,它僅公開一種將異步完成數(shù)據(jù)管理操作的方法。
現(xiàn)在讓我們添加實(shí)際的實(shí)現(xiàn)。

在Persistence層RepositoriesRepositories文件夾中的添加一個(gè)名為的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();
}
}
}

這是一個(gè)簡(jiǎn)單,干凈的實(shí)現(xiàn),僅在使用倉(cāng)儲(chǔ)修改完所有更改后,才將所有更改保存到數(shù)據(jù)庫中。
如果研究工作單元模式的實(shí)現(xiàn),則會(huì)發(fā)現(xiàn)實(shí)現(xiàn)回滾操作的更復(fù)雜的模式。

由于EF Core已經(jīng)在后臺(tái)實(shí)現(xiàn)了倉(cāng)儲(chǔ)模式和工作單元,因此我們不必在意回滾方法。

“ – 什么?那么為什么我們必須創(chuàng)建所有這些接口和類?”

將持久性邏輯與業(yè)務(wù)規(guī)則分開在代碼可重用性和維護(hù)方面具有許多優(yōu)勢(shì)。如果直接使用EF Core,我們最終將擁有更復(fù)雜的類,這些類將很難更改。

想象一下,將來您決定將ORM框架更改為其他框架,例如Dapper,或者由于性能而必須實(shí)施純SQL查詢。如果將查詢邏輯與服務(wù)耦合在一起,將很難更改該邏輯,因?yàn)槟仨氃谠S多類中進(jìn)行此操作。

使用倉(cāng)儲(chǔ)模式,您可以簡(jiǎn)單地實(shí)現(xiàn)一個(gè)新的倉(cāng)儲(chǔ)類并使用依賴注入將其綁定。

因此,基本上,如果您直接在服務(wù)中使用EF Core,并且必須進(jìn)行一些更改,那么您將獲得:

就像我說的那樣,EF Core在后臺(tái)實(shí)現(xiàn)了工作單元和倉(cāng)儲(chǔ)模式。我們可以將DbSet<>屬性視為倉(cāng)儲(chǔ)。而且,SaveChanges僅在所有數(shù)據(jù)庫操作成功的情況下才保留數(shù)據(jù)。

現(xiàn)在,您知道什么是工作單元以及為什么將其與倉(cāng)儲(chǔ)一起使用,讓我們實(shí)現(xiàn)真實(shí)服務(wù)的邏輯。

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}");
}
}
}

多虧了我們的解耦架構(gòu),我們可以簡(jiǎn)單地將實(shí)例UnitOfWork作為此類的依賴傳遞。
我們的業(yè)務(wù)邏輯非常簡(jiǎn)單。

首先,我們嘗試將新類別添加到數(shù)據(jù)庫中,然后API嘗試保存新類別,將所有內(nèi)容包裝在try-catch塊中。

如果失敗,則API會(huì)調(diào)用一些虛構(gòu)的日志記錄服務(wù),并返回指示失敗的響應(yīng)。

如果該過程順利完成,則應(yīng)用程序?qū)⒎祷爻晒憫?yīng),并發(fā)送我們的類別數(shù)據(jù)。簡(jiǎn)單吧?

提示:在現(xiàn)實(shí)世界的應(yīng)用程序中,您不應(yīng)將所有內(nèi)容包裝在通用的try-catch塊中,而應(yīng)分別處理所有可能的錯(cuò)誤。

簡(jiǎn)單地添加一個(gè)try-catch塊并不能解決大多數(shù)可能的失敗情況。請(qǐng)確保正確實(shí)現(xiàn)錯(cuò)誤處理。

測(cè)試我們的API之前的最后一步是將工作單元接口綁定到其各自的類。

將此新行添加到類的ConfigureServices方法中Startup:

services.AddScoped<IUnitOfWork, UnitOfWork>();

現(xiàn)在讓我們測(cè)試一下!


第14步-使用Postman測(cè)試我們的POST端點(diǎn)

重新啟動(dòng)我們的應(yīng)用程序dotnet run。

我們無法使用瀏覽器測(cè)試POST端點(diǎn)。讓我們使用Postman測(cè)試我們的端點(diǎn)。這是測(cè)試RESTful API的非常有用的工具。

打開Postman,然后關(guān)閉介紹性消息。您會(huì)看到這樣的屏幕:

屏幕顯示測(cè)試端點(diǎn)的選項(xiàng)

GET默認(rèn)情況下,將所選內(nèi)容更改為選擇框POST。

在Enter request URL字段中輸入API地址。

我們必須提供請(qǐng)求正文數(shù)據(jù)以發(fā)送到我們的API。單擊Body菜單項(xiàng),然后將其下方顯示的選項(xiàng)更改為raw。

Postman將在右側(cè)顯示一個(gè)Text選項(xiàng),將其更改為JSON (application/json)并粘貼以下JSON數(shù)據(jù):

{
"name": ""
}

發(fā)送請(qǐng)求前的屏幕

如您所見,我們將向我們的新端點(diǎn)發(fā)送一個(gè)空的名稱字符串。

點(diǎn)擊Send按鈕。您將收到如下輸出:

如您所見,我們的驗(yàn)證邏輯有效!

您還記得我們?yōu)槎它c(diǎn)創(chuàng)建的驗(yàn)證邏輯嗎?此輸出是它起作用的證明!

還要注意右側(cè)顯示的400狀態(tài)代碼。該BadRequest結(jié)果自動(dòng)將此狀態(tài)碼的響應(yīng)。

現(xiàn)在,讓我們將JSON數(shù)據(jù)更改為有效數(shù)據(jù),以查看新的響應(yīng):

最后,我們期望得到的結(jié)果

API正確創(chuàng)建了我們的新資源。

到目前為止,我們的API可以列出和創(chuàng)建類別。您學(xué)到了很多有關(guān)C#語言,ASP.NET Core框架以及構(gòu)造API的通用設(shè)計(jì)方法的知識(shí)。

讓我們繼續(xù)我們的類別API,創(chuàng)建用于更新類別的端點(diǎn)。

從現(xiàn)在開始,由于我向您解釋了大多數(shù)概念,因此我將加快解釋速度,并專注于新主題,以免浪費(fèi)您的時(shí)間。Let’s go!

第15步-更新類別

要更新類別,我們需要一個(gè)HTTP PUT端點(diǎn)。

我們必須編寫的邏輯與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邏輯進(jìn)行比較,您會(huì)注意到這里只有一個(gè)區(qū)別:HttPut屬性指定給定路由應(yīng)接收的參數(shù)。
我們將調(diào)用此端點(diǎn),將類別指定Id 為最后一個(gè)URL片段,例如/api/categories/1。ASP.NET Core管道將此片段解析為相同名稱的參數(shù)。

現(xiàn)在我們必須UpdateAsync在ICategoryService接口中定義方法簽名:

public interface ICategoryService
{
Task<IEnumerable<Category>> ListAsync();
Task<SaveCategoryResponse> SaveAsync(Category category);
Task<SaveCategoryResponse> UpdateAsync(int id, Category category);
}

現(xiàn)在讓我們轉(zhuǎn)向真正的邏輯。

第16步-更新邏輯

首先,要更新類別,我們需要從數(shù)據(jù)庫中返回當(dāng)前數(shù)據(jù)(如果存在)。我們還需要將其更新到我們的中DBSet<>。

讓我們?cè)贗CategoryService界面中添加兩個(gè)新的方法約定:

public interface ICategoryRepository
{
Task<IEnumerable<Category>> ListAsync();
Task AddAsync(Category category);
Task<Category> FindByIdAsync(int id);
void Update(Category category);
}

我們已經(jīng)定義了FindByIdAsync方法,該方法將從數(shù)據(jù)庫中異步返回一個(gè)類別,以及該Update方法。請(qǐng)注意,該Update方法不是異步的,因?yàn)镋F Core API不需要異步方法來更新模型。
現(xiàn)在,讓我們?cè)贑ategoryRepository類中實(shí)現(xiàn)真正的邏輯:

public async Task<Category> FindByIdAsync(int id)
    {       return await _context.Categories.FindAsync(id); }     public void Update(Category category)   {       _context.Categories.Update(category);   }

最后,我們可以對(duì)服務(wù)邏輯進(jìn)行編碼:

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嘗試從數(shù)據(jù)庫中獲取類別。如果結(jié)果為null,我們將返回一個(gè)響應(yīng),告知該類別不存在。如果類別存在,我們需要設(shè)置其新名稱。
然后,API會(huì)嘗試保存更改,例如創(chuàng)建新類別時(shí)。如果該過程完成,則該服務(wù)將返回成功響應(yīng)。如果不是,則執(zhí)行日志記錄邏輯,并且端點(diǎn)接收包含錯(cuò)誤消息的響應(yīng)。

現(xiàn)在讓我們對(duì)其進(jìn)行測(cè)試。首先,讓我們添加一個(gè)新類別Id以使用有效類別。我們可以使用播種到數(shù)據(jù)庫中的類別的標(biāo)識(shí)符,但是我想通過這種方式向您展示我們的API將更新正確的資源。

再次運(yùn)行該應(yīng)用程序,然后使用Postman將新類別發(fā)布到數(shù)據(jù)庫中:

添加新類別以供日后更新

使用一個(gè)可用的數(shù)據(jù)Id,將POST 選項(xiàng)更改PUT為選擇框,然后在URL的末尾添加ID值。將name屬性更改為其他名稱,然后發(fā)送請(qǐng)求以檢查結(jié)果:

類別數(shù)據(jù)已成功更新

您可以將GET請(qǐng)求發(fā)送到API端點(diǎn),以確保您正確編輯了類別名稱:

那是現(xiàn)在GET請(qǐng)求的結(jié)果

我們必須對(duì)類別執(zhí)行的最后一項(xiàng)操作是排除類別。讓我們創(chuàng)建一個(gè)HTTP Delete端點(diǎn)。

第17步-刪除類別

刪除類別的邏輯確實(shí)很容易實(shí)現(xiàn),因?yàn)槲覀兯璧拇蠖鄶?shù)方法都是先前構(gòu)建的。

這些是我們工作路線的必要步驟:

讓我們開始添加新的端點(diǎn)邏輯:

[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屬性還定義了一個(gè)id 模板。
在將DeleteAsync簽名添加到我們的ICategoryService接口之前,我們需要做一些小的重構(gòu)。

新的服務(wù)方法必須返回包含類別數(shù)據(jù)的響應(yīng),就像對(duì)PostAsyncand UpdateAsync方法所做的一樣。我們可以SaveCategoryResponse為此目的重用,但在這種情況下我們不會(huì)保存數(shù)據(jù)。

為了避免創(chuàng)建具有相同形狀的新類來滿足此要求,我們可以將我們重命名SaveCategoryResponse為CategoryResponse。

如果您使用的是Visual Studio Code,則可以打開SaveCategoryResponse類,將鼠標(biāo)光標(biāo)放在類名上方,然后使用選項(xiàng)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);
}

在實(shí)施刪除邏輯之前,我們需要在倉(cāng)儲(chǔ)中使用一種新方法。
將Remove方法簽名添加到ICategoryRepository接口:

void Remove(Category category);

現(xiàn)在,在倉(cāng)儲(chǔ)類上添加真正的實(shí)現(xiàn):

public void Remove(Category category)
{
_context.Categories.Remove(category);
}

EF Core要求將模型的實(shí)例傳遞給Remove方法,以正確了解我們要?jiǎng)h除的模型,而不是簡(jiǎn)單地傳遞Id。
最后,讓我們?cè)贑ategoryService類上實(shí)現(xiàn)邏輯:

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}");
}
}

這里沒有新內(nèi)容。該服務(wù)嘗試通過ID查找類別,然后調(diào)用我們的倉(cāng)儲(chǔ)以刪除類別。最后,工作單元完成將實(shí)際操作執(zhí)行到數(shù)據(jù)庫中的事務(wù)。
“-嘿,但是每個(gè)類別的產(chǎn)品呢?為避免出現(xiàn)錯(cuò)誤,您是否不需要先創(chuàng)建倉(cāng)儲(chǔ)并刪除產(chǎn)品?”

答案是否定的。借助EF Core跟蹤機(jī)制,當(dāng)我們從數(shù)據(jù)庫中加載模型時(shí),框架便知道了該模型具有哪些關(guān)系。如果我們刪除它,EF Core知道它應(yīng)該首先遞歸刪除所有相關(guān)模型。

在將類映射到數(shù)據(jù)庫表時(shí),我們可以禁用此功能,但這在本教程的范圍之外。如果您想了解此功能,請(qǐng)看這里。

現(xiàn)在是時(shí)候測(cè)試我們的新端點(diǎn)了。再次運(yùn)行該應(yīng)用程序,并使用Postman發(fā)送DELETE請(qǐng)求,如下所示:

如您所見,API毫無問題地刪除了現(xiàn)有類別

我們可以通過發(fā)送GET請(qǐng)求來檢查我們的API是否正常工作:

我們已經(jīng)完成了類別API。現(xiàn)在是時(shí)候轉(zhuǎn)向產(chǎn)品API。

步驟18-產(chǎn)品API

到目前為止,您已經(jīng)學(xué)習(xí)了如何實(shí)現(xiàn)所有基本的HTTP動(dòng)詞來使用ASP.NET Core處理CRUD操作。讓我們進(jìn)入實(shí)現(xiàn)產(chǎn)品API的下一個(gè)層次。

我將不再詳細(xì)介紹所有HTTP動(dòng)詞,因?yàn)檫@將是詳盡無遺的。在本教程的最后一部分,我將僅介紹GET請(qǐng)求,以向您展示在從數(shù)據(jù)庫查詢數(shù)據(jù)時(shí)如何包括相關(guān)實(shí)體,以及如何使用Description我們?yōu)镋UnitOfMeasurement 枚舉值定義的屬性。

將新控制器ProductsController添加到名為Controllers的文件夾中。

在這里編寫任何代碼之前,我們必須創(chuàng)建產(chǎn)品資源。

讓我刷新您的記憶,再次顯示我們的資源應(yīng)如何:

{
[
{
"id": 1,
"name": "Sugar",
"quantityInPackage": 1,
"unitOfMeasurement": "KG"
"category": {
"id": 3,
"name": "Sugar"
}
},
… // Other products
]
}

我們想要一個(gè)包含數(shù)據(jù)庫中所有產(chǎn)品的JSON數(shù)組。
JSON數(shù)據(jù)與產(chǎn)品模型有兩點(diǎn)不同:

為了表示度量單位,我們可以使用簡(jiǎn)單的字符串屬性代替枚舉類型(順便說一下,我們沒有JSON數(shù)據(jù)的默認(rèn)枚舉類型,因此我們必須將其轉(zhuǎn)換為其他類型)。

現(xiàn)在,我們現(xiàn)在要塑造新資源,讓我們創(chuàng)建它。ProductResource在Resources文件夾中添加一個(gè)新類:

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;}
}
}

現(xiàn)在,我們必須配置模型類和新資源類之間的映射。
映射配置將與用于其他映射的配置幾乎相同,但是在這里,我們必須處理將EUnitOfMeasurement枚舉轉(zhuǎn)換為字符串的操作。

您還記得StringValue應(yīng)用于枚舉類型的屬性嗎?現(xiàn)在,我將向您展示如何使用.NET框架的強(qiáng)大功能:反射 API提取此信息。

反射 API是一組強(qiáng)大的資源工具集,可讓我們提取和操作元數(shù)據(jù)。許多框架和庫(包括ASP.NET Core本身)都利用這些資源來處理許多后臺(tái)工作。

現(xiàn)在讓我們看看它在實(shí)踐中是如何工作的。將新類添加到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();
}
}
}

第一次看代碼可能會(huì)讓人感到恐懼,但這并不復(fù)雜。讓我們分解代碼定義以了解其工作原理。
首先,我們定義了一種通用方法(一種方法,該方法可以接收不止一種類型的參數(shù),在這種情況下,該方法由TEnum聲明表示),該方法接收給定的枚舉作為參數(shù)。

由于enum是C#中的保留關(guān)鍵字,因此我們?cè)趨?shù)名稱前面添加了@,以使其成為有效名稱。

該方法的第一步是使用該方法獲取參數(shù)的類型信息(類,接口,枚舉或結(jié)構(gòu)定義)GetType。

然后,該方法使用來獲取特定的枚舉值(例如Kilogram)GetField(@enum.ToString())。

下一行找到Description應(yīng)用于枚舉值的所有屬性,并將其數(shù)據(jù)存儲(chǔ)到數(shù)組中(在某些情況下,我們可以為同一屬性指定多個(gè)屬性)。

最后一行使用較短的語法來檢查我們是否至少有一個(gè)枚舉類型的描述屬性。如果有,我們將返回Description此屬性提供的值。如果不是,我們使用默認(rèn)的強(qiáng)制類型轉(zhuǎn)換將枚舉作為字符串返回。

?.操作者(零條件運(yùn)算)檢查該值是否null訪問其屬性之前。

??運(yùn)算符(空合并運(yùn)算符)告訴應(yīng)用程序在左邊的返回值,如果它不為空,或者在正確的,否則價(jià)值。

現(xiàn)在我們有了擴(kuò)展方法來提取描述,讓我們配置模型和資源之間的映射。多虧了AutoMapper,我們只需要多一行就可以做到這一點(diǎn)。

打開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使用新的擴(kuò)展方法將我們的EUnitOfMeasurement值轉(zhuǎn)換為包含其描述的字符串。簡(jiǎn)單吧?您可以閱讀官方文檔以了解完整語法。
注意,我們尚未為category屬性定義任何映射配置。因?yàn)槲覀冎盀轭悇e配置了映射,并且由于產(chǎn)品模型具有相同類型和名稱的category屬性,所以AutoMapper隱式知道應(yīng)該使用各自的配置來映射它。

現(xiàn)在,我們添加端點(diǎn)代碼。更改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;
}
}
}

基本上,為類別控制器定義的結(jié)構(gòu)相同。
讓我們進(jìn)入服務(wù)部分。將一個(gè)新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();
}
}

您應(yīng)該已經(jīng)意識(shí)到,在真正實(shí)現(xiàn)新服務(wù)之前,我們需要一個(gè)倉(cāng)儲(chǔ)。
IProductRepository在相應(yīng)的文件夾中添加一個(gè)名為的新接口:

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();
}
}

現(xiàn)在,我們實(shí)現(xiàn)倉(cāng)儲(chǔ)。除了必須在查詢數(shù)據(jù)時(shí)返回每個(gè)產(chǎn)品的相應(yīng)類別數(shù)據(jù)外,我們幾乎必須像對(duì)類別倉(cāng)儲(chǔ)一樣實(shí)現(xiàn)。
默認(rèn)情況下,EF Core在查詢數(shù)據(jù)時(shí)不包括與模型相關(guān)的實(shí)體,因?yàn)樗赡芊浅Bㄏ胂笠粋€(gè)具有十個(gè)相關(guān)實(shí)體的模型,所有相關(guān)實(shí)體都有自己的關(guān)系)。

要包括類別數(shù)據(jù),我們只需要多一行:

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();
}
}
}

請(qǐng)注意對(duì)的調(diào)用Include(p => p.Category)。我們可以鏈接此語法,以在查詢數(shù)據(jù)時(shí)包含盡可能多的實(shí)體。執(zhí)行選擇時(shí),EF Core會(huì)將其轉(zhuǎn)換為聯(lián)接。
現(xiàn)在,我們可以ProductService像處理類別一樣實(shí)現(xiàn)類:

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類的新依賴項(xiàng):

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();
}

最后,在測(cè)試API之前,讓我們AppDbContext在初始化應(yīng)用程序時(shí)更改類以包括一些產(chǎn)品,以便我們看到結(jié)果:

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,
}
);
}

我添加了兩個(gè)虛構(gòu)產(chǎn)品,將它們與初始化應(yīng)用程序時(shí)我們播種的類別相關(guān)聯(lián)。
該測(cè)試了!再次運(yùn)行API并發(fā)送GET請(qǐng)求以/api/products使用Postman:

就是這樣!恭喜你!

現(xiàn)在,您將了解如何使用解耦的代碼架構(gòu)使用ASP.NET Core構(gòu)建RESTful API。您了解了.NET Core框架的許多知識(shí),如何使用C#,EF Core和AutoMapper的基礎(chǔ)知識(shí)以及在設(shè)計(jì)應(yīng)用程序時(shí)要使用的許多有用的模式。

您可以檢查API的完整實(shí)現(xiàn),包括產(chǎn)品的其他HTTP動(dòng)詞,并檢查Github倉(cāng)儲(chǔ):

evgomes / supermarket-api

使用ASP.NET Core 2.2構(gòu)建的簡(jiǎn)單RESTful API,展示了如何使用分離的,可維護(hù)的……創(chuàng)建RESTful服務(wù)。github.com

結(jié)論

ASP.NET Core是創(chuàng)建Web應(yīng)用程序時(shí)使用的出色框架。它帶有許多有用的API,可用于構(gòu)建干凈,可維護(hù)的應(yīng)用程序。創(chuàng)建專業(yè)應(yīng)用程序時(shí),可以將其視為一種選擇。

文章轉(zhuǎn)自微信公眾號(hào)@DotNET技術(shù)圈

上一篇:

在C#中使用RESTful API的幾種好方法

下一篇:

在 .NET 和 Python 中創(chuàng)建了相同的 API — 哪個(gè)性能更好
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊(cè)

多API并行試用

數(shù)據(jù)驅(qū)動(dòng)選型,提升決策效率

查看全部API→
??

熱門場(chǎng)景實(shí)測(cè),選對(duì)API

#AI文本生成大模型API

對(duì)比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)

#AI深度推理大模型API

對(duì)比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個(gè)渠道
一鍵對(duì)比試用API 限時(shí)免費(fèi)