zejji / DbContextScopeEFCore

A version of Mehdi El Gueddari's DbContextScope library updated for EF Core with a number of improvements and bug fixes
9 stars 2 forks source link

AsyncLocal<DbContextScope?> is cleared by WPF after Window load #168

Open mortensp opened 3 months ago

mortensp commented 3 months ago

AmbientDbContextScope is reset by WPF after Window, Page etc. load. That didn't happen earlier when ThreadLocal was used!

zejji commented 3 months ago

Hi - I think the reason for this behaviour is explained here:

https://stackoverflow.com/questions/49334885/asynclocal-in-wpf-is-null-after-first-set-on-the-same-thread

However, I'm not yet sure what the correct fix, is as CallContext doesn't exist in .NET Core onwards. This will likely require a bit of research as to alternatives. We would need somewhere to store data that flows across async boundaries and also survives a reset of the ExecutionContext...

mortensp commented 3 months ago

Hi - Thanks for looking into this issue.

I do think that I have a working fix. My solution is to keep all the logic around the AsyncLocal and add a ThreadLocal as fallback. That is to:

  1. Only set the ThreadLocal value equal to the AsyncLocal value when the first one is unset (is null).
  2. Always return the AsyncLocal unless it's null, and then return the ThreadLocal value instead.

I have tested this in WPF using Caliburn.Micro and its Async methods to Open new views/viewModels and that works fine. My only concern at present is what will happen in the continuation part of await, when this lands on a different thread. I do believe that the AsyncLocal will be present but I hasn't been able to test that yes,

Another idea is to change the object/class stored in the Async and ThreadLocal variables to an immutable struct. This should create a new copy on each reassignment and therefore automatic eliminate the need for parent chaining and restoring,

Best Regards - Morten

mortensp commented 3 months ago

Here is my fix as off now. Note the conditional compilation symbol "MSPA" that has to be set in order to activate my fix. I could have made a Github branch, but i don't know how - sorry!

      private static readonly AsyncLocal<DbContextScope?> AmbientDbContextScope = new();

#if MSPA
        // Use ThreadLocal<> when AmbientDbContextScope is null
        private static readonly ThreadLocal<DbContextScope?> ThreadLocalDbContextScope = new();
#endif

        /// <summary>
        /// Makes the provided 'dbContextScope' available as the the ambient scope via the AsyncLocal.
        /// </summary>
        internal static void SetAmbientScope(DbContextScope newAmbientScope)
        {           
            ArgumentNullException.ThrowIfNull(newAmbientScope);

            var current = AmbientDbContextScope.Value;

            if (current == newAmbientScope)
                return;

            // Store the new scope in the AsyncLocal, making it the ambient scope
            AmbientDbContextScope.Value = newAmbientScope;
#if MSPA
            ThreadLocalDbContextScope.Value = AmbientDbContextScope.Value;
#endif
        }

        /// <summary>
        /// Clears the ambient scope from the AsyncLocal.
        /// </summary>
        internal static void RemoveAmbientScope()
        {
#if MSPA            
            if (ThreadLocalDbContextScope.Value == AmbientDbContextScope.Value)
                ThreadLocalDbContextScope.Value = null;
#endif                        
            AmbientDbContextScope.Value = null;
        }

        /// <summary>
        /// Get the current ambient scope or null if no ambient scope has been setup.
        /// </summary>
        internal static DbContextScope? GetAmbientScope()
        {
            // Retrieve the ambient scope (if any)
#if MSPA
            if (AmbientDbContextScope.Value is null)
                return ThreadLocalDbContextScope.Value;
            else
#endif
                return AmbientDbContextScope.Value;
        }