Overview
AccountDeleter is a .NET Framework 4.7.2 console application that runs as a long-lived background job (deployed as a Windows service via NSSM). It subscribes to an Azure Service Bus topic and processesAccountDeleteMessage messages, each of which carries a username and a named source (e.g., a self-service portal or an admin tool). For each message the job looks up the user in the Gallery database, runs a configurable set of eligibility evaluators to determine whether the account can be deleted automatically, and then either deletes it or sends a “cannot be automatically deleted” notification email to the user.
The job is built on the internal SubscriptionProcessorJob<T> base class, which handles the Service Bus subscription lifecycle — starting concurrent message processors, running for a configured duration, and gracefully shutting down. AccountDeleteMessageHandler is the single IMessageHandler<AccountDeleteMessage> implementation that contains the core orchestration logic. A debug mode (--Debug CLI argument) substitutes no-op implementations of the delete service, email service, and user service so that the entire flow can be exercised locally without touching a real database or sending live emails.
The key design decision is that deletion policy is fully data-driven: each named source in configuration carries its own list of EvaluatorKey values. The UserEvaluatorFactory resolves those keys to concrete IUserEvaluator instances and wraps them in an AggregateEvaluator that applies AND logic across all evaluators. This means new sources or policies can be added through configuration changes alone, without code changes.
Role in System
Source-Driven Policy
Every message carries a source name. Configuration maps each source to a set of evaluators, success/failure email templates, and a flag controlling whether a success email is sent at all.
Evaluator Pipeline
Eligibility is determined by an AND-chained aggregate of
IUserEvaluator implementations. Built-in evaluators check account confirmation status, package ownership, and organization admin membership.Email Notifications
After deletion (or rejection), the handler sends a templated email to the user with the Gallery owner CC’d. The
{username} placeholder in templates is replaced at send time by DisposableEmailBuilder.Debug Mode
Passing
--Debug on the command line swaps in EmptyDeleteAccountService, EmptyUserService, and DebugMessageService, which log actions but make no external calls. Audit logs write to the local filesystem instead of Azure Storage.Key Files and Classes
Dependencies
Internal Project References
Key Transitive Project References (via Validation.Common.Job and NuGetGallery.Services)
Key NuGet Package References (transitive)
Notable Patterns and Implementation Details
The
AccountDeleteMessageHandler treats UserNotFoundException as a successful completion (returns true). This is intentional: if a message is redelivered after a partial failure the user may already be gone, and re-queuing the message would cause infinite retries. Only UnknownSourceException causes the message to be marked as failed (false return), triggering redelivery.The
RespectEmailContactSetting flag in AccountDeleteConfiguration controls whether the handler skips sending a notification email to users who have opted out of contact. When true and the user’s EmailAllowed is false, deletion still proceeds but no email is sent. A telemetry event (EmailBlocked) is emitted in this case.- NSSM deployment: The
Scripts/directory contains PowerShell pre/post-deploy scripts andnssm.exe. The job is installed as a Windows service using NSSM (Non-Sucking Service Manager), which handles restarts and logging. - Scoped vs. transient DI: Gallery services (database contexts, repositories, evaluators) are registered as
Scopedto align with the per-message DI lifetime scope created byScopedMessageHandler<T>in the base framework. Telemetry and message handling infrastructure are registered asTransient. - Auditing in debug mode: In debug mode,
AuditingServicewrites to the local filesystem under<BaseDirectory>/auditing/. In normal mode, no defaultAuditingServiceis registered; the code silently skips it unless add-in assemblies in theadd-ins/directory export anIAuditingService. - Add-in pattern:
Job.GetAddInServices<T>()scans anadd-ins/subdirectory for MEF-exported services. This is used exclusively forIAuditingServicetoday, allowing audit backends to be deployed as drop-in assemblies without recompiling the job. - Orphan package policy: Accounts are always deleted with
AccountDeletionOrphanPackagePolicy.UnlistOrphans. If the deleted user was the sole owner of any package, those packages are automatically unlisted rather than deleted or transferred.