asp.net-coreaspnetboilerplate

'Cannot access a disposed object' once migrated ASP.NET Boilerplate from MVC5 to ASP.NET Core MVC


I have a ASP.NET boilerplate 7.4 MVC app using .NET framework 4.6 and that now I'm migrating to ASP.NET Core 8 (.NET8).

I have a problem in a MVC Controller when using AbpSession.Use(...). The problem is not present in the MVC5 app (same code)

This is the MVC Controller:

    public class BookMgmController : AbpController {
        ...

        [HttpPost]
        public async Task<JsonResult> Pay(PayRequestVM input) {
            ...
            using (AbpSession.Use(AbpSession.TenantId, myAuthorizedUserIdToAccessOrders)) {
                // create the order and put it into a status of 'order waiting for payments
                var order = await _orderAppService.CreateAsync(input);   // <--- problem!
                ...
            }   // using...
        }
    }

And the OrderAppService service:

    [AbpAuthorize(PermissionNames.Pages_Orders)]
    public class OrderAppService : ..., IOrderAppService {
        ...
        public override async Task<BasicOrderDto> CreateAsync(CreateOrderDto input) {
            ...
        }
        ...

        }
    }

When I try to invoke the CreateAsyc(...), ABP throws the following exception:

Cannot access a disposed object.
Object name: 'GPSoftware.Booking.Authorization.Users.UserManager'

As said, the problem is not present in .NET framework and disappears if I remove the AbpAuthorize attribute assigned to the OrderAppService.

Here below, the stack trace:

System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GPSoftware.Booking.Authorization.Users.UserManager'.
   at Microsoft.AspNetCore.Identity.UserManager`1.FindByIdAsync(String userId)
   at Abp.Authorization.Users.AbpUserManager`2.<>c__DisplayClass91_0.<<GetUserPermissionCacheItemAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Abp.Runtime.Caching.TypedCacheWrapper`2.<>c__DisplayClass21_0.<<GetAsync>b__0>d.MoveNext()
--- End of stack trace from previous location ---
   at Abp.Runtime.Caching.AbpCacheBase`2.GetAsync(TKey key, Func`2 factory)
   at Abp.Runtime.Caching.TypedCacheWrapper`2.GetAsync(TKey key, Func`2 factory)
   at Abp.Authorization.Users.AbpUserManager`2.GetUserPermissionCacheItemAsync(Int64 userId)
   at Abp.Authorization.Users.AbpUserManager`2.IsGrantedAsync(Int64 userId, Permission permission)
   at Abp.Authorization.Users.AbpUserManager`2.IsGrantedAsync(Int64 userId, String permissionName)
   at Abp.Authorization.PermissionChecker`2.IsGrantedAsync(Int64 userId, String permissionName)
   at Abp.Authorization.PermissionChecker`2.IsGrantedAsync(String permissionName)
   at Abp.Authorization.PermissionCheckerExtensions.IsGrantedAsync(IPermissionChecker permissionChecker, Boolean requiresAll, String[] permissionNames)
   at Abp.Authorization.PermissionCheckerExtensions.AuthorizeAsync(IPermissionChecker permissionChecker, Boolean requireAll, String[] permissionNames)
   at Abp.Authorization.AuthorizationHelper.AuthorizeAsync(IEnumerable`1 authorizeAttributes)
   at Abp.Authorization.AuthorizationHelper.CheckPermissionsAsync(MethodInfo methodInfo, Type type)
   at Abp.Authorization.AuthorizationHelper.AuthorizeAsync(MethodInfo methodInfo, Type type)
   at Abp.Authorization.AuthorizationInterceptor.InternalInterceptAsynchronous[TResult](IInvocation invocation)
   at Abp.Auditing.AuditingInterceptor.InternalInterceptAsynchronous[TResult](IInvocation invocation)
   at Abp.Auditing.AuditingInterceptor.InternalInterceptAsynchronous[TResult](IInvocation invocation)
   at GPSoftware.Booking.Web.Controllers.BookMgmController.DoPay(PayShoppingCartRequestVM input) in C:\WA\GPsoftware\PortamiInPista.NET\aspnet-core\src\web\Booking.Web.Mvc\Controllers\BookMgmController.cs:line 165
   at GPSoftware.Booking.Web.Controllers.BookMgmController.Pay(PayShoppingCartRequestVM input) in C:\WA\GPsoftware\PortamiInPista.NET\aspnet-core\src\web\Booking.Web.Mvc\Controllers\BookMgmController.cs:line 131

Update 1

Note that most of objects are injected into DI by the ABP framework and I checked they are not null at runtime (specifically, but not only, UserManager and AbpSession).

Update 2

Error does not occur on a simpler very similar code starting from the ABP template.

By the way, I went deeper in debugging and I arrived to the source code of Abp.Authorization.Users.AbpUserManager<TRole, TUser>, ancestor of UserManager. It has the following private method that fails when it invokes FindByIdAsync (and later on GetRolesAsync):

        private async Task<UserPermissionCacheItem> GetUserPermissionCacheItemAsync(long userId) {
            var cacheKey = userId + "@" + (GetCurrentTenantId() ?? 0);
            return await _cacheManager.GetUserPermissionCache().GetAsync(cacheKey, async () => {
                var user = await FindByIdAsync(userId.ToString());  // <= fails here
                if (user == null) {
                    return null;
                }

                var newCacheItem = new UserPermissionCacheItem(userId);

                foreach (var roleName in await GetRolesAsync(user)) { // <= fails here
                    newCacheItem.RoleIds.Add((await RoleManager.GetRoleByNameAsync(roleName)).Id);
                }

                foreach (var permissionInfo in await UserPermissionStore.GetPermissionsAsync(userId)) {
                    if (permissionInfo.IsGranted) {
                        newCacheItem.GrantedPermissions.Add(permissionInfo.Name);
                    } else {
                        newCacheItem.ProhibitedPermissions.Add(permissionInfo.Name);
                    }
                }

                return newCacheItem;
            });
        }

FindByIdAsync is a method of Microsoft.AspNetCore.Identity.UserManager<TUser>, still another ancestor. So I overrode it in my UserManager by commenting out the line ThrowIfDisposed() and it works!

        public override Task<User> FindByIdAsync(string userId) {
            //ThrowIfDisposed();
            return Store.FindByIdAsync(userId, CancellationToken);
        }

Now I can’t go any further because it's not so simple to override GetRolesAsync too.

The questions are: Why all this? And why the error doesn't occur in a simpler very similat code I tested starting from the original template?

Any suggestion?


Solution

  • *** This is not a real answer. It’s just a workaround to overcome the problem ***

    I overrode some methods in my UserManager and RoleManager to skip ThrowIfDisposed() in some specific cases:

    public class UserManager : AbpUserManager<Role, User> {
       ...
    
        public override async Task<User> FindByIdAsync(string userId) {
            try {
                // try former with the ancestor method
                return await base.FindByIdAsync(userId);
            }
            catch (ObjectDisposedException) when (Store != null) {
                // ignore the exception if I'm able to call the Store method
                return await Store.FindByIdAsync(userId, CancellationToken);
            }
        }
    
        public override async Task<IList<string>> GetRolesAsync(User user) {
            try {
                // try former with the ancestor method
                return await base.GetRolesAsync(user);
            }
            catch (ObjectDisposedException) when (Store != null) {
                // ignore the exception if I'm able to call the Store method
                Check.NotNull(user, nameof(user));
                var userRoleStore = (Store as IUserRoleStore<User>);
                if (userRoleStore != null) {
                    return await userRoleStore.GetRolesAsync(user, CancellationToken).ConfigureAwait(false);
                } else {
                    throw;
                }
            }
        }
    
        ...
    }
    

    and

    public class RoleManager : AbpRoleManager<Role, User> {
        ...
    
        public override async Task<Role> FindByNameAsync(string roleName) {
            try {
                // try former with the ancestor method
                return await base.FindByNameAsync(roleName);
            }
            catch (ObjectDisposedException) when (Store != null) {
                // ignore the exception if I'm able to call the Store method
                Check.NotNullOrEmpty(roleName, nameof(roleName));
                return await Store.FindByNameAsync(NormalizeKey(roleName), CancellationToken);
            }
        }
    
        ...
    }