Home > database >  Getting OData, DTO, Automapper and UnitOfWork to play nicely in aspnetboilerplate
Getting OData, DTO, Automapper and UnitOfWork to play nicely in aspnetboilerplate

Time:02-22

sI'm trying to get OData working on DTO objects instead of entities, with aspnetboilerplate.

I've made a controller, inspired from AbpODataEntityController.cs that inherits from AbpODataController.

I've got mapping between DTOs and Entities working using AutoMapper.ExpressionMapping's 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 
}

It -kinda- works. However, as soon as I start using $select in my OData requests, something happens where the UnitOfWork tries to dispose of the repositories while OData still has an open Datareader on the underlying connection used by the DbContext of the Repository and I get the following exception :

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)

In Postman, it still "looks good" as the response was already written and is "valid", but it seems that the response communication gets cut short by the exception and anything less robust will complain.

My actual controller looks like this :

    [AbpAuthorize]
    public class myEntityController : DtoODataControllerBase<myEntity, myDto>, ITransientDependency
    {
        public myEntityController (IRepository<myEntity> repository, IMapper mapper) : base(repository, mapper)
        {
        }
    }

Funny thing is, when using abp's AbpODataEntityController with an actual Entity, everything is fine, no dispose issue. I've tried turning off UoW on my controller and a few other things, but it didn't help and looking at the UnitOfWork middleware, i get that even if i disable the UoW, the UoW still gets disposed when the middleware finishes, thus triggering the issue.

The only difference seems to be the use of UseAsDataSource, guessing it keeps an Open Reader for... reasons ...

Any ideas / leads as to how I could get abp, automapper's expression mapping and odata to play well together ?

EDIT :

I was able to reproduce the issue using a simple ODataController with a DbContext, no repository, no Abp controllers. The UnitOfWorkMiddleware, when it gets disposed, disposes of the UnitOfWork itself which cleans up behind himself... but for some reason, using $select makes the mapper/expression mapper/odatacontroller keep the Datareader open... I'll keep diagnosing until I find what keeps a reader open... my current guess is ODataController, which is probably the one enumerating... I'll dig into it and report back...

CodePudding user response:

This is caused by a bug in AutoMapper.Extensions.ExpressionMapping, where the enumerator obtained from SingleQueryingEnumerable is not disposed (and thus _datareader.Dispose() is not called) when an arbitrary projection (select) is enumerated:

// 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())
        {
            ...
        }
        ...
    }
}

More info: Does foreach automatically call Dispose?

Solution

We can get them to play nicely with a non-transactional unit of work.

You may want this for $select queries even if the bug above is fixed.

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();
    //  }
});
  • Related