Adding content negotiation to minimal APIs with Carter

In this post I show how to use the open source library Carter to add content negotiation to an ASP.NET Core minimal API app

In my previous post I described how to return XML from a minimal API endpoint. In this post I look at an alternative approach, using the open source library, Carter.

Content Negotiation in minimal APIs with Carter

In my previous post I stated that:

Minimal APIs don't support conneg so if that's a feature you really need then it's probably best to use Web APIs instead.

While this is tehnically correct (you can read about content negotiation here), there's another option, as pointed out by Jonathan Channon:

Carter has been on my radar for a while, so this was the perfect excuse to give it a try! In this post I show a quick getting started with Carter, then create a custom IResponseNegoiator in Car ter to allow XML content negotiation with minimal APIs.

Getting started with Carter

As per the documentation

Carter is a framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing the code to be more explicit and most importantly more enjoyable.

You can think of Carter as adding some important extra features and structure to minimal APIs. Carter focuses on organising your APIs into modules, and layers on convenience extensions for validation, for working with files, and for content negotiation (the focus of this post)!

Let's start by creating a new minimal API application, and converting it to use Carter:

dotnet new web  dotnet new sln  dotnet sln add .  dotnet add package carter  

This creates a typical, empty, minimal API application:

var builder = WebApplication.CreateBuilder(args);  var app = builder.Build();    app.MapGet("/", "Hello World!");  app.Run();  

First off, let's convert this to return a Person object from the API:

var builder = WebApplication.CreateBuilder(args);  var app = builder.Build();    app.MapGet("/", () => new Person  {      FirstName = "Andrew",      LastName = "Lock"  });  app.Run();    public class Person  {      public string? FirstName { get; init; }      public string? LastName { get; init; }  }  

There's nothing "Carter-ised" about this yet, this is a simple minimal API that will return JSON. For the next step, we'll convert this app to Carter.

using Carter;  using Carter.Response;    var builder = WebApplication.CreateBuilder(args);  // รฐŸ'‡ Add the required Carter services  builder.Services.AddCarter();    var app = builder.Build();  // รฐŸ'‡ find all the Carter modules and register all the APIs  app.MapCarter();    app.Run();    // รฐŸ'‡ Create a Carter module for the API  public class PersonModule : ICarterModule  {      public void AddRoutes(IEndpointRouteBuilder app)      {          app.MapGet("/", () => new Person          {              FirstName = "Andrew",              LastName = "Lock"          });      }  }  

In the above example, we added the Carter services to the DI container using AddCarter(), created an ICarterModule, and registered all the modules in the app using MapCarter().

If you run the app now, it won't seem any different to a "normal" minimal API. The big advantage here is the structure afforded by the ICarterModule implementations:

A Carter app looks the same as a standard minimal API

Ok, now we have a Carter app, it's time to add in content negotiation.

Adding support for XML content negotation with Carter

To add content negotiation for additional media types (in addition to JSON) we need to do three things:< /p>

  1. Create a custom IResponseNegotiator that serializes to the required format.
  2. Register the IResponseNegotiator with Carter.
  3. Use the HttpResponse.Negotiate() extension method to run Carter's content negotation.

In this example we'll create an XML IResponseNegotiator so we can return XML to clients that support it.

In my previous post Muhammad Rehan Saeed pointed out that the DataContractSerializer is intended to be faster than the XmlSerializer I used in my previous post, so in this post I'll use DataContractSerializer!

1. Creating a custom IResponseNegotiator

To implement IResponseNegotiator you need to implement two methods:

public interface IResponseNegotiator  {      bool CanHandle(MediaTypeHeaderValue accept);      Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken);  }  

CanHandle passes in the Accept header value, and is used to check whether the client making the request can handle the format you support. In the Handle method, you serialize the provided model to the HttpResponse in the correct format.

In our case, we check to see if the client supports application/xml, and we serialize the model to XML using the DataContractSerializer. Note in this case I've jumped straight to using the RecyclableMemoryStreamManager, as I described in my previous post to reduce memory pressure over using MemoryStream.

using System.Runtime.Serialization;  using Carter;  using Microsoft.IO;  using Microsoft.Net.Http.Headers;    public class XmlResponseNegotiator : IResponseNegotiator  {      public bool CanHandle(MediaTypeHeaderValue accept)          => accept.MatchesMediaType("application/xml"); // รฐŸ'ˆ Does the client accept XML?        public async Task Handle(HttpRequest req, HttpResponse res, object model, CancellationToken cancellationToken)      {          res.ContentType = "application/xml";            // Create a serializer for the model type          var serializer = new DataContractSerializer(model.GetType());            // Rent a memory stream and serialize the model          using var ms = StreamManager.Instance.GetStream();          serializer.WriteObject(ms, model);          ms.Position = 0;            // Write the memory stream to the response Body          await ms.CopyToAsync(res.Body, cancellationToken);      }  }    public static class StreamManager  {      // รฐŸ'‡ Create a shared RecyclableMemoryStreamManager instance      public static readonly RecyclableMemoryStreamManager Instance = new();  }  

Once you've created the IResponseNegotiator implementation, you need to register it with Carter.

2. Registering the custom IResponseNegotiator

You register the IResponseNegotiator by configuring the options in the AddCarter() call and calling WithResponseNegotiator<T>():

using Carter;    var builder = WebApplication.CreateBuilder(args);    // รฐŸ'‡ Register the IResponseNegotiator with Carter   builder.Services.AddCarter(configurator: c => {      c.WithResponseNegotiator<XmlResponseNegotiator>();  });    var app = builder.Build();    app.MapCarter();    app.Run();  

The final step is to update the APIs in ICarterModule.

3. Use Carter's content negotiation

By default, minimal APIs in Carter return only JSON, the same as they do with "raw" minimal APIs. To use content negotiation, you have to call an extension method, HttpResponse.Negotiate(), and pass in the object to serialize:

public class HomeModule : ICarterModule  {      public void AddRoutes(IEndpointRouteBuilder app)      {          // รฐŸ'‡ Call Negotiate on the HttpResponse to trigger conneg          app.MapGet("/", (HttpResponse resp) => resp.Negotiate(new Person          {              FirstName = "Andrew",              LastName = "Lock"          }));      }  }  

With this change, Carter will check the Accept header to see which media types the client accepts (starting with the highest "quality" media types), and see if there is an IResponseNegotiator that can handle it. If no negotiator can be found, Carter will default to using JSON as a last resort.

With all the changes above, if you now hit the application above in a browser, then based on a typical Accept header like:.

text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9  

then the app will respond with XML!

App responding with XML

Summary

In this post I gave a brief introduction to Carter, and showed how you can use it to perform simple content negotiation in an ASP.NET Core app. Carter allows you to create multiple IResponseNegotiator implementations which each handle a single media type, such as application/xml. You can invoke the negotiators by calling the Carter extension method HttpResponse.Negotiate in your minimal API handlers. This will check which negotatiors are available, and use the best option based on the request's Accept header.

Namaste Devops is a one stop solution view, read and learn Devops Articles selected from worlds Top Devops content publishers inclusing AWS, Azure and others. All the credit/appreciations/issues apart from the Clean UI and faster loading time goes to original author.

Comments

Did you find the article or blog useful? Please share this among your dev friends or network.

An android app or website on your mind?

We build blazing fast Rest APIs and web-apps and love to discuss and develop on great product ideas over a Google meet call. Let's connect for a free consultation or project development.

Contact Us

Trending DevOps Articles

Working with System.Random and threads safely in .NET Core and .NET Framework

Popular DevOps Categories

Docker aws cdk application load balancer AWS CDK Application security AWS CDK application Application Load Balancers with DevOps Guru Auto scale group Automation Autoscale EC2 Autoscale VPC Autoscaling AWS Azure DevOps Big Data BigQuery CAMS DevOps Containers Data Observability Frequently Asked Devops Questions in Interviews GCP Large Table Export GCP Serverless Dataproc DB Export GTmetrix Page Speed 100% Google Page Speed 100% Healthy CI/CD Pipelines How to use AWS Developer Tools IDL web services Infrastructure as code Istio App Deploy Istio Gateways Istio Installation Istio Official Docs Istio Service Istio Traffic Management Java Database Export with GCP Jenkin K8 Kubernetes Large DB Export GCP Linux MSSQL March announcement MySQL Networking Popular DevOps Tools PostgreSQL Puppet Python Database Export with GCP Python GCP Large Table Export Python GCP Serverless Dataproc DB Export Python Postgres DB Export to BigQuery Sprint Top 100 Devops Questions TypeScript Client Generator anti-patterns of DevOps application performance monitoring (APM) aws amplify deploy blazor webassembly aws cdk application load balancer security group aws cdk construct example aws cdk l2 constructs aws cdk web application firewall aws codeguru reviewer cli command aws devops guru performance management aws service catalog best practices aws service catalog ci/cd aws service catalog examples azure Devops use cases azure devops whitepaper codeguru aws cli deploy asp.net core blazor webassembly devops guru for rds devops guru rds performance devops project explanation devops project ideas devops real time examples devops real time scenarios devops whitepaper aws docker-compose.yml health aware ci/cd pipeline example host and deploy asp.net core blazor webassembly on AWS scalable and secure CI/CD pipelines security vulnerabilities ci cd pipeline security vulnerabilities ci cd pipeline aws smithy code generation smithy server generator
Show more