石家庄做网站:在ASP.NET Core 2.0中创建Web API

让我们使用最新版本的ASP.NET Core和Entity Framework Core创建一个Web API。

在本指南中,我们将使用WideWorldImporters数据库来创建Web API。

REST API至少提供以下操作:

REST还有其他操作,但本指南不需要它们。

这些操作允许客户端通过REST API执行操作,因此我们的Web API必须包含这些操作。

WideWorldImporters 数据库包含4个模式:

在本指南中,我们将使用Warehouse.StockItems表格。我们将添加代码以使用此实体:允许检索库存项目,按ID检索库存项目,创建,更新和删除数据库中的库存项目。

此API的版本为1。

这是API的路由表:

动词网址描述
得到API / V1 /仓储/ StockItem检索库存商品
得到API / V1 /仓储/ StockItem / ID按ID检索库存项目
POSTAPI / V1 /仓储/ StockItem创建新的库存项目
API / V1 /仓储/ StockItem / ID更新现有库存项目
删除API / V1 /仓储/ StockItem / ID删除现有库存项目

请牢记这些路线,因为API必须实现所有路线。

先决条件

软件

技能

使用代码

对于本指南,源代码的工作目录是C:\ Projects

第01步 - 创建项目

打开Visual Studio并按照下列步骤操作:

  1. 转到文件>新建>项目

  2. 转到已安装> Visual C#> .NET Core

  3. 将项目名称设置为 WideWorldImporters.API

  4. 单击确定

创建项目

在下一个窗口中,选择API和.ASP.NET Core的最新版本,在本例中为2.1:

配置为Api

Visual Studio完成解决方案创建后,我们将看到此窗口:

Api概述

第02步 - 安装Nuget包

在此步骤中,我们需要安装以下NuGet包:

现在,我们将继续EntityFrameworkCore.SqlServer从Nuget 安装软件包,右键单击WideWorldImporters.API项目:

管理NuGet包

更改为“浏览”选项卡并键入Microsoft.EntityFrameworkCore.SqlServer

安装EntityFrameworkCore.SqlServer包

接下来,安装Swashbuckle.AspNetCore包:

安装Swashbuckle.AspNetCore包

Swashbuckle.AspNetCore package允许为Web API启用帮助页面。

这是项目的结构。

现在运行项目以检查解决方案是否准备就绪,按F5,Visual Studio将显示此浏览器窗口:

第一次运行

默认情况下,Visual Studio ValuesControllerControllers目录中添加一个带有名称的文件,将其从项目中删除。

步骤03 - 添加模型

现在,使用名称创建一个目录Models并添加以下文件:

Entities.cs将包含与Entity Framework Core相关的所有代码

Extensions.cs将包含DbContext和集合的扩展方法。

Requests.cs将包含请求的定义。

Responses.cs将包含响应的定义。

Entities.cs文件的代码

using System;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace WideWorldImporters.API.Models
{#pragma warning disable CS1591    public partial class StockItem
    {        public StockItem()
        {
        }        public StockItem(int? stockItemID)
        {
            StockItemID = stockItemID;
        }        public int? StockItemID { get; set; }        public string StockItemName { get; set; }        public int? SupplierID { get; set; }        public int? ColorID { get; set; }        public int? UnitPackageID { get; set; }        public int? OuterPackageID { get; set; }        public string Brand { get; set; }        public string Size { get; set; }        public int? LeadTimeDays { get; set; }        public int? QuantityPerOuter { get; set; }        public bool? IsChillerStock { get; set; }        public string Barcode { get; set; }        public decimal? TaxRate { get; set; }        public decimal? UnitPrice { get; set; }        public decimal? RecommendedRetailPrice { get; set; }        public decimal? TypicalWeightPerUnit { get; set; }        public string MarketingComments { get; set; }        public string InternalComments { get; set; }        public string CustomFields { get; set; }        public string Tags { get; set; }        public string SearchDetails { get; set; }        public int? LastEditedBy { get; set; }        public DateTime? ValidFrom { get; set; }        public DateTime? ValidTo { get; set; }
    }    public class StockItemsConfiguration : IEntityTypeConfiguration<StockItem>
    {        public void Configure(EntityTypeBuilder<StockItem> builder)
        {            // Set configuration for entity
            builder.ToTable("StockItems", "Warehouse");            // Set key for entity
            builder.HasKey(p => p.StockItemID);            // Set configuration for columns

            builder.Property(p => p.StockItemName).HasColumnType("nvarchar(200)").IsRequired();
            builder.Property(p => p.SupplierID).HasColumnType("int").IsRequired();
            builder.Property(p => p.ColorID).HasColumnType("int");
            builder.Property(p => p.UnitPackageID).HasColumnType("int").IsRequired();
            builder.Property(p => p.OuterPackageID).HasColumnType("int").IsRequired();
            builder.Property(p => p.Brand).HasColumnType("nvarchar(100)");
            builder.Property(p => p.Size).HasColumnType("nvarchar(40)");
            builder.Property(p => p.LeadTimeDays).HasColumnType("int").IsRequired();
            builder.Property(p => p.QuantityPerOuter).HasColumnType("int").IsRequired();
            builder.Property(p => p.IsChillerStock).HasColumnType("bit").IsRequired();
            builder.Property(p => p.Barcode).HasColumnType("nvarchar(100)");
            builder.Property(p => p.TaxRate).HasColumnType("decimal(18, 3)").IsRequired();
            builder.Property(p => p.UnitPrice).HasColumnType("decimal(18, 2)").IsRequired();
            builder.Property(p => p.RecommendedRetailPrice).HasColumnType("decimal(18, 2)");
            builder.Property(p => p.TypicalWeightPerUnit).HasColumnType("decimal(18, 3)").IsRequired();
            builder.Property(p => p.MarketingComments).HasColumnType("nvarchar(max)");
            builder.Property(p => p.InternalComments).HasColumnType("nvarchar(max)");
            builder.Property(p => p.CustomFields).HasColumnType("nvarchar(max)");
            builder.Property(p => p.LastEditedBy).HasColumnType("int").IsRequired();            // Computed columns

            builder
                .Property(p => p.StockItemID)
                .HasColumnType("int")
                .IsRequired()
                .HasComputedColumnSql("NEXT VALUE FOR [Sequences].[StockItemID]");

            builder
                .Property(p => p.Tags)
                .HasColumnType("nvarchar(max)")
                .HasComputedColumnSql("json_query([CustomFields],N'$.Tags')");

            builder
                .Property(p => p.SearchDetails)
                .HasColumnType("nvarchar(max)")
                .IsRequired()
                .HasComputedColumnSql("concat([StockItemName],N' ',[MarketingComments])");            // Columns with generated value on add or update

            builder
                .Property(p => p.ValidFrom)
                .HasColumnType("datetime2")
                .IsRequired()
                .ValueGeneratedOnAddOrUpdate();

            builder
                .Property(p => p.ValidTo)
                .HasColumnType("datetime2")
                .IsRequired()
                .ValueGeneratedOnAddOrUpdate();
        }
    }    public class WideWorldImportersDbContext : DbContext
    {        public WideWorldImportersDbContext(DbContextOptions<WideWorldImportersDbContext> options)
            : base(options)
        {
        }        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {            // Apply configurations for entity

            modelBuilder
                .ApplyConfiguration(new StockItemsConfiguration());            base.OnModelCreating(modelBuilder);
        }        public DbSet<StockItem> StockItems { get; set; }
    }#pragma warning restore CS1591}

Code for Extensions.cs文件:

using System.Linq;using System.Threading.Tasks;using Microsoft.EntityFrameworkCore;namespace WideWorldImporters.API.Models
{#pragma warning disable CS1591    public static class WideWorldImportersDbContextExtensions
    {        public static IQueryable<StockItem> GetStockItems(this WideWorldImportersDbContext dbContext, int pageSize = 10, int pageNumber = 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int? supplierID = null, int? unitPackageID = null)
        {            // Get query from DbSet
            var query = dbContext.StockItems.AsQueryable();            // Filter by: 'LastEditedBy'
            if (lastEditedBy.HasValue)
                query = query.Where(item => item.LastEditedBy == lastEditedBy);            // Filter by: 'ColorID'
            if (colorID.HasValue)
                query = query.Where(item => item.ColorID == colorID);            // Filter by: 'OuterPackageID'
            if (outerPackageID.HasValue)
                query = query.Where(item => item.OuterPackageID == outerPackageID);            // Filter by: 'SupplierID'
            if (supplierID.HasValue)
                query = query.Where(item => item.SupplierID == supplierID);            // Filter by: 'UnitPackageID'
            if (unitPackageID.HasValue)
                query = query.Where(item => item.UnitPackageID == unitPackageID);            return query;
        }        public static async Task<StockItem> GetStockItemsAsync(this WideWorldImportersDbContext dbContext, StockItem entity)
            => await dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemID == entity.StockItemID);        public static async Task<StockItem> GetStockItemsByStockItemNameAsync(this WideWorldImportersDbContext dbContext, StockItem entity)
            => await dbContext.StockItems.FirstOrDefaultAsync(item => item.StockItemName == entity.StockItemName);
    }    public static class IQueryableExtensions
    {        public static IQueryable<TModel> Paging<TModel>(this IQueryable<TModel> query, int pageSize = 0, int pageNumber = 0) where TModel : class
            => pageSize > 0 && pageNumber > 0 ? query.Skip((pageNumber - 1) * pageSize).Take(pageSize) : query;
    }#pragma warning restore CS1591}

Requests.cs文件的代码

using System;using System.ComponentModel.DataAnnotations;namespace WideWorldImporters.API.Models
{#pragma warning disable CS1591    public class PostStockItemsRequest
    {
        [Key]        public int? StockItemID { get; set; }

        [Required]
        [StringLength(200)]        public string StockItemName { get; set; }

        [Required]        public int? SupplierID { get; set; }        public int? ColorID { get; set; }

        [Required]        public int? UnitPackageID { get; set; }

        [Required]        public int? OuterPackageID { get; set; }

        [StringLength(100)]        public string Brand { get; set; }

        [StringLength(40)]        public string Size { get; set; }

        [Required]        public int? LeadTimeDays { get; set; }

        [Required]        public int? QuantityPerOuter { get; set; }

        [Required]        public bool? IsChillerStock { get; set; }

        [StringLength(100)]        public string Barcode { get; set; }

        [Required]        public decimal? TaxRate { get; set; }

        [Required]        public decimal? UnitPrice { get; set; }        public decimal? RecommendedRetailPrice { get; set; }

        [Required]        public decimal? TypicalWeightPerUnit { get; set; }        public string MarketingComments { get; set; }        public string InternalComments { get; set; }        public string CustomFields { get; set; }        public string Tags { get; set; }

        [Required]        public string SearchDetails { get; set; }

        [Required]        public int? LastEditedBy { get; set; }        public DateTime? ValidFrom { get; set; }        public DateTime? ValidTo { get; set; }
    }    public class PutStockItemsRequest
    {
        [Required]
        [StringLength(200)]        public string StockItemName { get; set; }

        [Required]        public int? SupplierID { get; set; }        public int? ColorID { get; set; }

        [Required]        public decimal? UnitPrice { get; set; }
    }    public static class Extensions
    {        public static StockItem ToEntity(this PostStockItemsRequest request)
            => new StockItem
            {
                StockItemID = request.StockItemID,
                StockItemName = request.StockItemName,
                SupplierID = request.SupplierID,
                ColorID = request.ColorID,
                UnitPackageID = request.UnitPackageID,
                OuterPackageID = request.OuterPackageID,
                Brand = request.Brand,
                Size = request.Size,
                LeadTimeDays = request.LeadTimeDays,
                QuantityPerOuter = request.QuantityPerOuter,
                IsChillerStock = request.IsChillerStock,
                Barcode = request.Barcode,
                TaxRate = request.TaxRate,
                UnitPrice = request.UnitPrice,
                RecommendedRetailPrice = request.RecommendedRetailPrice,
                TypicalWeightPerUnit = request.TypicalWeightPerUnit,
                MarketingComments = request.MarketingComments,
                InternalComments = request.InternalComments,
                CustomFields = request.CustomFields,
                Tags = request.Tags,
                SearchDetails = request.SearchDetails,
                LastEditedBy = request.LastEditedBy,
                ValidFrom = request.ValidFrom,
                ValidTo = request.ValidTo
            };
    }#pragma warning restore CS1591}

Responses.cs文件的代码

using System.Collections.Generic;using System.Net;using Microsoft.AspNetCore.Mvc;namespace WideWorldImporters.API.Models
{#pragma warning disable CS1591    public interface IResponse
    {        string Message { get; set; }        bool DidError { get; set; }        string ErrorMessage { get; set; }
    }    public interface ISingleResponse<TModel> : IResponse
    {
        TModel Model { get; set; }
    }    public interface IListResponse<TModel> : IResponse
    {
        IEnumerable<TModel> Model { get; set; }
    }    public interface IPagedResponse<TModel> : IListResponse<TModel>
    {        int ItemsCount { get; set; }        double PageCount { get; }
    }    public class Response : IResponse
    {        public string Message { get; set; }        public bool DidError { get; set; }        public string ErrorMessage { get; set; }
    }    public class SingleResponse<TModel> : ISingleResponse<TModel>
    {        public string Message { get; set; }        public bool DidError { get; set; }        public string ErrorMessage { get; set; }        public TModel Model { get; set; }
    }    public class ListResponse<TModel> : IListResponse<TModel>
    {        public string Message { get; set; }        public bool DidError { get; set; }        public string ErrorMessage { get; set; }        public IEnumerable<TModel> Model { get; set; }
    }    public class PagedResponse<TModel> : IPagedResponse<TModel>
    {        public string Message { get; set; }        public bool DidError { get; set; }        public string ErrorMessage { get; set; }        public IEnumerable<TModel> Model { get; set; }        public int PageSize { get; set; }        public int PageNumber { get; set; }        public int ItemsCount { get; set; }        public double PageCount
            => ItemsCount < PageSize ? 1 : (int)(((double)ItemsCount / PageSize) + 1);
    }    public static class ResponseExtensions
    {        public static IActionResult ToHttpResponse(this IResponse response)
        {            var status = response.DidError ? HttpStatusCode.InternalServerError : HttpStatusCode.OK;            return new ObjectResult(response)
            {
                StatusCode = (int)status
            };
        }        public static IActionResult ToHttpResponse<TModel>(this ISingleResponse<TModel> response)
        {            var status = HttpStatusCode.OK;            if (response.DidError)
                status = HttpStatusCode.InternalServerError;            else if (response.Model == null)
                status = HttpStatusCode.NotFound;            return new ObjectResult(response)
            {
                StatusCode = (int)status
            };
        }        public static IActionResult ToHttpResponse<TModel>(this IListResponse<TModel> response)
        {            var status = HttpStatusCode.OK;            if (response.DidError)
                status = HttpStatusCode.InternalServerError;            else if (response.Model == null)
                status = HttpStatusCode.NoContent;            return new ObjectResult(response)
            {
                StatusCode = (int)status
            };
        }
    }#pragma warning restore CS1591}

了解模型

实体

StockItemsclass是Warehouse.StockItems的表示

StockItemsConfigurationclass包含类的映射StockItems

WideWorldImportersDbContext class是数据库和C#代码之间的链接,这个类处理查询并提交数据库中的更改,当然还有另外一些事情。

EXTENSIONS

WideWorldImportersDbContextExtensions 包含DbContext实例的扩展方法,一种用于检索库存项目的方法,另一种用于按ID检索sotck库存项目,另一种用于按名称检索库存项目。

IQueryableExtensions包含扩展方法IQueryable,用于添加分页功能。

要求

我们有以下定义:

PostStockItemsRequest 表示用于创建新库存项目的模型,包含要保存在数据库中的所有必需属性。

PutStockItemsRequest代表机型更新现有库存项目,在这种情况下只包含4个属性:StockItemNameSupplierIDColorIDUnitPrice此类不包含StockItemID属性,因为id位于控制器操作的路径中。

请求模型不需要包含实体等所有属性,因为我们不需要在请求或响应中公开完整定义,使用具有少量属性的模型来限制数据是一种很好的做法。

Extensionsclass包含一个扩展方法PostStockItemsRequest,用于StockItem从请求模型返回的实例

回复

这些是接口:

这些接口中的每一个都有实现,如果返回对象而不将它们包装在这些模型中更简单,为什么我们需要这些定义呢?请记住,这个Web API将为客户端提供操作,具有UI或没有UI,如果发生错误,更容易拥有发送消息的属性,拥有模型或发送信息,此外,我们在响应中设置Http状态代码描述请求的结果。

这些类是通用的,因为通过这种方式,我们可以节省定义将来响应的时间,此Web API仅返回单个实体,列表和分页列表的响应。

ISingleResponse 表示对单个实体的响应。

IListResponse 表示带有列表的响应,例如,所有运送到现有订单而不进行分页。

IPagedResponse 表示具有分页的响应,例如日期范围内的所有订单。

ResponseExtensionsclass包含用于转换Http响应中的响应的扩展方法,这些方法InternalServerError在发生错误时返回(500)状态,OK(200)如果可以,则NotFound(404)如果数据库中不存在实体,或者NoContent(204)用于列表响应没有模特。

步骤04 - 添加控制器

现在,在Controllers目录内,添加名为WarehouseController.cs的代码文件并添加以下代码:

using System;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Logging;using WideWorldImporters.API.Models;namespace WideWorldImporters.API.Controllers
{#pragma warning disable CS1591    [ApiController]
    [Route("api/v1/[controller]")]    public class WarehouseController : ControllerBase
    {        protected readonly ILogger Logger;        protected readonly WideWorldImportersDbContext DbContext;        public WarehouseController(ILogger<WarehouseController> logger, WideWorldImportersDbContext dbContext)
        {
            Logger = logger;
            DbContext = dbContext;
        }#pragma warning restore CS1591
        // GET
        // api/v1/Warehouse/StockItem

        /// <summary>        /// Retrieves stock items        /// </summary>        /// <param name="pageSize">Page size</param>        /// <param name="pageNumber">Page number</param>        /// <param name="lastEditedBy">Last edit by (user id)</param>        /// <param name="colorID">Color id</param>        /// <param name="outerPackageID">Outer package id</param>        /// <param name="supplierID">Supplier id</param>        /// <param name="unitPackageID">Unit package id</param>        /// <returns>A response with stock items list</returns>        /// <response code="200">Returns the stock items list</response>        /// <response code="500">If there was an internal server error</response>        [HttpGet("StockItem")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]        public async Task<IActionResult> GetStockItemsAsync(int pageSize = 10, int pageNumber = 1, int? lastEditedBy = null, int? colorID = null, int? outerPackageID = null, int? supplierID = null, int? unitPackageID = null)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetStockItemsAsync));            var response = new PagedResponse<StockItem>();            try
            {                // Get the "proposed" query from repository
                var query = DbContext.GetStockItems();                // Set paging values
                response.PageSize = pageSize;
                response.PageNumber = pageNumber;                // Get the total rows
                response.ItemsCount = await query.CountAsync();                // Get the specific page from database
                response.Model = await query.Paging(pageSize, pageNumber).ToListAsync();

                response.Message = string.Format("Page {0} of {1}, Total of products: {2}.", pageNumber, response.PageCount, response.ItemsCount);

                Logger?.LogInformation("The stock items have been retrieved successfully.");
            }            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = "There was an internal error, please contact to technical support.";

                Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemsAsync), ex);
            }            return response.ToHttpResponse();
        }        // GET
        // api/v1/Warehouse/StockItem/5

        /// <summary>        /// Retrieves a stock item by ID        /// </summary>        /// <param name="id">Stock item id</param>        /// <returns>A response with stock item</returns>        /// <response code="200">Returns the stock items list</response>        /// <response code="404">If stock item is not exists</response>        /// <response code="500">If there was an internal server error</response>        [HttpGet("StockItem/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(404)]
        [ProducesResponseType(500)]        public async Task<IActionResult> GetStockItemAsync(int id)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(GetStockItemAsync));            var response = new SingleResponse<StockItem>();            try
            {                // Get the stock item by id
                response.Model = await DbContext.GetStockItemsAsync(new StockItem(id));
            }            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = "There was an internal error, please contact to technical support.";

                Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(GetStockItemAsync), ex);
            }            return response.ToHttpResponse();
        }        // POST
        // api/v1/Warehouse/StockItem/

        /// <summary>        /// Creates a new stock item        /// </summary>        /// <param name="request">Request model</param>        /// <returns>A response with new stock item</returns>        /// <response code="200">Returns the stock items list</response>        /// <response code="201">A response as creation of stock item</response>        /// <response code="400">For bad request</response>        /// <response code="500">If there was an internal server error</response>        [HttpPost("StockItem")]
        [ProducesResponseType(200)]
        [ProducesResponseType(201)]
        [ProducesResponseType(400)]
        [ProducesResponseType(500)]        public async Task<IActionResult> PostStockItemAsync([FromBody]PostStockItemsRequest request)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(PostStockItemAsync));            var response = new SingleResponse<StockItem>();            try
            {                var existingEntity = await DbContext
                    .GetStockItemsByStockItemNameAsync(new StockItem { StockItemName = request.StockItemName });                if (existingEntity != null)
                    ModelState.AddModelError("StockItemName", "Stock item name already exists");                if (!ModelState.IsValid)                    return BadRequest();                // Create entity from request model
                var entity = request.ToEntity();                // Add entity to repository
                DbContext.Add(entity);                // Save entity in database
                await DbContext.SaveChangesAsync();                // Set the entity to response model
                response.Model = entity;
            }            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = "There was an internal error, please contact to technical support.";

                Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PostStockItemAsync), ex);
            }            return response.ToHttpResponse();
        }        // PUT
        // api/v1/Warehouse/StockItem/5

        /// <summary>        /// Updates an existing stock item        /// </summary>        /// <param name="id">Stock item ID</param>        /// <param name="request">Request model</param>        /// <returns>A response as update stock item result</returns>        /// <response code="200">If stock item was updated successfully</response>        /// <response code="400">For bad request</response>        /// <response code="500">If there was an internal server error</response>        [HttpPut("StockItem/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(400)]
        [ProducesResponseType(500)]        public async Task<IActionResult> PutStockItemAsync(int id, [FromBody]PutStockItemsRequest request)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(PutStockItemAsync));            var response = new Response();            try
            {                // Get stock item by id
                var entity = await DbContext.GetStockItemsAsync(new StockItem(id));                // Validate if entity exists
                if (entity == null)                    return NotFound();                // Set changes to entity
                entity.StockItemName = request.StockItemName;
                entity.SupplierID = request.SupplierID;
                entity.ColorID = request.ColorID;
                entity.UnitPrice = request.UnitPrice;                // Update entity in repository
                DbContext.Update(entity);                // Save entity in database
                await DbContext.SaveChangesAsync();
            }            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = "There was an internal error, please contact to technical support.";

                Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(PutStockItemAsync), ex);
            }            return response.ToHttpResponse();
        }        // DELETE
        // api/v1/Warehouse/StockItem/5

        /// <summary>        /// Deletes an existing stock item        /// </summary>        /// <param name="id">Stock item ID</param>        /// <returns>A response as delete stock item result</returns>        /// <response code="200">If stock item was deleted successfully</response>        /// <response code="500">If there was an internal server error</response>        [HttpDelete("StockItem/{id}")]
        [ProducesResponseType(200)]
        [ProducesResponseType(500)]        public async Task<IActionResult> DeleteStockItemAsync(int id)
        {
            Logger?.LogDebug("'{0}' has been invoked", nameof(DeleteStockItemAsync));            var response = new Response();            try
            {                // Get stock item by id
                var entity = await DbContext.GetStockItemsAsync(new StockItem(id));                // Validate if entity exists
                if (entity == null)                    return NotFound();                // Remove entity from repository
                DbContext.Remove(entity);                // Delete entity in database
                await DbContext.SaveChangesAsync();
            }            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = "There was an internal error, please contact to technical support.";

                Logger?.LogCritical("There was an error on '{0}' invocation: {1}", nameof(DeleteStockItemAsync), ex);
            }            return response.ToHttpResponse();
        }
    }
}

所有控制器操作的过程是:

  1. 记录方法的调用。

  2. 根据操作(分页,列表或单个)创建响应实例。

  3. 通过DbContext实例执行对数据库的访问。

  4. 如果存储库调用失败,请将DidErrorproperty 设置true和设置ErrorMessage属性:出现内部错误,请联系技术支持。,因为不建议在响应中公开错误详细信息,所以最好将所有异常详细信息保存在日志文件中。

  5. 将结果作为Http响应返回。

请记住以Asyncsufix 结尾的方法的所有名称,因为所有操作都是异步的,但在Http属性中,我们不使用此后缀。

步骤05 - 设置依赖注入

ASP.NET核心能够原生方式依赖注入,这意味着我们不需要任何3 会谈框架在控制器注入依赖。

这是一个很大的挑战,因为我们需要从Web Forms和ASP.NET MVC改变主意,因为那些技术使用框架来注入依赖关系,这是一种奢侈,现在在ASP.NET Core依赖注入是一个基本方面。

ASP.NET Core的项目模板有一个带有名称的类Startup,在这个类中我们必须添加配置来为DbContext,Services,Loggers等注入实例。

修改Startup.cs文件的代码如下所示:

using System;using System.IO;using System.Reflection;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Logging;using Swashbuckle.AspNetCore.Swagger;using WideWorldImporters.API.Controllers;using WideWorldImporters.API.Models;namespace WideWorldImporters.API
{#pragma warning disable CS1591    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_1);            // Add configuration for DbContext
            // Use connection string from appsettings.json file
            services.AddDbContext<WideWorldImportersDbContext>(options =>
            {
                options.UseSqlServer(Configuration["AppSettings:ConnectionString"]);
            });            // Set up dependency injection for controller's logger
            services.AddScoped<ILogger, Logger<WarehouseController>>();            // Register the Swagger generator, defining 1 or more Swagger documents
            services.AddSwaggerGen(options =>
            {
                options.SwaggerDoc("v1", new Info { Title = "WideWorldImporters API", Version = "v1" });                // Get xml comments path
                var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";                var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);                // Set xml path
                options.IncludeXmlComments(xmlPath);
            });
        }        // 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();            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();            // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
            app.UseSwaggerUI(c =>
            {
                c.SwaggerEndpoint("/swagger/v1/swagger.json", "WideWorldImporters API V1");
            });

            app.UseMvc();
        }
    }#pragma warning restore CS1591}

ConfigureServices方法指定了如何解析依赖关系。我们需要建立DbContextLogging

Configure方法为Http请求运行时添加了配置。

步骤06 - 运行Web API

在运行Web API项目之前,在appsettings.json文件中添加连接字符串

{  "Logging": {    "LogLevel": {      "Default": "Warning"
    }
  },  "AllowedHosts": "*",  "AppSettings": {    "ConnectionString": "server=(local);database=WideWorldImporters;integrated security=yes;"
  }
}

要在帮助页面中显示说明,请为Web API项目启用XML文档:

  1. 右键单击Project> Properties

  2. 转到Build > Output

  3. 启用XML文档文件

  4. 保存更改


现在,按F5开始调试Web API项目,如果一切正常,我们将在浏览器中获得以下输出:

在浏览器中获取库存项目

另外,我们可以在另一个标签中加载帮助页面:

帮助页面

步骤07 - 添加单元测试

要为API项目添加单元测试,请按照下列步骤操作:

  1. 右键单击Solution> Add> New Project

  2. 转到已安装> Visual C#>测试> xUnit测试项目(.NET Core)

  3. 将项目名称设置为 WideWorldImporters.API.UnitTests

  4. 单击确定

添加单元测试项目

管理WideWorldImporters.API.UnitTests项目的参考

添加对Api项目的引用

现在添加WideWorldImporters.API项目的参考

单元测试参考经理Project.jpg

创建项目后,为项目添加以下NuGet包:

删除UnitTest1.cs文件。

保存更改并构建WideWorldImporters.API.UnitTests项目。

现在我们继续添加与单元测试相关的代码,这些测试将与内存数据库一起使用

什么是TDD?测试是当今常见的做法,因为通过单元测试,在发布之前很容易对功能进行测试,测试驱动开发(TDD)是定义单元测试和验证代码行为的方法。

TDD的另一个概念是AAA安排行动断言 ; Arrange是用于创建对象Act的块是用于放置方法的所有调用Assert的块是用于验证方法调用的结果的块。

由于我们正在使用内存数据库进行单元测试,因此我们需要创建一个类来模拟WideWorldImportersDbContext类,并添加数据以执行IWarehouseRepository操作测试

需要明确的是:这些单元测试不与SQL Server建立连接

对于单元测试,请添加以下文件:

DbContextMocker.cs文件的代码

using Microsoft.EntityFrameworkCore;using WideWorldImporters.API.Models;namespace WideWorldImporters.API.UnitTests
{    public static class DbContextMocker
    {        public static WideWorldImportersDbContext GetWideWorldImportersDbContext(string dbName)
        {            // Create options for DbContext instance
            var options = new DbContextOptionsBuilder<WideWorldImportersDbContext>()
                .UseInMemoryDatabase(databaseName: dbName)
                .Options;            // Create instance of DbContext
            var dbContext = new WideWorldImportersDbContext(options);            // Add entities in memory
            dbContext.Seed();            return dbContext;
        }
    }
}

DbContextExtensions.cs文件的代码

using System;using WideWorldImporters.API.Models;namespace WideWorldImporters.API.UnitTests
{    public static class DbContextExtensions
    {        public static void Seed(this WideWorldImportersDbContext dbContext)
        {            // Add entities for DbContext instance

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 1,
                StockItemName = "USB missile launcher (Green)",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 25.00m,
                RecommendedRetailPrice = 37.38m,
                TypicalWeightPerUnit = 0.300m,
                MarketingComments = "Complete with 12 projectiles",
                CustomFields = "{ \"CountryOfManufacture\": \"China\", \"Tags\": [\"USB Powered\"] }",
                Tags = "[\"USB Powered\"]",
                SearchDetails = "USB missile launcher (Green) Complete with 12 projectiles",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 2,
                StockItemName = "USB rocket launcher (Gray)",
                SupplierID = 12,
                ColorID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 25.00m,
                RecommendedRetailPrice = 37.38m,
                TypicalWeightPerUnit = 0.300m,
                MarketingComments = "Complete with 12 projectiles",
                CustomFields = "{ \"CountryOfManufacture\": \"China\", \"Tags\": [\"USB Powered\"] }",
                Tags = "[\"USB Powered\"]",
                SearchDetails = "USB rocket launcher (Gray) Complete with 12 projectiles",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 3,
                StockItemName = "Office cube periscope (Black)",
                SupplierID = 12,
                ColorID = 3,
                UnitPackageID = 7,
                OuterPackageID = 6,
                LeadTimeDays = 14,
                QuantityPerOuter = 10,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 18.50m,
                RecommendedRetailPrice = 27.66m,
                TypicalWeightPerUnit = 0.250m,
                MarketingComments = "Need to see over your cubicle wall? This is just what's needed.",
                CustomFields = "{ \"CountryOfManufacture\": \"China\", \"Tags\": [] }",
                Tags = "[]",
                SearchDetails = "Office cube periscope (Black) Need to see over your cubicle wall? This is just what's needed.",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:00:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 4,
                StockItemName = "USB food flash drive - sushi roll",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - sushi roll ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 5,
                StockItemName = "USB food flash drive - hamburger",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }",
                Tags = "[\"16GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - hamburger ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 6,
                StockItemName = "USB food flash drive - hot dog",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - hot dog ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 7,
                StockItemName = "USB food flash drive - pizza slice",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }",
                Tags = "[\"16GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - pizza slice ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 8,
                StockItemName = "USB food flash drive - dim sum 10 drive variety pack",
                SupplierID = 12,
                UnitPackageID = 9,
                OuterPackageID = 9,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 240.00m,
                RecommendedRetailPrice = 358.80m,
                TypicalWeightPerUnit = 0.500m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - dim sum 10 drive variety pack ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 9,
                StockItemName = "USB food flash drive - banana",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }",
                Tags = "[\"16GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - banana ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 10,
                StockItemName = "USB food flash drive - chocolate bar",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - chocolate bar ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 11,
                StockItemName = "USB food flash drive - cookie",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"16GB\",\"USB Powered\"] }",
                Tags = "[\"16GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - cookie ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.StockItems.Add(new StockItem
            {
                StockItemID = 12,
                StockItemName = "USB food flash drive - donut",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB food flash drive - donut ",
                LastEditedBy = 1,
                ValidFrom = Convert.ToDateTime("5/31/2016 11:11:00 PM"),
                ValidTo = Convert.ToDateTime("12/31/9999 11:59:59 PM")
            });

            dbContext.SaveChanges();
        }
    }
}

WarehouseControllerUnitTest.cs文件的代码

using System;using System.Threading.Tasks;using Microsoft.AspNetCore.Mvc;using WideWorldImporters.API.Controllers;using WideWorldImporters.API.Models;using Xunit;namespace WideWorldImporters.API.UnitTests
{    public class WarehouseControllerUnitTest
    {
        [Fact]        public async Task TestGetStockItemsAsync()
        {            // Arrange
            var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestGetStockItemsAsync));            var controller = new WarehouseController(null, dbContext);            // Act
            var response = await controller.GetStockItemsAsync() as ObjectResult;            var value = response.Value as IPagedResponse<StockItem>;

            dbContext.Dispose();            // Assert
            Assert.False(value.DidError);
        }

        [Fact]        public async Task TestGetStockItemAsync()
        {            // Arrange
            var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestGetStockItemAsync));            var controller = new WarehouseController(null, dbContext);            var id = 1;            // Act
            var response = await controller.GetStockItemAsync(id) as ObjectResult;            var value = response.Value as ISingleResponse<StockItem>;

            dbContext.Dispose();            // Assert
            Assert.False(value.DidError);
        }

        [Fact]        public async Task TestPostStockItemAsync()
        {            // Arrange
            var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestPostStockItemAsync));            var controller = new WarehouseController(null, dbContext);            var requestModel = new PostStockItemsRequest
            {
                StockItemID = 100,
                StockItemName = "USB anime flash drive - Goku",
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB anime flash drive - Goku",
                LastEditedBy = 1,
                ValidFrom = DateTime.Now,
                ValidTo = DateTime.Now.AddYears(5)
            };            // Act
            var response = await controller.PostStockItemAsync(requestModel) as ObjectResult;            var value = response.Value as ISingleResponse<StockItem>;

            dbContext.Dispose();            // Assert
            Assert.False(value.DidError);
        }

        [Fact]        public async Task TestPutStockItemAsync()
        {            // Arrange
            var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestPutStockItemAsync));            var controller = new WarehouseController(null, dbContext);            var id = 12;            var requestModel = new PutStockItemsRequest
            {
                StockItemName = "USB food flash drive (Update)",
                SupplierID = 12,
                ColorID = 3
            };            // Act
            var response = await controller.PutStockItemAsync(id, requestModel) as ObjectResult;            var value = response.Value as IResponse;

            dbContext.Dispose();            // Assert
            Assert.False(value.DidError);
        }

        [Fact]        public async Task TestDeleteStockItemAsync()
        {            // Arrange
            var dbContext = DbContextMocker.GetWideWorldImportersDbContext(nameof(TestDeleteStockItemAsync));            var controller = new WarehouseController(null, dbContext);            var id = 5;            // Act
            var response = await controller.DeleteStockItemAsync(id) as ObjectResult;            var value = response.Value as IResponse;

            dbContext.Dispose();            // Assert
            Assert.False(value.DidError);
        }
    }
}

我们可以看到,WarehouseControllerUnitTest包含Web API的所有测试,这些是方法:

方法描述
TestGetStockItemsAsync检索库存商品
TestGetStockItemAsync按ID检索现有库存料品
TestPostStockItemAsync创建新的库存项目
TestPutStockItemAsync更新现有库存项目
TestDeleteStockItemAsync删除现有库存项目

单元测试如何工作?

DbContextMockerWideWorldImportersDbContext在内存数据库中创建一个使用实例,该dbName参数设置内存数据库中的名称; 然后有一个Seed方法的调用,这个方法WideWorldImportersDbContext例如添加实体以提供结果。

DbContextExtensionsclass包含Seed扩展方法。

WarehouseControllerUnitTestclass包含类的所有测试WarehouseController

请记住,每个测试在每个测试方法内部使用不同的内存数据库。我们使用nameof运算符的测试方法名称在内存数据库中检索

在这个级别(单元测试),我们只需要检查存储库的操作,不需要使用SQL数据库(关系,事务等)。

单元测试的过程是:

  1. 创建一个实例 WideWorldImportersDbContext

  2. 创建一个控制器实例

  3. 调用控制器的方法

  4. 从控制器的调用中获取价值

  5. 处理WideWorldImportersDbContext实例

  6. 验证响应

运行单元测试

保存所有更改并构建WideWorldImporters.API.UnitTests项目。

现在,检查测试资源管理器中的测试:

单元测试的测试资源管理器

使用测试资源管理器运行所有测试,如果出现任何错误,请检查错误消息,查看代码并重复此过程。

步骤08 - 添加集成测试

要为API项目添加集成测试,请按照下列步骤操作:

  1. 右键单击Solution> Add> New Project

  2. 转到已安装> Visual C#>测试> xUnit测试项目(.NET Core)

  3. 将项目名称设置为 WideWorldImporters.API.IntegrationTests

  4. 单击确定

添加集成测试项目

管理WideWorldImporters.API.IntegrationTests项目的参考

添加对Api项目的引用

现在添加WideWorldImporters.API项目的参考

参考管理器集成测试项目

创建项目后,为项目添加以下NuGet包:

删除UnitTest1.cs文件。

保存更改并构建WideWorldImporters.API.IntegrationTests项目。

单元测试和集成测试有什么区别?对于单元测试,我们模拟Web API项目和集成测试的所有依赖项,我们运行一个模拟Web API执行的过程,这意味着Http请求。

现在我们继续添加与集成测试相关的代码。

对于这个项目,集成测试将执行Http请求,每个Http请求将对SQL Server实例中的现有数据库执行操作。我们将使用SQL Server的本地实例,这可以根据您的工作环境进行更改,我的意思是集成测试的范围。

TestFixture.cs文件的代码

 using System;using System.IO;using System.Net.Http;using System.Net.Http.Headers;using System.Reflection;using Microsoft.AspNetCore.Hosting;using Microsoft.AspNetCore.Mvc.ApplicationParts;using Microsoft.AspNetCore.Mvc.Controllers;using Microsoft.AspNetCore.Mvc.ViewComponents;using Microsoft.AspNetCore.TestHost;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;namespace WideWorldImporters.API.IntegrationTests
{    public class TestFixture<TStartup> : IDisposable
    {        public static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
        {            var projectName = startupAssembly.GetName().Name;            var applicationBasePath = AppContext.BaseDirectory;            var directoryInfo = new DirectoryInfo(applicationBasePath);            do
            {
                directoryInfo = directoryInfo.Parent;                var projectDirectoryInfo = new DirectoryInfo
                (Path.Combine(directoryInfo.FullName, projectRelativePath));                if (projectDirectoryInfo.Exists)                    if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, 
                    projectName, $"{projectName}.csproj")).Exists)                        return Path.Combine(projectDirectoryInfo.FullName, projectName);
            }            while (directoryInfo.Parent != null);            throw new Exception($"Project root could not be located 
            using the application root {applicationBasePath}.");
        }        private TestServer Server;        public TestFixture()
            : this(Path.Combine(""))
        {
        }        protected TestFixture(string relativeTargetProjectParentDir)
        {            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;            var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);            var configurationBuilder = new ConfigurationBuilder()
                .SetBasePath(contentRoot)
                .AddJsonFile("appsettings.json");            var webHostBuilder = new WebHostBuilder()
                .UseContentRoot(contentRoot)
                .ConfigureServices(InitializeServices)
                .UseConfiguration(configurationBuilder.Build())
                .UseEnvironment("Development")
                .UseStartup(typeof(TStartup));

            Server = new TestServer(webHostBuilder);

            Client = Server.CreateClient();
            Client.BaseAddress = new Uri("http://localhost:1234");
            Client.DefaultRequestHeaders.Accept.Clear();
            Client.DefaultRequestHeaders.Accept.Add
            (new MediaTypeWithQualityHeaderValue("application/json"));
        }        public void Dispose()
        {
            Client.Dispose();
            Server.Dispose();
        }        public HttpClient Client { get; }        protected virtual void InitializeServices(IServiceCollection services)
        {            var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;            var manager = new ApplicationPartManager();

            manager.ApplicationParts.Add(new AssemblyPart(startupAssembly));
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
            manager.FeatureProviders.Add(new ViewComponentFeatureProvider());

            services.AddSingleton(manager);
        }
    }
}

ContentHelper.cs文件的代码

using System.Net.Http;using System.Text;using Newtonsoft.Json;namespace WideWorldImporters.API.IntegrationTests
{    public static class ContentHelper
    {        public static StringContent GetStringContent(object obj)
            => new StringContent(JsonConvert.SerializeObject(obj), Encoding.Default, "application/json");
    }
}

WarehouseTests.cs文件的代码

using System;using System.Net.Http;using System.Threading.Tasks;using Newtonsoft.Json;using WideWorldImporters.API.Models;using Xunit;namespace WideWorldImporters.API.IntegrationTests
{    public class WarehouseTests : IClassFixture<TestFixture<Startup>>
    {        private HttpClient Client;        public WarehouseTests(TestFixture<Startup> fixture)
        {
            Client = fixture.Client;
        }

        [Fact]        public async Task TestGetStockItemsAsync()
        {            // Arrange
            var request = "/api/v1/Warehouse/StockItem";            // Act
            var response = await Client.GetAsync(request);            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]        public async Task TestGetStockItemAsync()
        {            // Arrange
            var request = "/api/v1/Warehouse/StockItem/1";            // Act
            var response = await Client.GetAsync(request);            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]        public async Task TestPostStockItemAsync()
        {            // Arrange
            var request = "/api/v1/Warehouse/StockItem";            var requestModel = new
            {
                StockItemName = string.Format("USB anime flash drive - Vegeta {0}", Guid.NewGuid()),
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 15.000m,
                UnitPrice = 32.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": \"Japan\", 
                \"Tags\": [\"32GB\",\"USB Powered\"] }",
                Tags = "[\"32GB\",\"USB Powered\"]",
                SearchDetails = "USB anime flash drive - Vegeta",
                LastEditedBy = 1,
                ValidFrom = DateTime.Now,
                ValidTo = DateTime.Now.AddYears(5)
            };            // Act
            var response = await Client.PostAsync
                           (request, ContentHelper.GetStringContent(request));            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]        public async Task TestPutStockItemAsync()
        {            // Arrange
            var requestUrl = "/api/v1/Warehouse/StockItem/1";            var requestModel = new
            {
                StockItemName = string.Format("USB anime flash drive - Vegeta {0}", Guid.NewGuid()),
                SupplierID = 12,
                Color = 3,
                UnitPrice = 39.00m
            };            // Act
            var response = await Client.PutAsync
                           (requestUrl, ContentHelper.GetStringContent(requestModel));            // Assert
            response.EnsureSuccessStatusCode();
        }

        [Fact]        public async Task TestDeleteStockItemAsync()
        {            // Arrange
            var postRequest = "/api/v1/Warehouse/StockItem";            var requestModel = new
            {
                StockItemName = string.Format("Product to delete {0}", Guid.NewGuid()),
                SupplierID = 12,
                UnitPackageID = 7,
                OuterPackageID = 7,
                LeadTimeDays = 14,
                QuantityPerOuter = 1,
                IsChillerStock = false,
                TaxRate = 10.000m,
                UnitPrice = 10.00m,
                RecommendedRetailPrice = 47.84m,
                TypicalWeightPerUnit = 0.050m,
                CustomFields = "{ \"CountryOfManufacture\": 
                \"USA\", \"Tags\": [\"Sample\"] }",
                Tags = "[\"Sample\"]",
                SearchDetails = "Product to delete",
                LastEditedBy = 1,
                ValidFrom = DateTime.Now,
                ValidTo = DateTime.Now.AddYears(5)
            };            // Act
            var postResponse = await Client.PostAsync
                               (postRequest, ContentHelper.GetStringContent(requestModel));            var jsonFromPostResponse = await postResponse.Content.ReadAsStringAsync();            var singleResponse = 
            JsonConvert.DeserializeObject<SingleResponse<StockItem>>(jsonFromPostResponse);            var deleteResponse = await Client.DeleteAsync
            (string.Format("/api/v1/Warehouse/StockItem/{0}", singleResponse.Model.StockItemID));            // Assert
            postResponse.EnsureSuccessStatusCode();
            Assert.False(singleResponse.DidError);
            deleteResponse.EnsureSuccessStatusCode();
        }
    }
}

我们可以看到,WarehouseTests包含Web API的所有测试,这些是方法:

方法描述
TestGetStockItemsAsync检索库存商品
TestGetStockItemAsync按ID检索现有库存料品
TestPostStockItemAsync创建新的库存项目
TestPutStockItemAsync更新现有库存项目
TestDeleteStockItemAsync删除现有库存项目

集成测试如何工作?

TestFixture<TStartup>class为Web API提供了一个Http客户端,使用Startup项目中的类作为引用来为客户端应用配置。

WarehouseTestsclass包含发送Web API的Http请求的所有方法,Http客户端的端口号是1234

ContentHelperclass包含一个帮助方法,可以StringContent 从请求模型创建JSON,这适用于POSTPUT请求。

集成测试的过程是:

  1. 在类构造函数中创建的Http客户端

  2. 定义请求:url和请求模型(如果适用)

  3. 发送请求

  4. 从响应中获取值

  5. 确保响应具有成功状态

运行集成测试

保存所有更改并构建WideWorldImporters.API.IntegrationTests项目,测试资源管理器将显示项目中的所有测试:

用于集成测试的测试资源管理器

请记住要执行集成测试,您需要运行SQL Server实例,appsettings.json文件中的连接字符串将用于与SQL Server建立连接。

现在运行所有集成测试,测试资源管理器如下图所示:

执行集成测试

如果执行集成测试时出现任何错误,请检查错误消息,查看代码并重复此过程。

代码挑战

此时,您具备扩展API的技能,将此作为挑战并添加以下测试(单元和集成):

测试描述
按参数获取库存商品为使库存物品通过搜索请求lastEditedBycolorIDouterPackageIDsupplierIDunitPackageID的参数。
获取不存在的库存商品使用不存在的ID获取库存项并检查Web API返回NotFound(404)状态。
添加具有现有名称的库存项目添加具有现有名称的库存项,并检查Web API返回BadRequest(400)状态。
添加没有必填字段的库存商品添加没有必填字段的库存项目并检查Web API返回BadRequest(400)状态。
更新不存在的库存项目使用不存在的ID更新库存项目并检查Web API返回NotFound(404)状态。
更新现有库存项目而不包含必填字段更新没有必填字段的现有库存项,并检查Web API返回BadRequest(400)状态。
删除不存在的库存项目使用不存在的ID删除库存项目并检查Web API返回NotFound(404)状态。
删除包含订单的库存商品使用不存在的ID删除库存项目并检查Web API返回NotFound(404)状态。

遵循单元和集成测试中使用的约定来完成此挑战。

祝好运!

代码改进

石家庄做网站:在ASP.NET Core 2.0中创建Web API

合作伙伴

网站备案:豫ICP备15023476号-1 唯特科技