Skip to main content

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 processes AccountDeleteMessage 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.
When IDeleteAccountService.DeleteAccountAsync() throws an unhandled exception the entire HandleAsync method re-throws, which causes the Service Bus message to be abandoned and redelivered. This means any transient infrastructure failure (database timeout, etc.) will automatically retry — but a systematic bug in the delete path will cause the message to exhaust its delivery count and go to the dead-letter queue.
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.
The evaluator system is extensible purely through configuration. To add a new deletion policy for a new source, add a SourceConfiguration entry with the desired Evaluators list (any combination of the four EvaluatorKey values) and matching email templates. No code changes are required unless a brand-new evaluator type is needed.
EmptyFeatureFlagService throws NotImplementedException for every method except ArePatternSetTfmHeuristicsEnabled(), which returns false. If the DeleteAccountService code path ever calls other feature flag methods at runtime this will cause an unhandled exception. This is a known limitation noted in the source: the stub was added specifically to satisfy the DeleteAccountService DI graph without pulling in a live feature flag backend.
  • NSSM deployment: The Scripts/ directory contains PowerShell pre/post-deploy scripts and nssm.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 Scoped to align with the per-message DI lifetime scope created by ScopedMessageHandler<T> in the base framework. Telemetry and message handling infrastructure are registered as Transient.
  • Auditing in debug mode: In debug mode, AuditingService writes to the local filesystem under <BaseDirectory>/auditing/. In normal mode, no default AuditingService is registered; the code silently skips it unless add-in assemblies in the add-ins/ directory export an IAuditingService.
  • Add-in pattern: Job.GetAddInServices<T>() scans an add-ins/ subdirectory for MEF-exported services. This is used exclusively for IAuditingService today, 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.