• The .NET development community has fallen into a dangerous pattern of cargo cult programming. We’ve taken valuable tools—interfaces, unit tests, and dependency injection—and turned them into mandatory rituals, applied reflexively regardless of context or actual need. The result? Codebases that are bloated, performance-degraded, and paradoxically harder to understand and maintain than the “legacy” code we’re trying to improve.

    The Interface Explosion

    Walk into any modern .NET codebase and you’ll find interfaces everywhere. IUserService, IOrderRepository, IEmailSender—each with exactly one implementation. This has become so normalized that developers create interfaces automatically, often before they’ve even written the concrete implementation.

    The stated justification: “It makes the code more testable and follows SOLID principles.”

    The reality: Most of these interfaces will never have a second implementation. They exist solely to satisfy a dependency injection container and enable mocking in unit tests. The cost is immediate and tangible:

    • Cognitive overhead: Developers must now navigate two files instead of one for every service
    • IDE confusion: “Go to definition” lands on an empty interface instead of actual implementation
    • False abstractions: Interfaces that mirror their implementations 1:1 provide no real abstraction
    • Maintenance burden: Every method signature change requires updates in two places

    Consider this typical example:

    What value does this interface provide? If you need to swap out the user service implementation, you’ll likely need to change the method signatures anyway. The interface has become a ceremony, not a tool.

    The Unit Testing Obsession

    Unit testing has become a religious practice in .NET development, with coverage percentages treated as moral imperatives. Teams spend enormous effort mocking dependencies, setting up test scenarios, and maintaining test suites that often provide little real confidence in the application’s behavior.

    The stated justification: “Unit tests catch bugs early and enable refactoring.”

    The reality: Most unit tests are tightly coupled to implementation details, making them brittle and expensive to maintain. Consider this typical service test:

    This test requires intimate knowledge of the internal implementation. If you refactor the service to process payment and notification in parallel, or decide to update the order status before sending the notification, this test breaks—even though the external behavior is identical.

    The test setup is complex, brittle, and tells us nothing about whether orders actually get processed correctly. It’s testing the orchestration of method calls, not the business logic. When this test passes, you still don’t know if:

    • The payment actually processes correctly
    • The database transaction completes
    • The email notification reaches the customer
    • The order status is persisted correctly

    Meanwhile, integration tests that actually exercise the full stack and catch real bugs get dismissed as “too slow” or “too complicated.” The result is test suites with high coverage numbers but low bug-detection rates, and developers who spend more time maintaining test mocks than fixing actual issues.

    The Dependency Injection Maze

    Dependency injection has become the default approach for any object creation in .NET applications. Every service, repository, and helper class gets registered in the DI container, creating a web of dependencies that’s difficult to understand and debug.

    The stated justification: “It enables loose coupling and makes testing easier.”

    The reality: DI containers have become a black box where object creation happens magically, making it harder to understand what’s actually running. The performance cost is real:

    • Memory overhead: The container must track all registrations and their lifetimes
    • CPU overhead: Runtime resolution is slower than compile-time instantiation
    • Startup time: Complex dependency graphs increase application startup time
    • Debugging complexity: Stack traces become cluttered with container resolution code

    Consider a simple controller that now requires half a dozen injected dependencies:

    This constructor is a maintenance nightmare. Adding a new dependency requires touching multiple files, and understanding what this controller actually does requires tracing through multiple service layers.

    The Performance Tax

    Each of these patterns carries a performance cost that accumulates across the application:

    • Interface calls: Virtual method calls are slower than direct calls
    • DI resolution: Runtime object creation is slower than compile-time instantiation
    • Excessive abstraction: Multiple layers of services calling other services increase call stack depth
    • Memory allocation: More objects, more interfaces, more garbage collection pressure

    In a typical web application, these costs might seem negligible for individual requests, but they compound. A simple operation that could be a direct database call becomes a journey through multiple service layers, each with its own overhead.

    The Maintainability Paradox

    The cruelest irony is that these patterns, adopted in the name of maintainability, often make code harder to maintain:

    • Onboarding complexity: New developers must learn the DI container configuration, understand the interface hierarchy, and navigate the service layer maze
    • Debugging difficulty: Following code execution requires jumping between interfaces and implementations
    • Change resistance: Simple changes require updates to multiple interfaces, services, and tests
    • Analysis paralysis: Developers spend more time designing abstractions than solving business problems

    A More Pragmatic Approach

    This isn’t an argument against interfaces, testing, or dependency injection entirely—it’s a call for pragmatic application:

    Use interfaces when you actually need them:

    • When you genuinely have multiple implementations
    • When you’re defining a contract for external consumers
    • When you need to isolate external dependencies for testing

    Focus on integration tests:

    • Test the behavior users actually care about
    • Use unit tests sparingly for complex business logic
    • Don’t test implementation details

    Apply dependency injection judiciously:

    • Use it for cross-cutting concerns (logging, configuration)
    • Consider direct instantiation for simple object graphs
    • Don’t inject everything just because you can

    Embrace simplicity:

    • Static classes aren’t evil for stateless operations
    • Direct database access isn’t always wrong
    • Simple, straightforward code is often the best code

    Conclusion

    The .NET community has created a culture where complexity is mistaken for sophistication, and over-engineering is rewarded as “best practice.” We’ve lost sight of the primary goal: building software that works reliably and can be understood and modified by the developers who come after us.

    It’s time to step back and ask: Does this interface actually provide value? Does this test actually catch bugs? Does this abstraction actually make the code easier to change?

    More often than not, the answer is no. It’s time to build simpler, more maintainable .NET applications by using the right tool for the job—not every tool we have available.

  • This is neat, instead of doing it the long way, if you need to pull your data into a dictionary, you can do it like this:

     

    Dictionary<string, int> facilityNpis = db.Facilities.Where(c => c.NPI.Trim().Length > 0).Select(a => new { a.NPI, a.ID }).AsEnumerable().ToDictionary(b => b.NPI, b => b.ID);

  • This came up a while back and had me a bit flustered for a bit.  You’re getting some data that you don’t have a ton of control over that you’re binding right into a ddl in WPF and you want to sort it because well, sorting is important.

    It isn’t too late!  You can do the following in your constructor and it’ll look fine.

    ddlAssignTo.Items.SortDescriptions.Add(new SortDescription(“Username”, ListSortDirection.Ascending));

  • Normally I’d leave this sort of thing to a DB to deal with but I stumbled on this a few months back and thought it was rather clever.

    USE MyDbName
    GO
    EXEC sp_MSforeachtable @command1=”print ‘?’”, @command2=”ALTER INDEX ALL ON ? REBUILD WITH (ONLINE=OFF)”
    GO
    SELECT * FROM sys.Dm_db_index_physical_stats(Db_id(‘MyDbName’),NULL,NULL,NULL,NULL) — This bit just tells you the results

  • It comes up from time to time where you have a DB you need to work with and you notice that some bastard didn’t put a PK on it.

    Heres a quick way to wedge that in without too much fuss.

    ALTER TABLE Accounts ADD ID int identity(1,1) not null
    GO
    ALTER TABLE Accounts ADD CONSTRAINT pk_Accounts_ID primary key(ID)
    GO

Design a site like this with WordPress.com
Get started