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 | Current, Permission, Staff |
| Singleton | 5 | IAmazon, IUser, and three localizer registrations (IString, IString, and the open generic IString) |
| Hosted service | 1 | Birthday, via Add |
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 |
|---|---|
Add · Configure< |
Default culture en, dd. |
Configure< |
Binds the whole configuration root |
Configure< |
Value — unbounded form fields |
Add |
Hr, console logger factory |
Add |
Ware, console logger factory |
Add |
Asks for JWT as the default scheme — overridden below |
Add |
Password policy, EF stores, Application, token providers |
Configure |
Access |
Configure< |
Validation |
Add |
Registered twice |
Add |
Called three times |
Add · Add |
SQL Server storage on Hangfire |
Add |
Hub mapped at / |
Add + localizer singletons |
Resources/, Shared |
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? |
|---|---|---|
IS3 |
S3 |
no |
IProc |
Proc |
yes |
IAllocation |
Allocation |
yes |
IBudget |
Budget |
yes |
IAttendances |
Attenadances |
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 BCrypt are each registered twice; Add twice; Add 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. 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 | IAmazon 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 | BCrypt is registered but never resolved |
Application 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¶
- Put the class in
BAL/(core),WareBAL/(warehouse) orNKO.Shared/BAL/(cross-cutting). - Constructor-inject
NKO_DBEntities1(orWarehouseDBContext) plus collaborators. - Add one
services.AddTransient<BThing>();next to its neighbours inStartup.cs. - Inject the concrete type into the controller — do not add an interface unless you need a second implementation; the codebase does not use them.
- Check it is not already registered. Three classes already are, twice.
Related¶
- Core Classes · Background Jobs
- Architecture — Request Pipeline · Technology Stack
- Recommendations — R1-4, R1-5, R6-4, R8-4.