Attempted to access an unloaded appdomain when using System.DirectoryServices

We've implemented a Membership Provider that authenticates to Active Directory and it's using System.DirectoryServices. While using this Membership Provider in an ASP.Net MVC 3 application on Visual Studio 2010 with webdev server we sometimes (1 out of 6 times) get an exception when logging in the application.

System.IO.FileNotFoundException: Could not load file or assembly 'System.Web' or one of its dependencies. The system cannot find the file specified.
File name: 'System.Web' 
at System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
at System.Reflection.RuntimeAssembly.LoadWithPartialNameInternal(AssemblyName an, Evidence securityEvidence, StackCrawlMark& stackMark)
at System.DirectoryServices.AccountManagement.UnsafeNativeMethods.IADsPathname.Retrieve(Int32 lnFormatType)
at System.DirectoryServices.AccountManagement.ADStoreCtx.LoadDomainInfo()
at System.DirectoryServices.AccountManagement.ADStoreCtx.get_DnsDomainName()
at System.DirectoryServices.AccountManagement.ADStoreCtx.GetGroupsMemberOfAZ(Principal p)
at System.DirectoryServices.AccountManagement.UserPrincipal.GetAuthorizationGroupsHelper()
at System.DirectoryServices.AccountManagement.UserPrincipal.GetAuthorizationGroups()

=== Pre-bind state information ===
LOG: DisplayName = System.Web (Partial)
WRN: Partial binding information was supplied for an assembly:
WRN: Assembly Name: System.Web | Domain ID: 2
WRN: A partial bind occurs when only part of the assembly display name is provided.
WRN: This might result in the binder loading an incorrect assembly.
WRN: It is recommended to provide a fully specified textual identity for the assembly,
WRN: that consists of the simple name, version, culture, and public key token.
WRN: See whitepaper http://go.microsoft.com/fwlink/?LinkId=109270 for more information and common solutions to this issue.
Calling assembly : HibernatingRhinos.Profiler.Appender, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0774796e73ebf640.

The calling assembly was HibernatingRhinos.Profiler.Appender so after disabling the profiler in log4net config we got to the real exception:

System.AppDomainUnloadedException: Attempted to access an unloaded appdomain. (Except   at System.StubHelpers.StubHelpers.InternalGetCOMHRExceptionObject(Int32 hr, IntPtr pCPCMD, Object pThis)
at System.StubHelpers.StubHelpers.GetCOMHRExceptionObject(Int32 hr, IntPtr pCPCMD, Object pThis)
at System.DirectoryServices.AccountManagement.UnsafeNativeMethods.IADsPathname.Retrieve(Int32 lnFormatType)
at System.DirectoryServices.AccountManagement.ADStoreCtx.LoadDomainInfo()
at System.DirectoryServices.AccountManagement.ADStoreCtx.get_DnsDomainName()
at System.DirectoryServices.AccountManagement.ADStoreCtx.GetGroupsMemberOfAZ(Principal p)
at System.DirectoryServices.AccountManagement.UserPrincipal.GetAuthorizationGroupsHelper()
at System.DirectoryServices.AccountManagement.UserPrincipal.GetAuthorizationGroups()

The exception is always thrown at the same method, but for now we are not able to reproduce it as it happens randomly, but approximately 1 out of 6 times. We do however not get the exception when using IIs instead of the built-in Visual Studio 2010 web server.

It probably has something to do with racing conditions when using multiple appdomains in the context of Visual Studio webdev, but that's just guessing. We would really like to know what's the cause of the problem as we don't want to have these exceptions in a production environment.

We found 2 similar cases but no one has found a real solution:

http://our.umbraco.org/forum/developers/extending-umbraco/19581-Problem-with-custom-membership-and-role-provider

http://forums.asp.net/t/1556949.aspx/1

Update 18-05-2011

The smallest amount of code (in asp.net mvc) to reproduce the exception, where userName is your Active Directory loginname.

using System.DirectoryServices.AccountManagement;
using System.Web.Mvc;

namespace ADBug.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            string userName = "nickvane";
            var principalContext = new PrincipalContext(ContextType.Domain);

            UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(
                principalContext,
                IdentityType.SamAccountName,
                userName);

            if (userPrincipal != null)
            {
                PrincipalSearchResult<Principal> list = userPrincipal.GetAuthorizationGroups();
            }

            return View();
        }
    }
}

Alas, the exception still happens at random, so no fully reproducable bug.


Solution 1:

Here is what works for me (.Net 4):

Instead of this:

principalContext = new PrincipalContext(ContextType.Domain)

create the principal context with the domain string as well:

E.g.

principalContext = new PrincipalContext(ContextType.Domain,"MYDOMAIN")

It should be fixed in 4.5. See comment, hasn't been fixed yet, but adding the second argument still works as workaround.

Solution 2:

We've solved it in code by retrying the call to GetAuthorizationGroups but with a sleep in between. It solves our problem but I'm not quite happy with it.

private PrincipalSearchResult<Principal> GetAuthorizationGroups(UserPrincipal userPrincipal, int tries)
{
    try
    {
        return userPrincipal.GetAuthorizationGroups();
    }
    catch (AppDomainUnloadedException ex)
    {
        if (tries > 5)
        {
            throw;
        }
        tries += 1;
        Thread.Sleep(1000);
        return GetAuthorizationGroups(userPrincipal, tries);
    }
}

If we get the exception then 1 retry is apparently enough.

Solution 3:

This solution is really slow, and when for example when you are using this in a webapplication GetAuthorizationGroups gets called very often which makes the site very slow. I worked implemented som caching instead, that makes alot faster after the first time. I am also retrying, because the exception still occurs.

First i override the GetRolesForUser method and implement the caching.

    public override string[] GetRolesForUser(string username)
    {
        // List of Windows groups for the given user.
        string[] roles;

        // Create a key for the requested user.
        string cacheKey = username + ":" + ApplicationName;

        // Get the cache for the current HTTP request.
        Cache cache = HttpContext.Current.Cache;
        // Attempt to fetch the list of roles from the cache.
        roles = cache[cacheKey] as string[];
        // If the list is not in the cache we will need to request it.
        if (null == roles)
        {
            // Allow the base implementation to load the list of roles.
            roles = GetRolesFromActiveDirectory(username);
            // Add the resulting list to the cache.
            cache.Insert(cacheKey, roles, null, Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration);
        }

        // Return the resulting list of roles.
        return roles;
    }

The GetRolesFromActiveDirectory looks like this.

    public String[] GetRolesFromActiveDirectory(String username)
    {            
        // If SQL Caching is enabled, try to pull a cached value.);));
        if (_EnableSqlCache)
        {
            String CachedValue;
            CachedValue = GetCacheItem('U', username);
            if (CachedValue != "*NotCached")
            {
                return CachedValue.Split(',');
            }
        }

        ArrayList results = new ArrayList();
        using (PrincipalContext context = new PrincipalContext(ContextType.Domain, null, _DomainDN))
        {
            try
            {                    
                UserPrincipal p = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username);

                var tries = 0;
                var groups = GetAuthorizationGroups(p, tries);

                foreach (GroupPrincipal group in groups)
                {
                    if (!_GroupsToIgnore.Contains(group.SamAccountName))
                    {
                        if (_IsAdditiveGroupMode)
                        {
                            if (_GroupsToUse.Contains(group.SamAccountName))
                            {
                                results.Add(group.SamAccountName);
                            }
                        }
                        else
                        {
                            results.Add(group.SamAccountName);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                throw new ProviderException("Unable to query Active Directory.", ex);
            }
        }
        // If SQL Caching is enabled, send value to cache
        if (_EnableSqlCache)
        {
            SetCacheItem('U', username, ArrayListToCSString(results));
        }

        return results.ToArray(typeof(String)) as String[];
    }

The last method is GetAuthorizationGroups and it looks like this.

    private PrincipalSearchResult<Principal> GetAuthorizationGroups(UserPrincipal userPrincipal, int tries)
    {
        try
        {
            return userPrincipal.GetAuthorizationGroups();
        }
        catch(FileNotFoundException ex)
        {
            if (tries > 5) throw;

            tries++;
            Thread.Sleep(1000);

            return GetAuthorizationGroups(userPrincipal, tries);
        }
        catch (AppDomainUnloadedException ex)
        {
            if (tries > 5) throw;

            tries++;
            Thread.Sleep(1000);

            return GetAuthorizationGroups(userPrincipal, tries);
        }
    }

I found out that caching the roles makes it a whole lot faster. Hope this helps someone. Cheers.

Solution 4:

I've encountered the same issue when using the ActiveDirectoryMembershipProvider. For me it was happening when I called Membership.ValidateUser() for the first time and the framework was trying to create the provider.

I noticed that my temporary development computer did not have Visual Studio 2010 SP1 installed so I installed it and that solved the problem for me.

Solution 5:

I've had the same issue, and I have found the answer in this post works. Seems to be an issue with the PrincipalContext constructor that only takes a ContextType as a parameter. I know this post is old, but thought I would link it for anyone in the future :)