譯者薦語:利用週末的時間,本人拜讀了長沙.NET技術社區翻譯的技術標準《微軟RESTFul API指南》,打算按照步驟寫一個完整的教程,後來無心中看到了這篇文章,與我要寫的主題有很多類似之處,特地翻譯下來,全文將近3萬字,值得你們收藏。尤爲是做者對待問題的嚴謹思惟,更是令我欽佩。javascript
查看譯文html
查看原文(https://www.freecodecamp.org/,持續更新)ios
RESTful不是一個新名詞。它是一種架構風格,這種架構風格使用Web服務從客戶端應用程序接收數據和向客戶端應用程序發送數據。其目標是集中不一樣客戶端應用程序將使用的數據。git
選擇正確的工具來編寫RESTful服務相當重要,由於咱們須要關注可伸縮性,維護,文檔以及全部其餘相關方面。在ASP.NET Core爲咱們提供了一個功能強大、易於使用的API,使用這些API將很好的實現這個目標。github
在本文中,我將向您展現如何使用ASP.NET Core框架爲「幾乎」現實世界的場景編寫結構良好的RESTful API。我將詳細介紹常見的模式和策略以簡化開發過程。web
我還將向您展現如何集成通用框架和庫,例如Entity Framework Core和AutoMapper,以提供必要的功能。shell
我但願您瞭解面向對象的編程概念。數據庫
接下來將介紹C#編程語言的許多細節,我還建議您具備該主題的基本知識。express
我還假設您知道什麼是REST,HTTP協議如何工做,什麼是API端點以及什麼是JSON。這是關於此主題的出色的入門教程。最後,您須要瞭解關係數據庫的工做原理。
要與我一塊兒編碼,您將必須安裝.NET Core 2.2以及Postman(我將用來測試API的工具)。我建議您使用諸如Visual Studio Code之類的代碼編輯器來開發API。選擇您喜歡的代碼編輯器。若是選擇Visual Studio Code做爲您的代碼編輯器,建議您安裝C#擴展以更好地突出顯示代碼。
您能夠在本文末尾找到該API的Github的連接,以檢查最終結果。
讓咱們爲一家超市編寫一個虛構的Web API。假設咱們必須實現如下範圍:
爲了簡化示例,我將不處理庫存產品,產品運輸,安全性和任何其餘功能。這個範圍足以向您展現ASP.NET Core的工做方式。
要開發此服務,咱們基本上須要兩個API 端點:一個用於管理類別,一個用於管理產品。在JSON通信方面,咱們能夠認爲響應以下:
API endpoint: /api/categories 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,並將其發送給客戶端。
如今,讓咱們爲服務實現真正的邏輯。
第13步—數據庫邏輯和工做單元模式
因爲咱們要將數據持久化到數據庫中,所以咱們須要在倉儲中使用一種新方法。
向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>();
如今讓咱們測試一下!
第14步-使用Postman測試咱們的POST端點
從新啓動咱們的應用程序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倉儲:
使用ASP.NET Core 2.2構建的簡單RESTful API,展現瞭如何使用分離的,可維護的……建立RESTful服務。github.com
ASP.NET Core是建立Web應用程序時使用的出色框架。它帶有許多有用的API,可用於構建乾淨,可維護的應用程序。建立專業應用程序時,能夠將其視爲一種選擇。
本文並未涵蓋專業API的全部方面,但您已學習了全部基礎知識。您還學到了許多有用的模式,能夠解決咱們天天面臨的模式。
但願您喜歡這篇文章,但願對您有所幫助。期待你的反饋,以便我能進一步提升。
進一步學習的可用參考資料