让 OData、DTO、Automapper 和 UnitOfWork 在 aspnetboilerplate 中很好地发挥作用
Getting OData, DTO, Automapper and UnitOfWork to play nicely in aspnetboilerplate
s我正在尝试使用 aspnetboilerplate 让 OData 在 DTO 对象而不是实体上工作。
我制作了一个控制器,灵感来自 AbpODataEntityController.cs,它继承自 AbpODataController
。
我使用 AutoMapper.ExpressionMapping 的 UseAsDataSource().For<Dto>()
在 DTO 和实体之间建立映射
public abstract class DtoODataControllerBase<TEntity, TEntityDto, TPrimaryKey> : AbpODataController
where TEntity : class, IEntity<TPrimaryKey>
where TPrimaryKey : IEquatable<TPrimaryKey>
{
private readonly IRepository<TEntity, TPrimaryKey> _repository;
private readonly IMapper _mapper;
protected DtoODataControllerBase(IRepository<TEntity, TPrimaryKey> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
[EnableQuery]
public virtual IQueryable<TEntityDto> Get()
{
CheckGetAllPermission();
return _repository.GetAll().UseAsDataSource(_mapper).For<TEntityDto>();
}
// Permission checking code removed for brevity
}
它有点管用。但是,一旦我开始在我的 OData 请求中使用 $select
,UnitOfWork 会尝试处理存储库,而 OData 在 DbContext 使用的基础连接上仍然有一个开放的 Datareader存储库,我得到以下异常:
System.InvalidOperationException: There is already an open DataReader associated with this Connection which must be closed first.
at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
at Microsoft.Data.SqlClient.SqlInternalTransaction.Rollback()
at Microsoft.Data.SqlClient.SqlInternalTransaction.Dispose(Boolean disposing)
at Microsoft.Data.SqlClient.SqlInternalTransaction.Dispose()
at Microsoft.Data.SqlClient.SqlTransaction.Dispose(Boolean disposing)
at System.Data.Common.DbTransaction.Dispose()
at Microsoft.EntityFrameworkCore.Storage.RelationalTransaction.Dispose()
at Abp.EntityFrameworkCore.Uow.DbContextEfCoreTransactionStrategy.Dispose(IIocResolver iocResolver)
at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.DisposeUow()
at Abp.Domain.Uow.UnitOfWorkBase.Dispose()
at Abp.AspNetCore.Uow.AbpUnitOfWorkMiddleware.Invoke(HttpContext httpContext)
at Abp.AspNetCore.Security.AbpSecurityHeadersMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
在 Postman 中,它仍然“看起来不错”,因为响应已经写入且“有效”,但似乎响应通信因异常而中断,任何不那么健壮的都会抱怨。
我的实际控制器是这样的:
[AbpAuthorize]
public class myEntityController : DtoODataControllerBase<myEntity, myDto>, ITransientDependency
{
public myEntityController (IRepository<myEntity> repository, IMapper mapper) : base(repository, mapper)
{
}
}
有趣的是,当将 abp 的 AbpODataEntityController
与实际实体一起使用时,一切都很好,没有处置问题。
我已经尝试关闭我的控制器上的 UoW 和其他一些东西,但它没有帮助并查看 UnitOfWork 中间件,我知道即使我禁用了 UoW,当中间件完成时 UoW 仍然会被处理掉,因此触发问题。
唯一的区别似乎是 UseAsDataSource
的使用,猜测它保持打开状态 Reader 原因...
关于如何让 abp、automapper 的表达式映射和 odata 很好地协同工作有什么想法/线索吗?
编辑:
我能够使用带有 DbContext、没有存储库、没有 Abp 控制器的简单 ODataController 重现该问题。
UnitOfWorkMiddleware,当它被处置时,处置 UnitOfWork 本身,后者在他自己身后清理......但由于某种原因,使用 $select 使得 mapper/expression mapper/odatacontroller 保留 Datareader 打开...
我会继续诊断,直到找到 reader 打开的原因...我目前的猜测是 ODataController,它可能是枚举的...我会深入研究并报告回来...
这是由于在枚举任意投影(select)时未调用bug in AutoMapper.Extensions.ExpressionMapping
, where the enumerator obtained from SingleQueryingEnumerable
is not disposed (and thus _datareader.Dispose()
造成的:
// case #2: query is arbitrary ("manual") projection
// example: users.UseAsDataSource().For<UserDto>().Select(user => user.Age).ToList()
// ...
else if (...)
{
...
var enumerator = sourceResult.GetEnumerator();
...
if (...)
{
...
while (enumerator.MoveNext())
{
...
}
...
}
}
更多信息:Does foreach automatically call Dispose?
解决方案
我们可以让他们在 non-transactional 个工作单元中很好地发挥作用。
即使上面的错误已修复,您也可能需要 $select
查询。
app.UseUnitOfWork(options =>
{
options.Filter = httpContext =>
httpContext.Request.Path.Value != null &&
httpContext.Request.Path.Value.StartsWith("/odata");
// +{
options.OptionsFactory = httpContext =>
httpContext.Request.Path.Value != null &&
httpContext.Request.Path.Value.StartsWith("/odata", StringComparison.InvariantCultureIgnoreCase) &&
httpContext.Request.Path.Value.EndsWith("Dtos", StringComparison.InvariantCultureIgnoreCase) &&
httpContext.Request.Query.Keys.Contains("$select", StringComparer.InvariantCultureIgnoreCase)
? new UnitOfWorkOptions { IsTransactional = false }
: new UnitOfWorkOptions();
// +}
});
s我正在尝试使用 aspnetboilerplate 让 OData 在 DTO 对象而不是实体上工作。
我制作了一个控制器,灵感来自 AbpODataEntityController.cs,它继承自 AbpODataController
。
我使用 AutoMapper.ExpressionMapping 的 UseAsDataSource().For<Dto>()
public abstract class DtoODataControllerBase<TEntity, TEntityDto, TPrimaryKey> : AbpODataController
where TEntity : class, IEntity<TPrimaryKey>
where TPrimaryKey : IEquatable<TPrimaryKey>
{
private readonly IRepository<TEntity, TPrimaryKey> _repository;
private readonly IMapper _mapper;
protected DtoODataControllerBase(IRepository<TEntity, TPrimaryKey> repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
[EnableQuery]
public virtual IQueryable<TEntityDto> Get()
{
CheckGetAllPermission();
return _repository.GetAll().UseAsDataSource(_mapper).For<TEntityDto>();
}
// Permission checking code removed for brevity
}
它有点管用。但是,一旦我开始在我的 OData 请求中使用 $select
,UnitOfWork 会尝试处理存储库,而 OData 在 DbContext 使用的基础连接上仍然有一个开放的 Datareader存储库,我得到以下异常:
System.InvalidOperationException: There is already an open DataReader associated with this Connection which must be closed first.
at Microsoft.Data.SqlClient.SqlInternalConnectionTds.ValidateConnectionForExecute(SqlCommand command)
at Microsoft.Data.SqlClient.SqlInternalTransaction.Rollback()
at Microsoft.Data.SqlClient.SqlInternalTransaction.Dispose(Boolean disposing)
at Microsoft.Data.SqlClient.SqlInternalTransaction.Dispose()
at Microsoft.Data.SqlClient.SqlTransaction.Dispose(Boolean disposing)
at System.Data.Common.DbTransaction.Dispose()
at Microsoft.EntityFrameworkCore.Storage.RelationalTransaction.Dispose()
at Abp.EntityFrameworkCore.Uow.DbContextEfCoreTransactionStrategy.Dispose(IIocResolver iocResolver)
at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.DisposeUow()
at Abp.Domain.Uow.UnitOfWorkBase.Dispose()
at Abp.AspNetCore.Uow.AbpUnitOfWorkMiddleware.Invoke(HttpContext httpContext)
at Abp.AspNetCore.Security.AbpSecurityHeadersMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
在 Postman 中,它仍然“看起来不错”,因为响应已经写入且“有效”,但似乎响应通信因异常而中断,任何不那么健壮的都会抱怨。
我的实际控制器是这样的:
[AbpAuthorize]
public class myEntityController : DtoODataControllerBase<myEntity, myDto>, ITransientDependency
{
public myEntityController (IRepository<myEntity> repository, IMapper mapper) : base(repository, mapper)
{
}
}
有趣的是,当将 abp 的 AbpODataEntityController
与实际实体一起使用时,一切都很好,没有处置问题。
我已经尝试关闭我的控制器上的 UoW 和其他一些东西,但它没有帮助并查看 UnitOfWork 中间件,我知道即使我禁用了 UoW,当中间件完成时 UoW 仍然会被处理掉,因此触发问题。
唯一的区别似乎是 UseAsDataSource
的使用,猜测它保持打开状态 Reader 原因...
关于如何让 abp、automapper 的表达式映射和 odata 很好地协同工作有什么想法/线索吗?
编辑:
我能够使用带有 DbContext、没有存储库、没有 Abp 控制器的简单 ODataController 重现该问题。 UnitOfWorkMiddleware,当它被处置时,处置 UnitOfWork 本身,后者在他自己身后清理......但由于某种原因,使用 $select 使得 mapper/expression mapper/odatacontroller 保留 Datareader 打开... 我会继续诊断,直到找到 reader 打开的原因...我目前的猜测是 ODataController,它可能是枚举的...我会深入研究并报告回来...
这是由于在枚举任意投影(select)时未调用bug in AutoMapper.Extensions.ExpressionMapping
, where the enumerator obtained from SingleQueryingEnumerable
is not disposed (and thus _datareader.Dispose()
造成的:
// case #2: query is arbitrary ("manual") projection // example: users.UseAsDataSource().For<UserDto>().Select(user => user.Age).ToList() // ... else if (...) { ... var enumerator = sourceResult.GetEnumerator(); ... if (...) { ... while (enumerator.MoveNext()) { ... } ... } }
更多信息:Does foreach automatically call Dispose?
解决方案
我们可以让他们在 non-transactional 个工作单元中很好地发挥作用。
即使上面的错误已修复,您也可能需要 $select
查询。
app.UseUnitOfWork(options =>
{
options.Filter = httpContext =>
httpContext.Request.Path.Value != null &&
httpContext.Request.Path.Value.StartsWith("/odata");
// +{
options.OptionsFactory = httpContext =>
httpContext.Request.Path.Value != null &&
httpContext.Request.Path.Value.StartsWith("/odata", StringComparison.InvariantCultureIgnoreCase) &&
httpContext.Request.Path.Value.EndsWith("Dtos", StringComparison.InvariantCultureIgnoreCase) &&
httpContext.Request.Query.Keys.Contains("$select", StringComparer.InvariantCultureIgnoreCase)
? new UnitOfWorkOptions { IsTransactional = false }
: new UnitOfWorkOptions();
// +}
});