本文概述
- 介绍
- 图层API
- 项目结构
- 实作
- API实施
- 应用配置
- 总结
在本文中, 我将介绍:
- 如何使用.NET Core, EF Core, AutoMapper和XUnit从头开始创建REST API
- 更改后如何确保API正常工作
- 如何尽可能简化REST API系统的开发和支持
ASP.NET Core在ASP.NET MVC / Web API的基础上提供了许多改进。首先, 它现在是一个框架, 而不是两个。我真的很喜欢它, 因为它很方便而且混乱的程度也较小。其次, 我们拥有没有任何其他库的日志记录和DI容器, 这节省了我的时间, 使我可以专注于编写更好的代码, 而不必选择和分析最佳的库。
什么是查询处理器?
当与系统的一个实体相关的所有业务逻辑都封装在一个服务中, 并且对该实体的任何访问或操作都通过该服务执行时, 查询处理器就是一种方法。该服务通常称为{EntityPluralName} QueryProcessor。如有必要, 查询处理器将为此实体包括CRUD(创建, 读取, 更新, 删除)方法。根据要求, 并非所有方法都可以实施。举一个具体的例子, 让我们看一下ChangePassword。如果查询处理器的方法需要输入数据, 则仅应提供所需的数据。通常, 对于每种方法, 都会创建一个单独的查询类, 在简单情况下, 有可能(但不希望)重用查询类。
我们的目的
在本文中, 我将向你展示如何为小型成本管理系统制作API, 包括身份验证和访问控制的基本设置, 但我不会涉及身份验证子系统。我将使用模块化测试涵盖系统的整个业务逻辑, 并在一个实体的示例上为每种API方法创建至少一个集成测试。
对已开发系统的要求:用户可以添加, 编辑, 删除其费用, 并且只能查看其费用。
该系统的完整代码可在Github上获得。
因此, 让我们开始设计一个小型但非常有用的系统。
图层API
文章图片
该图显示该系统将具有四层:
- 数据库-这里我们存储数据, 仅此而已, 没有逻辑。
- DAL-要访问数据, 我们使用工作单元模式, 在实现中, 我们将ORM EF Core与代码优先和迁移模式一起使用。
- 业务逻辑-为了封装业务逻辑, 我们使用查询处理器, 只有这一层处理业务逻辑。例外是最简单的验证, 例如必填字段, 它将通过API中的过滤器执行。
- REST API-客户端可以通过我们的API使用的实际接口将通过ASP.NET Core实现。路由配置由属性确定。
项目结构 我使用VS 2017 Professional创建项目。我通常在不同的文件夹上共享源代码和测试。感觉很舒适, 看起来不错, CI中的测试运行方便, 微软似乎建议这样做:
文章图片
项目简介:
项目 | 描述 |
---|---|
花费 | 控制器项目, 域模型与API模型之间的映射, API配置 |
普通费用 | 在这一点上, 收集了异常类, 这些异常类通过过滤器以某种方式解释, 以将正确的HTTP代码返回给用户错误 |
费用模型 | API模型专案 |
费用, 数据, 访问 | 接口和工作单位模式实施项目 |
费用数据模型 | 领域模型项目 |
费用查询 | 查询处理器和特定于查询的类的项目 |
Expenses.Security | 当前用户的安全上下文的接口和实现的项目 |
文章图片
通过模板创建的费用:
文章图片
通过模板在src文件夹中的其他项目:
文章图片
测试文件夹中的所有项目(按模板):
文章图片
实作 尽管本文已实现, 但本文将不介绍与UI关联的部分。
第一步是开发位于程序集Expenses.Data.Model中的数据模型:
文章图片
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程序集中实现了工作单位模板, 该项目的结构如下所示:
文章图片
组装需要以下库:
- Microsoft.EntityFrameworkCore.SqlServer
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程序集。我安装了以下库:
- 起订量
- 流利的断言
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
- 安装nuget软件包:
- 流利的断言
- 起订量
- Microsoft.AspNetCore.TestHost
- 建立项目结构
文章图片
实施集成测试以获取费用清单:
[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
文章图片
在下一步中, 请预先准备配置文件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> (); });
文章图片
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。
推荐阅读
- Python多线程和多处理教程
- YouTube API集成(使用Django上传视频)
- REST规范从未做过的5件事
- android中实现简单的聊天功能
- android启动时的广告
- Redux之中间件的原理和applyMiddlewareThunk的实现
- No mapping found for HTTP request with URI [/webapp/] in DispatcherServlet with name 'SpringMVC&
- [LeetCode] Friends Of Appropriate Ages 适合年龄段的朋友
- Android使用全局变量来传递数据