One criticism about Asp.Net web api is the ceremony to create a simple web api. This criticism has some reason: when you create a bare-bones web api with this command:

dotnet new webapi

You will get something like this:

As you can see, there is a lot of plumbing going on in Program.cs and Startup.cs and the real meat is in Controllers\WeatherForecastController.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

Even that has a lot going on: the ApiControllerRoute and HttpGet attributes, besides the code (well, that’s needed in any platform, anyway :-)). For comparison, we could create the same web api using node.js with Express with this code:

const express = require("express");

const app = express();

const nextElement = (n) => Math.floor(Math.random() * n);

const summaries = [
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];

app.get("/weatherforecast", (req, res, next) => {

    const forecasts = [...Array(5).keys()].map(i => (
        {
            "date": new Date() + i,
            "temperatureC": nextElement(75) - 20,
            "summary": summaries[nextElement(summaries.length)]
        }));
    
    res.json(forecasts);
});

app.listen(3000, () => {
    console.log("Server running on port 3000");
});

 

As you can see, there is a lot less code for the same api. The Express library takes care of setting up the server, listening to the port and mapping the route. The response for the request is the same as the one generated by the Asp.Net api.

To fix this issue, David Fowler, a Microsoft software architect designed a way to create Asp.Net web apis with low code. In fact, you can do it with a comparable amount of code than with Node. It’s called FeatherHTTP and it’s available here.

To use it, you must install the template and you can use it with dotnet new.

To install the template you can use this command:

dotnet new -i FeatherHttp.Templates::0.1.83-alpha.g15473de7d1 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json

When you do it, the option feather appears in dotnet new:

 

You can now create a new project with the command:

dotnet new feather -n FeatherApi

When the project is created, you can see that it’s much lighter than the standard code: there is only the csproj file and Program.cs with this code:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

var app = WebApplication.Create(args);

app.MapGet("/", async http =>
{
    await http.Response.WriteAsync("Hello World!");
});

await app.RunAsync();

Much smaller, no? As you can see, it uses the new C#9 feature “Top level statements” to reduce a little bit the code. We can change the code to have the same output of the standard code:

using System;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

var app = WebApplication.Create(args);
var Summaries = new[]
{
   "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", async http =>
{
    var rng = new System.Random();
    var weatherForecast = Enumerable.Range(1, 5)
        .Select(index => new WeatherForecast(
            DateTime.Now.AddDays(index), rng.Next(-20, 55), 
            Summaries[rng.Next(Summaries.Length)])).ToArray();
    http.Response.ContentType = "application/json";    
    await http.Response.WriteAsync(JsonSerializer.Serialize(weatherForecast));
});

await app.RunAsync();

public record WeatherForecast(DateTime Date, int TemperatureC,string Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

Now we have something comparable to Node! I’ve used here another feature of C#9, Records, to reduce a little the code size.

As we can see, with FeatherHttp, we can reduce the code size for an Asp.Net web api app, but can we still use it for a more complex API ?

We’ll try to reproduce the code used in my HttpRepl article here.

The first step is to copy the Customer class to the project. We’ll convert it to a record:

public record Customer(string CustomerId, string CompanyName,
  string ContactName, string ContactTitle, string Address,
  string City, string Region, string PostalCode, string Country,
  string Phone, string Fax);

Then we’ll copy the repository:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Xml.Linq;

namespace CustomerApi.Model
{
    public class CustomerRepository
    {
        private readonly IList<Customer> customers;

        public CustomerRepository()
        {
            var doc = XDocument.Load("Customers.xml");
            customers = new ObservableCollection<Customer>(
                doc.Descendants("Customer").Select(c => new Customer(
                    GetValueOrDefault(c, "CustomerID"), GetValueOrDefault(c, "CompanyName"),
                    GetValueOrDefault(c, "ContactName"), GetValueOrDefault(c, "ContactTitle"),
                    GetValueOrDefault(c, "Address"), GetValueOrDefault(c, "City"),
                    GetValueOrDefault(c, "Region"), GetValueOrDefault(c, "PostalCode"),
                    GetValueOrDefault(c, "Country"), GetValueOrDefault(c, "Phone"),
                    GetValueOrDefault(c, "Fax")
            )).ToList());
        }

        #region ICustomerRepository Members

        public bool Add(Customer customer)
        {
            if (customers.FirstOrDefault(c => c.CustomerId == customer.CustomerId) == null)
            {
                customers.Add(customer);
                return true;
            }
            return false;
        }

        public bool Remove(Customer customer)
        {
            if (customers.IndexOf(customer) >= 0)
            {
                customers.Remove(customer);
                return true;
            }
            return false;
        }

        public bool Update(Customer customer)
        {
            var currentCustomer = GetCustomer(customer.CustomerId);
            if (currentCustomer == null)
                return false;
            var index = customers.IndexOf(currentCustomer);
            currentCustomer = new Customer(customer.CustomerId, customer.CompanyName,
              customer.ContactName, customer.ContactTitle, customer.Address, customer.City,
              customer.Region, customer.PostalCode, customer.Country, customer.Phone, customer.Fax);
            customers[index] = currentCustomer;
            return true;
        }

        public bool Commit()
        {
            try
            {
                var doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
                var root = new XElement("Customers");
                foreach (Customer customer in customers)
                {
                    root.Add(new XElement("Customer",
                                          new XElement("CustomerID", customer.CustomerId),
                                          new XElement("CompanyName", customer.CompanyName),
                                          new XElement("ContactName", customer.ContactName),
                                          new XElement("ContactTitle", customer.ContactTitle),
                                          new XElement("Address", customer.Address),
                                          new XElement("City", customer.City),
                                          new XElement("Region", customer.Region),
                                          new XElement("PostalCode", customer.PostalCode),
                                          new XElement("Country", customer.Country),
                                          new XElement("Phone", customer.Phone),
                                          new XElement("Fax", customer.Fax)
                                 ));
                }
                doc.Add(root);
                doc.Save("Customers.xml");
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        public IEnumerable<Customer> GetAll() => customers;

        public Customer GetCustomer(string id) => customers.FirstOrDefault(c => string.Equals(c.CustomerId, id, StringComparison.CurrentCultureIgnoreCase));

        #endregion

        private static string GetValueOrDefault(XContainer el, string propertyName)
        {
            return el.Element(propertyName) == null ? string.Empty : el.Element(propertyName).Value;
        }
    }
}

One note here: as we declared Customer as a Record, it’s immutable, so we cannot simply replace its data with the new data in the Update method. Instead, we create a new customer and replace the current one with the newly created in the customers list.

We also need the customers.xml file that can be obtained at https://github.com/bsonnino/HttpRepl/blob/main/Customers.xml and add it to the project by adding this clause in the csproj file:

<ItemGroup >
  <None Update="customers.xml" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

Now, we will change the main program to create the API:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using CustomerApi.Model;
using System.Text.Json;
using System.Threading.Tasks;

var app = WebApplication.Create(args);

app.MapGet("/", GetAllCustomers);
app.MapGet("/{id}", GetCustomer);
app.MapPost("/", AddCustomer);
app.MapPut("/", UpdateCustomer);
app.MapDelete("/{id}", DeleteCustomer);

await app.RunAsync();

async Task GetAllCustomers(HttpContext http)
{
    var customerRepository = new CustomerRepository();
    http.Response.ContentType = "application/json";
    await http.Response.WriteAsync(JsonSerializer.Serialize(customerRepository.GetAll()));
}

async Task GetCustomer(HttpContext http)
{
    if (!http.Request.RouteValues.TryGet("id", out string id))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    Customer customer = customerRepository.GetCustomer(id);
    if (customer != null)
    {
        http.Response.ContentType = "application/json";
        await http.Response.WriteAsync(JsonSerializer.Serialize(customer));
    }
    else
        http.Response.StatusCode = 404;
    return;
}

async Task AddCustomer(HttpContext http)
{
    var customer = await http.Request.ReadFromJsonAsync<Customer>();
    if (string.IsNullOrWhiteSpace(customer.CustomerId))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    if (customerRepository.Add(customer))
    {
        customerRepository.Commit();
        http.Response.StatusCode = 201;
        http.Response.ContentType = "application/json";
        await http.Response.WriteAsync(JsonSerializer.Serialize(customer));
        return;
    }
    http.Response.StatusCode = 409;
    return;
}

async Task UpdateCustomer(HttpContext http)
{
    var customer = await http.Request.ReadFromJsonAsync<Customer>();
    if (string.IsNullOrWhiteSpace(customer.CustomerId))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    var currentCustomer = customerRepository.GetCustomer(customer.CustomerId);
    if (currentCustomer == null)
    {
        http.Response.StatusCode = 404;
        return;
    }
    if (customerRepository.Update(customer))
    {
        customerRepository.Commit();
        http.Response.ContentType = "application/json";
        await http.Response.WriteAsync(JsonSerializer.Serialize(customer));
        return;
    }
    http.Response.StatusCode = 204;
    return;
}


async Task DeleteCustomer(HttpContext http)
{
    if (!http.Request.RouteValues.TryGet("id", out string id))
    {
        http.Response.StatusCode = 400;
        return;
    }
    var customerRepository = new CustomerRepository();
    var currentCustomer = customerRepository.GetCustomer(id);
    if (currentCustomer == null)
    {
        http.Response.StatusCode = 404;
        return;
    }
    if (customerRepository.Remove(currentCustomer))
    {
        customerRepository.Commit();
        http.Response.StatusCode = 200;
        return;
    }
    http.Response.StatusCode = 204;
    return;
}

We are mapping the routes for all the desired actions. For each mapping, we’ve created a method to use when the corresponding route is being called:

  • For GetAll, we ar using the same method we’ve used with the WeatherForecast, getting all customers, serializing the array into a json string and writing the response
  • To get just one customer, where the id is in the route, we get the id with http.Request.RouteValues.TryGet, get the customer and write the response
  • To add and update the customer, we must get it from the body with await http.Request.ReadFromJsonAsync<Customer>(); Once we get it, we can add or update the customer repository
  • Delete is similar to getting just one customer, but we delete it from the repository, instead of returning it.

You can test the API by using the same methods we’ve used in the HttpRepl article, the API will be the same, but in this case, I’m not using the customers route. Another minor issue is that Swagger is still not available in FeatherHttp (https://github.com/featherhttp/framework/issues/34), but this doesn’t change the functionality at all.

FeatherHttp is not production ready, it’s still in alpha, but it shows a direction to lightweight Asp.Net web api applications. As you can see, all the ceremony from Asp.Net web api can be removed and you can finish with an app as small as a Node one, but written in C#.

All the source code for this article is at https://github.com/bsonnino/FeatherHttp