65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 1x 中的 Web API

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (82投票s)

2016 年 7 月 18 日

CPOL

8分钟阅读

viewsIcon

307495

在 ASP.NET Core 1x 中创建 Web API

引言

让我们使用 ASP.NET Core 和 Entity Framework Core 1x 来创建一个 Web API。

背景

如今,就任何组织的数据访问而言,我们需要跨平台共享信息,而 RESTful API 是企业解决方案的一部分。

必备组件

技能

  • C#
  • ORM(对象关系映射)
  • RESTful 服务

软件先决条件

  • Visual Studio 2015 Update 3
  • AdventureWorks 数据库 下载

Using the Code

查看此指南的新版本! 点击这里

步骤 01 - 在 Visual Studio 中创建项目

打开 Visual Studio,然后选择菜单 文件 > 新建 > 项目 > Visual C# - Web > ASP.NET Core Web 应用程序(.NET Core)。

New project

将项目名称设置为 AdventureWorksAPI,然后单击确定。

Select template

在模板中选择 Web API,设置“无身份验证”,取消选中“在云中托管”选项,然后单击确定。

项目创建完成后,我们可以运行该项目,将获得以下输出

First API run

此外,我们将在 appsettings.json 文件中添加连接字符串

App settings

步骤 02 - 添加 API 相关对象

我们需要为项目添加 Entity Framework 包,打开 project.json 文件,并添加 Entity Framework 包,正如我们在下图的第 7 行和第 8 行所示

Entity framework packages

保存更改并重新构建项目,如果一切正常,构建将不会出现任何编译错误。

此外,我们需要为项目创建以下目录

  • Extensions:用于扩展方法的占位符
  • Models:用于与数据库访问、建模和配置相关的对象的占位符
  • Responses:用于表示 HTTP 响应的对象的占位符
  • ViewModels:用于表示 HTTP 输出的对象的占位符

现在,我们将在 Controllers 目录中创建一个新控制器。

Add controller

ProductionController 类代码

using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.DataLayer;
using AdventureWorksAPI.Core.EntityLayer;
using AdventureWorksAPI.Responses;
using AdventureWorksAPI.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace AdventureWorksAPI.Controllers
{
    [Route("api/[controller]")]
    public class ProductionController : Controller
    {
        private IAdventureWorksRepository AdventureWorksRepository;

        public ProductionController(IAdventureWorksRepository repository)
        {
            AdventureWorksRepository = repository;
        }

        protected override void Dispose(Boolean disposing)
        {
            AdventureWorksRepository?.Dispose();

            base.Dispose(disposing);
        }

        // GET Production/Product
        /// <summary>
        /// Retrieves a list of products
        /// </summary>
        /// <param name="pageSize">Page size</param>
        /// <param name="pageNumber">Page number</param>
        /// <param name="name">Name</param>
        /// <returns>List response</returns>
        [HttpGet]
        [Route("Product")]
        public async Task<IActionResult> GetProductsAsync(Int32? pageSize = 10, Int32? pageNumber = 1, String name = null)
        {
            var response = new ListModelResponse<ProductViewModel>();

            try
            {
                response.PageSize = (Int32)pageSize;
                response.PageNumber = (Int32)pageNumber;

                response.Model = await AdventureWorksRepository
                        .GetProducts(response.PageSize, response.PageNumber, name)
                        .Select(item => item.ToViewModel())
                        .ToListAsync();

                response.Message = String.Format("Total of records: {0}", response.Model.Count());
            }
            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = ex.Message;
            }

            return response.ToHttpResponse();
        }

        // GET Production/Product/5
        /// <summary>
        /// Retrieves a specific product by id
        /// </summary>
        /// <param name="id">Product ID</param>
        /// <returns>Single response</returns>
        [HttpGet]
        [Route("Product/{id}")]
        public async Task<IActionResult> GetProductAsync(Int32 id)
        {
            var response = new SingleModelResponse<ProductViewModel>();

            try
            {
                var entity = await AdventureWorksRepository.GetProductAsync(new Product { ProductID = id });

                response.Model = entity?.ToViewModel();
            }
            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = ex.Message;
            }

            return response.ToHttpResponse();
        }

        // POST Production/Product/
        /// <summary>
        /// Creates a new product on Production catalog
        /// </summary>
        /// <param name="request">Product entry</param>
        /// <returns>Single response</returns>
        [HttpPost]
        [Route("Product")]
        public async Task<IActionResult> PostProductAsync([FromBody]ProductViewModel request)
        {
            var response = new SingleModelResponse<ProductViewModel>();

            try
            {
                var entity = await AdventureWorksRepository.AddProductAsync(request.ToEntity());

                response.Model = entity?.ToViewModel();
                response.Message = "The data was saved successfully";
            }
            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = ex.ToString();
            }

            return response.ToHttpResponse();
        }

        // PUT Production/Product/5
        /// <summary>
        /// Updates an existing product
        /// </summary>
        /// <param name="id">Product ID</param>
        /// <param name="request">Product entry</param>
        /// <returns>Single response</returns>
        [HttpPut]
        [Route("Product/{id}")]
        public async Task<IActionResult> PutProductAsync(Int32 id, [FromBody]ProductViewModel request)
        {
            var response = new SingleModelResponse<ProductViewModel>();

            try
            {
                var entity = await AdventureWorksRepository.UpdateProductAsync(request.ToEntity());

                response.Model = entity?.ToViewModel();
                response.Message = "The record was updated successfully";
            }
            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = ex.Message;
            }

            return response.ToHttpResponse();
        }

        // DELETE Production/Product/5
        /// <summary>
        /// Delete an existing product
        /// </summary>
        /// <param name="id">Product ID</param>
        /// <returns>Single response</returns>
        [HttpDelete]
        [Route("Product/{id}")]
        public async Task<IActionResult> DeleteProductAsync(Int32 id)
        {
            var response = new SingleModelResponse<ProductViewModel>();

            try
            {
                var entity = await AdventureWorksRepository.DeleteProductAsync(new Product { ProductID = id });

                response.Model = entity?.ToViewModel();
                response.Message = "The record was deleted successfully";
            }
            catch (Exception ex)
            {
                response.DidError = true;
                response.ErrorMessage = ex.Message;
            }

            return response.ToHttpResponse();
        }
    }
}

对于企业级实现,我们需要实现大型代码文件。在这种情况下,我们正在处理 Production schema,这意味着将需要 Production 命名空间中的所有实体。为了避免 C# 中出现大型代码文件,我们可以使用类定义中的 partial 关键字将其拆分为不同的代码文件。

Models 目录下,我们需要有以下文件

  • AdventureWorksDbContext.cs:通过 Entity Framework 进行数据库访问
  • AdventureWorksRepository.cs:存储库实现
  • AppSettings.cs:类型化的 appsettings
  • IAdventureWorksRepository.cs:契约(接口)
  • Product.cs:POCO
  • ProductMap.cs:POCO 类映射

所有这些都属于 Models 命名空间,因为它们代表了我们 API 中的数据库连接。

IAdventureWorksRepository 接口代码

using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.EntityLayer;

namespace AdventureWorksAPI.Core.DataLayer
{
    public interface IAdventureWorksRepository : IDisposable
    {
        IQueryable<Product> GetProducts(Int32 pageSize, Int32 pageNumber, String name);

        Task<Product> GetProductAsync(Product entity);

        Task<Product> AddProductAsync(Product entity);

        Task<Product> UpdateProductAsync(Product changes);

        Task<Product> DeleteProductAsync(Product changes);
    }
}

AdventureWorksRepository 类代码

using System;
using System.Linq;
using System.Threading.Tasks;
using AdventureWorksAPI.Core.EntityLayer;
using Microsoft.EntityFrameworkCore;

namespace AdventureWorksAPI.Core.DataLayer
{
    public class AdventureWorksRepository : IAdventureWorksRepository
    {
        private readonly AdventureWorksDbContext DbContext;
        private Boolean Disposed;

        public AdventureWorksRepository(AdventureWorksDbContext dbContext)
        {
            DbContext = dbContext;
        }

        public void Dispose()
        {
            if (!Disposed)
            {
                DbContext?.Dispose();

                Disposed = true;
            }
        }

        public IQueryable<Product> GetProducts(Int32 pageSize, Int32 pageNumber, String name)
        {
            var query = DbContext.Set<Product>().Skip((pageNumber - 1) * pageSize).Take(pageSize);

            if (!String.IsNullOrEmpty(name))
            {
                query = query.Where(item => item.Name.ToLower().Contains(name.ToLower()));
            }

            return query;
        }

        public Task<Product> GetProductAsync(Product entity)
        {
            return DbContext.Set<Product>().FirstOrDefaultAsync(item => item.ProductID == entity.ProductID);
        }

        public async Task<Product> AddProductAsync(Product entity)
        {
            entity.MakeFlag = false;
            entity.FinishedGoodsFlag = false;
            entity.SafetyStockLevel = 1;
            entity.ReorderPoint = 1;
            entity.StandardCost = 0.0m;
            entity.ListPrice = 0.0m;
            entity.DaysToManufacture = 0;
            entity.SellStartDate = DateTime.Now;
            entity.rowguid = Guid.NewGuid();
            entity.ModifiedDate = DateTime.Now;

            DbContext.Set<Product>().Add(entity);

            await DbContext.SaveChangesAsync();

            return entity;
        }

        public async Task<Product> UpdateProductAsync(Product changes)
        {
            var entity = await GetProductAsync(changes);

            if (entity != null)
            {
                entity.Name = changes.Name;
                entity.ProductNumber = changes.ProductNumber;

                await DbContext.SaveChangesAsync();
            }

            return entity;
        }

        public async Task<Product> DeleteProductAsync(Product changes)
        {
            var entity = await GetProductAsync(changes);

            if (entity != null)
            {
                DbContext.Set<Product>().Remove(entity);

                await DbContext.SaveChangesAsync();
            }

            return entity;
        }
    }
}

AdventureWorksDbContext 类代码

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace AdventureWorksAPI.Models
{
    public class AdventureWorksDbContext : Microsoft.EntityFrameworkCore.DbContext
    {
        public AdventureWorksDbContext(IOptions<AppSettings> appSettings)
        {
            ConnectionString = appSettings.Value.ConnectionString;
        }

        public String ConnectionString { get; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(ConnectionString);
            
            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.MapProduct();

            base.OnModelCreating(modelBuilder);
        }
    }
}

AppSettings 类代码

using System;

namespace AdventureWorksAPI.Models
{
    public class AppSettings
    {
        public String ConnectionString { get; set; }
    }
}

Product 类代码

using System;

namespace AdventureWorksAPI.Models
{
    public class Product
    {
        public Int32? ProductID { get; set; }

        public String Name { get; set; }

        public String ProductNumber { get; set; }

        public Boolean? MakeFlag { get; set; }

        public Boolean? FinishedGoodsFlag { get; set; }

        public Int16? SafetyStockLevel { get; set; }

        public Int16? ReorderPoint { get; set; }

        public Decimal? StandardCost { get; set; }

        public Decimal? ListPrice { get; set; }

        public Int32? DaysToManufacture { get; set; }

        public DateTime? SellStartDate { get; set; }

        public Guid? rowguid { get; set; }

        public DateTime? ModifiedDate { get; set; }
    }
}

ProductMap 类代码

using Microsoft.EntityFrameworkCore;

namespace AdventureWorksAPI.Models
{
    public static class ProductMap
    {
        public static ModelBuilder MapProduct(this ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<Product>();

            entity.ToTable("Product", "Production");

            entity.HasKey(p => new { p.ProductID });

            entity.Property(p => p.ProductID).UseSqlServerIdentityColumn();

            return modelBuilder;
        }
    }
}

正如我们所见,每个表都有不同的类

  1. POCO:将表表示为 CRL 对象
  2. Mapping:DbContext 中 POCO 对象的配置
  3. Mapper:根据属性名称匹配属性值的逻辑

有一个很大的问题,如果我们有 200 个映射表,这意味着我们需要每个类型有 200 个代码文件?答案是是!!!。有解决此问题的方法,我们可以搜索代码生成工具,或者我们可以全部编写。请查看链接部分中的 CatFactory 以了解有关 EF Core 代码生成的更多信息。无论如何,事实是我们必须定义此对象,因为在设计时,了解我们将在 API 中使用的类型非常有用。

Extensions 目录下,我们有以下文件

  • ProductViewModelMapper:用于将 Product POCO 类映射到 ProductViewModel 类的扩展
  • ResponseExtensions:用于创建 HTTP 响应的扩展方法

ProductViewModelMapper 类代码

using AdventureWorksAPI.Models;
using AdventureWorksAPI.ViewModels;

namespace AdventureWorksAPI.Extensions
{
    public static class ProductViewModelMapper
    {
        public static ProductViewModel ToViewModel(this Product entity)
        {
            return new ProductViewModel
            {
                ProductID = entity.ProductID,
                ProductName = entity.Name,
                ProductNumber = entity.ProductNumber
            };
        }
        
        public static Product ToEntity(this ProductViewModel viewModel)
        {
            return new Product
            {
                Name = viewModel.ProductName,
                ProductNumber = viewModel.ProductNumber
            };
        }
    }
}

为什么我们不使用映射器框架?此时,我们可以根据自己的偏好更改映射器。如果您想提高 C# 技能,可以添加一种动态映射方式。 :)

ResponseExtensions 类代码

using System;
using System.Net;
using Microsoft.AspNetCore.Mvc;

namespace AdventureWorksAPI.Responses
{
    public static class ResponseExtensions
    {
        public static IActionResult ToHttpResponse<TModel>(this IListModelResponse<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 = (Int32)status
            };
        }

        public static IActionResult ToHttpResponse<TModel>(this ISingleModelResponse<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 = (Int32)status
            };
        }
    }
}

Responses 目录下,我们需要有以下文件

  • IListModelResponse.cs:用于表示列表响应的接口
  • IResponse.cs:响应的通用接口
  • ISingleModelResponse.cs:用于表示单个响应(一个实体)的接口
  • ListModelResponse.cs:列表响应的实现
  • SingleModelResponse.cs:单个响应的实现

IListModelResponse 接口代码

using System;
using System.Collections.Generic;

namespace AdventureWorksAPI.Responses
{
    public interface IListModelResponse<TModel> : IResponse
    {
        Int32 PageSize { get; set; }

        Int32 PageNumber { get; set; }
        
        IEnumerable<TModel> Model { get; set; }
    }
}

IResponse 接口代码

using System;

namespace AdventureWorksAPI.Responses
{
    public interface IResponse
    {
        String Message { get; set; }

        Boolean DidError { get; set; }

        String ErrorMessage { get; set; }
    }
}

ISingleModelResponse 接口代码

namespace AdventureWorksAPI.Responses
{
    public interface ISingleModelResponse<TModel> : IResponse
    {
        TModel Model { get; set; }
    }
}

ListModelResponse 类代码

using System;
using System.Collections.Generic;

namespace AdventureWorksAPI.Responses
{
    public class ListModelResponse<TModel> : IListModelResponse<TModel>
    {
        public String Message { get; set; }

        public Boolean DidError { get; set; }

        public String ErrorMessage { get; set; }
        
        public Int32 PageSize { get; set; }

        public Int32 PageNumber { get; set; }

        public IEnumerable<TModel> Model { get; set; }
    }
}

SingleModelResponse 类代码

using System;

namespace AdventureWorksAPI.Responses
{
    public class SingleModelResponse<TModel> : ISingleModelResponse<TModel>
    {
        public String Message { get; set; }

        public Boolean DidError { get; set; }

        public String ErrorMessage { get; set; }

        public TModel Model { get; set; }
    }
}

ViewModels 目录下,我们有以下文件

  • ProductViewModelr:用于表示 Product 信息视图的模型。

ProductViewModel 类代码

using System;

namespace AdventureWorksAPI.ViewModels
{
    public class ProductViewModel
    {
        public Int32? ProductID { get; set; }

        public String ProductName { get; set; }

        public String ProductNumber { get; set; }
    }
}

视图模型仅包含我们想向客户端公开的属性。在这种情况下,我们在存储库的实现中处理 Product 实体的所有默认值。我们需要确保所有请求都使用存储库实现来设置默认属性值。

步骤 03 - 将所有服务整合在一起

ASP.NET Core 的主要变化之一是其依赖注入,现在它是“原生”的,我们无需安装额外的包。

此时,我们需要在 Startup 类中配置所有服务。在 ConfigureServices 方法中,我们需要设置将注入控制器的依赖项、契约名称解析器和类型化设置。

using AdventureWorksAPI.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;

namespace AdventureWorksAPI
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc().AddJsonOptions
            (a => a.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());

            services.AddEntityFrameworkSqlServer().AddDbContext<AdventureWorksDbContext>();

            services.AddScoped<IAdventureWorksRepository, AdventureWorksRepository>();

            services.AddOptions();

            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

            services.AddSingleton<IConfiguration>(Configuration);
        }

        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                              ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseMvc();
        }
    }
}

步骤 04 - 添加单元测试

如今,测试是必需的,因为有了单元测试,就可以在发布之前轻松地测试一个功能。测试驱动开发(TDD)是定义单元测试和验证代码行为的方法。

ASP.NET Core 包含许多关于测试的更改。有一个用于运行单元测试的命令行。我们需要更改当前代码以添加单元测试。

我们需要以下结构

Solution structure

要创建上述结构,请按照以下步骤操作

  1. 右键单击解决方案名称 > 打开命令提示符 > 默认(cmd)
  2. 创建“test”目录(mkdir test
  3. 进入“test”目录(cd test
  4. 创建“AdventureWorksAPI.Tests”目录(mkdir AdventureWorksAPI.Tests
  5. 进入“AdventureWorksAPI.Tests”目录(cd AdventureWorksAPI.Tests
  6. 创建单元测试项目(dotnet new -t xunittest
  7. 返回 Visual Studio 并为测试解决方案文件夹创建一个新解决方案文件夹,并命名为 test
  8. 将现有项目添加到测试解决方案文件夹(AdventureWorksAPI.Tests
  9. 删除 Tests.cs 文件并添加一个新文件:ProductionControllerTest.cs

RepositoryMocker 类代码

using AdventureWorksAPI.Core.DataLayer;
using Microsoft.Extensions.Options;

namespace AdventureWorksAPI.Tests
{
    public static class RepositoryMocker
    {
        public static IAdventureWorksRepository GetAdventureWorksRepository()
        {
            var appSettings = Options.Create(new AppSettings
            {
                ConnectionString = "server=(local);database=AdventureWorks2012;integrated security=yes;"
            });

            return new AdventureWorksRepository(new AdventureWorksDbContext(appSettings, new AdventureWorksEntityMapper()));
        }
    }
}

ProductionControllerTest 类代码

using System;
using System.Threading.Tasks;
using AdventureWorksAPI.Controllers;
using AdventureWorksAPI.Responses;
using AdventureWorksAPI.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Xunit;

namespace AdventureWorksAPI.Tests
{
    public class ProductionControllerTest
    {
        [Fact]
        public async Task TestGetProductsAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);

            // Act
            var response = await controller.GetProductsAsync() as ObjectResult;
            var value = response.Value as IListModelResponse<ProductViewModel>;

            controller.Dispose();

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

        [Fact]
        public async Task TestGetProductAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);
            var id = 1;

            // Act
            var response = await controller.GetProductAsync(id) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

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

        [Fact]
        public async Task TestGetNonExistingProductAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);
            var id = 0;

            // Act
            var response = await controller.GetProductAsync(id) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

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

        [Fact]
        public async Task TestPostProductAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);
            var request = new ProductViewModel
            {
                ProductName = String.Format("New test product {0}{1}{2}", DateTime.Now.Minute, DateTime.Now.Second, DateTime.Now.Millisecond),
                ProductNumber = String.Format("{0}{1}{2}", DateTime.Now.Minute, DateTime.Now.Second, DateTime.Now.Millisecond)
            };

            // Act
            var response = await controller.PostProductAsync(request) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

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

        [Fact]
        public async Task TestPutProductAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);
            var id = 1;
            var request = new ProductViewModel
            {
                ProductID = id,
                ProductName = "New product test II",
                ProductNumber = "XYZ"
            };

            // Act
            var response = await controller.PutProductAsync(id, request) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

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

        [Fact]
        public async Task TestDeleteProductAsync()
        {
            // Arrange
            var repository = RepositoryMocker.GetAdventureWorksRepository();
            var controller = new ProductionController(repository);
            var id = 1000;

            // Act
            var response = await controller.DeleteProductAsync(id) as ObjectResult;
            var value = response.Value as ISingleModelResponse<ProductViewModel>;

            repository.Dispose();

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

到目前为止,我们已经为 Web API 项目添加了单元测试。现在我们可以从命令行运行单元测试。打开一个命令行窗口,将目录更改为 AdventureWorksAPI.Tests 目录,然后键入此命令:dotnet test,我们将看到类似以下的输出

Unit Test Output

步骤 05 - 运行代码

项目成功构建且没有编译错误后,我们可以从 Visual Studio 运行项目。稍后,使用任何浏览器,我们都可以访问 API。

请记住,在我机器上,IIS Express 使用端口号 38126,这将在您的机器上有所不同。此外,在 ProductionController 中,我们有用于路由定义的 Route 属性。如果我们希望 API 解析为另一个名称,则必须更改 Route 属性的值。

正如我们所见,我们可以构建不同的 URL 来搜索产品

  • api/Production/Product/
  • api/Production/Product/?pageSize=12&pageNumber=1
  • api/Production/Product/?pageSize=5&pageNumber=1&name=a

默认列表输出:api/Production/Product/

API list result

带有页面大小和页码参数的列表输出:api/Production/Product/?pageSize=12&pageNumber=1

API list result with page size and page number parameters

单个输出:api/Production/Product/4

API single result

如果您无法以美观的方式查看 JSON,Chrome 上有一个查看器扩展 JSON Viewer

请记住,您可以使用其他工具测试您的 Web API,例如 Postman 下载

重构您的后端代码

正如我们目前所见,AdventureWorksAPI 项目中有很多对象。作为企业应用程序开发的一部分,不建议将所有对象都放在 API 项目中。我们将通过以下步骤拆分我们的 API 项目

  1. 右键单击解决方案名称
  2. 添加 > 新建项目 > .NET Core
  3. 将项目名称设置为 AdventureWorksAPI.Core
  4. 好的

现在我们为新项目添加 Entity Framework Core 包。

这是 AdventureWorksAPI.Core 项目的结构

  • DataLayer
  • EntityLayer

使用下图并将所有类重构为单个文件

Project refactoring structure

将此任务视为一个挑战。一旦您重构了所有代码,请将对 AdventureWorksAPI.Core 项目的引用添加到 AdventureWorksAPI 项目,保存所有更改并构建您的解决方案。您将在单元测试项目上遇到错误,因此请在单元测试项目中添加命名空间和引用。现在保存所有更改并构建您的解决方案。

如果一切正常,我们的应用程序就可以无错误地运行了。

代码改进

查看此指南的新版本! 点击这里

  1. 添加集成测试
  2. Web API 方法的日志记录
  3. 根据您的观点,还有其他改进之处,请在评论中告知我 :)

关注点

  • Entity Framework 现在是“Microsoft.EntityFrameworkCore”。
  • 为什么我们需要拥有类型化的响应?出于设计目的,拥有类型化的响应更灵活,可以避免开发阶段的常见错误,例如知道搜索结果是否为空以及避免意外行为。此外,通过类型化的响应,我们可以知道一个请求是否从服务器端出错(数据库连接、类型转换等)。
  • 为什么我们需要 ViewModels,而我们已经有了模型(POCOs)?想象一下,我们有一个包含 100 列的表,用于表示客户信息,并且根据特定要求;我们只需要返回客户 ID、联系人姓名、公司名称和国家/地区;我们可以使用匿名类型来解决这个问题,但正如我们在上面看到的,我们需要一个允许我们知道响应中有多少个字段的结构。无论如何,无论使用匿名类型还是不使用,我们都需要返回一个包含特定字段的对象,并且不暴露不必要的数据(电话、电子邮件等)。

相关链接

历史

  • 2016 年 7 月 18 日:初始版本
  • 2016 年 7 月 24 日:控制器的 CRUD 操作
  • 2016 年 10 月 24 日:单元测试
  • 2017 年 11 月 10 日:代码审查
  • 2018 年 10 月 23 日:添加新版本链接
© . All rights reserved.