译者荐语:利用周末的时间,本人拜读了长沙.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的全部方面,但您已学习了全部基础知识。您还学到了许多有用的模式,能够解决咱们天天面临的模式。
但愿您喜欢这篇文章,但愿对您有所帮助。期待你的反馈,以便我能进一步提升。
进一步学习的可用参考资料