Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

From Generic Errors to Informative Errors: Debugging ASP.NET Active Directory Integration


This is a overview on how proper exception handling transformed my debugging process from guesswork to targeted problem-solving with useful errors in the log.

The Problem: Generic Error Messages

When building an ASP.NET web application for Active Directory password resets, I initially encountered the most frustrating type of error - the generic, unhelpful kind that tells you something went wrong but gives no clue about what or why.

My original error handling looked like this:

try
{
    // Reset the password using the application pool identity
    WriteToLog(string.Format("Attempting to reset password for user '{0}'", 
    userPrincipalName));
    if (ResetADPassword(userPrincipalName, newPwd))
    {
        WriteToLog(string.Format("Password reset SUCCESSFUL for user '{0}'", 
    userPrincipalName));
        ShowSuccess("Password successfully reset.");
    }
    else
    {
        WriteToLog(string.Format("Password reset FAILED for user '{0}' - 
        Unable to reset password", userPrincipalName));
        ShowError("Failed to reset password. Please contact IT support.");
    }
}
catch (Exception ex)
{
    WriteToLog(string.Format("ERROR during password reset for user '{0}': {1}", 
    userPrincipalName, ex.Message));
    ShowError("Error: " + ex.Message);
}

The logs would show messages like:

ERROR during password reset for user 'TestUser42': Exception has been thrown by the 
target of an invocation.

This told me absolutely nothing useful. The error was happening somewhere in the ADSI calls, but I had no idea what the actual underlying problem was.

The Core Issue: Wrapper Exceptions

The problem was that .NET's DirectoryEntry.Invoke() method wraps the actual Active Directory errors in a TargetInvocationException. The real error - the one that would actually help me debug the issue - was buried in the InnerException property.

My original password reset method looked like this:

private bool ResetADPassword(string userPrincipalName, string newPassword)
{
    try
    {
        // ... user lookup code ...
        
        using (DirectoryEntry userEntry = result.GetDirectoryEntry())
        {
            // This is where the generic exception was thrown
            userEntry.Invoke("SetPassword", new object[] { newPassword });
            userEntry.CommitChanges();
            return true;
        }
    }
    catch (Exception ex)
    {
        WriteToLog(string.Format("LDAP password reset failed: {0}", ex.Message));
        throw new Exception("Error resetting password: " + ex.Message);
    }
}

When the SetPassword call failed, I would get:

  • What I saw: "Exception has been thrown by the target of an invocation"
  • What I needed: "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"

The Solution: Unwrapping Exception Layers

I completely rewrote the exception handling to dig deeper into the exception hierarchy and extract the meaningful error information:

private bool ResetADPassword(string userPrincipalName, string newPassword)
{
    try
    {
        // Extract username from UPN if it contains @
        string username = userPrincipalName;
        if (userPrincipalName.Contains("@"))
        {
            username = userPrincipalName.Split('@')[0];
        }
        
        // ... authentication context setup ...
        
        using (DirectoryEntry directoryEntry = new DirectoryEntry(ldapPath))
        {
            using (DirectorySearcher searcher = new DirectorySearcher(directoryEntry))
            {
                searcher.Filter = string.Format("(&(objectCategory=person)
                (objectClass=user)(sAMAccountName={0}))", username);
                searcher.SearchScope = SearchScope.Subtree;
                
                SearchResult result = searcher.FindOne();
                if (result != null)
                {
                    using (DirectoryEntry userEntry = result.GetDirectoryEntry())
                    {
                        try
                        {
                            // The actual password reset operation
                            userEntry.Invoke("SetPassword", new object[] { newPassword });
                            userEntry.CommitChanges();
                            WriteToLog(string.Format("Password reset successful for user:
                             {0}", username));
                            return true;
                        }
                        catch (System.Reflection.TargetInvocationException ex)
                        {
                            // This is the key change - unwrap the real exception
                            string innerMessage = ex.InnerException != null ? 
                            ex.InnerException.Message : ex.Message;
                            WriteToLog(string.Format("SetPassword failed for user {0}: {1}",
                            username, innerMessage));
                            
                            // Categorize errors based on the actual underlying issue
                            if (innerMessage.Contains("password does not meet") || 
                                innerMessage.Contains("password history") ||
                                innerMessage.Contains("recently used"))
                            {
                                WriteToLog(string.Format("Password policy violation for user
                                {0}: {1}", username, innerMessage));
                                throw new Exception("Password violates policy: " + 
                                innerMessage);
                            }
                            else if (innerMessage.Contains("access is denied") || 
                                     innerMessage.Contains("insufficient rights"))
                            {
                                WriteToLog(string.Format("Access denied setting password 
                                for user {0}: {1}", username, innerMessage));
                                throw new Exception("Access denied - insufficient 
                                permissions: " + innerMessage);
                            }
                            else
                            {
                                WriteToLog(string.Format("Unexpected error setting 
                                password for user {0}: {1}", username, innerMessage));
                                throw new Exception("Password reset failed: " + 
                                innerMessage);
                            }
                        }
                        catch (Exception ex)
                        {
                            WriteToLog(string.Format("General error setting password for 
                            user {0}: {1}", username, ex.Message));
                            throw new Exception("Password reset failed: " + ex.Message);
                        }
                    }
                }
                else
                {
                    WriteToLog(string.Format("User not found: {0}", username));
                    throw new Exception("User not found: " + username);
                }
            }
        }
    }
    catch (Exception ex)
    {
        // Prevent double-wrapping of exceptions
        if (ex.Message.StartsWith("Error resetting password:"))
        {
            throw;
        }
        else
        {
            throw new Exception("Error resetting password: " + ex.Message);
        }
    }
}

Transformation in Error Logs

After implementing the improved exception handling, my log messages transformed from useless to actionable:

Error before update:

2025-06-13 07:28:25 - LDAP password reset failed: Exception has been thrown by the target 
of an invocation.
2025-06-13 07:28:25 - ERROR during password reset for user 'TestUser42': Error resetting 
password: Exception has been thrown by the target of an invocation.

Error after update:

2025-06-13 07:34:20 - SetPassword failed for user TestUser42: Access is denied. 
(Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
2025-06-13 07:34:20 - Access denied setting password for user TestUser42: Access is denied. 
(Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
2025-06-13 07:34:20 - ERROR during password reset for user 'TestUser42': 
Error resetting password: Access denied - insufficient permissions: 
Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))

Error Categorization

The improved exception handling also allowed me to provide better feedback to end users by categorizing the types of errors:

// In the main exception handler
catch (Exception ex)
{
    WriteToLog(string.Format("ERROR during password reset for user '{0}': {1}", 
    userPrincipalName, ex.Message));
    WriteToLog(string.Format("Stack trace: {0}", ex.StackTrace));
    
    // Provide user-friendly messages based on error type
    if (ex.Message.Contains("Password violates policy"))
    {
        ShowError("Your new password doesn't meet the security requirements. 
        Please try a different password.");
    }
    else if (ex.Message.Contains("Access denied"))
    {
        ShowError("Unable to reset password due to security restrictions. 
        Please contact IT support.");
    }
    else if (ex.Message.Contains("User not found"))
    {
        ShowError("User account not found. Please check your username and try again.");
    }
    else
    {
        ShowError("An unexpected error occurred. Please contact IT support. Error: " + 
        ex.Message);
    }
}

HRESULT Preservation

The new approach preserves the Windows error codes (like 0x80070005) that are crucial for diagnosing Active Directory issues.

Stack Trace Logging

For debugging purposes, I ensured stack traces were always logged:

WriteToLog(string.Format("Stack trace: {0}", ex.StackTrace))

Conclusions

The transformation from generic to specific error handling turned hours of guesswork into minutes of targeted troubleshooting. When I saw Access is denied. (Exception from HRESULT: 0x80070005), I immediately knew to investigate Active Directory permissions rather than wasting time checking password policies, network connectivity, or application logic.

This approach not only solved my immediate debugging challenge but also made the application much more maintainable and supportable for future issues.

Previous Post Next Post

نموذج الاتصال