【问题标题】:Getting OData, DTO, Automapper and UnitOfWork to play nicely in aspnetboilerplate让 OData、DTO、Automapper 和 UnitOfWork 在 aspnetboilerplate 中发挥出色
【发布时间】:2022-02-20 16:45:12
【问题描述】:

我正在尝试使用 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 的使用,猜测它保持开放阅读器是出于……原因……

关于如何让 abp、automapper 的表达式映射和 odata 一起发挥作用的任何想法/线索?

编辑:

我能够使用带有 DbContext、没有存储库、没有 Abp 控制器的简单 ODataController 重现该问题。 UnitOfWorkMiddleware 被处理后,会处理 UnitOfWork 本身,它会在自己身后进行清理......但由于某种原因,使用 $select 会使映射器/表达式映射器/odatacontroller 保持 Datareader 处于打开状态...... 我会继续诊断,直到找到让读者保持开放的原因......我目前的猜测是 ODataController,它可能是枚举的......我会深入研究并报告......

【问题讨论】:

    标签: c# asp.net-core odata automapper aspnetboilerplate


    【解决方案1】:

    这是由AutoMapper.Extensions.ExpressionMapping 中的bug 引起的,其中在枚举任意投影(选择)时,从SingleQueryingEnumerable 获得的枚举数没有被释放(因此不会调用_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?

    解决方案

    我们可以让他们很好地使用非事务性工作单元。

    即使上面的错误已修复,您也可能希望对 $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();
        // +}
    });
    

    【讨论】:

    • 再次@aaron,你解决了我的问题。作为记录,我能够同时使用AutoMapper.AspNetCore.OData.EFCore v2.2.2 让我在 DTO 控制器上的 OData 在 aspnetboilerplate 中工作。但是您的解决方案要简单得多,并且适合我的需要。
    猜你喜欢
    • 2013-08-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-17
    • 2010-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多