diff --git a/Features/ADCompareService.cs b/Features/ADCompareService.cs
new file mode 100644
index 0000000..e86804a
--- /dev/null
+++ b/Features/ADCompareService.cs
@@ -0,0 +1,119 @@
+using Disco.Data.Repository;
+using Disco.Models.Repository;
+using Disco.Plugins.ADCompare.Models;
+using Disco.Services.Interop.ActiveDirectory;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Disco.Plugins.ADCompare.Features
+{
+ public class ADCompareService
+ {
+ private readonly DiscoDataContext database;
+
+ public ADCompareService(DiscoDataContext database)
+ {
+ this.database = database;
+ }
+
+ ///
+ /// Compare all Disco users against their Active Directory accounts.
+ /// Returns a summary with per-user comparison results.
+ ///
+ public ComparisonSummary CompareAllUsers()
+ {
+ var summary = new ComparisonSummary();
+
+ // Load all users from Disco database
+ var discoUsers = database.Users.ToList();
+ summary.TotalDiscoUsers = discoUsers.Count;
+
+ foreach (var discoUser in discoUsers)
+ {
+ var result = CompareUser(discoUser);
+ summary.Results.Add(result);
+ }
+
+ summary.UsersCompared = summary.Results.Count(r => r.UserFoundInAD);
+ summary.UsersNotFoundInAD = summary.Results.Count(r => !r.UserFoundInAD);
+ summary.UsersWithMismatches = summary.Results.Count(r => r.HasMismatches);
+ summary.UsersInSync = summary.Results.Count(r => r.UserFoundInAD && !r.HasMismatches && !r.ADAccountDisabled);
+ summary.ADAccountsDisabled = summary.Results.Count(r => r.ADAccountDisabled);
+
+ return summary;
+ }
+
+ ///
+ /// Compare a single Disco user against their AD account.
+ ///
+ public UserComparisonResult CompareUser(User discoUser)
+ {
+ var result = new UserComparisonResult
+ {
+ UserId = discoUser.UserId,
+ DisplayName = discoUser.DisplayName
+ };
+
+ try
+ {
+ // Look up the user in AD using their UserId (DOMAIN\username format)
+ var adUser = ActiveDirectory.RetrieveADUserAccount(discoUser.UserId);
+
+ if (adUser == null)
+ {
+ result.UserFoundInAD = false;
+ return result;
+ }
+
+ result.UserFoundInAD = true;
+ result.ADAccountDisabled = adUser.IsDisabled;
+
+ // Compare core fields
+ CompareField(result, "Display Name", discoUser.DisplayName, adUser.DisplayName);
+ CompareField(result, "Surname", discoUser.Surname, adUser.Surname);
+ CompareField(result, "Given Name", discoUser.GivenName, adUser.GivenName);
+ CompareField(result, "Email Address", discoUser.EmailAddress, adUser.Email);
+ CompareField(result, "Phone Number", discoUser.PhoneNumber, adUser.Phone);
+ }
+ catch (Exception ex)
+ {
+ // If we can't look up the user in AD, mark as not found
+ result.UserFoundInAD = false;
+ result.Mismatches.Add(new FieldMismatch(
+ "AD Lookup Error",
+ discoUser.UserId,
+ ex.Message
+ ));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Compare a single field between Disco and AD values.
+ /// Treats null and empty string as equivalent.
+ ///
+ private void CompareField(UserComparisonResult result, string fieldName, string discoValue, string adValue)
+ {
+ var normalisedDisco = NormaliseValue(discoValue);
+ var normalisedAD = NormaliseValue(adValue);
+
+ if (!string.Equals(normalisedDisco, normalisedAD, StringComparison.OrdinalIgnoreCase))
+ {
+ result.Mismatches.Add(new FieldMismatch(fieldName, discoValue ?? "(empty)", adValue ?? "(empty)"));
+ }
+ }
+
+ ///
+ /// Normalise a value for comparison - treat null and whitespace as empty.
+ ///
+ private string NormaliseValue(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return string.Empty;
+
+ return value.Trim();
+ }
+ }
+}