Skip to content

Source Code — Services & DI

Everything registered in Startup.ConfigureServices, what lifetime each gets, and the four registration defects worth knowing before you add to it.

Startup.cs is the single composition root — there are no IServiceCollection extension methods per feature, and no external DI container. The one extension is services.AddScheduledJobs() (Infrastructure/AddScheduledJobsExtention.cs), which registers a single hosted service.

Lifetimes at a glance

71 explicit registrations — 63 transient, 3 scoped, 5 singleton.

Lifetime Count What
Transient 63 Every B* business class, every *Handler, and most helpers
Scoped 3 CurrentRequestData, PermissionChecker, StaffPermissionchecker
Singleton 5 IAmazonSimpleNotificationService, IUserIdProvider, and three localizer registrations (IStringLocalizer, IStringLocalizerFactory, and the open generic IStringLocalizer<>)
Hosted service 1 BirthdayGreetingsJob, via AddScheduledJobs()

The two DbContexts are registered with AddDbContext<T>, which is Scoped by default. That is the important interaction: a transient B* class resolved twice in one request receives the same DbContext instance, so change tracking is shared across them for that request.

Why the Scoped three are Scoped

CurrentRequestData must be the same object for the filter that fills it and the controller that reads it — transient would hand the controller an empty one. PermissionChecker reads it, so it must share the scope. StaffPermissionchecker has no functional reason to be Scoped: it holds only the DbContext and keeps no state between calls, so it would behave identically as transient.

Framework registrations

In Startup.ConfigureServices, in order:

Call Purpose
AddOptions() · Configure<RequestLocalizationOptions> Default culture en, dd.MM.yyyy
Configure<BaseAppSettings>(Configuration) Binds the whole configuration root
Configure<FormOptions> ValueCountLimit = int.MaxValue — unbounded form fields
AddDbContext<NKO_DBEntities1> HrConnection, console logger factory
AddDbContext<WarehouseDBContext> WareConnection, console logger factory
AddAuthentication(...).AddJwtBearer(...) Asks for JWT as the default scheme — overridden below
AddIdentity<ApplicationUser, IdentityRole> Password policy, EF stores, ApplicationUserManager, token providers
ConfigureApplicationCookie AccessDeniedPath = /Error/UnAuthorised
Configure<SecurityStampValidatorOptions> ValidationInterval = TimeSpan.Zero
AddHttpContextAccessor() Registered twice
AddControllersWithViews() Called three times
AddHangfire(...) · AddHangfireServer() SQL Server storage on HangfireConnection
AddSignalR() Hub mapped at /hub
AddLocalization + localizer singletons Resources/, SharedResource

AddIdentity silently overrides the JWT default scheme

AddAuthentication sets DefaultAuthenticateScheme, DefaultScheme and DefaultChallengeScheme to JwtBearerDefaults.AuthenticationScheme. AddIdentity(...), called immediately afterwards, registers its own AuthenticationOptions configuration setting them back to IdentityConstants.ApplicationScheme. Configure callbacks run in registration order, so Identity's cookie wins and the browser UI works.

The application therefore depends on the order of two lines. Swapping them would switch everything to bearer authentication and break every session. Tracked as R6-4.

The transient registrations

56 distinct types, all AddTransient, across 63 registrations — seven types are registered twice (see Registration defects). This is wider than the 44 business classes: it also covers helpers such as ExportHelper, Utils and EmailManager. Grouped as they appear in the file:

Shared / cross-cutting — EmailManager, NextUserHelper, BCalendar, PermissionsHandler, BSign, BCryptPasswordHasher, RealTime, RealTimeNotifier, BUser, ExportHelper, BNotification, BArchiveFile, SharedBAL, Bsettings, AttenadancesHandler, NKO.Helpers.Security, AttendanceConfigs, Utils

Core (NKO.Main/BAL/) — BAnnouncement, BBranch, BLeave, BDonor, BTraining, BProject, BPayroll, BTimesheet, BOvertime, BAdvance, BEvaluation, BCommon, BJobs, BQuestionAnswer, BNote, BGeneral, BPurchaseRequest, BRFQ, BPurchaseOrder, BGRN, BPaymentOrder, BBudgetLinesCategory, AllocationHandler, BudgetLinesHandler, BExchangeRate, BMasterBudget, BHumanResource, ProcBudgetLineHandler

Warehouse (NKO.Main/WareBAL/) — BWarehouse, BAsset, BStock, BReport, DestructionHandler, WarehouseEvidencesHandler, GRNHandler, BMaintenance

BProvider is registered next to these but lives in NKO.Main/BAL/ — it is procurement code, not warehouse code. S3ServiceHandler is registered too, from NKO.Shared/Configs/.

Interface-backed services

Five services are registered against a contract — four of the 44 business classes, plus S3ServiceHandler from Configs/. The rest are injected as concrete types, so a consumer cannot be given a substitute — which, combined with the absence of a test project (R8-4), means the business layer is not unit-testable without a database.

Interface Implementation Also registered concretely?
IS3ServiceHandler S3ServiceHandler no
IProcBudgetLineHandler ProcBudgetLineHandler yes
IAllocationHandler AllocationHandler yes
IBudgetLinesHandler BudgetLinesHandler yes
IAttendancesHandler AttenadancesHandler yes

Four of the five are registered both ways. Because both registrations are transient, a class injecting IAllocationHandler and a class injecting AllocationHandler in the same request get two different instances. Harmless while these types are stateless, but it defeats the point of the abstraction.

Registration defects

# Defect Detail
1 Duplicate registrations BLeave, BTraining and BCryptPasswordHasher are each registered twice; AddHttpContextAccessor() twice; AddControllersWithViews() three times (lines ≈ 232, 265, 277). Last-wins for most, so no behaviour change — but it hides intent.
2 A second DI container is built mid-registration services.BuildServiceProvider().GetService<IOptions<BaseAppSettings>>() constructs a throwaway provider to read settings, duplicating every singleton created during that build. This is exactly what analyser rule ASP0000 flags. Tracked as R1-5.
3 A singleton nobody resolves IAmazonSimpleNotificationService is registered with credentials and a region, and is injected nowhere. Because it is a factory registration, the client is never actually constructed — the cost is only the dead code and the credentials it would carry. Tracked as R1-4.
4 BCryptPasswordHasher is registered but never resolved ApplicationUserManager news it up directly. Both registrations are dead.

The settings read in defect 2 can be done without a provider:

// current
settings = services.BuildServiceProvider().GetService<IOptions<BaseAppSettings>>().Value;

// equivalent, no throwaway container
settings = Configuration.Get<BaseAppSettings>();

AWS and storage

var options = Configuration.GetAWSOptions();
options.Region      = RegionEndpoint.GetBySystemName(settings.StorageConfigs.Region);
options.Credentials = new BasicAWSCredentials(settings.StorageConfigs.ClientId,
                                              settings.StorageConfigs.ClientSecret);
services.AddDefaultAWSOptions(options);   // called twice — once before Credentials is set
services.AddAWSService<IAmazonS3>();

AddDefaultAWSOptions(options) is called twice around the credential assignment. Since options is a reference, the second call is what takes effect and credentials are present — but the first call is redundant and reads as if credentials were deliberately omitted.

Application parts

Two assemblies are added so NKO.Shared's controllers and view components are discovered, and its embedded views are served:

.PartManager.ApplicationParts.Add(new AssemblyPart(typeof(AccountController).Assembly));
services.AddControllersWithViews()
        .AddApplicationPart(typeof(TopbarViewComponent).Assembly)
        .AddRazorRuntimeCompilation();
services.Configure<MvcRazorRuntimeCompilationOptions>(o =>
    o.FileProviders.Add(new EmbeddedFileProvider(assembly2)));

This is why /Account/*, /Files/*, /Common/* and /Culture/* resolve even though those controllers live in a different project.

Adding a service — the house pattern

  1. Put the class in BAL/ (core), WareBAL/ (warehouse) or NKO.Shared/BAL/ (cross-cutting).
  2. Constructor-inject NKO_DBEntities1 (or WarehouseDBContext) plus collaborators.
  3. Add one services.AddTransient<BThing>(); next to its neighbours in Startup.cs.
  4. Inject the concrete type into the controller — do not add an interface unless you need a second implementation; the codebase does not use them.
  5. Check it is not already registered. Three classes already are, twice.