VOOZH about

URL: https://dev.to/keizrivas/standardize-your-aspnet-core-api-with-one-line-of-code-2822

⇱ Standardize Your ASP.NET Core API with One Line of Code - DEV Community


Consistent routing and responses across your API — without attributes, base classes, or boilerplate.


The Problem: Inconsistent APIs

You start with a clean ASP.NET Core project.

A few sprints later:

GET /api/UserProfile/{userId}
POST /api/V1/Products/Create
GET /api/userOrders/GetRecent
  • Controllers use different casing styles
  • New endpoints introduce their own conventions
  • Error responses vary depending on who implemented them

Now your API is inconsistent — and your frontend team has to guess how each endpoint behaves.

Sounds familiar?
This isn’t a framework problem.
It’s a convention problem.


The One-Line Fix

Instead of enforcing conventions manually, your API follows them automatically using AspNetConventions:

dotnet add package AspNetConventions
builder.Services.AddControllers()
 .AddAspNetConventions(); // ← That's it

No attributes. No base classes. No custom middleware.

From this single registration, your API gets four features automatically.


1. Route Standardization (Zero Code Changes)

Your controller stays exactly the same:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
 [HttpGet("[action]/{userId}")]
 public IActionResult GetUserDetails(int userId)
 {
 return Ok(new { userId, name = "John Doe" });
 }
}

Before:

GET /api/Users/GetUserDetails/{userId}

After:

GET /api/users/get-user-details/{user-id}

What gets standardized automatically:

  • Controller names → consistent casing (kebab-case by default)
  • Action names → transformed and stripped of HTTP prefixes
  • Route parameters → consistent format

No refactoring. No attributes. No rewrites. Model binding still works — {user-id} maps back to your userId parameter transparently.

Want a different style? Just change one option:

options.Route.CaseStyle = CaseStyle.CamelCase;

2. Response Standardization

Different endpoints often return different shapes.

That inconsistency leaks into your frontend, mobile apps, and integrations.

Before:

{"userId":123,"name":"John Doe"}{"UserId":456,"FullName":"Jane Smith"}"System.ArgumentNullException: Value cannot be null."

After — every response follows the same predictable structure:

Success:

{"status":"success","statusCode":200,"message":"Request completed successfully.","data":{"userId":123,"name":"John Doe"},"metadata":{"requestType":"GET","timestamp":"2026-04-19T14:32:00.000Z","traceId":"00-ed89d1cc507c35126d6f0e933984f774-99b8b9a3feb75652-00","path":"/api/user-profile/get-by-id/123"}}

Error:

{"status":"failure","statusCode":400,"type":"VALIDATION_ERROR","message":"One or more validation errors occurred.","errors":[{"Email":["'Email' is not a valid email address."]}],"metadata":{"requestType":"POST","timestamp":"2026-04-19T14:33:00.000Z","traceId":"00-8e5513ae9369648487c2323d9a3508aa-2a8f92c7d45d3f74-00","path":"/api/user-profile/create-account"}}

3. Exception Handling

Without centralized handling, you end up:

  • Writing try/catch in controllers
  • Returning different error formats
  • Missing logging in some paths

AspNetConventions handles exceptions globally.

So your controller actions stay clean:

[HttpGet("GetById/{userId}")]
public ActionResult GetById(int userId)
{
 var user = _users.Find(userId)
 ?? throw new KeyNotFoundException($"User {userId} was not found.");

 return Ok(user);
}

The KeyNotFoundException is automatically caught, mapped to 404 Not Found, and returned in the standard error envelope. No try/catch. No custom middleware. No boilerplate.
The same applies to ValidationException → 400, UnauthorizedAccessException → 401, InvalidOperationException → 409, and more — all out of the box.

Need to handle your own domain exceptions?
AspNetConventions supports exception mappers.


4. JSON Serialization

Define how your API serializes JSON once — apply it everywhere.

builder.Services.AddControllers()
 .AddAspNetConventions(options =>
 {
 options.Json.ConfigureTypes = cfg =>
 {
 // Ignore a property globally
 cfg.IgnorePropertyName("InternalKey");

 // Configure a specific type
 cfg.Type<User>(type =>
 {
 type.Property(x => x.Id).Order(0);
 type.Property(x => x.Password).Ignore();
 type.Property(x => x.Alias).Name("userName");
 });
 };
 });

You can also implement class-based configuration for your own types.


Not Just MVC

AspNetConventions works the same way with Minimal APIs and Razor Pages — same conventions, same response envelope, same exception handling:

Minimal APIs:

var api = app.UseAspNetConventions();
api.MapGet("Products/GetFeatured", () => Results.Ok(featured));
// → GET /products/get-featured

Razor Pages:

// Pages/UserProfile/EditAddress.cshtml
// → /user-profile/edit-address/{user-id}/{address-id}

Define your conventions once — apply them everywhere.


Customization (When You Need It)

The defaults are sensible, but you’re not locked in.

builder.Services.AddControllers()
 .AddAspNetConventions(options =>
 {
 // Routes
 options.Route.CaseStyle = CasingStyle.SnakeCase;
 options.Route.Controllers.RemoveActionPrefixes.Add("Get");
 options.Route.Controllers.ExcludeControllers.Add("LegacyController");

 // JSON
 options.Json.CaseStyle = CasingStyle.SnakeCase;
 options.Json.ScanAssemblies(typeof(UserJsonConfiguration).Assembly);

 // Responses
 options.Response.IncludeMetadata = false;
 options.Response.Pagination.DefaultPageSize = 50;
 options.Response.ErrorResponse.DefaultStatusCode = HttpStatusCode.BadRequest;

 // Exceptions
 options.ExceptionHandling.Mappers.Add(new OrderNotFoundMapper());
 options.ExceptionHandling.ExcludeException.Add(typeof(OperationCanceledException));
 });

What You Get

Before After
Route style Mixed, manual Consistent, automatic
Response shape Varies by developer Same envelope everywhere
Exception handling try/catch per controller One global pipeline
JSON settings Scattered One config block

Try It

dotnet add package AspNetConventions

MIT licensed. Contributions welcome.