ASP.NET Core 1x 中的 Web API
在 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)。
将项目名称设置为 AdventureWorksAPI
,然后单击确定。
在模板中选择 Web API,设置“无身份验证”,取消选中“在云中托管”选项,然后单击确定。
项目创建完成后,我们可以运行该项目,将获得以下输出
此外,我们将在 appsettings.json 文件中添加连接字符串
步骤 02 - 添加 API 相关对象
我们需要为项目添加 Entity Framework 包,打开 project.json 文件,并添加 Entity Framework 包,正如我们在下图的第 7 行和第 8 行所示
保存更改并重新构建项目,如果一切正常,构建将不会出现任何编译错误。
此外,我们需要为项目创建以下目录
- Extensions:用于扩展方法的占位符
- Models:用于与数据库访问、建模和配置相关的对象的占位符
- Responses:用于表示 HTTP 响应的对象的占位符
- ViewModels:用于表示 HTTP 输出的对象的占位符
现在,我们将在 Controllers 目录中创建一个新控制器。
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;
}
}
}
正如我们所见,每个表都有不同的类
- POCO:将表表示为 CRL 对象
- Mapping:DbContext 中 POCO 对象的配置
- 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 包含许多关于测试的更改。有一个用于运行单元测试的命令行。我们需要更改当前代码以添加单元测试。
我们需要以下结构
要创建上述结构,请按照以下步骤操作
- 右键单击解决方案名称 > 打开命令提示符 > 默认(cmd)
- 创建“test”目录(
mkdir test
) - 进入“test”目录(
cd test
) - 创建“AdventureWorksAPI.Tests”目录(
mkdir AdventureWorksAPI.Tests
) - 进入“AdventureWorksAPI.Tests”目录(
cd AdventureWorksAPI.Tests
) - 创建单元测试项目(
dotnet new -t xunittest
) - 返回 Visual Studio 并为测试解决方案文件夹创建一个新解决方案文件夹,并命名为 test
- 将现有项目添加到测试解决方案文件夹(
AdventureWorksAPI.Tests
) - 删除 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
,我们将看到类似以下的输出
步骤 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/Production/Product/?pageSize=12&pageNumber=1
单个输出:api/Production/Product/4
如果您无法以美观的方式查看 JSON,Chrome 上有一个查看器扩展 JSON Viewer。
请记住,您可以使用其他工具测试您的 Web API,例如 Postman 下载。
重构您的后端代码
正如我们目前所见,AdventureWorksAPI 项目中有很多对象。作为企业应用程序开发的一部分,不建议将所有对象都放在 API 项目中。我们将通过以下步骤拆分我们的 API 项目
- 右键单击解决方案名称
- 添加 > 新建项目 > .NET Core
- 将项目名称设置为 AdventureWorksAPI.Core
- 好的
现在我们为新项目添加 Entity Framework Core 包。
这是 AdventureWorksAPI.Core 项目的结构
- DataLayer
- EntityLayer
使用下图并将所有类重构为单个文件
将此任务视为一个挑战。一旦您重构了所有代码,请将对 AdventureWorksAPI.Core 项目的引用添加到 AdventureWorksAPI 项目,保存所有更改并构建您的解决方案。您将在单元测试项目上遇到错误,因此请在单元测试项目中添加命名空间和引用。现在保存所有更改并构建您的解决方案。
如果一切正常,我们的应用程序就可以无错误地运行了。
代码改进
查看此指南的新版本! 点击这里!
- 添加集成测试
- Web API 方法的日志记录
- 根据您的观点,还有其他改进之处,请在评论中告知我 :)
关注点
- Entity Framework 现在是“
Microsoft.EntityFrameworkCore
”。 - 为什么我们需要拥有类型化的响应?出于设计目的,拥有类型化的响应更灵活,可以避免开发阶段的常见错误,例如知道搜索结果是否为空以及避免意外行为。此外,通过类型化的响应,我们可以知道一个请求是否从服务器端出错(数据库连接、类型转换等)。
- 为什么我们需要
ViewModels
,而我们已经有了模型(POCOs)?想象一下,我们有一个包含 100 列的表,用于表示客户信息,并且根据特定要求;我们只需要返回客户 ID、联系人姓名、公司名称和国家/地区;我们可以使用匿名类型来解决这个问题,但正如我们在上面看到的,我们需要一个允许我们知道响应中有多少个字段的结构。无论如何,无论使用匿名类型还是不使用,我们都需要返回一个包含特定字段的对象,并且不暴露不必要的数据(电话、电子邮件等)。
相关链接
历史
- 2016 年 7 月 18 日:初始版本
- 2016 年 7 月 24 日:控制器的 CRUD 操作
- 2016 年 10 月 24 日:单元测试
- 2017 年 11 月 10 日:代码审查
- 2018 年 10 月 23 日:添加新版本链接