使用ASP.NET Core构建ASP.NET Web API

本文概述

  • 介绍
  • 图层API
  • 项目结构
  • 实作
  • API实施
  • 应用配置
  • 总结
介绍 几年前, 我收到了” Pro ASP.NET Web API” 一书。本文是本书的主要内容, 一些CQRS和我自己开发客户端-服务器系统的经验的一部分。
在本文中, 我将介绍:
  • 如何使用.NET Core, EF Core, AutoMapper和XUnit从头开始创建REST API
  • 更改后如何确保API正常工作
  • 如何尽可能简化REST API系统的开发和支持
为什么选择ASP.NET Core?
ASP.NET Core在ASP.NET MVC / Web API的基础上提供了许多改进。首先, 它现在是一个框架, 而不是两个。我真的很喜欢它, 因为它很方便而且混乱的程度也较小。其次, 我们拥有没有任何其他库的日志记录和DI容器, 这节省了我的时间, 使我可以专注于编写更好的代码, 而不必选择和分析最佳的库。
什么是查询处理器?
当与系统的一个实体相关的所有业务逻辑都封装在一个服务中, 并且对该实体的任何访问或操作都通过该服务执行时, 查询处理器就是一种方法。该服务通常称为{EntityPluralName} QueryProcessor。如有必要, 查询处理器将为此实体包括CRUD(创建, 读取, 更新, 删除)方法。根据要求, 并非所有方法都可以实施。举一个具体的例子, 让我们看一下ChangePassword。如果查询处理器的方法需要输入数据, 则仅应提供所需的数据。通常, 对于每种方法, 都会创建一个单独的查询类, 在简单情况下, 有可能(但不希望)重用查询类。
我们的目的
在本文中, 我将向你展示如何为小型成本管理系统制作API, 包括身份验证和访问控制的基本设置, 但我不会涉及身份验证子系统。我将使用模块化测试涵盖系统的整个业务逻辑, 并在一个实体的示例上为每种API方法创建至少一个集成测试。
对已开发系统的要求:用户可以添加, 编辑, 删除其费用, 并且只能查看其费用。
该系统的完整代码可在Github上获得。
因此, 让我们开始设计一个小型但非常有用的系统。
图层API
使用ASP.NET Core构建ASP.NET Web API

文章图片
该图显示该系统将具有四层:
  • 数据库-这里我们存储数据, 仅此而已, 没有逻辑。
  • DAL-要访问数据, 我们使用工作单元模式, 在实现中, 我们将ORM EF Core与代码优先和迁移模式一起使用。
  • 业务逻辑-为了封装业务逻辑, 我们使用查询处理器, 只有这一层处理业务逻辑。例外是最简单的验证, 例如必填字段, 它将通过API中的过滤器执行。
  • REST API-客户端可以通过我们的API使用的实际接口将通过ASP.NET Core实现。路由配置由属性确定。
除了描述的层, 我们还有几个重要的概念。首先是数据模型的分离。客户端数据模型主要用于REST API层。它将查询转换为域模型, 反之亦然, 从域模型转换为客户端数据模型, 但是查询模型也可以在查询处理器中使用。转换是使用AutoMapper完成的。
项目结构 我使用VS 2017 Professional创建项目。我通常在不同的文件夹上共享源代码和测试。感觉很舒适, 看起来不错, CI中的测试运行方便, 微软似乎建议这样做:
使用ASP.NET Core构建ASP.NET Web API

文章图片
项目简介:
项目 描述
花费 控制器项目, 域模型与API模型之间的映射, API配置
普通费用 在这一点上, 收集了异常类, 这些异常类通过过滤器以某种方式解释, 以将正确的HTTP代码返回给用户错误
费用模型 API模型专案
费用, 数据, 访问 接口和工作单位模式实施项目
费用数据模型 领域模型项目
费用查询 查询处理器和特定于查询的类的项目
Expenses.Security 当前用户的安全上下文的接口和实现的项目
项目之间的参考:
使用ASP.NET Core构建ASP.NET Web API

文章图片
通过模板创建的费用:
使用ASP.NET Core构建ASP.NET Web API

文章图片
通过模板在src文件夹中的其他项目:
使用ASP.NET Core构建ASP.NET Web API

文章图片
测试文件夹中的所有项目(按模板):
使用ASP.NET Core构建ASP.NET Web API

文章图片
实作 尽管本文已实现, 但本文将不介绍与UI关联的部分。
第一步是开发位于程序集Expenses.Data.Model中的数据模型:
使用ASP.NET Core构建ASP.NET Web API

文章图片
Expense类包含以下属性:
public class Expense { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public virtual User User { get; set; } public bool IsDeleted { get; set; } }

此类通过IsDeleted属性支持” 软删除” , 并且包含所有数据, 而这只花了特定用户一笔钱, 将来对我们有用。
User, Role和UserRole类引用访问子系统。该系统不伪装成年度系统, 对该子系统的描述也不是本文的目的。因此, 将省略数据模型和实现的一些细节。访问组织系统可以用更完善的系统代替, 而无需更改业务逻辑。
接下来, 在Expenses.Data.Access程序集中实现了工作单位模板, 该项目的结构如下所示:
使用ASP.NET Core构建ASP.NET Web API

文章图片
组装需要以下库:
  • Microsoft.EntityFrameworkCore.SqlServer
有必要实现一个EF上下文, 该上下文将自动在特定文件夹中查找映射:
public class MainDbContext : DbContext { public MainDbContext(DbContextOptions< MainDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var mappings = MappingsHelper.GetMainMappings(); foreach (var mapping in mappings) { mapping.Visit(modelBuilder); } } }

映射是通过MappingsHelper类完成的:
public static class MappingsHelper { public static IEnumerable< IMap> GetMainMappings() { var assemblyTypes = typeof(UserMap).GetTypeInfo().Assembly.DefinedTypes; var mappings = assemblyTypes // ReSharper disable once AssignNullToNotNullAttribute .Where(t => t.Namespace != null & & t.Namespace.Contains(typeof(UserMap).Namespace)) .Where(t => typeof(IMap).GetTypeInfo().IsAssignableFrom(t)); mappings = mappings.Where(x => !x.IsAbstract); return mappings.Select(m => (IMap) Activator.CreateInstance(m.AsType())).ToArray(); } }

到类的映射位于Maps文件夹中, 以及Expenses的映射:
public class ExpenseMap : IMap { public void Visit(ModelBuilder builder) { builder.Entity< Expense> () .ToTable("Expenses") .HasKey(x => x.Id); } }

接口IUnitOfWork:
public interface IUnitOfWork : IDisposable { ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot); void Add< T> (T obj) where T: class ; void Update< T> (T obj) where T : class; void Remove< T> (T obj) where T : class; IQueryable< T> Query< T> () where T : class; void Commit(); Task CommitAsync(); void Attach< T> (T obj) where T : class; }

它的实现是EF DbContext的包装器:
public class EFUnitOfWork : IUnitOfWork { private DbContext _context; public EFUnitOfWork(DbContext context) { _context = context; } public DbContext Context => _context; public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot) { return new DbTransaction(_context.Database.BeginTransaction(isolationLevel)); } public void Add< T> (T obj) where T : class { var set = _context.Set< T> (); set.Add(obj); } public void Update< T> (T obj) where T : class { var set = _context.Set< T> (); set.Attach(obj); _context.Entry(obj).State = EntityState.Modified; } void IUnitOfWork.Remove< T> (T obj) { var set = _context.Set< T> (); set.Remove(obj); } public IQueryable< T> Query< T> () where T : class { return _context.Set< T> (); } public void Commit() { _context.SaveChanges(); } public async Task CommitAsync() { await _context.SaveChangesAsync(); } public void Attach< T> (T newUser) where T : class { var set = _context.Set< T> (); set.Attach(newUser); } public void Dispose() { _context = null; } }

在此应用程序中实现的接口ITransaction将不被使用:
public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

它的实现只包装了EF事务:
public class DbTransaction : ITransaction { private readonly IDbContextTransaction _efTransaction; public DbTransaction(IDbContextTransaction efTransaction) { _efTransaction = efTransaction; } public void Commit() { _efTransaction.Commit(); } public void Rollback() { _efTransaction.Rollback(); } public void Dispose() { _efTransaction.Dispose(); } }

同样在此阶段, 对于单元测试, 需要ISecurityContext接口, 该接口定义API的当前用户(项目为Expenses.Security):
public interface ISecurityContext { User User { get; } bool IsAdministrator { get; } }

接下来, 你需要定义查询处理器的接口和实现, 其中将包含用于处理成本的所有业务逻辑, 在我们的示例中为IExpensesQueryProcessor和ExpensesQueryProcessor:
public interface IExpensesQueryProcessor { IQueryable< Expense> Get(); Expense Get(int id); Task< Expense> Create(CreateExpenseModel model); Task< Expense> Update(int id, UpdateExpenseModel model); Task Delete(int id); }public class ExpensesQueryProcessor : IExpensesQueryProcessor { public IQueryable< Expense> Get() { throw new NotImplementedException(); } public Expense Get(int id) { throw new NotImplementedException(); } public Task< Expense> Create(CreateExpenseModel model) { throw new NotImplementedException(); } public Task< Expense> Update(int id, UpdateExpenseModel model) { throw new NotImplementedException(); } public Task Delete(int id) { throw new NotImplementedException(); } }

下一步是配置Expenses.Queries.Tests程序集。我安装了以下库:
  • 起订量
  • 流利的断言
然后在Expenses.Queries.Tests程序集中, 定义单元测试的固定装置并描述我们的单元测试:
public class ExpensesQueryProcessorTests { private Mock< IUnitOfWork> _uow; private List< Expense> _expenseList; private IExpensesQueryProcessor _query; private Random _random; private User _currentUser; private Mock< ISecurityContext> _securityContext; public ExpensesQueryProcessorTests() { _random = new Random(); _uow = new Mock< IUnitOfWork> (); _expenseList = new List< Expense> (); _uow.Setup(x => x.Query< Expense> ()).Returns(() => _expenseList.AsQueryable()); _currentUser = new User{Id = _random.Next()}; _securityContext = new Mock< ISecurityContext> (MockBehavior.Strict); _securityContext.Setup(x => x.User).Returns(_currentUser); _securityContext.Setup(x => x.IsAdministrator).Returns(false); _query = new ExpensesQueryProcessor(_uow.Object, _securityContext.Object); } [Fact] public void GetShouldReturnAll() { _expenseList.Add(new Expense{UserId = _currentUser.Id}); var result = _query.Get().ToList(); result.Count.Should().Be(1); } [Fact] public void GetShouldReturnOnlyUserExpenses() { _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get().ToList(); result.Count().Should().Be(1); result[0].UserId.Should().Be(_currentUser.Id); } [Fact] public void GetShouldReturnAllExpensesForAdministrator() { _securityContext.Setup(x => x.IsAdministrator).Returns(true); _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get(); result.Count().Should().Be(2); } [Fact] public void GetShouldReturnAllExceptDeleted() { _expenseList.Add(new Expense { UserId = _currentUser.Id }); _expenseList.Add(new Expense { UserId = _currentUser.Id, IsDeleted = true}); var result = _query.Get(); result.Count().Should().Be(1); } [Fact] public void GetShouldReturnById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); var result = _query.Get(expense.Id); result.Should().Be(expense); } [Fact] public void GetShouldThrowExceptionIfExpenseOfOtherUser() { var expense = new Expense { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow< NotFoundException> (); } [Fact] public void GetShouldThrowExceptionIfItemIsNotFoundById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); Action get = () => { _query.Get(_random.Next()); }; get.ShouldThrow< NotFoundException> (); } [Fact] public void GetShouldThrowExceptionIfUserIsDeleted() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id, IsDeleted = true}; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow< NotFoundException> (); } [Fact] public async Task CreateShouldSaveNew() { var model = new CreateExpenseModel { Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now }; var result = await _query.Create(model); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); result.UserId.Should().Be(_currentUser.Id); _uow.Verify(x => x.Add(result)); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task UpdateShouldUpdateFields() { var user = new Expense {Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); var model = new UpdateExpenseModel { Comment = _random.Next().ToString(), Description = _random.Next().ToString(), Amount = _random.Next(), Date = DateTime.Now }; var result = await _query.Update(user.Id, model); result.Should().Be(user); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); _uow.Verify(x => x.CommitAsync()); }[Fact] public void UpdateShoudlThrowExceptionIfItemIsNotFound() { Action create = () => { var result = _query.Update(_random.Next(), new UpdateExpenseModel()).Result; }; create.ShouldThrow< NotFoundException> (); } [Fact] public async Task DeleteShouldMarkAsDeleted() { var user = new Expense() { Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); await _query.Delete(user.Id); user.IsDeleted.Should().BeTrue(); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task DeleteShoudlThrowExceptionIfItemIsNotBelongTheUser() { var expense = new Expense() { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action execute = () => { _query.Delete(expense.Id).Wait(); }; execute.ShouldThrow< NotFoundException> (); } [Fact] public void DeleteShoudlThrowExceptionIfItemIsNotFound() { Action execute = () => { _query.Delete(_random.Next()).Wait(); }; execute.ShouldThrow< NotFoundException> (); }

描述了单元测试之后, 描述了查询处理器的实现:
public class ExpensesQueryProcessor : IExpensesQueryProcessor { private readonly IUnitOfWork _uow; private readonly ISecurityContext _securityContext; public ExpensesQueryProcessor(IUnitOfWork uow, ISecurityContext securityContext) { _uow = uow; _securityContext = securityContext; } public IQueryable< Expense> Get() { var query = GetQuery(); return query; } private IQueryable< Expense> GetQuery() { var q = _uow.Query< Expense> () .Where(x => !x.IsDeleted); if (!_securityContext.IsAdministrator) { var userId = _securityContext.User.Id; q = q.Where(x => x.UserId == userId); } return q; } public Expense Get(int id) { var user = GetQuery().FirstOrDefault(x => x.Id == id); if (user == null) { throw new NotFoundException("Expense is not found"); } return user; } public async Task< Expense> Create(CreateExpenseModel model) { var item = new Expense { UserId = _securityContext.User.Id, Amount = model.Amount, Comment = model.Comment, Date = model.Date, Description = model.Description, }; _uow.Add(item); await _uow.CommitAsync(); return item; } public async Task< Expense> Update(int id, UpdateExpenseModel model) { var expense = GetQuery().FirstOrDefault(x => x.Id == id); if (expense == null) { throw new NotFoundException("Expense is not found"); } expense.Amount = model.Amount; expense.Comment = model.Comment; expense.Description = model.Description; expense.Date = model.Date; await _uow.CommitAsync(); return expense; } public async Task Delete(int id) { var user = GetQuery().FirstOrDefault(u => u.Id == id); if (user == null) { throw new NotFoundException("Expense is not found"); } if (user.IsDeleted) return; user.IsDeleted = true; await _uow.CommitAsync(); } }

一旦业务逻辑准备就绪, 我便开始编写API集成测试以确定API合同。
第一步是准备项目Expenses.Api.IntegrationTests
  1. 安装nuget软件包:
    • 流利的断言
    • 起订量
    • Microsoft.AspNetCore.TestHost
  2. 建立项目结构
    使用ASP.NET Core构建ASP.NET Web API

    文章图片
    实施集成测试以获取费用清单:
    [Collection("ApiCollection")] public class GetListShould { private readonly ApiServer _server; private readonly HttpClient _client; public GetListShould(ApiServer server) { _server = server; _client = server.Client; } public static async Task< DataResult< ExpenseModel> > Get(HttpClient client) { var response = await client.GetAsync($"api/Expenses"); response.EnsureSuccessStatusCode(); var responseText = await response.Content.ReadAsStringAsync(); var items = JsonConvert.DeserializeObject< DataResult< ExpenseModel> > (responseText); return items; } [Fact] public async Task ReturnAnyList() { var items = await Get(_client); items.Should().NotBeNull(); } }

    集成测试的实现, 用于通过id获取费用数据:
    [Collection("ApiCollection")] public class GetItemShould { private readonly ApiServer _server; private readonly HttpClient _client; private Random _random; public GetItemShould(ApiServer server) { _server = server; _client = _server.Client; _random = new Random(); } [Fact] public async Task ReturnItemById() { var item = await new PostShould(_server).CreateNew(); var result = await GetById(_client, item.Id); result.Should().NotBeNull(); } public static async Task< ExpenseModel> GetById(HttpClient client, int id) { var response = await client.GetAsync(new Uri($"api/Expenses/{id}", UriKind.Relative)); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject< ExpenseModel> (result); } [Fact] public async Task ShouldReturn404StatusIfNotFound() { var response = await _client.GetAsync(new Uri($"api/Expenses/-1", UriKind.Relative)); response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.NotFound); } }

    实施集成测试以产生费用:
    [Collection("ApiCollection")] public class PostShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private Random _random; public PostShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task< ExpenseModel> CreateNew() { var requestItem = new CreateExpenseModel() { Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now.AddMinutes(-15), Description = _random.Next().ToString() }; var createdItem = await _client.PostAsync< ExpenseModel> ("api/Expenses", requestItem); createdItem.Id.Should().BeGreaterThan(0); createdItem.Amount.Should().Be(requestItem.Amount); createdItem.Comment.Should().Be(requestItem.Comment); createdItem.Date.Should().Be(requestItem.Date); createdItem.Description.Should().Be(requestItem.Description); createdItem.Username.Should().Be("admin admin"); return createdItem; } }

    实施集成测试以更改费用:
    [Collection("ApiCollection")] public class PutShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private readonly Random _random; public PutShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task UpdateExistingItem() { var item = await new PostShould(_server).CreateNew(); var requestItem = new UpdateExpenseModel { Date = DateTime.Now, Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString() }; await _client.PutAsync< ExpenseModel> ($"api/Expenses/{item.Id}", requestItem); var updatedItem = await GetItemShould.GetById(_client.Client, item.Id); updatedItem.Date.Should().Be(requestItem.Date); updatedItem.Description.Should().Be(requestItem.Description); updatedItem.Amount.Should().Be(requestItem.Amount); updatedItem.Comment.Should().Contain(requestItem.Comment); } }

    实施集成测试以消除费用:
    [Collection("ApiCollection")] public class DeleteShould { private readonly ApiServer _server; private readonly HttpClient _client; public DeleteShould(ApiServer server) { _server = server; _client = server.Client; } [Fact] public async Task DeleteExistingItem() { var item = await new PostShould(_server).CreateNew(); var response = await _client.DeleteAsync(new Uri($"api/Expenses/{item.Id}", UriKind.Relative)); response.EnsureSuccessStatusCode(); } }

    至此, 我们已经完全定义了REST API合同, 现在我可以在ASP.NET Core的基础上开始实现它了。
    API实施 准备项目费用。为此, 我需要安装以下库:
    • 自动文件夹
    • AutoQueryable.AspNetCore.Filter
    • Microsoft.ApplicationInsights.AspNetCore
    • Microsoft.EntityFrameworkCore.SqlServer
    • Microsoft.EntityFrameworkCore.SqlServer.Design
    • Microsoft.EntityFrameworkCore.Tools
    • Swashbuckle.AspNetCore
    之后, 你需要通过打开程序包管理器控制台, 切换到Expenses.Data.Access项目(因为EF上下文位于其中)并运行Add-Migration InitialCreate命令来开始为数据库创建初始迁移:
    使用ASP.NET Core构建ASP.NET Web API

    文章图片
    在下一步中, 请预先准备配置文件appsettings.json, 准备后仍将其复制到项目Expenses.Api.IntegrationTests中, 因为从此处开始, 我们将运行测试实例API。
    { "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "Data": { "main": "Data Source=.; Initial Catalog=expenses.main; Integrated Security=true; Max Pool Size=1000; Min Pool Size=12; Pooling=True; " }, "ApplicationInsights": { "InstrumentationKey": "Your ApplicationInsights key" } }

    日志记录部分是自动创建的。我添加了” 数据” 部分, 以将连接字符串存储到数据库和我的ApplicationInsights键。
    应用配置 你必须配置我们的应用程序中可用的其他服务:
    打开ApplicationInsights:services.AddApplicationInsightsTelemetry(Configuration);
    通过调用注册服务:ContainerSetup.Setup(services, Configuration);
    ContainerSetup是一个创建的类, 因此我们不必将所有服务注册都存储在Startup类中。该类位于Expenses项目的IoC文件夹中:
    public static class ContainerSetup { public static void Setup(IServiceCollection services, IConfigurationRoot configuration) { AddUow(services, configuration); AddQueries(services); ConfigureAutoMapper(services); ConfigureAuth(services); } private static void ConfigureAuth(IServiceCollection services) { services.AddSingleton< IHttpContextAccessor, HttpContextAccessor> (); services.AddScoped< ITokenBuilder, TokenBuilder> (); services.AddScoped< ISecurityContext, SecurityContext> (); } private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient< IAutoMapper, AutoMapperAdapter> (); } private static void AddUow(IServiceCollection services, IConfigurationRoot configuration) { var connectionString = configuration["Data:main"]; services.AddEntityFrameworkSqlServer(); services.AddDbContext< MainDbContext> (options => options.UseSqlServer(connectionString)); services.AddScoped< IUnitOfWork> (ctx => new EFUnitOfWork(ctx.GetRequiredService< MainDbContext> ())); services.AddScoped< IActionTransactionHelper, ActionTransactionHelper> (); services.AddScoped< UnitOfWorkFilterAttribute> (); } private static void AddQueries(IServiceCollection services) { var exampleProcessorType = typeof(UsersQueryProcessor); var types = (from t in exampleProcessorType.GetTypeInfo().Assembly.GetTypes() where t.Namespace == exampleProcessorType.Namespace & & t.GetTypeInfo().IsClass & & t.GetTypeInfo().GetCustomAttribute< CompilerGeneratedAttribute> () == null select t).ToArray(); foreach (var type in types) { var interfaceQ = type.GetTypeInfo().GetInterfaces().First(); services.AddScoped(interfaceQ, type); } } }

    此类中的几乎所有代码都可以说明一切, 但是我想再多介绍一下ConfigureAutoMapper方法。
    private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient< IAutoMapper, AutoMapperAdapter> (); }

    此方法使用helper类查找模型与实体之间的所有映射, 反之亦然, 并获取IMapper接口以创建将在控制器中使用的IAutoMapper包装器。这个包装器没有什么特别的, 它只是为AutoMapper方法提供了一个方便的接口。
    public class AutoMapperAdapter : IAutoMapper { private readonly IMapper _mapper; public AutoMapperAdapter(IMapper mapper) { _mapper = mapper; } public IConfigurationProvider Configuration => _mapper.ConfigurationProvider; public T Map< T> (object objectToMap) { return _mapper.Map< T> (objectToMap); } public TResult[] Map< TSource, TResult> (IEnumerable< TSource> sourceQuery) { return sourceQuery.Select(x => _mapper.Map< TResult> (x)).ToArray(); } public IQueryable< TResult> Map< TSource, TResult> (IQueryable< TSource> sourceQuery) { return sourceQuery.ProjectTo< TResult> (_mapper.ConfigurationProvider); } public void Map< TSource, TDestination> (TSource source, TDestination destination) { _mapper.Map(source, destination); } }

    要配置AutoMapper, 请使用helper类, 该类的任务是搜索特定名称空间类的映射。所有映射都位于” 费用/映射” 文件夹中:
    public static class AutoMapperConfigurator { private static readonly object Lock = new object(); private static MapperConfiguration _configuration; public static MapperConfiguration Configure() { lock (Lock) { if (_configuration != null) return _configuration; var thisType = typeof(AutoMapperConfigurator); var configInterfaceType = typeof(IAutoMapperTypeConfigurator); var configurators = thisType.GetTypeInfo().Assembly.GetTypes() .Where(x => !string.IsNullOrWhiteSpace(x.Namespace)) // ReSharper disable once AssignNullToNotNullAttribute .Where(x => x.Namespace.Contains(thisType.Namespace)) .Where(x => x.GetTypeInfo().GetInterface(configInterfaceType.Name) != null) .Select(x => (IAutoMapperTypeConfigurator)Activator.CreateInstance(x)) .ToArray(); void AggregatedConfigurator(IMapperConfigurationExpression config) { foreach (var configurator in configurators) { configurator.Configure(config); } } _configuration = new MapperConfiguration(AggregatedConfigurator); return _configuration; } } }

    所有映射必须实现特定的接口:
    public interface IAutoMapperTypeConfigurator { void Configure(IMapperConfigurationExpression configuration); }

    从实体到模型的映射示例:
    public class ExpenseMap : IAutoMapperTypeConfigurator { public void Configure(IMapperConfigurationExpression configuration) { var map = configuration.CreateMap< Expense, ExpenseModel> (); map.ForMember(x => x.Username, x => x.MapFrom(y => y.User.FirstName + " " + y.User.LastName)); } }

    同样, 在Startup.ConfigureServices方法中, 配置通过JWT Bearer令牌的身份验证:
    services.AddAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser().Build()); });

    并且这些服务注册了ISecurityContext的实现, 该实现实际上将用于确定当前用户:
    public class SecurityContext : ISecurityContext { private readonly IHttpContextAccessor _contextAccessor; private readonly IUnitOfWork _uow; private User _user; public SecurityContext(IHttpContextAccessor contextAccessor, IUnitOfWork uow) { _contextAccessor = contextAccessor; _uow = uow; } public User User { get { if (_user != null) return _user; var username = _contextAccessor.HttpContext.User.Identity.Name; _user = _uow.Query< User> () .Where(x => x.Username == username) .Include(x => x.Roles) .ThenInclude(x => x.Role) .FirstOrDefault(); if (_user == null) { throw new UnauthorizedAccessException("User is not found"); } return _user; } } public bool IsAdministrator { get { return User.Roles.Any(x => x.Role.Name == Roles.Administrator); } } }

    此外, 我们对默认的MVC注册进行了一些更改, 以便使用自定义错误过滤器将异常转换为正确的错误代码:
    services.AddMvc(options => {options.Filters.Add(new ApiExceptionFilter()); });
    实现ApiExceptionFilter过滤器:
    public class ApiExceptionFilter : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { if (context.Exception is NotFoundException) { // handle explicit 'known' API errors var ex = context.Exception as NotFoundException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } else if (context.Exception is BadRequestException) { // handle explicit 'known' API errors var ex = context.Exception as BadRequestException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } else if (context.Exception is UnauthorizedAccessException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else if (context.Exception is ForbiddenException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } base.OnException(context); } }

    为了获得其他https://www.srcmini.com/api的出色API描述, 请不要忘记Swagger, 这一点很重要:
    services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info {Title = "Expenses", Version = "v1"}); c.OperationFilter< AuthorizationHeaderParameterOperationFilter> (); });

    使用ASP.NET Core构建ASP.NET Web API

    文章图片
    Startup.Configure方法将调用添加到InitDatabase方法, 该方法将自动迁移数据库, 直到最后一次迁移:
    private void InitDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService< IServiceScopeFactory> ().CreateScope()) { var context = serviceScope.ServiceProvider.GetService< MainDbContext> (); context.Database.Migrate(); } }

    仅当应用程序在开发环境中运行并且不需要身份验证才能访问它时, 才打开Swagger:
    app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); });

    接下来, 我们连接身份验证(详细信息可以在存储库中找到):
    ConfigureAuthentication(app);
    此时, 你可以运行集成测试, 并确保所有内容都已编译, 但是没有任何效果, 请转到控制器ExpensesController。
    注意:所有控制器都位于Expenses / Server文件夹中, 并有条件地分为两个文件夹:Controllers和RestApi。在文件夹中, 控制器是在旧的良好MVC中充当控制器的控制器, 即返回标记, 而在RestApi中则是REST控制器。
    你必须创建Expenses / Server / RestApi / ExpensesController类并从Controller类继承它:
    public class ExpensesController : Controller { }

    接下来, 通过使用属性[Route(” api / [controller]” )]标记该类, 配置?/ api / Expenses类型的路由。
    要访问业务逻辑和映射器, 你需要注入以下服务:
    private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; }

    在此阶段, 你可以开始实现方法。第一种方法是获取费用清单:
    [HttpGet] [QueryableResult] public IQueryable< ExpenseModel> Get() { var result = _query.Get(); var models = _mapper.Map< Expense, ExpenseModel> (result); return models; }

    该方法的实现非常简单, 我们可以查询到数据库的查询, 该查询从ExpensesQueryProcessor映射到IQueryable < ExpenseModel> 中, 该查询又返回结果。
    此处的自定义属性是QueryableResult, 它使用AutoQueryable库在服务器端处理分页, 筛选和排序。该属性位于” 费用/过滤器” 文件夹中。结果, 此过滤器将DataResult < ExpenseModel> 类型的数据返回给API客户端。
    public class QueryableResult : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Exception != null) return; dynamic query = ((ObjectResult)context.Result).Value; if (query == null) throw new Exception("Unable to retreive value of IQueryable from context result."); Type entityType = query.GetType().GenericTypeArguments[0]; var commands = context.HttpContext.Request.Query.ContainsKey("commands") ? context.HttpContext.Request.Query["commands"] : new StringValues(); var data = http://www.srcmini.com/QueryableHelper.GetAutoQuery(commands, entityType, query, new AutoQueryableProfile {UnselectableProperties = new string[0]}); var total = System.Linq.Queryable.Count(query); context.Result = new OkObjectResult(new DataResult{Data = data, Total = total}); } }

    另外, 让我们看一下Post方法的实现, 创建一个流:
    [HttpPost] [ValidateModel] public async Task< ExpenseModel> Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map< ExpenseModel> (item); return model; }

    在这里, 你应注意属性ValidateModel, 该属性根据数据注释属性对输入数据进行简单验证, 这是通过内置的MVC检查完成的。
    public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }

    【使用ASP.NET Core构建ASP.NET Web API】ExpensesController的完整代码:
    [Route("api/[controller]")] public class ExpensesController : Controller { private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; } [HttpGet] [QueryableResult] public IQueryable< ExpenseModel> Get() { var result = _query.Get(); var models = _mapper.Map< Expense, ExpenseModel> (result); return models; } [HttpGet("{id}")] public ExpenseModel Get(int id) { var item = _query.Get(id); var model = _mapper.Map< ExpenseModel> (item); return model; } [HttpPost] [ValidateModel] public async Task< ExpenseModel> Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map< ExpenseModel> (item); return model; } [HttpPut("{id}")] [ValidateModel] public async Task< ExpenseModel> Put(int id, [FromBody]UpdateExpenseModel requestModel) { var item = await _query.Update(id, requestModel); var model = _mapper.Map< ExpenseModel> (item); return model; } [HttpDelete("{id}")] public async Task Delete(int id) { await _query.Delete(id); } }

    总结 我将从问题开始:主要问题是解决方案的初始配置和理解应用程序各层的复杂性, 但是随着应用程序复杂性的增加, 系统的复杂性几乎不变, 这是一个很大的问题。加上这种系统时。而且非常重要的一点是, 我们有一个API, 针对该API有一套集成测试和一套完整的业务逻辑单元测试。业务逻辑与所使用的服务器技术完全分开, 可以进行全面测试。该解决方案非常适合具有复杂API和复杂业务逻辑的系统。
    如果你想构建一个使用你的API的Angular应用, 请查看srcminier Pablo Albella的同伴Angular 5和ASP.NET Core。

      推荐阅读