Partager via


Journalisation HTTP dans ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 9 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 9 de cet article.

La journalisation HTTP est un intergiciel qui journalise des informations sur les requêtes HTTP entrantes et les réponses HTTP. La journalisation HTTP fournit les journaux suivants :

  • Informations sur les requêtes HTTP
  • Propriétés communes
  • En-têtes
  • Corps
  • Informations de réponse HTTP

La journalisation HTTP peut :

  • Journaliser toutes les requêtes et les réponses ou uniquement les requêtes et réponses qui répondent à certains critères.
  • Sélectionner les parties de la requête et de la réponse qui sont journalisées.
  • Vous autoriser à rédiger des informations sensibles à partir des journaux d’activité.

La journalisation HTTP peut réduire les performances d’une application, en particulier lors de la journalisation des corps de requête et de réponse. Tenez compte de l’impact sur les performances lorsque vous sélectionnez les champs à journaliser. Testez l’impact des propriétés de journalisation sélectionnées sur les performances.

Avertissement

La journalisation HTTP peut potentiellement journaliser des informations d’identification personnelle. Tenez compte des risques, et évitez de journaliser des informations sensibles.

Activer la journalisation HTTP

La journalisation HTTP est activée en appelant AddHttpLogging et UseHttpLogging, comme illustré dans l’exemple suivant :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(o => { });

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Hello World!");

app.Run();

L’expression lambda vide dans l’exemple précédent d’appel AddHttpLogging ajoute l’intergiciel avec la configuration par défaut. Par défaut, la journalisation HTTP journalise les propriétés courantes telles que le chemin d’accès, le code d’état et les en-têtes des requêtes et des réponses.

Ajoutez la ligne suivante au fichier appsettings.Development.json au niveau "LogLevel": { afin que les journaux HTTP soient affichés :

 "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"

Avec la configuration par défaut, une requête et une réponse sont enregistrées sous la forme d’une paire de messages similaire à l’exemple suivant :

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/2
      Method: GET
      Scheme: https
      PathBase:
      Path: /
      Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
      Host: localhost:52941
      User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.61
      Accept-Encoding: gzip, deflate, br
      Accept-Language: en-US,en;q=0.9
      Upgrade-Insecure-Requests: [Redacted]
      sec-ch-ua: [Redacted]
      sec-ch-ua-mobile: [Redacted]
      sec-ch-ua-platform: [Redacted]
      sec-fetch-site: [Redacted]
      sec-fetch-mode: [Redacted]
      sec-fetch-user: [Redacted]
      sec-fetch-dest: [Redacted]
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: text/plain; charset=utf-8
      Date: Tue, 24 Oct 2023 02:03:53 GMT
      Server: Kestrel

Options de journalisation HTTP

Pour configurer des options globales pour l’intergiciel de journalisation HTTP, appelez-AddHttpLogging dans Program.csà l’aide de l’expression lambda pour configurer HttpLoggingOptions.

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

Remarque

Dans l’exemple précédent et les exemples suivants, UseHttpLogging est appelé après UseStaticFiles, la journalisation HTTP n’est donc pas activée pour le fichier statique. Pour activer la journalisation HTTP de fichier statique, appelez UseHttpLogging avant UseStaticFiles.

LoggingFields

HttpLoggingOptions.LoggingFields est un indicateur d’énumération qui configure certaines parties de la requête et de la réponse à journaliser. HttpLoggingOptions.LoggingFields a la valeur par défaut RequestPropertiesAndHeaders | ResponsePropertiesAndHeaders.

RequestHeaders et ResponseHeaders

RequestHeaders et ResponseHeaders sont des ensembles d’en-têtes HTTP journalisés. Les valeurs d’en-tête sont journalisées uniquement pour les noms d’en-tête qui se trouvent dans ces collections. Le code suivant ajoute sec-ch-ua à RequestHeaders, de sorte que la valeur de l’en-tête sec-ch-ua est journalisée. Et il ajoute MyResponseHeader à ResponseHeaders, de sorte que la valeur de l’en-tête MyResponseHeader est journalisée. Si ces lignes sont supprimées, les valeurs de ces en-têtes sont [Redacted].

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

MediaTypeOptions

MediaTypeOptions fournit la configuration permettant de sélectionner l’encodage à utiliser pour un type de média spécifique.

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

Cette approche peut également être utilisée pour activer la journalisation des données qui ne sont pas journalisées par défaut (par exemple, les données de formulaire, qui peuvent avoir un type de média tel que application/x-www-form-urlencoded ou multipart/form-data).

Méthodes MediaTypeOptions

RequestBodyLogLimit et ResponseBodyLogLimit

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

CombineLogs

Définir CombineLogs sur true configure l’intergiciel pour consolider tous ses journaux activés pour une requête et une réponse dans un journal à la fin. Cela inclut la requête, le corps de la requête, la réponse, le corps de la réponse et la durée.

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;
    logging.CombineLogs = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

Configuration spécifique au point de terminaison

Pour une configuration spécifique au point de terminaison dans des applications API minimales, une méthode d’extension WithHttpLogging est disponible. L’exemple suivant montre comment configurer la journalisation HTTP pour un point de terminaison :

app.MapGet("/response", () => "Hello World! (logging response)")
    .WithHttpLogging(HttpLoggingFields.ResponsePropertiesAndHeaders);

Pour la configuration spécifique au point de terminaison dans les applications qui utilisent des contrôleurs, l’attribut [HttpLogging] est disponible. L’attribut peut également être utilisé dans des applications API minimales, comme illustré dans l’exemple suivant :

app.MapGet("/duration", [HttpLogging(loggingFields: HttpLoggingFields.Duration)]
    () => "Hello World! (logging duration)");

IHttpLoggingInterceptor

IHttpLoggingInterceptor est l’interface d’un service qui peut être implémenté pour gérer les rappels par requête et par réponse pour personnaliser les détails enregistrés. Tous les paramètres de journal spécifiques au point de terminaison sont appliqués en premier et peuvent ensuite être remplacés dans ces rappels. Une implémentation peut :

  • Inspecter une requête ou une réponse.
  • Activer ou désactiver un HttpLoggingFields quelconque.
  • Ajuster la quantité du corps de la requête ou de la réponse journalisée.
  • Ajouter des champs personnalisés aux journaux.

Inscrire une implémentation IHttpLoggingInterceptor en appelant AddHttpLoggingInterceptor<T> dans Program.cs. Si plusieurs instances IHttpLoggingInterceptor sont inscrites, elles sont exécutées dans l’ordre dans lequel elles ont été enregistrées.

L'exemple suivant montre comment enregistrer une implémentation IHttpLoggingInterceptor :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.Duration;
});
builder.Services.AddHttpLoggingInterceptor<SampleHttpLoggingInterceptor>();

L'exemple suivant est une implémentation IHttpLoggingInterceptor qui :

  • Inspecte la méthode de requête et désactive la journalisation des requêtes POST.
  • Pour les requêtes non POST :
    • Rédige le chemin de requête, les en-têtes de requête et les en-têtes de réponse.
    • Ajoute des champs personnalisés et des valeurs de champ aux journaux de requête et de réponse.
using Microsoft.AspNetCore.HttpLogging;

namespace HttpLoggingSample;

internal sealed class SampleHttpLoggingInterceptor : IHttpLoggingInterceptor
{
    public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext.Request.Method == "POST")
        {
            // Don't log anything if the request is a POST.
            logContext.LoggingFields = HttpLoggingFields.None;
        }

        // Don't enrich if we're not going to log any part of the request.
        if (!logContext.IsAnyEnabled(HttpLoggingFields.Request))
        {
            return default;
        }

        if (logContext.TryDisable(HttpLoggingFields.RequestPath))
        {
            RedactPath(logContext);
        }

        if (logContext.TryDisable(HttpLoggingFields.RequestHeaders))
        {
            RedactRequestHeaders(logContext);
        }

        EnrichRequest(logContext);

        return default;
    }

    public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
    {
        // Don't enrich if we're not going to log any part of the response
        if (!logContext.IsAnyEnabled(HttpLoggingFields.Response))
        {
            return default;
        }

        if (logContext.TryDisable(HttpLoggingFields.ResponseHeaders))
        {
            RedactResponseHeaders(logContext);
        }

        EnrichResponse(logContext);

        return default;
    }

    private void RedactPath(HttpLoggingInterceptorContext logContext)
    {
        logContext.AddParameter(nameof(logContext.HttpContext.Request.Path), "RedactedPath");
    }

    private void RedactRequestHeaders(HttpLoggingInterceptorContext logContext)
    {
        foreach (var header in logContext.HttpContext.Request.Headers)
        {
            logContext.AddParameter(header.Key, "RedactedHeader");
        }
    }

    private void EnrichRequest(HttpLoggingInterceptorContext logContext)
    {
        logContext.AddParameter("RequestEnrichment", "Stuff");
    }

    private void RedactResponseHeaders(HttpLoggingInterceptorContext logContext)
    {
        foreach (var header in logContext.HttpContext.Response.Headers)
        {
            logContext.AddParameter(header.Key, "RedactedHeader");
        }
    }

    private void EnrichResponse(HttpLoggingInterceptorContext logContext)
    {
        logContext.AddParameter("ResponseEnrichment", "Stuff");
    }
}

Avec cet intercepteur, une requête POST ne génère aucun journal même si la journalisation HTTP est configurée pour journaliser HttpLoggingFields.All. Une requête GET génère des journaux similaires à l’exemple suivant :

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Path: RedactedPath
      Accept: RedactedHeader
      Host: RedactedHeader
      User-Agent: RedactedHeader
      Accept-Encoding: RedactedHeader
      Accept-Language: RedactedHeader
      Upgrade-Insecure-Requests: RedactedHeader
      sec-ch-ua: RedactedHeader
      sec-ch-ua-mobile: RedactedHeader
      sec-ch-ua-platform: RedactedHeader
      sec-fetch-site: RedactedHeader
      sec-fetch-mode: RedactedHeader
      sec-fetch-user: RedactedHeader
      sec-fetch-dest: RedactedHeader
      RequestEnrichment: Stuff
      Protocol: HTTP/2
      Method: GET
      Scheme: https
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      Content-Type: RedactedHeader
      MyResponseHeader: RedactedHeader
      ResponseEnrichment: Stuff
      StatusCode: 200
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[4]
      ResponseBody: Hello World!
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[8]
      Duration: 2.2778ms

Ordre de priorité de la configuration de journalisation

La liste suivante montre l’ordre de priorité pour la configuration de la journalisation :

  1. Configuration globale à partir de HttpLoggingOptions, définie en appelant AddHttpLogging.
  2. La configuration spécifique au point de terminaison à partir de l’attribut [HttpLogging] ou de la méthode d’extension WithHttpLogging remplace la configuration globale.
  3. IHttpLoggingInterceptor est appelé avec les résultats et peut modifier davantage la configuration par requête.

La journalisation HTTP est un middleware qui journalise des informations sur les requêtes HTTP entrantes et les réponses HTTP. La journalisation HTTP fournit les journaux suivants :

  • Informations sur les requêtes HTTP
  • Propriétés communes
  • En-têtes
  • Corps
  • Informations de réponse HTTP

La journalisation HTTP est utile dans plusieurs scénarios :

  • Enregistrer des informations sur les requêtes entrantes et les réponses.
  • Filtrer les parties des requêtes et des réponses à journaliser.
  • Filtrage des en-têtes à journaliser.

La journalisation HTTP peut réduire les performances d’une application, en particulier lors de la journalisation des corps de requête et de réponse. Tenez compte de l’impact sur les performances lorsque vous sélectionnez les champs à journaliser. Testez l’impact des propriétés de journalisation sélectionnées sur les performances.

Avertissement

La journalisation HTTP peut potentiellement journaliser des informations d’identification personnelle. Tenez compte des risques, et évitez de journaliser des informations sensibles.

Activation de la journalisation HTTP

La journalisation HTTP est activée avec UseHttpLogging, qui ajoute un middleware de journalisation HTTP.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseHttpLogging();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();

app.MapGet("/", () => "Hello World!");

app.Run();

Par défaut, la journalisation HTTP journalise les propriétés courantes telles que le chemin, le code d’état et les en-têtes des requêtes et des réponses. Ajoutez la ligne suivante au fichier appsettings.Development.json au niveau "LogLevel": { afin que les journaux HTTP soient affichés :

 "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"

La sortie est journalisée en tant que message unique au niveau LogLevel.Information.

Exemple de sortie de requête

Options de journalisation HTTP

Pour configurer le middleware de journalisation HTTP, appelez AddHttpLogging dans Program.cs.

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

Remarque

Dans l’exemple précédent et les exemples suivants, UseHttpLogging est appelé après UseStaticFiles, la journalisation HTTP n’est donc pas activée pour le fichier statique. Pour activer la journalisation HTTP de fichier statique, appelez UseHttpLogging avant UseStaticFiles.

LoggingFields

HttpLoggingOptions.LoggingFields est un indicateur d’énumération qui configure certaines parties de la requête et de la réponse à journaliser. HttpLoggingOptions.LoggingFields a la valeur par défaut RequestPropertiesAndHeaders | ResponsePropertiesAndHeaders.

RequestHeaders

Headers sont un ensemble d’en-têtes de requête HTTP autorisés à être journalisés. Les valeurs d’en-tête sont journalisées uniquement pour les noms d’en-tête qui se trouvent dans cette collection. Le code suivant journalise l’en-tête de requête "sec-ch-ua". Si logging.RequestHeaders.Add("sec-ch-ua"); est supprimé, la valeur de l’en-tête de requête "sec-ch-ua" sera supprimée. Le code mis en évidence ci-dessous appelle HttpLoggingOptions.RequestHeaders et HttpLoggingOptions.ResponseHeaders :

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

MediaTypeOptions

MediaTypeOptions fournit la configuration permettant de sélectionner l’encodage à utiliser pour un type de média spécifique.

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();

Cette approche peut également être utilisée pour activer la journalisation des données qui ne sont pas journalisées par défaut. Par exemple, les données de formulaire, qui peuvent avoir un type de média tel que application/x-www-form-urlencoded ou multipart/form-data.

Méthodes MediaTypeOptions

RequestBodyLogLimit et ResponseBodyLogLimit

using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.All;
    logging.RequestHeaders.Add("sec-ch-ua");
    logging.ResponseHeaders.Add("MyResponseHeader");
    logging.MediaTypeOptions.AddText("application/javascript");
    logging.RequestBodyLogLimit = 4096;
    logging.ResponseBodyLogLimit = 4096;

});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
}

app.UseStaticFiles();

app.UseHttpLogging(); 

app.Use(async (context, next) =>
{
    context.Response.Headers["MyResponseHeader"] =
        new string[] { "My Response Header Value" };

    await next();
});

app.MapGet("/", () => "Hello World!");

app.Run();