IT教程 ·

运用ASP.NET Core构建RESTful API的手艺指南

幻读在 InnoDB 中是被如何解决的?

译者荐语:运用周末的时刻,本人拜读了长沙.NET手艺社区翻译的手艺规范《》,盘算依据步骤写一个完全的教程,厥后无意中看到了这篇文章,与我要写的主题有不少类似之处,特地翻译下来,全文快要3万字,值得人人珍藏。尤其是作者看待问题的严谨头脑,更是令我佩服。

一步一步的构建整齐、可庇护的RESTful APIs

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第1张

总览

RESTful不是一个新名词。它是一种架构作风,这类架构作风运用Web效劳从客户端运用程序吸收数据和向客户端运用程序发送数据。其目标是鸠合差别客户端运用程序将运用的数据。

挑选准确的东西来编写RESTful效劳至关主要,因为我们须要关注可伸缩性,庇护,文档以及统统其他相干方面。在 Core为我们供应了一个功用壮大、易于运用的API,运用这些API将很好的完成这个目标。

在本文中,我将向您展现怎样运用ASP.NET Core框架为“险些”现实天下的场景编写组织优越的RESTful API。我将细致引见罕见的情势和战略以简化开发历程。

我还将向您展现怎样集成通用框架和库,比方和,以供应必要的功用。

先决条件

我愿望您相识面向对象的编程观点。

纵然我将引见的许多细节,我照样发起您具有该主题的基础学问。

我还假定您晓得什么是REST,怎样事变,什么是API端点以及什么是。关于此主题。末了,您须要相识关联数据库的事变原理。

要与我一同编码,您将必须装置以及(我将用来测试API的东西)。我发起您运用诸如类的代码编辑器来开发API。挑选您喜好的代码编辑器。假如挑选Visual Studio Code作为您的代码编辑器,发起您装置以更好地凸起显现代码。

您可以在本文末端找到该API的Github的链接,以搜检终究效果。

局限

让我们为一家超市编写一个假造的Web API。假定我们必须完成以下局限:

  • 建立一个RESTful效劳,该效劳许可客户端运用程序治理超市的产物目次。它须要公然端点以建立,读取,编辑和删除产物种别,比方乳制品和化妆品,还须要治理这些种别的产物。
  • 关于种别,我们须要存储其称号。关于产物,我们须要存储其称号,器量单位(比方,按分量丈量的产物为KG),包装中的数目(比方,假如一包饼干是10,则为10)及其各自的种别。

为了简化示例,我将不处置惩罚库存产物,产物运输,平安性和任何其他功用。这个局限足以向您展现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
  ]
}

让我们入手下手编写运用程序。

第1步-建立API

起首,我们必须为Web效劳建立文件夹组织,然后我们必须运用来构建基础的Web API。翻开终端或敕令提醒符(取决于您运用的操纵体系),并顺次键入以下敕令:

mkdir src/Supermarket.API

cd src/Supermarket.API

dotnet new webapi

前两个敕令只是为API建立一个新目次,然后将当前位置变动成新文件夹。末了一个遵照Web API模板生成一个新项目,这是我们正在开发的运用程序。您可以浏览有关这些敕令和其他项目模板的更多信息,并可以经由过程来生成其他项目模板。
如今,新目次将具有以下组织:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第2张

项目组织

组织概述

ASP.NET Core运用程序由在类中设置的一组(运用程序流水线中的小块运用程序,用于处置惩罚请乞降响应)构成Startup。假如您之前已运用过之类的框架,那末这个观点对您来讲并非什么新颖事物。

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端点时,我将细致引见每一个。如今,只需删除此类,因为我们不会运用它。

第2步-建立范畴模子

我将运用一些设想观点,以使运用程序简朴易庇护。

编写可以由您自身明白和庇护的代码并不难,然则您必须切记您将成为团队的一部份。假如您不注重怎样编写代码,那末效果将是一个庞然大物,这将使您和您的团队成员头痛不已。听起来很极度吧?然则置信我,这就是现实。

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第3张

权衡好代码的规范是WTF的频次。原图来自,发表于。该图遵照CC-BY-2.0。

在Supermarket.API目次中,建立一个名为的新文件夹Domain。在新的范畴文件夹中,建立另一个名为的文件夹Models。我们必须增加到此文件夹的第一个模子是Category。最初,它将是一个简朴的类。这意味着该类将仅具有形貌其基础信息的属性。

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端点。

第3步-种别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手艺的公司是新人。

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第4张

“-好的,我们定义了此接口,然则它什么也没做。有什么用?”

假如您来自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。我们将运用此字段接见种别效劳完成的要领。
趁便说一下,下划线前缀是示意字段的另一个通用商定。特别地,的不发起运用此,然则这是一种异常广泛的做法,可以防止运用“ this”关键字来区分类字段和局部变量。我个人认为浏览起来要清洁很多,而且许多框架和库都运用此商定。

在组织函数下,我定义了用于处置惩罚请求的要领/api/categories。该HttpGet 属性通知ASP.NET Core管道运用该属性来处置惩罚GET请求(可以省略此属性,然则最好编写它以便于浏览)。

该要领运用我们的CategoryService实例列出统统种别,然后将种别返回给客户端。框架管道将数据序列化为JSON对象。IEnumerable范例通知框架,我们想要返回一个种别的罗列,而Task范例(运用async关键字润饰)通知管道,这个要领应当异步实行。末了,当我们定义一个异步要领时,我们必须运用await关键字来处置惩罚须要一些时刻的使命。

好的,我们定义了API的初始组织。如今,有必要真正完成种别效劳。

步骤4-完成种别效劳

在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操纵,从而将数据库接见与运用程序的其余部份断绝开。

我们的效劳须要挪用种别仓储,以猎取列表对象。

从观点上讲,效劳可以与一个或多个仓储或其他效劳“对话”以实行操纵。

建立用于处置惩罚数据接见逻辑的新定义好像是过剩的,然则您将在一段时刻内看到将这类逻辑与效劳类断绝是异常有益的。

让我们建立一个仓储,该仓储担任与数据库通讯,作为耐久化保存种别的一种体式格局。

步骤5-种别仓储和耐久层

在该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属性。这些属性是将模子映照到数据库表的鸠合(唯一对象的鸠合)。

别的,我们必须将模子的属性映照到响应的列,指定哪些属性是主键,哪些是外键,列范例等。我们可以运用称为的功用来掩盖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等数据库,则没必要指定这些标识符。假如您想相识此行动,请搜检。

在完成数据库高低文类以后,我们可以完成种别仓储。增加一个名为新的文件夹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 ,这是最有效的要领。这类体式格局仅当您挪用将数据转换为鸠合的要领或运用要领猎取特定数据时才实行查询。

如今,我们有了种别控制器,效劳和仓储库的代码完成。

我们将关注点分脱离来,建立了只实行应做的事变的类。

测试运用程序之前的末了一步是运用ASP.NET Core依靠项注入机制将我们的接口绑定到响应的类。

第6步-设置依靠注入

如今是时刻让您终究相识此观点的事变原理了。

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第5张

在运用程序的根文件夹中,翻开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作为组织函数参数的实例的类时,都应运用该类的雷同实例。假如内存中没有实例,则管道将建立一个新实例,并在给定请求时期在须要它的统统类中重用它。如许,您无需在须要运用时手动建立类实例。

假如你想相识其他有关生命周期的学问,可以浏览。

依靠注入手艺为我们供应了许多上风,比方:

  • 代码可重用性;
  • 更高的生产力,因为当我们不能不变动完成时,我们无需省心去变动您运用该功用的一百个处所;
  • 您可以轻松地测试运用程序,因为我们可以运用mock(类的伪完成)断绝必须测试的内容,而我们必须将接口作为组织函数参数举行通报。
  • 当一个类须要经由过程组织函数吸收更多的依靠关联时,您没必要手动变动正在建立实例的统统位置(太赞了!)。

设置数据库高低文以后,我们还将我们的效劳和仓储绑定到响应的类。

services.AddScoped<ICategoryRepository, CategoryRepository>();

services.AddScoped<ICategoryService, CategoryService>();

在这里,我们还运用了scoped生存周期,因为这些类在内部必须运用数据库高低文类。在这类状况下,指定雷同的局限是有意义的。
如今我们设置了依靠绑定,我们必须在Program类上举行一些小的变动,以便数据库准确地初始化种子数据。此步骤仅在运用内存数据库供应程序时才须要实行(请参阅以相识缘由)。

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端点了。

第7步-测试种别

在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:UsersevgomesAppDataLocalASP.NETDataProtection-Keys’ as key repository and Windows DPAPI to encrypt keys at rest.

Hosting environment: Development

Content root path: C:UsersevgomesDesktopTutorialssrcSupermarket.API

Now listening on: https://localhost:5001

Now listening on: http://localhost:5000

Application started. Press Ctrl+C to shut down.

您可以看到挪用了EF Core来初始化数据库。末了几行显现运用程序在哪一个端口上运转。
翻开浏览器,然后导航到  (或控制台输出上显现的URL)。假如您发明因为HTTPS致使的平安毛病,则只需为运用程序增加一个破例。

浏览器将显现以下JSON数据作为输出:

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

在这里,我们看到设置数据库高低文时增加到数据库的数据。此输出确认我们的代码正在运转。
您运用很少的代码行建立了GET API端点,而且因为当前API项目标架构情势,您的代码组织确切很轻易变动。

如今,该向您展如今因为营业须要而不能不对其举行变动时,变动此代码有何等轻易。

步骤8-建立种别资本

假如您还记得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是.NET天下中异常盛行的库,而且在许多贸易和开源项目中运用。

在敕令行中输入以下敕令,以将AutoMapper增加到我们的运用程序中:

dotnet add package AutoMapper

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

要运用AutoMapper,我们必须做两件事:

  • 注册它以举行依靠注入;
  • 建立一个类,该类将通知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,不是吗?

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第6张

如今,运用dotnet run敕令再次启动API,然后转到以检察新的JSON响应。

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第7张

这是您应当看到的响应数据

我们已有了GET端点。如今,让我们为POST(建立)种别建立一个新端点。

第9步-建立新种别

在处置惩罚资本建立时,我们必须体贴许多事变,比方:

  • 数据考证和数据完全性;
  • 受权建立资本;
  • 毛病处置惩罚;
  • 正在纪录。

在本教程中,我不会显现怎样处置惩罚身份考证和受权,然则您可以浏览教程,相识怎样轻松完成这些功用。

别的,有一个异常盛行的框架称为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发送到我们的运用程序时,框架将自动将其剖析为我们的新类。

如今,让我们完成路由逻辑。我们必须遵照一些步骤才胜利建立新种别:

  • 起首,我们必须考证传入的请求。假如请求无效,我们必须返回包含毛病音讯的毛病请求响应;
  • 然后,假如请求有效,则必须运用AutoMapper将新资本映照到种别模子类。
  • 如今,我们须要挪用我们的效劳,通知它保存我们的新种别。假如实行保存逻辑没有问题,它将返回一个包含我们新种别数据的响应。假如没有,它应当给我们一个指导,表明该历程失利了,并大概涌现毛病音讯。
  • 末了,假如有毛病,我们将返回毛病的请求。假如没有,我们将新的种别模子映照到种别资本,并向客户端返回包含新种别数据的胜利响应。

这好像很庞杂,然则运用为API构建的效劳架构来完成此逻辑确切很轻易。

让我们入手下手考证传入的请求。

步骤10-运用模子状况考证请求主体

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代码。

该扩大运用,这是.NET的异常有效的功用,它使我们可以运用链式语法来查询和转换数据。此处的表达式将考证毛病要领转换为包含毛病音讯的字符串列表。

Supermarket.API.Extensions在举行下一步之前,将称号空间导入Categories控制器。

using Supermarket.API.Extensions;

让我们经由过程将新资本映照到种别模子类来继承完成端点逻辑。

步骤11-映照新资本

我们已定义了映照设置文件,可以将模子转换为资本。如今,我们须要一个与之相反的新设置项。

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

第12步-运用请求-响应情势来处置惩罚保存逻辑

如今我们必须完成最风趣的逻辑:保存一个新种别。我们愿望我们的效劳可以做到。

因为连接到数据库时涌现问题,或许因为任何内部营业划定规矩使我们的数据无效,因而保存逻辑大概会失利。

假如涌现问题,我们不能简朴地抛出一个毛病,因为它大概会住手API,而且客户端运用程序也不晓得怎样处置惩罚该问题。别的,我们大概会有某种日记纪录机制来纪录毛病。

保存要领的商定(即要领的署名和响应范例)须要指导我们是不是准确切行了该历程。假如处置惩罚一般,我们将吸收种别数据。假如没有,我们最少必须收到一条毛病音讯,通知您该历程失利的缘由。

我们可以经由过程运用request-response情势来完成此功用。这类企业设想情势将我们的请乞降响应参数封装到类中,以封装我们的效劳将用于处置惩罚某些使命并将信息返回给正在运用该效劳的类的信息。

这类情势为我们供应了一些上风,比方:

  • 假如我们须要变动效劳以吸收更多参数,则没必要损坏其署名;
  • 我们可认为我们的请乞降/或响应定义规范合同;
  • 我们可以在不住手运用程序流程的状况下处置惩罚营业逻辑和潜伏的失利,而且我们不须要运用大批的try-catch块。

让我们为处置惩罚数据变动的效劳要领建立一个规范响应范例。关于这类范例的每一个请求,我们都想晓得该请求是不是被准确切行。假如失利,我们要向客户端返回毛病音讯。

在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属性,假如请求胜利完成,该属性将包含我们的种别数据。
请注重,我为此类定义了三种差别的组织函数:

  • 一个私有的,它将把胜利和音讯参数通报给基类,并设置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);
        }
    }

我们只需将种别通报给此要领,它将处置惩罚保存模子数据,编排仓储和其他必要效劳所需的统统逻辑。
请注重,因为我们不须要任何其他参数来实行此使命,因而我不在此处建立特定的请求类。有一个名为的 —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。假如这些要领之一在更新历程当中失利,会发作什么?终究会致使数据不一致。

只要在统统完成以后,我们才应当将变动保存到数据库中。为此,我们必须运用,这基础上是大多数数据库完成的功用,只要在完成庞杂的操纵后才保存数据。

“-好的,所以假如我们不能在这里保存东西,我们应当在那里做?”

处置惩罚此问题的罕见情势是。此情势包含一个类,该类将我们的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框架变动成其他框架,比方,或许因为机能而必须实行纯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,然后封闭引见性音讯。您会看到如许的屏幕:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第8张

屏幕显现测试端点的选项

GET默许状况下,将所选内容变动成挑选框POST。

在Enter request URL字段中输入API地点。

我们必须供应请求正文数据以发送到我们的API。单击Body菜单项,然后将其下方显现的选项变动成raw。

Postman将在右边显现一个Text选项,将其变动成JSON (application/json)并粘贴以下JSON数据:

{
  "name": ""
}

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第9张

发送请求前的屏幕

如您所见,我们将向我们的新端点发送一个空的称号字符串。

点击Send按钮。您将收到以下输出:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第10张

如您所见,我们的考证逻辑有效!

您还记得我们为端点建立的考证逻辑吗?此输出是它起作用的证实!

还要注重右边显现的400状况代码。该BadRequest效果自动将此状况码的响应。

如今,让我们将JSON数据变动成有效数据,以检察新的响应:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第11张

末了,我们希冀获得的效果

API准确建立了我们的新资本。

到现在为止,我们的API可以列出和建立种别。您学到了许多有关C#言语,ASP.NET Core框架以及组织API的通用设想要领的学问。

让我们继承我们的种别API,建立用于更新种别的端点。

从如今入手下手,因为我向您诠释了大多数观点,因而我将加速诠释速率,并专注于新主题,以避免糟蹋您的时刻。 Let’s go!

第15步-更新种别

要更新种别,我们须要一个HTTP PUT端点。

我们必须编写的逻辑与POST逻辑异常类似:

  • 起首,我们必须运用来考证传入的请求ModelState。
  • 假如请求有效,则API应运用AutoMapper将传入资本映照到模子类。
  • 然后,我们须要挪用我们的效劳,通知它更新种别,供应响应的种别Id和更新的数据;
  • 假如Id数据库中没有给定的种别,我们将返回毛病的请求。我们可以运用NotFound效果来替代,然则关于这个局限而言,这并不主要,因为我们向客户端运用程序供应了毛病音讯。
  • 假如准确切行了保存逻辑,则效劳必须返回包含更新的种别数据的响应。假如不是,它应当给我们指导该历程失利,并显现一条音讯指导缘由;
  • 末了,假如有毛病,则API返回毛病的请求。假如不是,它将更新的种别模子映照到种别资本,并将胜利响应返回给客户端运用程序。

让我们将新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);
    }

如今让我们转向真正的逻辑。

第16步-更新逻辑

起首,要更新种别,我们须要从数据库中返回当前数据(假如存在)。我们还须要将其更新到我们的中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将新种别宣布到数据库中:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第12张

增加新种别以供往后更新

运用一个可用的数据Id,将POST 选项变动PUT为挑选框,然后在URL的末端增加ID值。将name属性变动成其他称号,然后发送请求以搜检效果:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第13张

种别数据已胜利更新

您可以将GET请求发送到API端点,以确保您准确编辑了种别称号:

运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第14张

那是如今GET请求的效果

我们必须对种别实行的末了一项操纵是消除种别。让我们建立一个HTTP Delete端点。

第17步-删除种别

删除种别的逻辑确切很轻易完成,因为我们所需的大多数要领都是先前构建的。

这些是我们事变线路的必要步骤:

  • API须要挪用我们的效劳,通知它删除我们的种别,并供应响应的Id;
  • 假如数据库中没有具有给定ID的种别,则该效劳应返回一条音讯指出该种别;
  • 假如实行删除逻辑没有问题,则效劳应返回包含我们已删除种别数据的响应。假如没有,它应当给我们一个指导,表明该历程失利了,并大概涌现毛病音讯。
  • 末了,假如有毛病,则API返回毛病的请求。假如不是,则API会将更新的种别映照到资本,并向客户端返回胜利响应。

让我们入手下手增加新的端点逻辑:

[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 来重定名该类:
运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第15张

确保也重定名文件名。

让我们将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晓得它应当起首递归删除统统相干模子。

在将类映照到数据库表时,我们可以禁用此功用,但这在本教程的局限以外。假如您想相识此功用,。

如今是时刻测试我们的新端点了。再次运转该运用程序,并运用Postman发送DELETE请求,以下所示:
运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第16张

如您所见,API毫无问题地删除了现有种别

我们可以经由过程发送GET请求来搜检我们的API是不是一般事变:
运用ASP.NET Core构建RESTful API的手艺指南 IT教程 第17张

我们已完成了种别API。如今是时刻转向产物API。

步骤18-产物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数据与产物模子有两点差别:

  • 丈量单位以较短的体式格局显现,仅显现其缩写。
  • 我们输出种别数据而不包含CategoryId属性。

为了示意器量单位,我们可以运用简朴的字符串属性替代罗列范例(趁便说一下,我们没有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是一组壮大的资本东西集,可以让我们提取和操纵元数据。许多框架和库(包含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是建立Web运用程序时运用的精彩框架。它带有许多有效的API,可用于构建清洁,可庇护的运用程序。建立专业运用程序时,可以将其视为一种挑选。

本文并未涵盖专业API的统统方面,但您已进修了统统基础学问。您还学到了许多有效的情势,可以处理我们天天面对的情势。

愿望您喜好这篇文章,愿望对您有所协助。期待你的反应,以便我能进一步进步。

进一步进修的可用参考资料

5分钟看懂系列:HTTP缓存机制详解

参与评论