I have an issue using c# on .Net 4 in a MVC web application, where when I query Active Directory, I frequently get an error: Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014).
The strange thing is, that it will work flawlessly for a time, and then it will just start happening, and then just disappear again.
I have made a few modifications to the function to get it to work , but they all seem to fail. I am wondering if I am doing something wrong, or if there is a better way to do it.
Here is my current function, that will accept a loginId, and a PrincipalContext. The loginId can either be the user DisplayName i.e "John Smith", or DOMAINNAME\josmi. The default is to use the first 2 letters of their firstname, and then the first 3 letters of their surname. There is a check in there if this is not the case. This part if fine.
public List<ADGroup> GetMemberGroups(string loginId, PrincipalContext principalContext, int tries = 0)
{
var result = new List<ADGroup>();
try
{
var samAccountName = "";
if (loginId.Contains(" "))
{
var fName = loginId.Split(Char.Parse(" "))[0].ToLower();
var sName = loginId.Split(Char.Parse(" "))[1].ToLower();
if (sName.Trim().Length == 2)
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 4) : fName.Substring(0, 3), sName.Substring(0, 2));
else
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 3) : fName.Substring(0, 2), sName.Substring(0, 3));
}
else
samAccountName = loginId.Substring(loginId.IndexOf(@"\") + 1);
var authPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName);
if (authPrincipal == null)
throw new Exception(string.Format("authPrincipal is null for loginId - {0}", loginId));
var firstLevelGroups = authPrincipal.GetGroups();
AddGroups(firstLevelGroups, ref result);
}
catch
{
if (tries > 5)
throw;
tries += 1;
System.Threading.Thread.Sleep(1000);
GetMemberGroups(loginId, principalContext, tries);
}
return result;
}
private void AddGroups(PrincipalSearchResult<Principal> principal, ref List<ADGroup> returnList)
{
foreach (var item in principal)
{
if (item.GetGroups().Count() > 0)
AddGroups(item.GetGroups(), ref returnList);
returnList.Add(new ADGroup(item.SamAccountName, item.Sid.Value));
}
}
This function is called like this:
MembershipGroups = ad.GetMemberGroups(user.SamAccountName, new PrincipalContext(ContextType.Domain));
The the error that I SOMETIMES get is:
System.AppDomainUnloadedException: Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014) 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_UserSuppliedServerName() at System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.BuildPathFromDN(String dn) at System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.MoveNextPrimaryGroupDN() at System.DirectoryServices.AccountManagement.ADDNLinkedAttrSet.MoveNext() at System.DirectoryServices.AccountManagement.FindResultEnumerator
1.MoveNext() at System.DirectoryServices.AccountManagement.FindResultEnumerator
1.System.Collections.IEnumerator.MoveNext()
looking though reflector at System.DirectoryServices.AccountManagement the internal class "UnsafeNativeMethods" is implemented in native code, so UserSuppliedServerName one level up is all I can go on without looking at the CLR VM, (frankly im not sure even how to do that) Seems that a node is failing to return its primary group, so perhaps consider other implementations, after a bit of googling ive come across these that may help
Active Directory and nested groups this one may be promising heres the code sample..
public IList<string> FindUserGroupsLdap(string username)
{
// setup credentials and connection
var credentials = new NetworkCredential("username", "password", "domain");
var ldapidentifier = new LdapDirectoryIdentifier("server", 389, true, false);
var ldapConn = new LdapConnection(ldapidentifier, credentials);
// retrieving the rootDomainNamingContext, this will make sure we query the absolute root
var getRootRequest = new SearchRequest(string.Empty, "objectClass=*", SearchScope.Base, "rootDomainNamingContext");
var rootResponse = (SearchResponse)ldapConn.SendRequest(getRootRequest);
var rootContext = rootResponse.Entries[0].Attributes["rootDomainNamingContext"][0].ToString();
// retrieve the user
string ldapFilter = string.Format("(&(objectCategory=person)(sAMAccountName={0}))", username);
var getUserRequest = new SearchRequest(rootContext, ldapFilter, SearchScope.Subtree, null);
var userResponse = (SearchResponse)ldapConn.SendRequest(getUserRequest);
// send a new request to retrieve the tokenGroups attribute, we can not do this with our previous request since
// tokenGroups needs SearchScope.Base (dont know why...)
var tokenRequest = new SearchRequest(userResponse.Entries[0].DistinguishedName, "(&(objectCategory=person))", SearchScope.Base, "tokenGroups");
var tokenResponse = (SearchResponse)ldapConn.SendRequest(tokenRequest);
var tokengroups = tokenResponse.Entries[0].Attributes["tokenGroups"].GetValues(typeof(byte[]));
// build query string this query will then look like (|(objectSid=sid)(objectSid=sid2)(objectSid=sid3))
// we need to convert the given bytes to a hexadecimal representation because thats the way they
// sit in ActiveDirectory
var sb = new StringBuilder();
sb.Append("(|");
for (int i = 0; i < tokengroups.Length; i++)
{
var arr = (byte[])tokengroups[i];
sb.AppendFormat("(objectSid={0})", BuildHexString(arr));
}
sb.Append(")");
// send the request with our build query. This will retrieve all groups with the given objectSid
var groupsRequest = new SearchRequest(rootContext, sb.ToString(), SearchScope.Subtree, "sAMAccountName");
var groupsResponse = (SearchResponse)ldapConn.SendRequest(groupsRequest);
// loop trough and get the sAMAccountName (normal, readable name)
var userMemberOfGroups = new List<string>();
foreach (SearchResultEntry entry in groupsResponse.Entries)
userMemberOfGroups.Add(entry.Attributes["sAMAccountName"][0].ToString());
return userMemberOfGroups;
}
private string BuildHexString(byte[] bytes)
{
var sb = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
sb.AppendFormat("\\{0}", bytes[i].ToString("X2"));
return sb.ToString();
}
These are more for info purposes
I don't know how PrincipalContext
is being passed in, here, but one thing I noticed in my own code and research when I had this error, I had:
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain);
UserPrincipal oUserPrincipal = UserPrincipal.FindByIdentity(oPrincipalContext , strUserName);
Where strUserName
was some user, i.e. DOMAIN\johndoe
I was calling that code (which was in a separate function) and returning the UserPrincipal
object as up
and passing it to:
using (PrincipalSearchResult<Principal> result = up.GetGroups())
{
// do something with result, here
}
result
wouldn't be null, but after I checked for that condition, I checked if result.Count() > 0
, and that's when it would fail (sometimes - though I could re-create the conditions when it would happen by clicking on a particular tab in my app that called this code - even though the same code was called onload of my app and had no issues). The Message
property in result
was Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014)
.
I found in a similar post to this one that all I had to do was specify the domain in my PrincipalContext
. Since I could not hard code mine in, as we move our code between Dev, Test, and Production environments where they have different domains for each of these, I was able to specify it as Environment.UserDomainName
:
PrincipalContext oPrincipalContext = new PrincipalContext(ContextType.Domain, Environment.UserDomainName);
This got rid of the error, for me.
This issue is the same as Determine if user is in AD group for .NET 4.0 application
It appears to be a bug in ADSI that was resolved with a hotfix. Windows 7 SP1 and Windows Server 2008 R2 SP1 don't include the fix, so it will need to be manually deployed on your development machines and server environments.
You could put in some logging to narrow down the problem. That Thread.Sleep
does not look like something one would want in a web application :)
If you are getting exceptions maybe you could handle them differently.
I reckon your AppDomain is being recycled while AD is doing its voodoo. Adding logging to the Application_End
could also provide some clues.
try
public List<ADGroup> GetMemberGroups(string loginId, PrincipalContext principalContext, int tries = 0)
{
var result = new List<ADGroup>();
bool Done = false;
try
{
var samAccountName = "";
if (loginId.Contains(" "))
{
var fName = loginId.Split(Char.Parse(" "))[0].ToLower();
var sName = loginId.Split(Char.Parse(" "))[1].ToLower();
if (sName.Trim().Length == 2)
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 4) : fName.Substring(0, 3), sName.Substring(0, 2));
else
samAccountName = string.Format("{0}{1}", fName.StartsWith(".") ? fName.Substring(0, 3) : fName.Substring(0, 2), sName.Substring(0, 3));
}
else
samAccountName = loginId.Substring(loginId.IndexOf(@"\") + 1);
var authPrincipal = UserPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, samAccountName);
if (authPrincipal == null)
throw new Exception(string.Format("authPrincipal is null for loginId - {0}", loginId));
var firstLevelGroups = authPrincipal.GetGroups();
AddGroups(firstLevelGroups, ref result);
Done = true;
}
catch
{
if (tries > 5)
throw;
tries += 1;
}
if ( ( !Done) && (tries < 6) )
{
System.Threading.Thread.Sleep(1000);
result = GetMemberGroups(loginId, principalContext, tries);
}
return result;
}
private void AddGroups(PrincipalSearchResult<Principal> principal, ref List<ADGroup> returnList)
{
if ( principal == null )
return;
foreach (var item in principal)
{
if (item.GetGroups().Count() > 0)
AddGroups(item.GetGroups(), ref returnList);
returnList.Add(new ADGroup(item.SamAccountName, item.Sid.Value));
}
}
When an exception happens you called the function again from the catch-block (depending on the value of tries) but discarded its return value - so even if the second/third... call worked you returned an empty result to the original caller. I changed that so the result won't be discarded anymore...
In the second function you never checked the principal param for null before starting the foreach... I changed that too...
And I removed the recursion from within the catch block catch (although I am really not sure whether this change has any real effect).
User contributions licensed under CC BY-SA 3.0