Zalando’s RESTful API Guidelines describes in detail how they design RESTful APIs and even offer a configurable OpenAPI Linter called zally
.
In this blog post, I want to show you how to configure ASP.NET Core 5.0 Web APIs to conform to three specific rules regarding path and JSON body layout. For that propose I modified the “weather forecast” starter template to demonstrate the rules which can be found on my GitHub profile
.
- Rule #118
: Property names must be ASCII snake_case (and never camelCase)
- Rule #129
: use lowercase separate words with hyphens for path segments
- Rule #130
: use snake_case(nevercamelCase) for query parameters
Rule #118
: Property names must be ASCII snake_case (and never camelCase)
First let us look how the default JSON body output looks like. Have a look at WeatherForecast class has the properties TemperatureCelsius and TemperatureFahrenheit.
public class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public int TemperatureFahrenheit => 32 + (int) (TemperatureCelsius / 0.5556);
    public string Summary { get; set; }
}
Without changing the setting of the Framework, we get the snakeCase formatting „temperatureCelsius“ and „temperatureFahrenheit“ as JSON property names.
[
  {
    "date": "2021-07-07T08:31:49.9734021+02:00",
    "temperatureCelsius": 39,
    "temperatureFahrenheit": 102,
    "summary": "Warm"
  },
  {
    "date": "2021-07-09T08:31:49.9734114+02:00",
    "temperatureCelsius": 29,
    "temperatureFahrenheit": 84,
    "summary": "Scorching"
  }
]
By default, APS.NET Core 5.0 uses System.Text.Json which currently only supports camelCase to serialize and deserialize objects into JSON. There is an open issue
 on the .NET runtime repository to add snake_case support.
Solution #1 use [JsonPropertyName]
The simple solution is to use [JsonPropertyName] Annotation from System.Text.Json.Serialization and define all property names by hand.
    public class WeatherForecast
    {
        public DateTime Date { get; set; }
        [JsonPropertyName("temperature_celsius")]
        public int TemperatureCelsius { get; set; }
        [JsonPropertyName("temperature_fahrenheit")]
        public int TemperatureFahrenheit => 32 + (int)(TemperatureCelsius / 0.5556);
        public string Summary { get; set; }
    }
Which produces the desired output.
[
  {
    "date": "2021-07-07T09:19:30.9345519+02:00",
    "temperature_celsius": 43,
    "temperature_fahrenheit": 109,
    "summary": "Sweltering"
  },
  {
    "date": "2021-07-08T09:19:30.9345608+02:00",
    "temperature_celsius": 5,
    "temperature_fahrenheit": 40,
    "summary": "Hot"
  }
]
Solution #2 use Newtonsoft.Json
Another solution is to use Newtonsoft.Json for serialization and configure it to use the SnakeCaseNamingStrategy. Here for you need to add a dependency on Microsoft.AspNetCore.Mvc.NewtonsoftJson and Swashbuckle.AspNetCore.Newtonsoft. Then configure ASP.NET Core and Swashbuckle to use Newtonsoft.Json instead of System.Text.Json.
  <itemgroup>
    <packagereference include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" version="5.0.7"></packagereference>
    <packagereference include="Swashbuckle.AspNetCore" version="6.1.4"></packagereference>
    <packagereference include="Swashbuckle.AspNetCore.Newtonsoft" version="6.1.4"></packagereference>
  </itemgroup>
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddNewtonsoftJson(options =>
        {
            options.SerializerSettings.ContractResolver = new DefaultContractResolver
            {                        
                NamingStrategy = new SnakeCaseNamingStrategy()
            };
            options.SerializerSettings.Converters.Add(new StringEnumConverter());
        });
    services.AddSwaggerGenNewtonsoftSupport();
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo {Title = "ZalandoRestfulApiGuidelinesWebApi", Version = "v1"});
    });
}
Rule #129
: use lowercase separate words with hyphens for path segments
By default, the Framework uses the Route Attribute [Route(„[controller]“)] to generate the path. For example, the WeatherForecastController will be reachable unter the /WeatherForecast path. But that does not comply to the rule. /weather-forecast would be compliant.
[ApiController]
[Route("[controller]")]
[Consumes("application/json")]
[Produces("application/json")]
public class WeatherForecastController : ControllerBase
{
    ...
}
Solution #1 explicit define the [Route]
Simply use a constant string in the [Route] attribute. see line 2: [Route(„weather-forecast“)]
[ApiController]
[Route("weather-forecast")]
[Consumes("application/json")]
[Produces("application/json")]
public class WeatherForecastController : ControllerBase
{
    ...
}
Use of an RouteTokenTransformerConvention to conform to this rule.
// Source https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0#use-a-parameter-transformer-to-customize-token-replacement
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }
        return Regex.Replace(value.ToString(),
            "([a-z])([A-Z])",
            "$1-$2",
            RegexOptions.CultureInvariant,
            TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{ 
    services.AddControllers(options =>
    {
      	options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
    });
}
Rule #130
: use snake_case (never camelCase) for query parameters
Use of [FromQuery] Attribute to define the correct formatting.
  [HttpGet]
  public IEnumerable<WeatherForecast> Search(
      [FromQuery(Name = "q")] string query,
      [FromQuery(Name = "created_before")] string createdBefore)
  {
    ...
  }
Links