The Dependency Graveyard
AutoMapper referenced and never mapped, a ghost OpenXml import, a full-framework MVC5 reference in a Core app, Hangfire packages with no jobs, unconfigured Serilog sinks, and an unregistered middleware - what unused packages cost even when nothing calls them.
Open the attendance platform's package references and about a third of them have no callers. Not “few” — none. AutoMapper with no maps, an Open XML SDK with no documents, a full-framework MVC5 reference in an ASP.NET Core app, five Hangfire packages with zero jobs, Serilog sinks that are never configured, and a middleware class that is never registered. Across the previous thirteen parts I have pointed at these graves individually as they came up; this part visits them together, because a codebase's unused dependencies say as much about its history as its used ones — and they are not free just because nothing calls them.
The headstones
AutoMapper — referenced, zero maps. The package is present. There is no Profile, no CreateMap, no injected IMapper, nowhere. Every DTO projection in the app is hand-written .Select(e => new Dto { ... }). There is one artefact: a fully commented-out mapping class that would have mapped an HR-system JSON object to an employee entity. So the story is legible — someone intended to adopt AutoMapper, wrote the first profile, commented it out (probably while debugging a mapping that misbehaved), hand-wrote the projection instead, and left the package. The reference is a monument to an intention. Hand-written projections are, for the record, a perfectly good choice — often better than AutoMapper for explicitness — but then remove the package, because a reference with no calls tells the next reader a lie about how mapping works here.
The OpenXml ghost. From part nine: DocumentFormat.OpenXml referenced, used for exactly one thing — a stray using DocumentFormat.OpenXml.Bibliography; at the top of an unrelated controller, referencing not a single type. An evaluation that lost to EPPlus, leaving its import behind. The using compiled because unused imports always do, and so a heavyweight package rode every deploy for a namespace directive nobody needed.
MVC5 in a Core app. The Azure helper project pulled in Microsoft.AspNet.Mvc 5.2.7 — the full-framework ASP.NET MVC, the one that predates ASP.NET Core entirely. Nothing used it. It is almost certainly a transitive artefact of the EF6 CodeFirstStoreFunctions experiment from part seven, or a copy-paste of a reference block from the old MVC5 codebase this app was ported from. Whatever the origin, it is the single most confusing line in the dependency list — a reviewer sees “MVC5” in a Core app and assumes something profound and wrong is happening, when the truth is duller: nobody removed it.
Hangfire — five packages, zero jobs. The app referenced the full Hangfire stack — core, the SQL Server storage, the ASP.NET Core integration — and the database carried a complete Hangfire schema, eleven tables' worth. There is no AddHangfire, no UseHangfireDashboard, no job class, no dashboard-auth filter. Not one line of Hangfire code in the web solution. This is the estate's most interesting absence, and the companion series opens on it — The Database Is the Message Bus — because the platform's background work happens in Azure Functions and a console job instead, coordinated through tables. The Hangfire schema is the fossil of a third scheduler that was planned, provisioned in the database, and never wired up. An absence is a finding: the schema is real, the code was never written, and describing “the scheduler that never arrived” honestly means saying exactly that.
Serilog sinks that go nowhere. Serilog was configured twice — once from configuration, once with an explicit WriteTo.MSSqlServer — and referenced sinks for Elasticsearch and Console that were never actually configured with a target. Add the App Insights telemetry, the DIY StatusSheet job-log table, and an ActionLog audit table, and the platform had four parallel logging systems, two of them half-wired — no single answer to “where do I look when something breaks”. For the graveyard, the note is narrower: package references for sinks you never point anywhere are latency and attack surface with no upside.
The unregistered middleware. A LoggingExceptionMiddleware class existed, fully written, implementing the exception-handling middleware shape — and was never added to the pipeline. The app used the built-in UseExceptionHandler instead. So a hand-rolled duplicate of a framework feature sat in the codebase, compiled, tested by nobody, invoked by nothing, quietly implying that error handling worked differently than it did. Dead code that looks live is worse than dead code that looks dead: a reader trying to understand error handling finds this class, reads it carefully, and reasons about a code path that never executes.
What a grave costs
The reflexive defence is “an unused package doesn't run, so it can't hurt”. That is wrong in five distinct ways, and naming them is the point of this whole part.
- Supply-chain surface. Every referenced package is code you ship and a dependency you must patch when it has a CVE. An Elasticsearch sink you never configured still gets flagged by every scanner, still demands a version bump on every advisory, still sits in your SBOM. You carry the maintenance cost of software you derive no benefit from.
- Cognitive cost. MVC5 in a Core app, AutoMapper with no maps, an unregistered exception middleware — each one makes the next reader stop, investigate, and build a wrong mental model before discovering the truth is “nobody removed it”. Every false reference is a small tax on comprehension, paid by everyone who reads the code after you.
- The transitive drag. Unused packages pull their own dependencies. The MVC5 reference dragged full-framework assemblies into a Core app's resolution graph; the EF6 library dragged an entire alternate ORM. Bloat compounds — you carry not just the grave, but everything the grave leaned on.
- False signal. A dependency list is documentation: it tells a newcomer what technologies the app uses. When a third of it is fiction, the document lies. “Does this app use Hangfire?” has a five-package answer that is no.
- The lock-step upgrade trap. Dead references still constrain your framework and package upgrades — you cannot move to a runtime the EF6 library forbids, even though nothing uses the EF6 library, until you remove it. Graves vote in every upgrade decision.
Reading the cemetery
There is a reward for touring the dead: the graveyard is the most honest history the codebase keeps. Used dependencies tell you what the app does; unused ones tell you what it tried. AutoMapper's commented-out profile is the memory of an abandoned mapping strategy. The OpenXml ghost is an export engine that lost a bake-off. The Hangfire schema is a scheduler that was designed and never built. MVC5 is the scar of a port from full-framework. Each grave is a decision reversed, a plan shelved, an experiment concluded — and none of it made it into a commit message or an ADR, because deleting-and-documenting is exactly the discipline a team under deadline skips first.
The remediation is the least glamorous work in software and among the highest-leverage: delete the reference, delete the ghost import, delete the unregistered class, and — if the knowledge is worth keeping — write the one-line tombstone that the code was standing in for. “We evaluated OpenXml and chose EPPlus.” “Background work moved to Functions; Hangfire schema is vestigial.” Git keeps the code; the repository should keep the decision. A dependency you removed and documented teaches; a dependency you left in place merely misleads.
The ledger
- AutoMapper, OpenXml, MVC5 — remove them; each implies a technology the app does not use.
- Hangfire — the fossil of a planned third scheduler; the schema is real, the code never existed. Name the absence.
- Four logging systems — two half-wired; consolidate, and drop the sink packages that point nowhere.
- The unregistered middleware — delete; dead code shaped like live code is the most misleading kind.
- The habit — delete-and-document, not delete-quietly and never delete. The tombstone is the deliverable.
Fourteen parts of anatomy come down to one question: what did a hand-rolled intranet app, built under deadline and shipped for years, actually teach me? The honest retrospective — what earned its keep, what I would never do again, and what employee-monitoring data demands that we never gave it — is the final part.