
.NET Core provides a powerful framework for building robust and scalable applications, but even experienced developers can fall into common anti-patterns that hinder maintainability and performance. Understanding these pitfalls and adopting best practices ensures clean, efficient, and future-proof code. In this article, we’ll explore some of the most notorious anti-patterns in .NET Core, why they are problematic, and how to fix them with better coding practices.
God Object / Big Ball of Mud — The All-in-One Mess
A God Object is a class that takes on too many responsibilities, attempting to manage multiple concerns within a single entity. This violates the Single Responsibility Principle (SRP) and results in tightly coupled code that is difficult to test, extend, and maintain. Over time, such classes grow into an unstructured, monolithic Big Ball of Mud, making debugging and refactoring increasingly complex. This anti-pattern leads to brittle codebases where even small changes can introduce unintended side effects.
❌Anti-Pattern Example
This code centralizes multiple responsibilities — validation, payment processing, customer notification, and inventory updates — into a single method.
public class OrderManager
{
public void ProcessOrder(Order order)
{
// Validate Order
// Charge Payment
// Notify Customer
// Update Inventory
}
}
❌Violates Single Responsibility Principle (SRP) — The method ProcessOrder
is responsible for too many things, making it hard to maintain and test. If one part changes (e.g., payment processing), you might have to modify this method, increasing the risk of breaking unrelated functionality.
❌Tightly Coupled Code — This method likely depends on multiple services (validation, payment, notification, and inventory), making it difficult to modify or replace individual parts.
❌Difficult to Extend — If you want to support new payment methods or change how notifications work, you’d have to modify this class instead of extending functionality through separate components.
❌Hard to Unit Test — Each unit test would have to cover all four responsibilities, leading to complex test cases with multiple dependencies.
✅Anti-Pattern Fix
Refactor responsibilities into smaller, well-defined services.
public class OrderService
{
private readonly IPaymentService _paymentService;
private readonly INotificationService _notificationService;
private readonly IInventoryService _inventoryService;
public OrderService(IPaymentService paymentService, INotificationService notificationService, IInventoryService inventoryService)
{
_paymentService = paymentService;
_notificationService = notificationService;
_inventoryService = inventoryService;
}
public void ProcessOrder(Order order)
{
_paymentService.Charge(order);
_notificationService.NotifyCustomer(order);
_inventoryService.UpdateStock(order);
}
}
✅Decouples Order Processing Responsibilities — Instead of OrderManager
handling multiple tasks, it now delegates them to separate services:
_paymentService.Charge(order)
→ Handles payment processing._notificationService.NotifyCustomer(order)
→ Handles customer notifications._inventoryService.UpdateStock(order)
→ Updates inventory after order processing.
This makes the code easier to maintain and modify without affecting unrelated functionality.
✅Improves Testability — Dependencies (IPaymentService
, INotificationService
, IInventoryService
) are injected through the constructor.
This allows for mocking these dependencies during unit testing, making it easier to test OrderService
in isolation.
✅Enhances Code Reusability — Each service is now reusable in other parts of the application (e.g., IPaymentService
could be used for subscriptions, refunds, etc.).
You can swap out implementations (e.g., different payment gateways) without modifying OrderService
.
Service Locator — Hidden Dependencies Trap
The Service Locator pattern is an anti-pattern that centralizes dependency resolution but obscures the true dependencies of a class. Instead of explicitly injecting dependencies via constructor or method parameters, objects retrieve them from a global service locator, making it unclear which dependencies a class actually requires. This leads to hidden dependencies, reduced testability, and tight coupling, as classes become reliant on the service locator rather than well-defined abstractions. Debugging and maintaining such code becomes challenging, as dependencies are resolved at runtime rather than being explicitly declared, making the system harder to refactor.
❌Anti-Pattern Example
This OrderProcessor
class retrieves an IPaymentService
instance using a Service Locator, then calls ProcessPayment()
. Instead of having IPaymentService
injected via the constructor, it fetches it dynamically at runtime using ServiceLocator.GetService<IPaymentService>()
.
public class OrderProcessor
{
public void Process()
{
var paymentService = ServiceLocator.GetService<IPaymentService>();
paymentService.ProcessPayment();
}
}
❌Hidden Dependencies — The OrderProcessor
class does not explicitly declare its dependencies. Anyone reading the class cannot immediately tell what services it relies on, making the code harder to understand and maintain.
❌Violates Dependency Injection (DI) Principles — Instead of injecting dependencies through the constructor, this approach pulls dependencies from a global locator, making it difficult to manage dependencies explicitly.
❌Harder to Unit Test — Since dependencies are resolved at runtime, mocking IPaymentService
in tests becomes cumbersome. You would need to configure the service locator within your test framework, adding unnecessary complexity.
❌Tight Coupling to Service Locator — The class is now dependent on the service locator itself, making it less flexible. If you ever decide to switch to constructor injection, every reference to ServiceLocator.GetService<T>()
must be refactored.
✅Anti-Pattern Fix
This OrderProcessor
class is responsible for processing an order by calling _paymentService.ProcessPayment()
. Instead of retrieving the dependency dynamically (as in the Service Locator anti-pattern), it receives an IPaymentService
instance through constructor injection, making dependencies explicit.
public class OrderProcessor
{
private readonly IPaymentService _paymentService;
public OrderProcessor(IPaymentService paymentService)
{
_paymentService = paymentService;
}
public void Process()
{
_paymentService.ProcessPayment();
}
}
✅Eliminates Hidden Dependencies — Unlike the Service Locator pattern, where dependencies are fetched at runtime, this approach makes dependencies explicit in the constructor. Anyone reading the class immediately understands that OrderProcessor
depends on IPaymentService
.
✅ Improves Testability — Since dependencies are injected, you can easily provide a mock IPaymentService
when unit testing.
var mockPaymentService = new Mock<IPaymentService>();
var orderProcessor = new OrderProcessor(mockPaymentService.Object);
This allows for isolated tests without needing to configure a global service locator.
✅ Reduces Tight Coupling — OrderProcessor
no longer depends on a static ServiceLocator
, making it more modular and adaptable to changes.
✅ Supports Dependency Injection (DI) Containers — This approach aligns with modern DI frameworks (like ASP.NET Core’s built-in DI), making it easier to manage dependencies at runtime.
Overusing Static Classes — The Inflexible Dependency Trap
Overusing static classes is an anti-pattern because it creates tight coupling and inflexible code. Since static classes are globally accessible, they hide dependencies, making it difficult to track where services are used. They are also non-mockable, complicating unit testing, and prevent easy modification or extension of behavior at runtime. As the system grows, the excessive use of static classes leads to a fragile codebase that is hard to refactor, maintain, or evolve, making changes riskier and more difficult to manage.
❌Anti-Pattern Example
This Logger
class is a static class with a single method, Log()
, that outputs a log message to the console. The method is globally accessible because it is static, and any part of the application can call Logger.Log(message)
to log messages.
public static class Logger
{
public static void Log(string message)
{
Console.WriteLine(message);
}
}
❌Global State and Tight Coupling — The static class Logger
creates a global point of access. This tight coupling makes it hard to replace or modify the logging mechanism without affecting the entire codebase. The logger is directly coupled to the console, limiting flexibility.
❌Hard to Test — Since the class is static, you cannot easily mock or replace it in unit tests. This makes it difficult to write unit tests for classes that depend on logging because you can’t easily verify or control the output.
❌Inflexibility — Static classes are non-extensible. If you later want to use a different logging strategy (e.g., writing to a file or a remote logging service), you cannot do so without refactoring all the code that uses Logger
.
❌Single Responsibility Violation — The Logger
class is responsible for handling logging, but because it is static and globally accessible, it encourages the violation of SRP by potentially being used in too many parts of the application.
✅Anti-Pattern Fix
This code defines an ILoggerService
interface with a Log()
method and a concrete implementation, LoggerService
, that writes log messages to the console. The LoggerService
class implements the ILoggerService
interface, providing the actual logging functionality.
public interface ILoggerService
{
void Log(string message);
}
public class LoggerService : ILoggerService
{
public void Log(string message) => Console.WriteLine(message);
}
✅Decoupling with Dependency Injection — By using an interface (ILoggerService
), the code decouples the logging functionality from the application logic. This allows for flexibility, as you can easily switch to a different logging implementation (e.g., file logging, remote logging, etc.) without modifying the dependent classes.
✅Testability — The ILoggerService
interface allows you to easily mock the logger in unit tests. For example, you can mock the Log()
method to verify that logging is happening correctly without actually writing to the console or file system.
✅Extensibility — The use of interfaces and implementation classes makes it easy to extend or change the logging behavior without altering existing code. You can now inject different logging implementations as needed, supporting open/closed principle (open for extension, closed for modification).
✅Single Responsibility Principle (SRP) — By separating the logging concern into its own class (LoggerService
), it adheres to SRP, ensuring that the LoggerService
is responsible only for logging, while other classes can focus on their own responsibilities.
Ignoring Asynchronous Programming Best Practices — The Performance Pitfall
Ignoring asynchronous programming best practices is an anti-pattern because it can lead to performance bottlenecks and poor scalability in an application. When asynchronous operations are mishandled — such as blocking on asynchronous code using .Result
or .Wait()
—it can cause thread pool starvation, delays in processing, and unresponsiveness, especially under heavy load. Additionally, improper error handling and not properly leveraging async
/await
can introduce subtle bugs and make code harder to debug. By ignoring these best practices, you risk making your application inefficient, difficult to maintain, and prone to scaling issues, ultimately compromising its overall performance and reliability.
❌Anti-Pattern Example
This code is attempting to fetch data from a remote API by calling _httpClient.GetStringAsync("https://api.example.com")
asynchronously, but it blocks the asynchronous operation by using .Result
. The Result
property blocks the calling thread until the asynchronous operation completes and returns the result.
public void GetData()
{
var result = _httpClient.GetStringAsync("https://api.example.com").Result;
}
❌Blocking on Asynchronous Code — Using .Result
to block an asynchronous call is a common anti-pattern. This turns an asynchronous operation into a synchronous one, defeating the purpose of async/await, which is to free up the thread to perform other tasks while waiting for the result. This can cause thread pool starvation, where threads become blocked and cannot be reused, leading to performance degradation.
❌Deadlock Risk — If this code is called on the UI thread or a context that has a synchronization context (like in ASP.NET or Windows Forms), it can lead to deadlocks. The Result
call waits for the asynchronous operation to complete, but that operation might be trying to post back to the UI thread (or the original calling context), which is already blocked.
❌Inefficient Resource Use — Blocking on asynchronous code reduces the benefits of asynchronous programming, which is designed to allow threads to remain available for other tasks while waiting for I/O-bound operations (like HTTP requests). This results in less efficient resource use, particularly in high-performance, scalable applications.
❌Harder to Maintain — Blocking asynchronous code makes it harder to reason about and can lead to subtle bugs, especially when combining synchronous and asynchronous logic. It’s difficult to maintain and debug because the code execution flow is not as intuitive as using async
/await
properly.
✅Anti-Pattern Fix
The correct way to handle asynchronous calls is to use async
and await
. This allows the thread to remain unblocked while waiting for the asynchronous task to complete, and it follows best practices for asynchronous programming.
public async Task GetDataAsync()
{
var result = await _httpClient.GetStringAsync("https://api.example.com");
}
✅Non-blocking Code — By using await
, the calling thread is free to perform other tasks while waiting for the GetStringAsync
method to complete, which is the intended purpose of asynchronous programming.
✅Avoids Deadlocks — The use of await
ensures that the calling thread doesn’t block, avoiding the potential for deadlocks, especially in environments with synchronization contexts, such as UI or ASP.NET applications.
✅Improved Performance — Since the thread is not blocked waiting for the HTTP request to finish, resources can be better utilized, leading to better overall performance and scalability.
✅Readability and Maintainability — Using async
and await
makes the asynchronous flow of control explicit, which is easier to understand and maintain, especially as the complexity of the application grows.
Swallowing Exceptions / Catch-All Blocks — The Silent Failure Trap
Swallowing exceptions or using catch-all blocks (e.g., catch (Exception ex) { }
) is an anti-pattern because it hides errors and prevents the application from reacting to unexpected issues properly. By catching all exceptions without logging or rethrowing them, the application silently ignores potential problems, making it difficult to diagnose issues during runtime. This practice also masks bugs, logic errors, or external failures that could otherwise be handled appropriately. It leads to poor maintainability and reduced reliability as developers are unable to track the root cause of failures, which can result in undetected problems escalating into larger issues over time.
❌Anti-Pattern Example
This code is attempting to execute some logic within the try
block. If any exception occurs during the execution, it is caught by the catch
block. However, the catch
block does nothing with the caught exception. It simply swallows the exception by leaving the catch
block empty, meaning no logging, rethrowing, or handling of the error is performed.
try
{
// Some logic
}
catch (Exception ex) { }
❌Swallowing Exceptions — By catching all exceptions and not handling them (e.g., logging or rethrowing), the code silently ignores potential errors, making it difficult to detect and debug issues. This can lead to hidden failures, where the program continues running without ever notifying the user or developers of critical issues.
❌Lack of Visibility — Since the exception is not logged or rethrown, no information about what went wrong is available. This makes troubleshooting almost impossible and leaves the application in an unpredictable state.
❌Risk of Masking Bugs — Catching and ignoring exceptions can hide bugs or logic errors in the application. If an exception is swallowed, the underlying problem isn’t addressed, and the code continues running with potentially faulty data or behavior.
❌Reduced Maintainability — Future developers (or even the original developer) may struggle to understand why exceptions were ignored, leading to poor code quality and increased difficulty in maintaining the application. Additionally, not handling the exception could lead to more serious issues later in the system’s execution.
✅Anti-Pattern Fix
This code is attempting to execute some logic within the try
block. If a SqlException
(a specific type of exception related to SQL Server) occurs, it is caught by the catch
block. In the catch
block, the exception is logged using _logger.LogError(ex, "Database error occurred")
, and then the exception is rethrown using throw
. This allows for proper handling of the exception while ensuring that it is not silently swallowed.
try
{
// Some logic
}
catch (SqlException ex)
{
_logger.LogError(ex, "Database error occurred");
throw;
}
✅Logs the Exception — By logging the exception (_logger.LogError(ex, "Database error occurred")
), the error is captured and documented. This provides valuable information for debugging, allowing developers to understand what went wrong and track the issue in logs.
✅Rethrows the Exception — After logging the exception, the exception is rethrown with throw;
. This ensures that the exception is not silently ignored but allows the higher layers of the application to handle it as necessary. This might include rolling back transactions, showing error messages to users, or triggering other actions to deal with the issue.
✅Specific Exception Handling — Instead of catching all exceptions (Exception
), this code catches a specific exception (SqlException
). Catching specific exceptions helps ensure that only relevant errors are handled by this block, while others can be allowed to propagate or be handled elsewhere. This is more robust and avoids masking other unrelated errors.
✅Improves Maintainability — By logging the exception and allowing it to propagate, this approach ensures that errors are not hidden, making the system more reliable and easier to maintain. Developers will be able to identify database issues quickly via logs, and the program won’t continue running in an invalid state.
By avoiding these common anti-patterns, .NET Core developers can build applications that are scalable, maintainable, and testable. Adhering to SOLID principles, using dependency injection properly, and following best practices ensure clean and efficient code. Keeping these pitfalls in mind will lead to more reliable and professional development practices.