让 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();
    // +}
});