URL Shortener als minimale API

Ich wollte mal wieder in Ruhe das Thema ‚Minimale API‘, welches mit .NET 6 Einzug gehalten hat, anschauen. Dazu versuche ich meist ein kleines Beispiel-Projekt zu finden, da man so das Konzept in einer „realen“ Umgebung einmal testen kann. Als Beispiel habe ich mich für einen kleinen URL Shortener entschieden. Unsere API soll zwei Endpunkte bereitstellen. Ein Endpunkt dient zum generieren der „Short-URL“ mit gleichzeitigem Abspeichern der Information in einer lokalen Datenbank und einen zweiten Endpunkt zum Abrufen der langen URL durch Übergabe des Codes.

Wir starten zunächst Visual Studio und suchen nach Web Api. Anschließend wählen wir das Template ASP.NET Core Web API aus.

Anschließend geben wir dem Projekt einen Namen, z.B. UrlShortener.

Im anschließenden Dialog können wir noch ein paar weitere Details konfigurieren. Hier stellen wir sicher, dass der Haken bei Enable Docker gesetzt ist, so dass wir unser Projekt später ganz einfach als Docker Container laufen lassen können. Außerdem entfernen wir noch den Haken bei Use controllers (uncheck to use minimal APIs), so dass wir tatsächlich eine minimale API als Vorlage haben.

Wie üblich, öffnen wir zunächst den NuGet Package Manager und updaten die vorliegenden Packages.

Wir wechseln auf den Tab Browse und suchen nach LiteDB. Anschließend fügen wir das Package unserem Projekt hinzu.

Den Vorgang wiederholen wir noch für das Package Hashids.net.

Wir erstellen nun zunächst eine neuen Ordner mit den Namen Models in unserem Projekt. Darin erstellen wir eine neue Klasse UrlInfo. Diese legen wir als Record and und übergeben die Parameter Id und Url.

namespace UrlShortener.Models;

public record UrlInfo(int Id, string Url);

Anschließend öffnen wir die Program.cs-Datei und entfernen zunächst den gesamten Code in dieser Klasse. Anschließend beginnen wir mit der Basis-Konfiguration. Wir registrieren zunächst die LiteDatabase und übergeben als Parameter den Namen für unsere Datenbank. Ebenso beginnen wir mit der Konfiguration von Swagger, so dass wir relativ leicht eine Option zum Testen unserer API haben.

using LiteDB;
using Microsoft.OpenApi.Models;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddSingleton<ILiteDatabase, LiteDatabase>(_ => 
    new LiteDatabase("url.db"));

// Register Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(setup => setup.SwaggerDoc("v1", new OpenApiInfo()
{
    Description = "Simple API to shorten URLs",
    Title = "Url Shorter",
    Version = "v1",
    Contact = new OpenApiContact()
    {
        Name = "Thomas Sebastian Jensen",
        Url = new Uri("https://www.tsjdev-apps.de")
    }
}));

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();



// CODE WILL FOLLOW

app.Run();

Nun beginnen wir mit der Logik für unseren URL Shortener. Der folgende Code wird an der Stelle CODE WILL FOLLOW eingefügt. Zunächst initialisieren wir das Package Hashids.net. Hierzu erstellen wir eine neue Instanz von Hashids und übergeben einen SALT, in meinem Fall URL Shortener. Außerdem können wir noch die Länge für den Hash definieren. Ich habe mich für die Länge 5 entschieden.

// Configure HashIds.NET
Hashids _hashIds = new("URL Shortener", 5);

Im nächsten Schritt erstellen wir den Endpunkt zum Anlegen einer neuen URL. Hierzu verwenden wir einen POST-Request und erwarten, dass uns ein UrlInfo-Objekt geschickt wird. Außerdem benötigen wir noch Zugriff auf die Datenbank, welche wir uns ebenfalls als Parameter übergeben lassen können. Zunächst überprüfen wir, ob ein UrlInfo-Objekt übergeben wurde. Sollte dies nicht der Fall sein, so beenden wir direkt mit einem 400-Statuscode. Anschließend nutzen wir die Datenbank und schauen, ob es bereits einen Eintrag für die übergebene URL gibt. Ist dies der Fall, so geben wir einen Statuscode 200 mit dem passenden Hash-Wert zurück. Sollte noch kein Eintrag vorhanden sein, so fügen wir diesen unserer Datenbank hinzu und geben ebenfalls den Hash-Wert mit einem Statuscode 201 zurück. Über die Methode Produces geben wir noch die Information an, welche Statuscode unser Endpunkt zurück liefern kann. Diese Information wird für die Swagger-Dokumentation verwendet.

app.MapPost("/add", (UrlInfo urlInfo, ILiteDatabase context) =>
{
    // check if an URL is provided
    if (urlInfo is null || string.IsNullOrEmpty(urlInfo.Url))
        return Results.BadRequest("Please provide a valid UrlInfo object.");

    // get the collection from the database
    ILiteCollection<UrlInfo> collection = context
        .GetCollection<UrlInfo>(BsonAutoId.Int32);

    // check if an entry with the corresponding url is already part of the database
    UrlInfo entry = collection.Query()
        .Where(x => x.Url.Equals(urlInfo.Url)).FirstOrDefault();

    // if there is already an entry in the database, just return the hashed valued
    if (entry is not null)
        return Results.Ok(_hashIds.Encode(entry.Id));

    // otherwise just insert the url info into the database and return the hashed valued
    BsonValue documentId = collection.Insert(urlInfo);
    string encodedId = _hashIds.Encode(documentId);
    return Results.Created(encodedId, encodedId);
})
    .Produces<string>(StatusCodes.Status200OK)
    .Produces<string>(StatusCodes.Status201Created)
    .Produces(StatusCodes.Status400BadRequest);

Nun fehlt uns nur noch der zweite Endpunkt. Dieses wird ein GET-Request. Wir lassen uns über den URL-Pfad den Hash-Wert übergeben und benötigen auch hier wieder Zugriff auf die Datenbank. Anschließend dekodieren wir zunächst den Hash-Wert in die Id, bevor wir in der Datenbank nach diesem Wert suchen. Sollte dieser vorhanden sein, so geben wir die passende URL mit dem Statuscode 200 zurück. Wenn der Wert nicht in der Datenbank vorhanden ist, so geben wir den Statuscode 404 zurück. Auch hier nutzen wir die Methode Produces, um die passenden Statuscodes für unseren Endpunkt zu definieren.

app.MapGet("/{shortUrl}", (string shortUrl, ILiteDatabase context) =>
{
    // decode the short url into the corresponding id
    int[] ids = _hashIds.Decode(shortUrl);
    int tempraryId = ids[0];

    // get the collection from the database
    ILiteCollection<UrlInfo> collection = context
        .GetCollection<UrlInfo>();

    // try to get the entry with the corresponding id from the database
    UrlInfo entry = collection.Query()
        .Where(x => x.Id.Equals(tempraryId)).FirstOrDefault();

    // if the url info is present in the database, just return the url
    if (entry is not null)
        return Results.Ok(entry.Url);

    // otherwise return the status code 'not found'
    return Results.NotFound();
})
    .Produces<string>(StatusCodes.Status200OK)
    .Produces(StatusCodes.Status404NotFound);

Schauen wir uns das ganze doch nun einmal an. Solltet ihr Docker Desktop auf eurem Rechner installiert haben, so könnt ihr die Docker-Variante starten oder ansonsten klassisch die UrlShortener-Variante. Es sollte sich die Swagger-Dokumentation öffnen und hier können wir die beiden Endpunkte direkt testen. Ich habe meine Webseite https://www.tsjdev-apps.de einmal angelegt und erhalte den Hash-Wert 7V0PV zurück. Diesen Wert kann ich jetzt an den zweiten Endpunkt übergeben und erhalte so die passende URL wieder zurück.

In diesem Blog-Post habe ich euch gezeigt, wie man mit wenig Aufwand und der Hilfe der minimalen API einen einfachen URL Shortener schreiben kann, welcher nun zum Beispiel auf Azure gehostet oder auch als Docker Image bereitgestellt werden kann. Den aktuellen Code-Stand findet ihr außerdem auf GitHub.

XAML-Studio zum Erstellen von XAML-Layouts WordGuess – Wordle-Klon als Konsolen-App Rekursive Funktionen in C#, Python und Racket