Building APIs with MongoDB and .NET Minimal APIs

2023, Feb 12    

Prerequisites

Before we get started we will need to have at least .NET (currently .NET 7) installed, this can be installed on any platform (Windows, Linux, MacOs).

Once installed, feel free to download one of the many IDEs that can help you write .NET code.

We’ll also need MongoDB running locally, the simplest way to have MongoDB running is via running the following docker command

What we’re going to build

We’re going to build a simple API that allows us to create and list companies, companies are also going to have a list of offices associated with them too. Let’s take the following example requests as what we’ll be building.

List companies

GET /companies

[
    {
        "id": "5a934e000102030405000000",
        "name": "NVIDIA",
        "offices" : [
            {
                "id": "Reading",
                "address" : {
                        "line1" : "100 Brook Dr",
                        "line2" : "Reading",
                        "postalCode": "RG2 6UJ",
                        "country": "United Kingdom"
                    }
                }
        ]
    },
    {
        "id": "5a934e000102030405000001",
        "name": "Google",
        "offices" : [
            {
                "id": "Brussels",
                "address" : {
                        "line1" : "Chaussee d'Etterbeek 180",
                        "line2" : "Brussels",
                        "postalCode": "1040",
                        "country": "Belgium"
                    }
                },
            {
                "id": "London–6PS",
                "address" : {
                    "line1" : "6 Pancras Square",
                    "line2" : "London",
                    "postalCode": "N1C 4AG",
                    "country": "United Kingdom"
                }
            }
        ]
    },
    
]

Add new company

POST /companies
{
    "name": "NVIDIA",
    "offices" : [
        {
            "id": "Reading",
            "address" : {
                    "line1" : "100 Brook Dr",
                    "line2" : "Reading",
                    "postalCode": "RG2 6UJ",
                    "country": "United Kingdom"
                }
            }
    ]
}

{
    "id": "5a934e000102030405000000",
    "name": "NVIDIA",
    "offices" : [
        {
            "id": "Reading",
            "address" : {
                    "line1" : "100 Brook Dr",
                    "line2" : "Reading",
                    "postalCode": "RG2 6UJ",
                    "country": "United Kingdom"
                }
            }
    ]
}

Get company offices

GET /companies/5a934e000102030405000000/offices

[
    {
        "id": "Reading",
        "address" : {
                "line1" : "100 Brook Dr",
                "line2" : "Reading",
                "postalCode": "RG2 6UJ",
                "country": "United Kingdom"
            }
    }
]

Project setup

Let’s start by setting up a new API project within .NET, we can do this via the command line using the dotnet CLI. Navigate to a new empty directory where you want to put the project and execute the following command.

dotnet new web

Next we’ll install the MongoDB driver so we can talk to the database, this again can be done via the dotnet CLI.

dotnet add package MongoDB.Driver 

Database models

We’ll create a few database models for our service, these will be Company, Office and Address and will match the above same request data. We will use record type within .NET to make these immutable.

public record Company(ObjectId Id, string Name, IReadOnlyCollection<Office> Offices);
public record Office(string Id, Address Address);
public record Address(string Line1, string Line2, string PostalCode, string Country);

Service collection setup

Let’s start by configuring the service collection within .NET, this is so that our endpoints can access MongoDB via the MongoClient. If we open the Program.cs file then we can add the following lines for adding extra configuration to the service collection builder.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MongoClient>(_ => new MongoClient());
builder.Services.AddSingleton<IMongoDatabase>(
    provider => provider.GetRequiredService<MongoClient>().GetDatabase("building-apis"));
builder.Services.AddSingleton<IMongoCollection<Company>>(
    provider => provider.GetRequiredService<IMongoDatabase>().GetCollection<Company>("companies"));

Building the endpoints

Get / Create Companies

We’ll add the 2 endpoints for getting and creating companies, this is fairly simple within minimal apis, we just need to call the MapGet and MapPost methods on app

var app = builder.Build();

app.MapGet("/companies", async (IMongoCollection<Company> collection)
    => TypedResults.Ok(await collection.Find(Builders<Company>.Filter.Empty).ToListAsync()));

app.MapPost("/companies", async (IMongoCollection<Company> collection, Company company)
    =>
{
    // Make sure the Id is set to Empty so that the database generates us a new Id
    company = company with { Id = ObjectId.Empty };
    await collection.InsertOneAsync(company);
    return TypedResults.Ok(company);
});

If we try out the above code we’ll notice that the id’s that are getting returned look like the following:

{
  "id": {
    "timestamp": 1676208890,
    "machine": 16007445,
    "pid": 15784,
    "increment": 4971809,
    "creationTime": "2023-02-12T13:34:50Z"
  },
  // ...
}

This is because System.Text.Json doesn’t understand what to do with the type ObjectId for serialization so it traverses the object and serialize each property instead. We can get around this by adding our own custom JsonConverter, we’ll not go in the full details but this can be found on the Microsoft Website - How to write custom converters.

public class ObjectIdJsonConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new (reader.GetString());
    
    public override void Write(Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteStringValue(value.ToString());
}

We can then reconfigure the JsonOptions options on startup to include this extra convertor.

builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.Converters.Add(new ObjectIdJsonConverter());
});

Now you’ll notice our response gets generated correctly

{
  "id": "63e8ed91117cdedb48a3dac5",
  "name": "NVIDIA",
  "offices": [
    {
      "id": "Reading",
      "address": {
        "line1": "100 Brook Dr",
        "line2": "Reading",
        "postalCode": "RG2 6UJ",
        "country": "United Kingdom"
      }
    }
  ]
}

Get company offices

The other endpoint we want to build is to just be able to fetch all of a single company offices. We can add another MapGet configuration which fetches a company based on id and projects the offices.

app.MapGet("/companies/{companyId}/offices", async (IMongoCollection<Company> collection, ObjectId companyId)
    =>
{
    var offices = await collection.Find(
            Builders<Company>.Filter.Eq(x => x.Id, companyId))
        .Project(x => x.Offices)
        .FirstOrDefaultAsync();
    
    return TypedResults.Ok(offices);
});