6.7 KiB
Disco AD Compare Plugin - Session: Initial Build & Iteration
Date: 2026-04-28 Project: disco-ad-compare-plugin Status: ongoing Tags: disco-ict, plugin, active-directory, c-sharp, managed-by, uiextension
Context
Built a Disco ICT plugin from scratch that compares the AD computer managedBy attribute against the Disco AssignedUserId for devices, and reports mismatches. The plugin was developed iteratively through many build-fix cycles due to constraints of the target environment (no Visual Studio, only .NET Framework 4.0 MSBuild, Disco installed at C:\Program Files\Disco\WebApp\bin).
Key Decisions
- C# 5 only — The server has only .NET Framework MSBuild v4.0.30319 (no VS installed). No string interpolation (
$""), no null-conditional (?.), no auto-property initializers, no expression-bodied members. All code usesstring.Format(), explicit null checks, constructors for list init. - ToolsVersion 4.0 / TargetFramework v4.5 — Downgraded from 15.0/4.7.2 to avoid MSB3644 errors. The warnings about architecture mismatch (MSIL vs AMD64) are harmless.
- Manual manifest.json — No ManifestGenerator.exe available in the install. Build script creates manifest manually with correct field names matching
PluginFeatureManifest.cs:Id,Name,TypeName,PrimaryFeature,CategoryTypeName. - CategoryTypeName for UIExtension — Must be the generic type definition:
Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature``1(backtick-1 for one generic param). Confirmed by comparing with working plugins (CasperSuite, PasswordManager, DocFill). - DN-to-DN comparison — AD
managedByreturns a Distinguished Name, notDOMAIN\username. Must look up the Disco assigned user in AD to get their DN, then compare DN-to-DN (case-insensitive). Initial version comparedDOMAIN\usernameagainst DN which always mismatched. - Filter invalid devices — Skip devices without
DOMAIN\ComputerNameformat (no backslash) to avoid "The Id must include the Domain" errors. Also exclude devices not found in AD from the results table (counted separately). - Server-side rendering for web handler — jQuery/AJAX approach failed because the standalone plugin page doesn't load Disco's layout/scripts. Rewrote to render full HTML server-side with inline CSS. The
/Runaction generates the complete page. - Plugin URL routing — Disco route is
Plugin/{PluginId}/{PluginAction}where PluginAction is required. URL must be/Plugin/Disco.Plugins.ADCompare/Runnot/Plugin/Disco.Plugins.ADCompare. - Build as .zip then rename —
Compress-Archiveonly supports.zipextension. Build script creates.zipthen renames to.discoPlugin. - HintPath regex replacement — Single regex
<HintPath>[^<]+\\([^\\<]+\.dll)</HintPath>replaces ALL assembly references to point atDiscoBinPath, handling Disco, Newtonsoft, MVC, WebPages, Razor, and EntityFramework in one pass.
Technical Details
Build Command
.\Build-Plugin.ps1 -DiscoBinPath "C:\Program Files\Disco\WebApp\bin"
Disco Plugin Paths
- Installed plugins:
C:\Disco\Plugins\<PluginId>\ - Plugin packages:
C:\Disco\PluginPackages\ - Plugin storage:
C:\Disco\PluginStorage\ - Disco binaries:
C:\Program Files\Disco\WebApp\bin\ - Disco logs: CSV format, viewable in Disco web UI under Configuration
Key Disco API Patterns
ActiveDirectory.RetrieveADMachineAccount(domainId, additionalProperties)— returns AD computer, null if not foundActiveDirectory.RetrieveADUserAccount(userId)— looks up byDOMAIN\usernameadAccount.GetPropertyValue<T>("attributeName")— reads AD attributes likemanagedBy,lastLogonTimestampUIExtensionFeature<DeviceShowModel>— inject HTML on device pagesUIExtensionFeature<UserShowModel>— inject HTML on user pagesPluginWebHandler.ExecuteAction(ActionName)— web handler routingPluginConfigurationHandlerwithWebViewPage— required for plugin system
Assembly References Required
System, System.Core, System.Data, System.Data.Entity, System.Web, System.ComponentModel.DataAnnotations, EntityFramework, System.Web.Mvc, System.Web.WebPages, System.Web.Razor, Newtonsoft.Json, Disco.Models, Disco.Data, Disco.Services
Manifest Feature Format (exact JSON shape)
{
"Id": "ADCompareDeviceUI",
"Name": "Device Page - AD Compare",
"TypeName": "Disco.Plugins.ADCompare.Features.DeviceUIExtension",
"PrimaryFeature": false,
"CategoryTypeName": "Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature`1"
}
Code Changes
ADComparePlugin.cs— Main plugin class, empty Initialize (features self-register via manifest)Features/DeviceCompareService.cs— Core comparison: DN-to-DN matching, filters invalid/missing devicesFeatures/DeviceUIExtension.cs— Device page panel: managedBy comparison, last login, AD statusFeatures/UserUIExtension.cs— User page panel: last login, AD status, OU location, 30-day inactivity warningModels/DeviceComparisonResult.cs— Result + summary models with SkippedDevices counterWebHandler/ADCompareWebHandler.cs— Server-side rendered dashboard at/Run, CSV export at/ExportConfigurationHandler/ADCompareConfigurationHandler.cs— Required config handler with WebViewPageProperties/AssemblyInfo.cs— Version 1.3.1.0, author Jess RogersonBuild-Plugin.ps1— Build + package script, regex HintPath resolution, manual manifest generationDisco.Plugins.ADCompare.csproj— ToolsVersion 4.0, .NET 4.5, all references.gitignore— Standard .NET ignoresREADME.md— Full documentation
Open Questions / Next Steps
- UIExtension panels not rendering — Features initialize without errors in Disco logs (
Initialized Plugin Feature: 'DeviceUIExtension'), manifest format matches working plugins exactly, but HTML panels don't appear on device/user pages. TheRegister()call inInitialize()override runs. Need to investigate: doesLiteral()work? DoesNothing()suppress? Should we check how CasperSuite'sCasperSuiteUIExt_DeviceShow.ExecuteActionreturns its result vs our approach? May need to examine the Disco Views (.cshtml) that render UIExtension slots to understand where the HTML is injected. - Comparison dashboard works perfectly —
/Plugin/Disco.Plugins.ADCompare/Runrenders correctly with summary stats, mismatch table, and CSV export. - Credit line — Plugin should show "Created by: Jess Rogerson (yelling commands at Claude.AI)" in a footer or about section.
Artifacts
- Gitea repo: https://gitea.hideawaygaming.com.au/jessikitty/disco-ad-compare-plugin
- Source Disco repo (forked): https://gitea.hideawaygaming.com.au/jessikitty/Disco (branch: master)