Querying Active Directory from MVC result in: Attempted to access an unloaded appdomain. (Exception from HRESULT: 0x80131014)
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.Starts开发者_如何学JAVAWith(".") ? 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
- How to use the PrimaryGroupID attribute to find the primary group for a user
- Determining User Group Membership in Active Directory and ADAM
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.
http://support.microsoft.com/kb/2683913
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).
精彩评论