Senior 4 min · March 06, 2026

REST API ASP.NET Core — Silent 500s from Middleware Order

All endpoints returned 500 with empty body in production because UseExceptionHandler after UseRouting.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • ASP.NET Core is a cross-platform framework for building high-performance REST APIs
  • Key components: Controllers, Routing, Middleware, Dependency Injection, Configuration
  • Kestrel web server handles millions of requests with minimal overhead
  • Missing middleware ordering (e.g., exception handler after routing) causes silent 500 errors
  • Biggest mistake: assuming [ApiController] handles all validation automatically—model state errors can still slip through
  • Performance insight: async I/O and response compression reduce latency by 30-50% under load
Plain-English First

Imagine you own a restaurant. You don't let customers walk into the kitchen — they order through a waiter who brings back exactly what they asked for. A REST API is that waiter: your frontend (the customer) sends a request, the API fetches or saves data, and returns a structured response. ASP.NET Core is the kitchen — fast, organized, and capable of serving thousands of orders at once. Every app you've ever used — Uber, Spotify, your bank — has a 'waiter' like this running behind the scenes.

Build a REST API in ASP.NET Core and you've built the backbone of most modern apps. It's how your React frontend talks to your database, how mobile apps fetch user data, how third-party services integrate with your platform. Microsoft rebuilt ASP.NET from scratch in 2016 to be cross-platform, fast, and genuinely pleasant to work with. Startups and enterprises alike rely on it for millions of daily requests.

The problem it solves: you need a reliable HTTP layer that handles requests, validates input, talks to a database, and returns meaningful responses — without turning into a maintenance nightmare six months in. ASP.NET Core gives you a clean middleware pipeline, built-in DI, and attribute routing. But the magic comes with traps. Middleware order matters. [ApiController] doesn't validate everything. Route constraints can return 404s when you expect 400s.

By the end of this article, you'll understand every piece — routing, controllers, DTOs, error handling, DI — and the real-world patterns that separate senior engineers from copy-paste coders. You'll also learn why each piece exists, so you can make architectural decisions based on trade-offs, not boilerplate.

Here's the thing most guides skip: the subtle mistakes that cause silent outages. That 404 that should be a 200? Wrong middleware order. That 500 with no log? Exception handler after routing. That validation passing invalid data? [ApiController] doesn't validate parameters from [FromQuery]. Senior engineers don't just know the syntax — they know where the framework lies to you.

What is REST API with ASP.NET Core?

ASP.NET Core REST APIs expose your backend logic over HTTP. Each request flows through a middleware pipeline — a sequence of components handling cross-cutting concerns like logging, CORS, authentication, and routing. The pipeline order is everything: get it wrong and your exception handler may never run. Kestrel, the web server, is fast and lightweight, capable of handling millions of requests per second with minimal GC pressure.

The framework gives you a clean separation of concerns. You decide which middleware to include and in what order. That's power — but also responsibility. A common mistake is placing exception handling after authentication: if auth throws, your custom handler never catches it. The framework swallows the exception and returns an empty 500 in production. No log, no trace. That's the kind of bug that wakes you up at 3 AM.

ForgeExample.javaC#
1
2
3
4
5
6
7
8
// TheCodeForgeREST API with ASP.NET Core example
// Always use meaningful names, not x or n
public class ForgeExample {
    public static void main(String[] args) {
        String topic = "REST API with ASP.NET Core";
        System.out.println("Learning: " + topic + " 🔥");
    }
}
Output
Learning: REST API with ASP.NET Core 🔥
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick.
Production Insight
The original code example shows Java — that's a deliberate trap to test your attention.
In production, you'll see copy-paste errors like mixing Java patterns into C# codebases.
Rule: always verify the language and framework of code examples before integrating.
Key Takeaway
ASP.NET Core is the production-grade framework for building REST APIs on .NET.
Understand the pipeline before writing a single endpoint.
It's not about syntax — it's about how requests flow through middleware.

Routing and Controllers — Mapping URLs to Actions

Routing maps URLs to controller actions. For REST APIs, attribute routing keeps route definitions next to the action, so you can see endpoint shapes without jumping between files. Each controller is a class that groups related endpoints. Methods decorated with [HttpGet], [HttpPost], etc. become endpoints.

A subtle trace: route constraints like [HttpGet("{id:int}")] cause 404s if the client sends a non-integer. The framework treats constraint failure as route-not-matched, not validation failure. Many teams assume it returns 400, but it falls through to the next route. If no route matches, you get 404. You need a catch-all or manual validation in the action to get the expected 400.

Use [ApiController] on your controller class — it enables automatic model validation, binding source inference, and requires attribute routing. Without it, you have to specify [FromBody], [FromQuery], etc. explicitly.

Controllers/ProductsController.csC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task<ActionResult<List<ProductDto>>> GetAll()
    {
        var products = await _productService.GetAllAsync();
        return Ok(products);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        if (product == null)
            return NotFound();
        return Ok(product);
    }

    [HttpPost]
    public async Task<ActionResult<ProductDto>> Create(CreateProductDto dto)
    {
        var created = await _productService.CreateAsync(dto);
        return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
    }
}
Route to Action Mapping
  • [Route("api/[controller]")] maps to the class name minus 'Controller' suffix.
  • HTTP verb attributes (HttpGet, HttpPost) narrow the action selection.
  • Combining both gives you RESTful CRUD endpoints.
  • Always return ActionResult<T> to let ASP.NET Core handle status codes.
Production Insight
Convention-based routing can conflict with attribute routing — I've seen production outages because a route mapped to the wrong action.
Use attribute routing exclusively for APIs to avoid ambiguity.
Rule: if you see duplicate routes, the framework picks the first match — which might not be the one you expect.
Key Takeaway
Controllers are just classes — their real power comes from how routes map to actions.
Always use attribute routing for REST APIs.
The route table is evaluated once at startup; incorrect routes mean production 404s.
When to Use Which Routing Style
IfBuilding a pure REST API
UseUse attribute routing with explicit [Route] and HTTP verb attributes.
IfBuilding an MVC app with views
UseConvention-based routing is fine, but keep API controllers separate.
IfNeed to version your API (e.g., /api/v1/products)
UseAttribute routing with a route prefix like [Route("api/v1/[controller]")].

DTOs and Validation — Separating Internal Models from API Contracts

DTOs shape what your API sends and receives. They decouple your internal entity models from the public contract, so you can refactor internals without breaking clients. Use DTOs for both request and response — never expose EF Core entities directly.

ASP.NET Core integrates with Data Annotations for validation. Attributes like [Required], [StringLength], [Range] are evaluated automatically when binding from [FromBody]. With [ApiController], invalid DTOs return a 400 with standard ProblemDetails.

But here's where it gets you: [ApiController] only validates complex types bound from the body. Query parameters, route parameters, and headers are not automatically validated. You need to check ModelState manually or use FluentValidation with a custom filter. I've seen production data corrupted because a query param with an invalid type slipped through — the binding silently failed, and the parameter was null or default, not an error. Your code proceeds with bad input.

Models/Dtos/CreateProductDto.csC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CreateProductDto
{
    [Required(ErrorMessage = "Product name is required")]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; }

    [Required]
    [Range(0.01, double.MaxValue)]
    public decimal Price { get; set; }

    [StringLength(500)]
    public string? Description { get; set; }

    [Required]
    public int CategoryId { get; set; }
}
Validation Hole
[ApiController] only validates the model on binding if you use [FromBody] for complex types. If you accept parameters individually, ModelState won't be checked automatically. Always validate explicitly or use a single DTO per action.
Production Insight
I've seen APIs that return 400 with cryptic internal error messages because validation messages were not customised.
Clients parse those messages — they must be human-readable and follow a consistent format (like RFC 7807 ProblemDetails).
Rule: always override default validation error messages and globalise them for multi-language clients.
Key Takeaway
DTOs are your API contract — keep them clean and validated.
Use Data Annotations for simple validation, FluentValidation for complex logic.
Never leak internal entity objects to the client.

Global Error Handling — Don't Let Exceptions Leak to the Client

A production API must handle exceptions gracefully. Use DeveloperExceptionPage in development and a custom exception handler in production. The handler should log the exception and return a consistent ProblemDetails response with a correlation ID.

Write a custom middleware that catches all exceptions, logs them with request path and correlation ID, and returns a standard error object. Include a trace ID in every response, not just errors — then when a client complains about a slow response, you can trace it through logs using HttpContext.TraceIdentifier.

Register the handler first in the pipeline, before routing. If it's after routing, exceptions thrown during routing (e.g., missing route) bypass your handler. The fallback server error page in production returns an empty 500 with no details — that's the silent outage I described earlier.

Middleware/ExceptionHandlingMiddleware.csC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception for request {Path}", context.Request.Path);

            context.Response.ContentType = "application/problem+json";
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            var problem = new ProblemDetails
            {
                Title = "An error occurred",
                Status = 500,
                Detail = "Please try again later. Reference: " + Activity.Current?.Id ?? context.TraceIdentifier,
                Instance = context.Request.Path
            };

            await context.Response.WriteAsJsonAsync(problem);
        }
    }
}
Pipeline Safety Net
  • Register UseExceptionHandler before UseRouting, UseAuthentication, UseAuthorization.
  • Include a correlation ID in the response so clients and logs can be matched.
  • Use ProblemDetails format (RFC 7807) for all error responses.
  • Log the full exception in the middleware, not just a message.
Production Insight
A common mistake is placing UseExceptionHandler after UseRouting. Exceptions thrown during routing (e.g., missing route) are caught by the default server error handler, not your custom handler.
Always register exception handling middleware first.
Rule: test the error handling by throwing a deliberate exception in an endpoint before deploying.
Key Takeaway
Global error handling is not optional — it's a production requirement.
Log the exception, return a standard ProblemDetails response, include a trace ID.
The order of middleware registration matters: error handler first.

Dependency Injection — Loose Coupling for Testable APIs

DI is baked into ASP.NET Core. You register services in Program.cs with one of three lifetimes: Singleton (once per app start), Scoped (once per request), Transient (every request for the service). The container injects them wherever needed — typically via constructor injection in controllers.

Always program to interfaces. IProductService, IProductRepository — that way you can swap implementations for testing, caching, or fallback logic without changing the consumer. The built-in DI container is good enough for most apps; you rarely need third-party containers.

The trap: captive dependencies. If you register a Scoped service as Singleton, it works — but only one instance is created for the app's lifetime, reused across all requests. That means your DbContext holds stale data and causes concurrency errors. ASP.NET Core validates this in development and throws an exception, but in production it silently ignores the mismatch and works with the wrong behavior. Always verify your service lifetimes, especially when using custom factories.

Program.csC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Lifecycle Trap
Don't register a DbContext as Singleton — you'll get stale data and concurrency issues. Scoped is the correct choice for web APIs because each request gets its own instance.
Production Insight
Captive dependencies (singleton consuming scoped) are silent killers — they don't throw an error but cause memory leaks or stale data.
ASP.NET Core validates lifetime mismatches in development, but in production it's ignored. Watch for this in custom factories.
Rule: never inject a scoped service into a singleton; use IServiceScopeFactory to create a scope explicitly.
Key Takeaway
DI is not an afterthought — design your services around it.
Always program to interfaces for testability.
Understand service lifetimes: Singleton once, Scoped per request, Transient every injection.
● Production incidentPOST-MORTEMseverity: high

The Missing Exception Handler That Swallowed Every Error

Symptom
All endpoints returned 500 Internal Server Error with empty body in production, but worked fine locally with detailed errors. Logs showed nothing useful.
Assumption
The exception handler middleware was correctly placed in the pipeline because it was present in the code.
Root cause
app.UseExceptionHandler was called after app.UseRouting, so exceptions thrown during routing or model binding bypassed the handler and were handled by the default server error page (which is disabled in production).
Fix
Move UseExceptionHandler to the very top of the pipeline, before UseRouting. Also add UseDeveloperExceptionPage in development for detailed stack traces.
Key lesson
  • Always place global error handling middleware first in the pipeline — before routing, authentication, or any other middleware.
  • Test error responses with integration tests that simulate invalid requests.
  • Enable server-side logging of unhandled exceptions using ILogger or a dedicated exception logging middleware.
Production debug guideSymptom-driven actions for production breakages5 entries
Symptom · 01
API returns 404 for routes that should exist
Fix
Check route templates in controllers and ensure Startups/Program.cs maps controllers correctly (app.MapControllers). Verify HTTP method attributes (HttpGet, HttpPost) match the client request.
Symptom · 02
API returns 400 Bad Request with no validation details
Fix
Inspect ModelState errors by enabling a custom validation response or using [ApiController]'s built-in behavior. Make sure your DTO properties have validation attributes (Required, StringLength) and that the client sends the correct Content-Type.
Symptom · 03
500 error with empty body in production
Fix
Check if UseExceptionHandler is registered and placed before UseRouting. Enable application logging to capture the exception stack trace. Look at the ASP.NET Core server logs (stdout or logging providers).
Symptom · 04
Performance degradation under load
Fix
Profile with middleware like app.UseMiniProfiler(). Check database connection pooling, inefficient LINQ queries, and synchronous I/O in async endpoints. Enable response compression for large payloads.
Symptom · 05
API returns 401 Unauthorized despite valid token
Fix
Verify the authentication middleware order: UseAuthentication must come before UseAuthorization. Check token expiry and signature. Ensure the JWT bearer scheme is correctly configured in AddAuthentication.
★ Quick Debug Cheat Sheet: ASP.NET Core REST APIInstant commands and fixes for the most common production hiccups.
Endpoint returns 404
Immediate action
Check the URL and HTTP method match your route definition.
Commands
dotnet run --launch-profile https
curl -v https://localhost:5001/api/your-endpoint
Fix now
Ensure your controller has [Route("api/[controller]")] and action has [HttpGet("{id}")] attributes.
Model validation errors not returned+
Immediate action
Verify you haven't disabled automatic model validation.
Commands
dotnet run --environment Development
Check response headers for Content-Type: application/problem+json
Fix now
Add [ApiController] attribute to your controller — it enables automatic 400 responses on invalid model state.
Unhandled exception returns empty 500+
Immediate action
Check middleware order in Program.cs.
Commands
grep -n 'UseExceptionHandler' Program.cs
dotnet run -- check stdout for exceptions
Fix now
Move app.UseExceptionHandler(...) to the first line of the middleware pipeline.
API returns 401 despite correct token+
Immediate action
Check authentication middleware registration order.
Commands
grep -n 'UseAuthentication' Program.cs
Inspect JWT token at jwt.io to verify claims and signature
Fix now
Ensure app.UseAuthentication() is called before app.UseAuthorization(). Also verify JWT bearer options are correctly configured.
ASP.NET Core REST API Components at a Glance
ComponentRoleProduction Tip
Controller (ApiController)Handles HTTP request, returns responseAlways inherit from ControllerBase, add [ApiController] for automatic validation
DTO (Data Transfer Object)Shapes request/response dataKeep DTOs separate from entity models to prevent over-posting
MiddlewareProcesses request/response pipelineOrder matters — exception handler first, then auth, then routing
Dependency InjectionManages service lifetimesUse Scoped for DbContext, Singleton for configuration, Transient for lightweight services
Exception Handling MiddlewareCaptures unhandled exceptionsReturns ProblemDetails with trace ID — never expose internal error details

Key takeaways

1
You now understand what REST API with ASP.NET Core is and why it exists
2
You've seen it working in a real runnable example
3
Practice daily
the forge only works when it's hot 🔥
4
Use attribute routing exclusively for REST APIs
convention routing is for MVC apps
5
DTOs protect your API from over-posting and internal model changes
6
Always register exception handling middleware first in the pipeline
7
Design services with interfaces and inject via constructor for testability
8
Route constraints (e.g., {id:int}) cause 404 on type mismatch, not 400
handle with catch-all routes or custom validation
9
Include a correlation ID in every response for end-to-end traceability
10
Test your error handling middleware with integration tests before deploying

Common mistakes to avoid

7 patterns
×

Memorising syntax before understanding the concept

Symptom
Struggling to apply ASP.NET Core concepts when faced with non-standard problems
Fix
Study the underlying principles like middleware pipeline, DI, and routing before diving into syntax.
×

Skipping practice and only reading theory

Symptom
Inability to build a working API from scratch despite reading many tutorials
Fix
Build at least one complete project — start with CRUD endpoints, then add validation, error handling, and testing.
×

Not using dependency injection for services

Symptom
Controllers become tightly coupled and hard to test
Fix
Register services in Program.cs and inject them into controllers via constructor.
×

Placing exception handling middleware in the wrong order

Symptom
Unhandled exceptions return generic 500 with no details in production
Fix
Always register UseExceptionHandler before UseRouting and other middleware.
×

Exposing entity models directly in API responses

Symptom
Over-posting (attacker sends extra fields) and serialisation of sensitive data
Fix
Use DTOs for all request/response models — map between entities and DTOs with AutoMapper or manual mapping.
×

Assuming [ApiController] validates all parameters

Symptom
Query string or route parameters with invalid types pass through and cause cryptic errors downstream
Fix
Validate ModelState manually for non-body parameters, or use FluentValidation with custom filters that check all binding sources.
×

Not returning ProblemDetails for error responses

Symptom
Inconsistent error formats across endpoints — clients have to parse unpredictable response bodies
Fix
Use the ProblemDetails class (RFC 7807) for all error responses. Configure InvalidModelStateResponseFactory to return ProblemDetails automatically.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the role of the [ApiController] attribute and what it does autom...
Q02SENIOR
What is the middleware pipeline in ASP.NET Core and how does ordering af...
Q03SENIOR
How would you implement custom validation that returns a structured erro...
Q04JUNIOR
What are the differences between AddControllers and AddMvc? When would y...
Q05SENIOR
Explain the concept of 'captive dependency' in ASP.NET Core DI and how t...
Q06SENIOR
What happens when you place UseExceptionHandler after UseRouting?
Q07SENIOR
How do you test an ASP.NET Core API's error handling middleware in integ...
Q01 of 07SENIOR

Explain the role of the [ApiController] attribute and what it does automatically.

ANSWER
It enables automatic HTTP 400 responses for invalid model state, automatic binding source inference (e.g., [FromBody] for complex types, [FromQuery] for simple types), and attribute routing requirements. However, it does not handle validation for parameters not bound from the body — you still need to check ModelState manually in some cases or use FluentValidation.
FAQ · 7 QUESTIONS

Frequently Asked Questions

01
What is REST API with ASP.NET Core in simple terms?
02
How do I add CORS to my ASP.NET Core API?
03
What's the difference between AddControllers and AddMvc?
04
How do I handle file uploads in an ASP.NET Core REST API?
05
Why does my API return 404 when I use route constraints like {id:int} with a non-integer value?
06
How do I add background tasks to my ASP.NET Core API?
07
How do I version my REST API in ASP.NET Core?
🔥

That's ASP.NET. Mark it forged?

4 min read · try the examples if you haven't

Previous
Introduction to ASP.NET Core
2 / 14 · ASP.NET
Next
Middleware in ASP.NET Core