12 Critical Web API Anti-Patterns Silently Draining Your .NET Apps
Discover the most common anti-patterns in .NET Web APIs that erode performance, scalability, and maintainability. Learn how to identify and fix these issues before they cause costly production outages.
Author
AmethiSoft AI TeamPublished
March 19, 2026Read Time
14 min read
Introduction: The Hidden Killers of Your .NET Web APIs
In my 12 years of experience reviewing countless codebases, a recurring theme emerges: many .NET applications suffer from common Web API anti-patterns that silently degrade performance, compromise security, and complicate maintenance. Most teams remain unaware of these lurking issues until a critical production incident forces an investigation. By then, the cost of remediation is often significantly higher.
This article aims to shed light on 12 prevalent Web API anti-patterns in the .NET ecosystem. Weโll explore why each one is problematic and, more importantly, provide practical, actionable solutions to fortify your applications against these silent killers. Understanding and addressing these patterns is not just about avoiding failure; itโs about building more robust, scalable, and maintainable systems from the ground up.

The 12 Web API Anti-Patterns and Their Fixes
Letโs dive into the list, understanding the โwhat not to doโ and the โhow to do it right.โ
1. Fat Controllers
โ The Problem: Business logic, validation, and data access concerns are all crammed into controller actions. This leads to bloated, untestable, and difficult-to-maintain controllers that violate the Single Responsibility Principle.
โ The Fix: Decouple responsibilities. Move business logic into dedicated service layers, command/query handlers (e.g., using MediatR), or application services. Controllers should primarily be responsible for handling HTTP requests, delegating work, and returning appropriate HTTP responses.
Example: Before Refactoring (Fat Controller)
// Controllers/ProductsController.cs - BEFORE
public class ProductsController : ControllerBase
{
private readonly ApplicationDbContext _context;
public ProductsController(ApplicationDbContext context)
{
_context = context;
}
[HttpPost("create")]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest request)
{
// 1. Input Validation (should be in a validator)
if (string.IsNullOrWhiteSpace(request.Name) || request.Price <= 0)
{
return BadRequest("Invalid product data.");
}
// 2. Business Logic (should be in a service/handler)
var product = new Product
{
Id = Guid.NewGuid(),
Name = request.Name,
Price = request.Price,
CreatedDate = DateTime.UtcNow
};
// 3. Data Access (should be in a repository/service)
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProductById), new { id = product.Id }, product);
}
}
Example: After Refactoring (Lean Controller with Service Layer)
Here, the controller delegates the core logic to a ProductService.
// Services/ProductService.cs
public class ProductService
{
private readonly ApplicationDbContext _context;
public ProductService(ApplicationDbContext context)
{
_context = context;
}
public async Task<ProductDto> CreateProductAsync(CreateProductCommand command)
{
// Business logic and data access
var product = new Product
{
Id = Guid.NewGuid(),
Name = command.Name,
Price = command.Price,
CreatedDate = DateTime.UtcNow
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return new ProductDto { Id = product.Id, Name = product.Name, Price = product.Price };
}
}
// Controllers/ProductsController.cs - AFTER
public class ProductsController : ControllerBase
{
private readonly ProductService _productService; // Injected service
public ProductsController(ProductService productService)
{
_productService = productService;
}
[HttpPost("create")]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest request)
{
// Map request to a command/DTO if necessary
var command = new CreateProductCommand(request.Name, request.Price);
var productDto = await _productService.CreateProductAsync(command);
return CreatedAtAction(nameof(GetProductById), new { id = productDto.Id }, productDto);
}
}
2. No Input Validation
โ The Problem: Accepting raw user input without any checks. This opens your application to security vulnerabilities (e.g., SQL injection, XSS), crashes due to invalid data types, and inconsistent data states.
โ
The Fix: Validate every request. Utilize robust validation frameworks like FluentValidation, or leverage DataAnnotations with ModelState.IsValid for basic checks. Perform both client-side and server-side validation.
// Models/CreateProductRequest.cs
public class CreateProductRequest
{
[Required(ErrorMessage = "Product name is required.")]
[StringLength(100, MinimumLength = 3, ErrorMessage = "Name must be between 3 and 100 characters.")]
public string Name { get; set; }
[Range(0.01, 10000.00, ErrorMessage = "Price must be between 0.01 and 10000.00.")]
public decimal Price { get; set; }
}
// Controllers/ProductsController.cs (Validation via DataAnnotations)
public class ProductsController : ControllerBase
{
[HttpPost("create")]
public IActionResult CreateProduct([FromBody] CreateProductRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState); // Returns detailed validation errors
}
// ... proceed with valid request
return Ok();
}
}
3. Returning Raw Exceptions
โ The Problem: Exposing raw stack traces and internal error messages to API clients. This is a security risk, leaking sensitive system information, and provides a poor developer experience for API consumers.
โ
The Fix: Implement global exception handling. Convert unhandled exceptions into standardized, client-friendly error responses, often using the ProblemDetails specification (RFC 7807) for rich, consistent error reporting.
// Example: Basic Exception Handling Middleware (Program.cs / Startup.cs)
// UseExceptionHandler can be configured directly in Program.cs
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/problem+json";
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
var problemDetails = new ProblemDetails
{
Status = (int)HttpStatusCode.InternalServerError,
Type = "https://datatracker.ietf.org/doc/html/rfc7807", // Generic type
Title = "An error occurred while processing your request.",
Detail = exception?.Message, // For development, include message. For production, generalize.
Instance = context.Request.Path
};
// Log the full exception for internal monitoring
// logger.LogError(exception, "Unhandled exception occurred: {Message}", exception.Message);
await context.Response.WriteAsJsonAsync(problemDetails);
});
});
4. Blocking Async with .Result or .Wait()
โ The Problem: Calling .Result or .Wait() on asynchronous operations in an async method. This blocks the calling thread, leads to thread pool starvation, deadlocks (especially in UI contexts or ASP.NET Core with SynchronizationContext), and severely degrades scalability under load.
โ
The Fix: Use async/await consistently โall the way downโ the call stack. Avoid mixing synchronous blocking calls with asynchronous operations.
// โ Anti-Pattern: Blocking async
public IActionResult GetProductBlocking(Guid id)
{
var product = _productService.GetProductAsync(id).Result; // BLOCKS!
return Ok(product);
}
// โ
Correct Pattern: Async all the way
public async Task<IActionResult> GetProductAsync(Guid id)
{
var product = await _productService.GetProductAsync(id); // AWAITS!
return Ok(product);
}
5. Ignoring CancellationTokens
โ The Problem: Not passing and respecting CancellationTokens throughout asynchronous operations. This wastes server resources by continuing to process requests that the client has already abandoned, especially common in long-running operations.
โ
The Fix: Accept CancellationToken as a parameter in all async endpoint methods, services, and data access layers. Pass it down to any underlying asynchronous operations (e.g., HttpClient.GetAsync, DbContext.SaveChangesAsync).
// Controllers/ProductsController.cs
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> GetProductById(Guid id, CancellationToken cancellationToken)
{
// Pass cancellationToken down to the service
var product = await _productService.GetProductAsync(id, cancellationToken);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
}
// Services/ProductService.cs
public class ProductService
{
private readonly ApplicationDbContext _context;
public ProductService(ApplicationDbContext context)
{
_context = context;
}
public async Task<ProductDto> GetProductAsync(Guid id, CancellationToken cancellationToken)
{
// Pass cancellationToken to EF Core operations
var product = await _context.Products.FindAsync(new object[] { id }, cancellationToken);
// ... map to DTO ...
return new ProductDto { /* ... */ };
}
}
6. No Pagination
โ The Problem: Returning entire database tables or large collections in a single response. This consumes excessive memory and bandwidth, leading to slow response times, client-side performance issues, and potential denial-of-service attacks.
โ
The Fix: Implement pagination, filtering, and sorting on every collection endpoint. Use parameters like pageNumber, pageSize, sortBy, and filter to allow clients to request manageable subsets of data.
// Example: Basic Pagination
public class PagedList<T> : List<T>
{
public int CurrentPage { get; private set; }
public int TotalPages { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public PagedList(List<T> items, int count, int pageNumber, int pageSize)
{
TotalCount = count;
PageSize = pageSize;
CurrentPage = pageNumber;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
AddRange(items);
}
public static async Task<PagedList<T>> ToPagedListAsync(IQueryable<T> source, int pageNumber, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToListAsync();
return new PagedList<T>(items, count, pageNumber, pageSize);
}
}
// Controllers/ProductsController.cs
public class ProductsController : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetProducts(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string searchTerm = null)
{
IQueryable<Product> query = _context.Products;
if (!string.IsNullOrWhiteSpace(searchTerm))
{
query = query.Where(p => p.Name.Contains(searchTerm));
}
var pagedProducts = await PagedList<Product>.ToPagedListAsync(query, pageNumber, pageSize);
// Add pagination metadata to response headers or a wrapper object
Response.Headers.Add("X-Pagination",
JsonSerializer.Serialize(new { pagedProducts.CurrentPage, pagedProducts.PageSize, pagedProducts.TotalCount, pagedProducts.TotalPages }));
return Ok(pagedProducts);
}
}
7. Wrong HTTP Status Codes
โ The Problem: Consistently returning 200 OK for everything, even errors or non-success states. This misleads API clients, complicates error handling, and violates REST principles.
โ The Fix: Use proper HTTP status codes to accurately convey the outcome of an API request.
200 OK: General success.201 Created: Resource successfully created (e.g., POST).204 No Content: Successful request, but no content to return (e.g., DELETE).400 Bad Request: Client-side input validation failure.401 Unauthorized: Authentication required or failed.403 Forbidden: Authenticated, but lacks necessary permissions.404 Not Found: Resource does not exist.409 Conflict: Request conflicts with the current state of the resource.422 Unprocessable Entity: Semantic validation failure (e.g., business rule violation).500 Internal Server Error: Unhandled server-side error.
8. Over-fetching Data
โ The Problem: Querying all columns and joins when you only need a few fields for a specific API response. This wastes database resources, network bandwidth, and serialization time.
โ
The Fix: Use projections (Select()) with Entity Framework Core or other ORMs to fetch only the data genuinely required by the API client. Map directly to a lighter DTO.
// โ Anti-Pattern: Over-fetching
// var product = await _context.Products.Include(p => p.Category).FirstOrDefaultAsync(p => p.Id == id);
// return Ok(new ProductDto { Id = product.Id, Name = product.Name }); // Still fetches Category even if not used
// โ
Correct Pattern: Using projection
[HttpGet("{id}")]
public async Task<IActionResult> GetProductNameAndPrice(Guid id)
{
var productDto = await _context.Products
.Where(p => p.Id == id)
.Select(p => new ProductDto { Id = p.Id, Name = p.Name, Price = p.Price }) // Project to DTO
.FirstOrDefaultAsync();
if (productDto == null)
{
return NotFound();
}
return Ok(productDto);
}
9. Returning EF Entities as API Responses
โ The Problem: Directly exposing your database models (EF Core entities) to API clients. This couples your API contract to your database schema, potentially exposing sensitive fields, creating tight coupling, and making future schema changes difficult without breaking clients.
โ The Fix: Always map to dedicated Data Transfer Objects (DTOs) or response models. This allows you to control the serialization, hide internal details, and evolve your API contract independently of your persistence layer. Libraries like AutoMapper can streamline this process.
// Models/Product.cs (EF Core Entity)
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public DateTime CreatedDate { get; set; }
public string InternalSKU { get; set; } // Sensitive/internal field
}
// DTOs/ProductDto.cs (API Response Model)
public class ProductDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// Controllers/ProductsController.cs
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> GetProductById(Guid id)
{
var product = await _context.Products.FindAsync(id);
if (product == null) return NotFound();
// โ
Map to DTO before returning
var productDto = new ProductDto
{
Id = product.Id,
Name = product.Name,
Price = product.Price
};
return Ok(productDto);
}
}
10. No Rate Limiting
โ The Problem: Leaving your API wide open to abuse, excessive requests, and potential Denial-of-Service (DDoS) attacks. This can degrade performance for legitimate users and incur significant infrastructure costs.
โ The Fix: Implement rate limiting. ASP.NET Core offers built-in Rate Limiting middleware that can be configured globally, per endpoint, or per consumer (e.g., by IP address or API key).
// Program.cs - Configure Rate Limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", policy =>
{
policy.PermitLimit = 5; // 5 requests
policy.Window = TimeSpan.FromSeconds(10); // every 10 seconds
policy.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
policy.QueueLimit = 0; // No queueing for this example
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter(); // Apply globally
// In your controller or endpoint
[EnableRateLimiting("fixed")]
[HttpGet("limited")]
public IActionResult GetLimitedData()
{
return Ok("This endpoint is rate-limited.");
}
11. No Observability
โ The Problem: Zero visibility into what is happening inside your API. Without proper logging, tracing, and metrics, diagnosing issues in production becomes a โneedle in a haystackโ problem, leading to longer downtimes and frustrated teams.
โ The Fix: Implement robust observability.
- Structured Logging: Use Serilog or NLog to output structured logs that are easy to query and analyze.
- Distributed Tracing: Employ OpenTelemetry to trace requests across multiple services, providing full context for transactions.
- Metrics: Collect key performance indicators (e.g., request duration, error rates) using OpenTelemetry metrics or Prometheus.
12. No Idempotency on Mutating Endpoints
โ The Problem: Allowing retries on POST operations (or other mutating operations) to create duplicate records or unwanted side effects. If a client retries a POST due to a network glitch, it might unintentionally create the same resource multiple times.
โ
The Fix: Implement idempotency keys on mutating endpoints, especially POST. A client sends a unique, client-generated key with the request. The server then checks if a request with that key has already been processed. If so, it returns the original successful response without re-executing the operation.
// Example: Idempotency Middleware (Conceptual)
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Method == HttpMethods.Post)
{
var idempotencyKey = context.Request.Headers["X-Idempotency-Key"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(idempotencyKey))
{
// 1. Check if key exists in cache/database
// If exists, retrieve stored response and return it
if (_idempotencyStore.TryGetStoredResponse(idempotencyKey, out var storedResponse))
{
context.Response.StatusCode = storedResponse.StatusCode;
await context.Response.WriteAsync(storedResponse.Body);
return;
}
// 2. Process request
await next(context);
// 3. Store response with idempotency key
_idempotencyStore.StoreResponse(idempotencyKey, context.Response);
return;
}
}
await next(context);
}
Real-World Application and Business Value
Addressing these anti-patterns is not merely a matter of academic best practices; it directly translates into tangible business value and improved developer experience:
For Developers:
- Easier Maintenance: Clean, modular code is simpler to understand, debug, and extend.
- Reduced Bugs: Robust validation and proper error handling minimize defects and edge-case failures.
- Improved Collaboration: Standardized patterns make it easier for teams to work on the same codebase.
- Faster Development: Well-structured systems allow for quicker feature implementation and refactoring.
- Better Testability: Decoupled components are inherently easier to unit test, leading to higher code quality.
For Businesses:
- Enhanced Performance & Scalability: Efficient resource usage means applications can handle more users with less infrastructure, reducing operational costs.
- Increased Reliability & Uptime: Fewer production issues, quicker recovery from errors, and graceful degradation improve user satisfaction and trust.
- Stronger Security Posture: Input validation and proper error handling mitigate common attack vectors.
- Reduced Technical Debt: Proactively addressing anti-patterns prevents accumulating costly debt that hinders future innovation.
- Improved User Experience: Fast, responsive, and reliable APIs lead to happier customers and better engagement.
Future Outlook and Best Practices
The landscape of .NET development is constantly evolving. Staying ahead means:
- Continuous Learning: Keep up with the latest ASP.NET Core features, performance improvements, and security recommendations.
- Automated Code Reviews: Integrate static analysis tools (e.g., SonarQube, Roslyn analyzers) into your CI/CD pipeline to catch anti-patterns early.
- Prioritize Observability: Invest in robust logging, tracing, and monitoring from day one. Itโs not an afterthought.
- Embrace Async/Await Fully: Ensure your entire stack is non-blocking to maximize throughput.
- Adopt API Design Principles: Think of your API as a product. Design it for consumer usability, consistency, and future extensibility.
- Regular Refactoring: Schedule dedicated time for refactoring to continuously improve code quality and address emerging anti-patterns.
By consciously avoiding these anti-patterns and adopting the recommended fixes, youโre not just writing better code; youโre building a foundation for sustainable, high-performing .NET applications that can truly thrive under pressure.
Disclaimer: This blog post was generated with the assistance of AI to provide recent technical insights. While we strive for accuracy, please verify critical technical details before using them in production or for legal decisions.
AmethiSoft AI Team
Insights Team at AmethiSoft
AI Assistance Notice
This article was prepared with the assistance of Artificial Intelligence to provide timely and comprehensive technical insights. While our team reviews all content for relevance and accuracy, we recommend verifying critical technical details for your specific production environment. AmethiSoft is committed to transparency in AI usage.