Referencia rápida de las API mínimas
Nota:
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulte la versión de .NET 9 de este artículo.
Este documento:
- Proporciona una referencia rápida para las API mínimas.
- Está pensado para desarrolladores experimentados. Para consultar una introducción, vea Tutorial: Creación de una API mínima con ASP.NET Core.
Las API mínimas constan de lo siguiente:
WebApplication
Una plantilla de ASP.NET Core genera el código siguiente:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
El código anterior se puede crear a través de dotnet new web
en la línea de comandos o seleccionando la plantilla web vacía en Visual Studio.
El código siguiente crea una clase WebApplication (app
) sin crear explícitamente una clase WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
inicializa una nueva instancia de la clase WebApplication con valores predeterminados preconfigurados.
WebApplication
agrega automáticamente el siguiente middleware en Minimal API applications
en función de determinadas condiciones:
UseDeveloperExceptionPage
se agrega primero cuandoHostingEnvironment
es"Development"
.UseRouting
se agrega en segundo lugar si el código de usuario aún no llamóUseRouting
a y si hay puntos de conexión configurados, por ejemploapp.MapGet
.UseEndpoints
se agrega al final de la canalización de middleware si hay algún punto de conexión configurado.UseAuthentication
se agrega inmediatamente después deUseRouting
si el código de usuario no llamó aún aUseAuthentication
y siIAuthenticationSchemeProvider
se puede detectar en el proveedor de servicios.IAuthenticationSchemeProvider
se agrega de forma predeterminada cuando se usaAddAuthentication
, y los servicios se detectan medianteIServiceProviderIsService
.UseAuthorization
se agrega a continuación si el código de usuario no llamó aún aUseAuthorization
y siIAuthorizationHandlerProvider
se puede detectar en el proveedor de servicios.IAuthorizationHandlerProvider
se agrega de forma predeterminada cuando se usaAddAuthorization
, y los servicios se detectan medianteIServiceProviderIsService
.- El middleware y los puntos de conexión configurados por el usuario se agregan entre
UseRouting
yUseEndpoints
.
El código siguiente es eficazmente lo que produce el middleware automático que se agrega a la aplicación:
if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
if (isAuthenticationConfigured)
{
app.UseAuthentication();
}
if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints
app.UseEndpoints(e => {});
En algunos casos, la configuración predeterminada del middleware no es correcta para la aplicación y requiere modificación. Por ejemplo, UseCors se debe llamar a antes UseAuthentication de y UseAuthorization. La aplicación debe llamar a UseAuthentication
y UseAuthorization
si se llama a UseCors
:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
Si se debe ejecutar el middleware antes de que se produzca la coincidencia de rutas, se debe llamar a UseRouting y se debe colocar el middleware antes de la llamada a UseRouting
. UseEndpoints no es necesario en este caso, ya que se agrega automáticamente como se ha descrito anteriormente:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
Al agregar un middleware de terminal:
- El middleware debe agregarse después de
UseEndpoints
. - La aplicación debe llamar
UseRouting
a yUseEndpoints
para que el middleware de terminal se pueda colocar en la ubicación correcta.
app.UseRouting();
app.MapGet("/", () => "hello world");
app.UseEndpoints(e => {});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
El middleware de terminal es un middleware que se ejecuta si ningún punto de conexión controla la solicitud.
Trabajo con puertos
Cuando se crea una aplicación web con Visual Studio o dotnet new
, se crea un archivo Properties/launchSettings.json
que especifica los puertos a los que responde la aplicación. En los siguientes ejemplos de configuración de los puertos, al ejecutar la aplicación desde Visual Studio se devuelve un cuadro de diálogo de error Unable to connect to web server 'AppName'
. Visual Studio devuelve un error porque espera el puerto especificado en Properties/launchSettings.json
, pero la aplicación usa el puerto que especifica app.Run("http://localhost:3000")
. Ejecute los siguientes ejemplos de cambio de puertos desde la línea de comandos.
En las secciones siguientes se establece el puerto al que responde la aplicación.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
En el código anterior, la aplicación responde al puerto 3000
.
Varios puertos
En el código siguiente, la aplicación responde a los puertos 3000
y 4000
.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Establecimiento del puerto desde la línea de comandos
El siguiente comando hace que la aplicación responda al puerto 7777
:
dotnet run --urls="https://localhost:7777"
Si el punto de conexión Kestrel también está configurado en el archivo appsettings.json
, se usa la dirección URL especificada del archivo appsettings.json
. Para más información, vea Configuración del punto de conexión de Kestrel.
Lectura del puerto desde el entorno
El código siguiente lee el puerto desde el entorno:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
La manera preferida de establecer el puerto desde el entorno es usar la variable de entorno ASPNETCORE_URLS
, que se muestra en la sección siguiente.
Establecimiento de los puertos mediante la variable de entorno ASPNETCORE_URLS
La variable de entorno ASPNETCORE_URLS
está disponible para establecer el puerto:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
admite varias direcciones URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
Escucha en todas las interfaces
En los ejemplos siguientes se muestra la escucha en todas las interfaces.
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Escucha en todas las interfaces mediante ASPNETCORE_URLS
Los ejemplos anteriores pueden usar ASPNETCORE_URLS
.
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
Escucha en todas las interfaces mediante ASPNETCORE_HTTPS_PORTS
Los ejemplos anteriores pueden usar ASPNETCORE_HTTPS_PORTS
y ASPNETCORE_HTTP_PORTS
.
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
Para obtener más información, vea Configuración de puntos de conexión para el servidor web Kestrel de ASP.NET Core.
Especificación de HTTPS con certificado de desarrollo
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Para más información sobre el certificado de desarrollo, vea Confianza en el certificado de desarrollo HTTPS de ASP.NET Core en Windows y macOS.
Especificación de HTTPS mediante un certificado personalizado
En las secciones siguientes se muestra cómo especificar el certificado personalizado con el archivo appsettings.json
y mediante la configuración.
Especificación del certificado personalizado con appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Especificación del certificado personalizado mediante la configuración
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Uso de las API de certificado
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Lectura del entorno
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
Para más información sobre el uso de los entornos, consulte Uso de varios entornos en ASP.NET Core.
Configuración
El código siguiente lee del sistema de configuración:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
Para más información, consulte Configuración en ASP.NET Core.
Registro
El código siguiente escribe un mensaje en el registro al iniciar la aplicación:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
Para obtener más información, vea Registro en .NET Core y ASP.NET Core.
Acceso al contenedor de inserción de dependencias (DI)
En el código siguiente se muestra cómo obtener servicios del contenedor de DI durante el inicio de la aplicación:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
El siguiente programa muestra cómo acceder a las claves del contenedor de inserción de dependencias mediante el [FromKeyedServices]
atributo :
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
var app = builder.Build();
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
Para obtener más información sobre DI, consulte Inserción de dependencias en ASP.NET Core.
WebApplicationBuilder
Esta sección contiene código de ejemplo mediante WebApplicationBuilder.
Cambio de la raíz del contenido, el nombre de la aplicación y el entorno
El código siguiente establece la raíz del contenido, el nombre de la aplicación y el entorno:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder inicializa una nueva instancia de la clase WebApplicationBuilder con valores predeterminados preconfigurados.
Para obtener más información, consulte Información general de los conceptos básicos de ASP.NET Core.
Cambio de la raíz del contenido, del nombre de la aplicación y del entorno mediante variables de entorno o la línea de comandos
En la tabla siguiente se muestra la variable de entorno y el argumento de la línea de comandos usados para cambiar la raíz del contenido, el nombre de la aplicación y el entorno:
feature | Variable de entorno | Argumento de línea de comandos |
---|---|---|
Nombre de la aplicación | ASPNETCORE_APPLICATIONNAME | --applicationName |
Nombre del entorno | ASPNETCORE_ENVIRONMENT | --environment |
Raíz del contenido | ASPNETCORE_CONTENTROOT | --contentRoot |
Incorporación de proveedores de configuración
En el ejemplo siguiente se agrega el proveedor de configuración INI:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Para obtener información detallada, vea Proveedores de configuración de archivos en Configuración en ASP.NET Core.
Lectura de la configuración
De forma predeterminada, WebApplicationBuilder lee la configuración de varios orígenes, incluidos:
appSettings.json
yappSettings.{environment}.json
- Variables de entorno
- Línea de comandos
Para obtener una lista completa de orígenes de configuración leídos, vea Configuración predeterminada en Configuración en ASP.NET Core.
El código siguiente lee HelloKey
de la configuración y muestra el valor en el punto de conexión /
. Si el valor de configuración es null, "Hello" se asigna a message
:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Lectura del entorno
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Adición de proveedores de registro
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
Agrega servicios
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
Personalización de IHostBuilder
Se puede acceder a los métodos de extensión existentes en IHostBuilder mediante la propiedad de host:
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Personalización de IWebHostBuilder
Se puede acceder a los métodos de extensión en IWebHostBuilder mediante la propiedad WebApplicationBuilder.WebHost.
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
Cambio de la raíz web
De forma predeterminada, la raíz web guarda relación con la raíz de contenido de la carpeta wwwroot
. La raíz web es donde el middleware de archivos estáticos busca archivos estáticos. La raíz web se puede cambiar con WebHostOptions
, la línea de comandos o el método UseWebRoot:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Contenedor de inserción de dependencias (ID) personalizado
En el ejemplo siguiente se usa Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
Adición de middleware
Cualquier middleware de ASP.NET Core existente se puede configurar en WebApplication
:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
Para obtener más información, consulte Middleware de ASP.NET Core.
Página de excepciones para el desarrollador
WebApplication.CreateBuilder inicializa una nueva instancia de la clase WebApplicationBuilder con valores predeterminados preconfigurados. La página de excepciones para el desarrollador está habilitada en los valores predeterminados preconfigurados. Cuando se ejecuta el código siguiente en el entorno de desarrollo, la navegación a /
representa una página descriptiva que muestra la excepción.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
Middleware de ASP.NET Core
En la tabla siguiente se enumeran algunos de los middleware que se usan con frecuencia mediante API mínimas.
Software intermedio | Descripción | API |
---|---|---|
Autenticación | Proporciona compatibilidad con autenticación. | UseAuthentication |
Autorización | Proporciona compatibilidad con la autorización. | UseAuthorization |
CORS | Configura el uso compartido de recursos entre orígenes. | UseCors |
Controlador de excepciones | Controla globalmente las excepciones generadas por la canalización de middleware. | UseExceptionHandler |
Encabezados reenviados | Reenvía encabezados con proxy a la solicitud actual. | UseForwardedHeaders |
Redireccionamiento de HTTPS | Redirige todas las solicitudes HTTP a HTTPS. | UseHttpsRedirection |
Seguridad de transporte estricta de HTTP (HSTS) | Middleware de mejora de seguridad que agrega un encabezado de respuesta especial. | UseHsts |
Registro de solicitudes | Proporciona compatibilidad con el registro de solicitudes y respuestas HTTP. | UseHttpLogging |
Tiempos de espera de la solicitud | Proporciona asistencia para configurar los tiempos de espera de la solicitud, los valores predeterminados globales y por punto de conexión. | UseRequestTimeouts |
Registro de solicitudes W3C | Proporciona compatibilidad con el registro de solicitudes y respuestas HTTP en el formato W3C. | UseW3CLogging |
Almacenamiento en caché de respuestas | Proporciona compatibilidad con la captura de respuestas. | UseResponseCaching |
Compresión de respuesta | Proporciona compatibilidad con la compresión de respuestas. | UseResponseCompression |
Sesión | Proporciona compatibilidad con la administración de sesiones de usuario. | UseSession |
Archivos estáticos | Proporciona compatibilidad con la proporción de archivos estáticos y la exploración de directorios. | UseStaticFiles, UseFileServer |
WebSockets | Habilita el protocolo WebSockets. | UseWebSockets |
En las secciones siguientes se trata la gestión de solicitudes: enrutamiento, enlace de parámetros y respuestas.
Enrutamiento
Un elemento WebApplication
configurado admite Map{Verb}
y MapMethods donde {Verb}
es un método HTTP con mayúsculas y minúsculas Camel, como Get
, Post
, Put
o Delete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
Los argumentos Delegate que se pasan a estos métodos se denominan "controladores de ruta".
Controladores de ruta
Los controladores de ruta son métodos que se ejecutan cuando coincide una ruta. Los controladores de ruta pueden ser una expresión lambda, una función local, un método de instancia o un método estático. Los controladores de ruta pueden ser sincrónicos o asincrónicos.
Expresión lambda
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
Función local
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Método de instancia
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Método estático
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Punto de conexión definido fuera de Program.cs
.
Las API mínimas no tienen que encontrarse en Program.cs
.
Program.cs
using MinAPISeparateFile;
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}
Consulte también Grupos de rutas más adelante en este artículo.
Puntos de conexión con nombre y generación de vínculos
Se pueden asignar nombres a los puntos de conexión para generar direcciones URL al punto de conexión. El uso de un punto de conexión con nombre evita tener que codificar las rutas de acceso de forma rígida en una aplicación:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
El código anterior muestra The link to the hello route is /hello
desde el punto de conexión /
.
NOTA: Los nombres de punto de conexión distinguen mayúsculas de minúsculas.
Nombres de punto de conexión:
- Debe ser único globalmente.
- Se usan como identificador de operación de OpenAPI cuando se habilita la compatibilidad con OpenAPI. Para obtener más información, consulte OpenAPI.
Parámetros de ruta
Los parámetros de ruta se pueden capturar como parte de la definición del patrón de ruta:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
El código anterior devuelve The user id is 3 and book id is 7
a partir del URI /users/3/books/7
.
El controlador de rutas puede declarar los parámetros que se capturan. Cuando se realiza una solicitud en una ruta con parámetros declarados para la captura, los parámetros se analizan y se pasan al controlador. Esto facilita la captura de los valores en un método con seguridad de tipos. En el código anterior, userId
y bookId
son int
.
En el código anterior, si alguno de los valores de ruta no se puede convertir en int
, se produce una excepción. La solicitud GET /users/hello/books/3
produce la siguiente excepción:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Rutas con carácter comodín y de captura total
La siguiente ruta de captura total devuelve Routing to hello
del punto de conexión "/posts/hello":
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Restricciones de ruta
Las restricciones de ruta restringen el comportamiento de coincidencia de una ruta.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
En la tabla siguiente se muestran las plantillas de ruta anteriores y su comportamiento:
Plantilla de ruta | URI coincidente de ejemplo |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
Para más información, vea Referencia de restricción de ruta en Enrutamiento en ASP.NET Core.
Grupos de rutas
El método de extensión MapGroup ayuda a organizar grupos de puntos de conexión con un prefijo común. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata, que agregan metadatos de punto de conexión.
Por ejemplo, el código siguiente crea dos grupos similares de puntos de conexión:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
En este escenario, puede usar una dirección relativa para el encabezado Location
en el resultado 201 Created
:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
El primer grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /public/todos
y que sean accesibles sin autenticación. El segundo grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /private/todos
y que requieran autenticación.
La fábrica de filtros de punto de conexiónQueryPrivateTodos
es una función local que modifica los parámetros TodoDb
del controlador de ruta para permitir el acceso y almacenar datos privados de tareas pendientes.
Los grupos de rutas también admiten grupos anidados y patrones de prefijo complejos con parámetros y restricciones de ruta. En el ejemplo siguiente, y el controlador de rutas asignado al grupo user
puede capturar los parámetros de ruta {org}
y {group}
definidos en los prefijos del grupo externo.
El prefijo también puede estar vacío. Esto puede ser útil para agregar metadatos de punto de conexión o filtros a un grupo de puntos de conexión sin cambiar el patrón de ruta.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
La adición de filtros o metadatos a un grupo se comporta del mismo modo que la adición individual a cada punto de conexión antes de agregar filtros o metadatos adicionales que quizás se hayan agregado a un grupo interno o a un punto de conexión específico.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
En el ejemplo anterior, el filtro externo registrará la solicitud entrante antes que el filtro interno aunque se haya agregado en segundo lugar. Dado que los filtros se aplicaron a diferentes grupos, el orden en que se agregaron el uno con respecto al otro no es importante. El orden en que se agregan los filtros es importante si se aplican al mismo grupo o punto de conexión específico.
Una solicitud a /outer/inner/
registrará lo siguiente:
/outer group filter
/inner group filter
MapGet filter
Enlace de parámetros
El enlace de parámetros es el proceso de convertir los datos de solicitud en parámetros fuertemente tipados que se expresan mediante controladores de ruta. Un origen de enlace determina desde dónde se enlazan los parámetros. Los orígenes de enlace pueden ser explícitos o inferidos en función del método HTTP y el tipo de parámetro.
Orígenes de enlace admitidos:
- Valores de ruta
- Cadena de consulta
- Encabezado
- Cuerpo (como JSON)
- Valores de formulario
- Servicios proporcionados por la inserción de dependencias
- Personalizado
En el siguiente GET
, el controlador de ruta usa algunos de estos orígenes de enlace de parámetros:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
En la tabla siguiente se muestra la relación entre los parámetros utilizados en el ejemplo anterior y los orígenes de enlace asociados.
Parámetro | Origen de enlace |
---|---|
id |
valor de ruta |
page |
cadena de consulta |
customHeader |
header |
service |
Proporcionado por la inserción de dependencias |
Los métodos HTTP GET
, HEAD
, OPTIONS
y DELETE
no se enlazan implícitamente desde el cuerpo. Para enlazar desde el cuerpo (como JSON) para estos métodos HTTP, enlace explícitamente con [FromBody]
o lea desde HttpRequest.
En el siguiente ejemplo, el controlador de ruta POST usa un origen de cuerpo de enlace (como JSON) para el parámetro person
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Los parámetros de los ejemplos anteriores se enlazan automáticamente a partir de los datos de solicitud. Para demostrar la comodidad que proporciona el enlace de parámetros, los siguientes controladores de ruta muestran cómo leer los datos de solicitud directamente desde la solicitud:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
Enlace de parámetros explícitos
Los atributos se pueden usar para declarar explícitamente desde dónde se enlazan los parámetros.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
Parámetro | Origen de enlace |
---|---|
id |
valor de ruta con el nombre id |
page |
cadena de consulta con el nombre "p" |
service |
Proporcionado por la inserción de dependencias |
contentType |
encabezado con el nombre "Content-Type" |
Enlace explícito a partir de valores de formulario
El atributo [FromForm]
enlaza los valores de formulario:
app.MapPost("/todos", async ([FromForm] string name,
[FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
var todo = new Todo
{
Name = name,
Visibility = visibility
};
if (attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await attachment.CopyToAsync(stream);
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
Una alternativa es usar el atributo [AsParameters]
con un tipo personalizado que tenga propiedades anotadas con [FromForm]
. Por ejemplo, el siguiente código enlaza valores de formulario a propiedades de la estructura de registro NewTodoRequest
:
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
var todo = new Todo
{
Name = request.Name,
Visibility = request.Visibility
};
if (request.Attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await request.Attachment.CopyToAsync(stream);
todo.Attachment = attachmentName;
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
[FromForm] Visibility Visibility, IFormFile? Attachment);
Para obtener más información, vea la sección AsParameters más adelante en este artículo.
El código de ejemplo completo está en el repositorio AspNetCore.Docs.Samples.
Enlace seguro desde IFormFile e IFormFileCollection
El enlace de formulario complejo se admite mediante IFormFile y IFormFileCollection mediante el [FromForm]
:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
([FromForm] FileUploadForm fileUploadForm, HttpContext context,
IAntiforgery antiforgery) =>
{
await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
fileUploadForm.Name!, app.Environment.ContentRootPath);
return TypedResults.Ok($"Your file with the description:" +
$" {fileUploadForm.Description} has been uploaded successfully");
});
app.Run();
Los parámetros enlazados a la solicitud con [FromForm]
incluyen un token antifalsificación. El token antifalsificación se valida cuando se procesa la solicitud. Para más información, consulte Antifalsificación con API mínima.
Para obtener más información, consulte Enlace de formularios en API mínimas.
El código de ejemplo completo está en el repositorio AspNetCore.Docs.Samples.
Enlace de parámetros con inserción de dependencias
El enlace de parámetros para las API mínimas enlaza parámetros mediante la inserción de dependencias cuando el tipo está configurado como servicio. No es necesario aplicar explícitamente el atributo [FromServices]
a un parámetro. En el código siguiente, ambas acciones devuelven la hora:
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
var app = builder.Build();
app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
Parámetros opcionales
Los parámetros declarados en controladores de ruta se tratan como obligatorios:
- Si una solicitud coincide con la ruta, el controlador de rutas solo se ejecuta si se proporcionan todos los parámetros necesarios en la solicitud.
- Si no se proporcionan todos los parámetros necesarios, se producirá un error.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
BadHttpRequestException : no se proporcionó el parámetro necesario "int pageNumber" de la cadena de consulta. |
/products/1 |
Error HTTP 404, no se encuentra ninguna ruta coincidente |
Para que pageNumber
sea opcional, defina el tipo como opcional o proporcione un valor predeterminado:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";
app.MapGet("/products2", ListProducts);
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
1 devuelto |
/products2 |
1 devuelto |
El valor predeterminado y que admite un valor NULL anterior se aplica a todos los orígenes:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
El código anterior llama al método con un producto NULL si no se envía ningún cuerpo de la solicitud.
NOTA: Si se proporcionan datos no válidos y el parámetro admite un valor NULL, el controlador de rutas no se ejecuta.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
1 devuelto |
/products?pageNumber=two |
BadHttpRequestException : error al enlazar el parámetro "Nullable<int> pageNumber" a partir de "two". |
/products/two |
Error HTTP 404, no se encuentra ninguna ruta coincidente |
Vea la sección Errores de enlace para más información.
Tipos especiales
Los siguientes tipos se enlazan sin atributos explícitos:
HttpContext: contexto que contiene toda la información sobre la solicitud o respuesta HTTP actual:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest y HttpResponse: solicitud HTTP y respuesta HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken: token de cancelación asociado a la solicitud HTTP actual:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: usuario asociado a la solicitud, enlazado desde HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Enlazar el cuerpo de la solicitud como Stream
o PipeReader
El cuerpo de la solicitud puede enlazarse como Stream
o PipeReader
para admitir sin problemas los escenarios en los que el usuario tiene que procesar datos y:
- Almacenar los datos en Blob Storage o ponerlos en cola en un proveedor de colas.
- Procesar los datos almacenados con un proceso de trabajo o una función en la nube.
Por ejemplo, los datos pueden ponerse en cola en Azure Queue Storage o almacenarse en Azure Blob Storage.
El código siguiente implementa una cola en segundo plano:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
El código siguiente enlaza el cuerpo de la solicitud a un objeto Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
El código siguiente muestra el archivo Program.cs
completo:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- Cuando se leen datos,
Stream
es el mismo objeto queHttpRequest.Body
. - El cuerpo de la solicitud no se almacena en búfer de forma predeterminada. Una vez leído el cuerpo, no se puede rebobinar. La secuencia no se puede leer varias veces.
Stream
yPipeReader
no se pueden usar fuera del controlador de acciones mínimas, ya que los búferes subyacentes se eliminarán o reutilizarán.
Cargas de archivos mediante IFormFile e IFormFileCollection
El código siguiente usa IFormFile y IFormFileCollection para cargar el archivo:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Las solicitudes de carga de archivos autenticadas se admiten mediante un encabezado de autorización, un certificado de cliente o un cookie encabezado.
Enlace a formularios con IFormCollection, IFormFile e IFormFileCollection
Se admite el enlace desde los parámetros basados en formularios mediante IFormCollection, IFormFile y IFormFileCollection. Los metadatos de OpenAPI se deducen para que los parámetros de formulario admitan la integración con la interfaz de usuario de Swagger.
El código siguiente carga archivos mediante el enlace inferido del tipo IFormFile
:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg,
.jpeg, .png" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>,
BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
app.Run();
Advertencia: Al implementar formularios, la aplicación debe evitar los ataques de falsificación de solicitud entre sitios (XSRF/CSRF). En el código anterior, se usa el servicio IAntiforgery para evitar los ataques XSRF mediante la generación y validación de un token antifalsificación:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg,
.jpeg, .png" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>,
BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
app.Run();
Para más información sobre los ataques XSRF, consulte Antifalsificación con API mínimas
Para obtener más información, consulte Enlace de formularios en API mínimas;
Enlazar a colecciones y tipos complejos desde los formularios
El enlace es compatible con:
- Colecciones, por ejemplo, Lista y Diccionario
- Tipos complejos, por ejemplo,
Todo
oProject
El código siguiente muestra:
- Un punto de conexión mínimo que enlaza una entrada de formulario de varias partes a un objeto complejo.
- Cómo usar los servicios antifalsificación para admitir la generación y validación de tokens antifalsificación.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html><body>
<form action="/todo" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}"
type="hidden" value="{token.RequestToken}" />
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="checkbox" name="isCompleted" value="true" />
<input type="submit" />
<input name="isCompleted" type="hidden" value="false" />
</form>
</body></html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>>
([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
try
{
await antiforgery.ValidateRequestAsync(context);
return TypedResults.Ok(todo);
}
catch (AntiforgeryValidationException e)
{
return TypedResults.BadRequest("Invalid antiforgery token");
}
});
app.Run();
class Todo
{
public string Name { get; set; } = string.Empty;
public bool IsCompleted { get; set; } = false;
public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}
En el código anterior:
- El parámetro de destino debe anotarse con el atributo
[FromForm]
para eliminar la ambigüedad de los parámetros que se deben leer del cuerpo JSON. - El enlace de tipos complejos o de colección no es compatible con las API mínimas que se compilan con el generador de delegados de solicitud.
- El marcado muestra una entrada oculta adicional con un nombre de
isCompleted
y un valor defalse
. Si la casillaisCompleted
está activada cuando se envía el formulario, los valorestrue
yfalse
se envían como valores. Si la casilla de verificación no está activada, solo se envía el valor de entrada ocultofalse
. El proceso de vinculación de modelos de ASP.NET Core solo lee el primer valor cuando se vincula a un valorbool
, lo que da como resultadotrue
para casillas de verificación marcadas yfalse
para casillas de verificación sin marcar.
Un ejemplo de los datos de formulario enviados al punto de conexión anterior tiene el siguiente aspecto:
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
Enlace de matrices y valores de cadena desde encabezados y cadenas de consulta
En el código siguiente se muestra cómo enlazar cadenas de consulta a una matriz de tipos primitivos, matrices de cadena y StringValues:
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
El enlace de cadenas de consulta o valores de encabezado a una matriz de tipos complejos se admite cuando el tipo tiene TryParse
implementado. El código siguiente se enlaza a una matriz de cadena y devuelve todos los elementos con las etiquetas especificadas:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
En el código siguiente se muestra el modelo y la implementación de TryParse
necesaria:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
El código siguiente se enlaza a una matriz int
:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Para probar el código anterior, agregue el siguiente punto de conexión para rellenar la base de datos con elementos Todo
:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Use una herramienta como HttpRepl
para pasar los datos siguientes al punto de conexión anterior:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
El código siguiente se enlaza a la clave de encabezado X-Todo-Id
y devuelve los elementos Todo
con valores Id
coincidentes:
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Nota
Al enlazar un objeto string[]
desde una cadena de consulta, la ausencia de cualquier valor de cadena de consulta coincidente dará como resultado una matriz vacía en lugar de un valor NULL.
Enlace de parámetros para listas de argumentos con [AsParameters]
AsParametersAttribute permite el enlace de parámetros simple a tipos y no a un enlace de modelo complejo o recursivo.
Observe el código siguiente:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
Tenga en cuenta el siguiente punto de conexión GET
:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
Se puede usar lo siguiente struct
para reemplazar los parámetros resaltados anteriores:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
El punto de conexión GET
refactorizado usa el anterior struct
con el atributo AsParameters:
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
En el código siguiente se muestran puntos de conexión adicionales en la aplicación:
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Las siguientes clases se usan para refactorizar las listas de parámetros:
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
En el código siguiente se muestran los puntos de conexión refactorizados mediante AsParameters
y las clases y anteriores struct
:
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Los siguientes tipos record
se pueden usar para reemplazar los parámetros anteriores:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
El uso de struct
con AsParameters
puede ser más eficaz que usar un tipo record
.
El código de ejemplo completo en el repositorio AspNetCore.Docs.Samples.
Enlace personalizado
Hay dos maneras de personalizar el enlace de parámetros:
- Para los orígenes de enlace de ruta, consulta y encabezado, enlace tipos personalizados mediante la adición de un método
TryParse
estático para el tipo. - Controle el proceso de enlace mediante la implementación de un método
BindAsync
en un tipo.
TryParse
TryParse
tiene dos API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
El código siguiente muestra Point: 12.3, 10.1
con el URI /map?Point=12.3,10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
tiene las siguientes API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
El código siguiente muestra SortBy:xyz, SortDirection:Desc, CurrentPage:99
con el URI /products?SortBy=xyz&SortDir=Desc&Page=99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
Errores de enlace
Cuando se produce un error en el enlace, el marco registra un mensaje de depuración y devuelve varios códigos de estado al cliente en función del modo de error.
Modo de error | Tipo de parámetro que admite un valor NULL | Origen de enlace | status code |
---|---|---|---|
{ParameterType}.TryParse devuelve false . |
sí | ruta/consulta/encabezado | 400 |
{ParameterType}.BindAsync devuelve null . |
sí | custom | 400 |
{ParameterType}.BindAsync genera |
No importa | custom | 500 |
Error al deserializar el cuerpo JSON | No importa | body | 400 |
Tipo de contenido incorrecto (no application/json ) |
No importa | body | 415 |
Prioridad de enlace
Reglas para determinar un origen de enlace a partir de un parámetro:
- Atributo explícito definido en el parámetro (atributos From*) en el orden siguiente:
- Valores de ruta:
[FromRoute]
- Cadena de consulta:
[FromQuery]
- Encabezado:
[FromHeader]
- Cuerpo:
[FromBody]
- Formulario:
[FromForm]
- Servicio:
[FromServices]
- Valores de parámetros:
[AsParameters]
- Valores de ruta:
- Tipos especiales
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormCollection
(HttpContext.Request.Form
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- El tipo de parámetro tiene un método
BindAsync
estático válido. - El tipo de parámetro es una cadena o tiene un método
TryParse
estático válido.- Si el nombre del parámetro existe en la plantilla de ruta, por ejemplo,
app.Map("/todo/{id}", (int id) => {});
, se enlaza desde la ruta. - Se enlaza a partir de la cadena de consulta.
- Si el nombre del parámetro existe en la plantilla de ruta, por ejemplo,
- Si el tipo de parámetro es un servicio proporcionado por la inserción de dependencias, usa ese servicio como origen.
- El parámetro procede del cuerpo.
Configuración de opciones de deserialización de JSON para enlace de cuerpo
El origen de enlace de cuerpo usa System.Text.Json para la deserialización. No es posible cambiar este valor predeterminado, pero se pueden configurar las opciones de serialización y deserialización de JSON.
Configuración global de las opciones de deserialización de JSON
Las opciones que se aplican globalmente a una aplicación se pueden configurar invocando ConfigureHttpJsonOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Dado que el código de ejemplo configura la serialización y la deserialización, puede leer NameField
e incluir NameField
en el JSON de salida.
Configuración de las opciones de deserialización de JSON para un punto de conexión
ReadFromJsonAsync tiene sobrecargas que aceptan un objeto JsonSerializerOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Dado que el código anterior tan solo aplica las opciones personalizadas a la deserialización, el JSON de salida excluye NameField
.
Lectura del cuerpo de la solicitud
Lea el cuerpo de la solicitud directamente mediante un parámetro HttpContext o HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
El código anterior:
- Tiene acceso al cuerpo de la solicitud mediante HttpRequest.BodyReader.
- Copia el cuerpo de la solicitud en un archivo local.
Respuestas
Los controladores de ruta admiten los siguientes tipos de valores devueltos:
- Basado en
IResult
: incluyeTask<IResult>
yValueTask<IResult>
. string
: incluyeTask<string>
yValueTask<string>
.T
(cualquier otro tipo): incluyeTask<T>
yValueTask<T>
.
Valor devuelto | Comportamiento | Content-Type |
---|---|---|
IResult |
El marco llama a IResult.ExecuteAsync | Decidido por la implementación de IResult |
string |
El marco escribe la cadena directamente en la respuesta | text/plain |
T (cualquier otro tipo) |
El marco JSON serializa la respuesta | application/json |
Para obtener una guía más detallada sobre los valores devueltos del controlador de rutas, consulte Creación de respuestas en aplicaciones de API mínimas.
Valores devueltos de ejemplo
valores devueltos de cadena
app.MapGet("/hello", () => "Hello World");
valores devueltos JSON
app.MapGet("/hello", () => new { Message = "Hello World" });
Devolución de TypedResults
El código siguiente devuelve un TypedResults:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Devolver TypedResults
es preferible a devolver Results. Para más información, consulte TypedResults frente a Results.
valores devueltos IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
En el ejemplo siguiente se usan los tipos de resultados integrados para personalizar la respuesta:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Código de estado personalizado
app.MapGet("/405", () => Results.StatusCode(405));
Texto
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
Consulte Creación de respuestas en aplicaciones de API mínimas para ver más ejemplos.
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Archivo
app.MapGet("/download", () => Results.File("myfile.text"));
Resultados integrados
Existen asistentes de resultados comunes en las clases estáticas Results y TypedResults. Devolver TypedResults
es preferible a devolver Results
. Para más información, consulte TypedResults frente a Results.
Personalización de resultados
Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Resultados con tipo
La interfaz IResult puede representar los valores devueltos de las API mínimas que no usan la compatibilidad implícita con JSON para serializar el objeto devuelto en la respuesta HTTP. La clase estática Results se usa para crear distintos objetos IResult
que representan diferentes tipos de respuestas. Por ejemplo, establecer el código de estado de respuesta o redirigir a otra dirección URL.
Los tipos que implementan IResult
son públicos, lo que permite las aserciones de tipos al realizar pruebas. Por ejemplo:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
Puede ver los tipos de valor devueltos de los métodos correspondientes en la clase TypedResults estática para buscar el tipo público IResult
correcto al que se va a convertir.
Consulte Creación de respuestas en aplicaciones de API mínimas para ver más ejemplos.
Filters
Para obtener más información, consulte Filtros en las aplicaciones de API mínimas.
Authorization
Las rutas se pueden proteger mediante directivas de autorización. Se pueden declarar mediante el atributo [Authorize]
o con el método RequireAuthorization:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
El código anterior se puede escribir con RequireAuthorization:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
En el ejemplo siguiente se usa la autorización basada en directivas:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
Permitir que los usuarios no autenticados accedan a un punto de conexión
[AllowAnonymous]
permite a los usuarios no autenticados acceder a los puntos de conexión:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Las rutas se pueden habilitar para CORS mediante directivas de CORS. CORS se puede declarar mediante el atributo [EnableCors]
o con el método RequireCors. Los ejemplos siguientes habilitan CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
Para obtener más información, vea Habilitar solicitudes entre orígenes (CORS) en ASP.NET Core.
ValidateScopes y ValidateOnBuild
ValidateScopes y ValidateOnBuild están habilitados de forma predeterminada en el entorno de desarrollo, pero deshabilitados en otros entornos.
Cuando ValidateOnBuild
es true
, el contenedor de inserción de dependencias valida la configuración del servicio en tiempo de compilación. Si la configuración del servicio no es válida, se produce un error en la compilación en el inicio de la aplicación, en lugar de en tiempo de ejecución cuando se solicita el servicio.
Cuando ValidateScopes
es true
, el contenedor de DI valida que un servicio con ámbito no se resuelve desde el ámbito raíz. La resolución de un servicio con ámbito desde el ámbito raíz puede provocar una pérdida de memoria porque los servicios se conservan en la memoria más tiempo que el ámbito de la solicitud.
ValidateScopes
y ValidateOnBuild
son false de forma predeterminada en modos que no son de desarrollo por motivos de rendimiento.
El código siguiente muestra ValidateScopes
que está habilitado de forma predeterminada en modo de desarrollo, pero deshabilitado en modo de versión:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<MyScopedService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
}
else
{
Console.WriteLine("Release environment");
}
app.MapGet("/", context =>
{
// Intentionally getting service provider from app, not from the request
// This causes an exception from attempting to resolve a scoped service
// outside of a scope.
// Throws System.InvalidOperationException:
// 'Cannot resolve scoped service 'MyScopedService' from root provider.'
var service = app.Services.GetRequiredService<MyScopedService>();
return context.Response.WriteAsync("Service resolved");
});
app.Run();
public class MyScopedService { }
El código siguiente muestra ValidateOnBuild
que está habilitado de forma predeterminada en modo de desarrollo, pero deshabilitado en modo de versión:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();
// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
}
else
{
Console.WriteLine("Release environment");
}
app.MapGet("/", context =>
{
var service = context.RequestServices.GetRequiredService<MyScopedService>();
return context.Response.WriteAsync("Service resolved correctly!");
});
app.Run();
public class MyScopedService { }
public class AnotherService
{
public AnotherService(BrokenService brokenService) { }
}
public class BrokenService { }
El código siguiente deshabilita ValidateScopes
y ValidateOnBuild
en Development
:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
// Doesn't detect the validation problems because ValidateScopes is false.
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = false;
options.ValidateOnBuild = false;
});
}
Consulte también
- Referencia rápida de las API mínimas
- Generación de documentos de OpenAPI
- Creación de respuestas en aplicaciones de API mínimas
- Filtros en las aplicaciones de API mínimas
- Control de errores en API mínimas
- Autenticación y autorización en API mínimas
- Prueba de las aplicaciones de API mínimas
- Enrutamiento de cortocircuito
- Identity Puntos de conexión de API
- Compatibilidad con contenedores de inserción de dependencias del servicio con claves
- Un vistazo en segundo plano de los puntos de conexión de API mínimos
- Organizar las API mínimas de ASP.NET Core
- Discusión de validación de Fluent en GitHub
Este documento:
- Proporciona una referencia rápida para las API mínimas.
- Está pensado para desarrolladores experimentados. Para consultar una introducción, vea Tutorial: Creación de una API mínima con ASP.NET Core.
Las API mínimas constan de lo siguiente:
WebApplication
Una plantilla de ASP.NET Core genera el código siguiente:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
El código anterior se puede crear a través de dotnet new web
en la línea de comandos o seleccionando la plantilla web vacía en Visual Studio.
El código siguiente crea una clase WebApplication (app
) sin crear explícitamente una clase WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
inicializa una nueva instancia de la clase WebApplication con valores predeterminados preconfigurados.
WebApplication
agrega automáticamente el siguiente middleware en Minimal API applications
en función de determinadas condiciones:
UseDeveloperExceptionPage
se agrega primero cuandoHostingEnvironment
es"Development"
.UseRouting
se agrega en segundo lugar si el código de usuario aún no llamóUseRouting
a y si hay puntos de conexión configurados, por ejemploapp.MapGet
.UseEndpoints
se agrega al final de la canalización de middleware si hay algún punto de conexión configurado.UseAuthentication
se agrega inmediatamente después deUseRouting
si el código de usuario no llamó aún aUseAuthentication
y siIAuthenticationSchemeProvider
se puede detectar en el proveedor de servicios.IAuthenticationSchemeProvider
se agrega de forma predeterminada cuando se usaAddAuthentication
, y los servicios se detectan medianteIServiceProviderIsService
.UseAuthorization
se agrega a continuación si el código de usuario no llamó aún aUseAuthorization
y siIAuthorizationHandlerProvider
se puede detectar en el proveedor de servicios.IAuthorizationHandlerProvider
se agrega de forma predeterminada cuando se usaAddAuthorization
, y los servicios se detectan medianteIServiceProviderIsService
.- El middleware y los puntos de conexión configurados por el usuario se agregan entre
UseRouting
yUseEndpoints
.
El código siguiente es eficazmente lo que produce el middleware automático que se agrega a la aplicación:
if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
if (isAuthenticationConfigured)
{
app.UseAuthentication();
}
if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints
app.UseEndpoints(e => {});
En algunos casos, la configuración predeterminada del middleware no es correcta para la aplicación y requiere modificación. Por ejemplo, UseCors se debe llamar a antes UseAuthentication de y UseAuthorization. La aplicación debe llamar a UseAuthentication
y UseAuthorization
si se llama a UseCors
:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
Si se debe ejecutar el middleware antes de que se produzca la coincidencia de rutas, se debe llamar a UseRouting y se debe colocar el middleware antes de la llamada a UseRouting
. UseEndpoints no es necesario en este caso, ya que se agrega automáticamente como se ha descrito anteriormente:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
Al agregar un middleware de terminal:
- El middleware debe agregarse después de
UseEndpoints
. - La aplicación debe llamar
UseRouting
a yUseEndpoints
para que el middleware de terminal se pueda colocar en la ubicación correcta.
app.UseRouting();
app.MapGet("/", () => "hello world");
app.UseEndpoints(e => {});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
El middleware de terminal es un middleware que se ejecuta si ningún punto de conexión controla la solicitud.
Trabajo con puertos
Cuando se crea una aplicación web con Visual Studio o dotnet new
, se crea un archivo Properties/launchSettings.json
que especifica los puertos a los que responde la aplicación. En los siguientes ejemplos de configuración de los puertos, al ejecutar la aplicación desde Visual Studio se devuelve un cuadro de diálogo de error Unable to connect to web server 'AppName'
. Visual Studio devuelve un error porque espera el puerto especificado en Properties/launchSettings.json
, pero la aplicación usa el puerto que especifica app.Run("http://localhost:3000")
. Ejecute los siguientes ejemplos de cambio de puertos desde la línea de comandos.
En las secciones siguientes se establece el puerto al que responde la aplicación.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
En el código anterior, la aplicación responde al puerto 3000
.
Varios puertos
En el código siguiente, la aplicación responde a los puertos 3000
y 4000
.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Establecimiento del puerto desde la línea de comandos
El siguiente comando hace que la aplicación responda al puerto 7777
:
dotnet run --urls="https://localhost:7777"
Si el punto de conexión Kestrel también está configurado en el archivo appsettings.json
, se usa la dirección URL especificada del archivo appsettings.json
. Para más información, vea Configuración del punto de conexión de Kestrel.
Lectura del puerto desde el entorno
El código siguiente lee el puerto desde el entorno:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
La manera preferida de establecer el puerto desde el entorno es usar la variable de entorno ASPNETCORE_URLS
, que se muestra en la sección siguiente.
Establecimiento de los puertos mediante la variable de entorno ASPNETCORE_URLS
La variable de entorno ASPNETCORE_URLS
está disponible para establecer el puerto:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
admite varias direcciones URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
Para más información sobre el uso de los entornos, consulte Uso de varios entornos en ASP.NET Core.
Escucha en todas las interfaces
En los ejemplos siguientes se muestra la escucha en todas las interfaces.
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Escucha en todas las interfaces mediante ASPNETCORE_URLS
Los ejemplos anteriores pueden usar ASPNETCORE_URLS
.
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
Especificación de HTTPS con certificado de desarrollo
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Para más información sobre el certificado de desarrollo, vea Confianza en el certificado de desarrollo HTTPS de ASP.NET Core en Windows y macOS.
Especificación de HTTPS mediante un certificado personalizado
En las secciones siguientes se muestra cómo especificar el certificado personalizado con el archivo appsettings.json
y mediante la configuración.
Especificación del certificado personalizado con appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Especificación del certificado personalizado mediante la configuración
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Uso de las API de certificado
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Configuración
El código siguiente lee del sistema de configuración:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
Para más información, consulte Configuración en ASP.NET Core.
Registro
El código siguiente escribe un mensaje en el registro al iniciar la aplicación:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
Para obtener más información, vea Registro en .NET Core y ASP.NET Core.
Acceso al contenedor de inserción de dependencias (DI)
En el código siguiente se muestra cómo obtener servicios del contenedor de DI durante el inicio de la aplicación:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
Para más información, consulte Inserción de dependencias en ASP.NET Core.
WebApplicationBuilder
Esta sección contiene código de ejemplo mediante WebApplicationBuilder.
Cambio de la raíz del contenido, el nombre de la aplicación y el entorno
El código siguiente establece la raíz del contenido, el nombre de la aplicación y el entorno:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder inicializa una nueva instancia de la clase WebApplicationBuilder con valores predeterminados preconfigurados.
Para obtener más información, consulte Información general de los conceptos básicos de ASP.NET Core.
Cambio de la raíz del contenido, el nombre de la aplicación y el entorno mediante variables de entorno o la línea de comandos
En la tabla siguiente se muestra la variable de entorno y el argumento de la línea de comandos usados para cambiar la raíz del contenido, el nombre de la aplicación y el entorno:
feature | Variable de entorno | Argumento de línea de comandos |
---|---|---|
Nombre de la aplicación | ASPNETCORE_APPLICATIONNAME | --applicationName |
Nombre del entorno | ASPNETCORE_ENVIRONMENT | --environment |
Raíz del contenido | ASPNETCORE_CONTENTROOT | --contentRoot |
Incorporación de proveedores de configuración
En el ejemplo siguiente se agrega el proveedor de configuración INI:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Para obtener información detallada, vea Proveedores de configuración de archivos en Configuración en ASP.NET Core.
Lectura de la configuración
De forma predeterminada, WebApplicationBuilder lee la configuración de varios orígenes, incluidos:
appSettings.json
yappSettings.{environment}.json
- Variables de entorno
- Línea de comandos
El código siguiente lee HelloKey
de la configuración y muestra el valor en el punto de conexión /
. Si el valor de configuración es null, "Hello" se asigna a message
:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Para obtener una lista completa de orígenes de configuración leídos, vea Configuración predeterminada en Configuración en ASP.NET Core.
Adición de proveedores de registro
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
Agrega servicios
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
Personalización de IHostBuilder
Se puede acceder a los métodos de extensión existentes en IHostBuilder mediante la propiedad de host:
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Personalización de IWebHostBuilder
Se puede acceder a los métodos de extensión en IWebHostBuilder mediante la propiedad WebApplicationBuilder.WebHost.
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
Cambio de la raíz web
De forma predeterminada, la raíz web guarda relación con la raíz de contenido de la carpeta wwwroot
. La raíz web es donde el middleware de archivos estáticos busca archivos estáticos. La raíz web se puede cambiar con WebHostOptions
, la línea de comandos o el método UseWebRoot:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Contenedor de inserción de dependencias (ID) personalizado
En el ejemplo siguiente se usa Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
Adición de middleware
Cualquier middleware de ASP.NET Core existente se puede configurar en WebApplication
:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
Para obtener más información, consulte Middleware de ASP.NET Core.
Página de excepciones para el desarrollador
WebApplication.CreateBuilder inicializa una nueva instancia de la clase WebApplicationBuilder con valores predeterminados preconfigurados. La página de excepciones para el desarrollador está habilitada en los valores predeterminados preconfigurados. Cuando se ejecuta el código siguiente en el entorno de desarrollo, la navegación a /
representa una página descriptiva que muestra la excepción.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
Middleware de ASP.NET Core
En la tabla siguiente se enumeran algunos de los middleware que se usan con frecuencia mediante API mínimas.
Software intermedio | Descripción | API |
---|---|---|
Autenticación | Proporciona compatibilidad con autenticación. | UseAuthentication |
Autorización | Proporciona compatibilidad con la autorización. | UseAuthorization |
CORS | Configura el uso compartido de recursos entre orígenes. | UseCors |
Controlador de excepciones | Controla globalmente las excepciones generadas por la canalización de middleware. | UseExceptionHandler |
Encabezados reenviados | Reenvía encabezados con proxy a la solicitud actual. | UseForwardedHeaders |
Redireccionamiento de HTTPS | Redirige todas las solicitudes HTTP a HTTPS. | UseHttpsRedirection |
Seguridad de transporte estricta de HTTP (HSTS) | Middleware de mejora de seguridad que agrega un encabezado de respuesta especial. | UseHsts |
Registro de solicitudes | Proporciona compatibilidad con el registro de solicitudes y respuestas HTTP. | UseHttpLogging |
Tiempos de espera de la solicitud | Proporciona asistencia para configurar los tiempos de espera de la solicitud, los valores predeterminados globales y por punto de conexión. | UseRequestTimeouts |
Registro de solicitudes W3C | Proporciona compatibilidad con el registro de solicitudes y respuestas HTTP en el formato W3C. | UseW3CLogging |
Almacenamiento en caché de respuestas | Proporciona compatibilidad con la captura de respuestas. | UseResponseCaching |
Compresión de respuesta | Proporciona compatibilidad con la compresión de respuestas. | UseResponseCompression |
Sesión | Proporciona compatibilidad con la administración de sesiones de usuario. | UseSession |
Archivos estáticos | Proporciona compatibilidad con la proporción de archivos estáticos y la exploración de directorios. | UseStaticFiles, UseFileServer |
WebSockets | Habilita el protocolo WebSockets. | UseWebSockets |
En las secciones siguientes se trata la gestión de solicitudes: enrutamiento, enlace de parámetros y respuestas.
Enrutamiento
Un elemento WebApplication
configurado admite Map{Verb}
y MapMethods donde {Verb}
es un método HTTP con mayúsculas y minúsculas Camel, como Get
, Post
, Put
o Delete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
Los argumentos Delegate que se pasan a estos métodos se denominan "controladores de ruta".
Controladores de ruta
Los controladores de ruta son métodos que se ejecutan cuando coincide una ruta. Los controladores de ruta pueden ser una expresión lambda, una función local, un método de instancia o un método estático. Los controladores de ruta pueden ser sincrónicos o asincrónicos.
Expresión lambda
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
Función local
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Método de instancia
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Método estático
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Punto de conexión definido fuera de Program.cs
.
Las API mínimas no tienen que encontrarse en Program.cs
.
Program.cs
using MinAPISeparateFile;
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}
Consulte también Grupos de rutas más adelante en este artículo.
Puntos de conexión con nombre y generación de vínculos
Se pueden asignar nombres a los puntos de conexión para generar direcciones URL al punto de conexión. El uso de un punto de conexión con nombre evita tener que codificar las rutas de acceso de forma rígida en una aplicación:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
El código anterior muestra The link to the hello route is /hello
desde el punto de conexión /
.
NOTA: Los nombres de punto de conexión distinguen mayúsculas de minúsculas.
Nombres de punto de conexión:
- Debe ser único globalmente.
- Se usan como identificador de operación de OpenAPI cuando se habilita la compatibilidad con OpenAPI. Para obtener más información, consulte OpenAPI.
Parámetros de ruta
Los parámetros de ruta se pueden capturar como parte de la definición del patrón de ruta:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
El código anterior devuelve The user id is 3 and book id is 7
a partir del URI /users/3/books/7
.
El controlador de rutas puede declarar los parámetros que se capturan. Cuando se realiza una solicitud en una ruta con parámetros declarados para la captura, los parámetros se analizan y se pasan al controlador. Esto facilita la captura de los valores en un método con seguridad de tipos. En el código anterior, userId
y bookId
son int
.
En el código anterior, si alguno de los valores de ruta no se puede convertir en int
, se produce una excepción. La solicitud GET /users/hello/books/3
produce la siguiente excepción:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Rutas con carácter comodín y de captura total
La siguiente ruta de captura total devuelve Routing to hello
del punto de conexión "/posts/hello":
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Restricciones de ruta
Las restricciones de ruta restringen el comportamiento de coincidencia de una ruta.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
En la tabla siguiente se muestran las plantillas de ruta anteriores y su comportamiento:
Plantilla de ruta | URI coincidente de ejemplo |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
Para más información, vea Referencia de restricción de ruta en Enrutamiento en ASP.NET Core.
Grupos de rutas
El método de extensión MapGroup ayuda a organizar grupos de puntos de conexión con un prefijo común. Reduce el código repetitivo y permite personalizar grupos completos de puntos de conexión con una sola llamada a métodos como RequireAuthorization y WithMetadata, que agregan metadatos de punto de conexión.
Por ejemplo, el código siguiente crea dos grupos similares de puntos de conexión:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
En este escenario, puede usar una dirección relativa para el encabezado Location
en el resultado 201 Created
:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
El primer grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /public/todos
y que sean accesibles sin autenticación. El segundo grupo de puntos de conexión solo coincidirá con las solicitudes con el prefijo /private/todos
y que requieran autenticación.
La fábrica de filtros de punto de conexiónQueryPrivateTodos
es una función local que modifica los parámetros TodoDb
del controlador de ruta para permitir el acceso y almacenar datos privados de tareas pendientes.
Los grupos de rutas también admiten grupos anidados y patrones de prefijo complejos con parámetros y restricciones de ruta. En el ejemplo siguiente, y el controlador de rutas asignado al grupo user
puede capturar los parámetros de ruta {org}
y {group}
definidos en los prefijos del grupo externo.
El prefijo también puede estar vacío. Esto puede ser útil para agregar metadatos de punto de conexión o filtros a un grupo de puntos de conexión sin cambiar el patrón de ruta.
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
La adición de filtros o metadatos a un grupo se comporta del mismo modo que la adición individual a cada punto de conexión antes de agregar filtros o metadatos adicionales que quizás se hayan agregado a un grupo interno o a un punto de conexión específico.
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
En el ejemplo anterior, el filtro externo registrará la solicitud entrante antes que el filtro interno aunque se haya agregado en segundo lugar. Dado que los filtros se aplicaron a diferentes grupos, el orden en que se agregaron el uno con respecto al otro no es importante. El orden en que se agregan los filtros es importante si se aplican al mismo grupo o punto de conexión específico.
Una solicitud a /outer/inner/
registrará lo siguiente:
/outer group filter
/inner group filter
MapGet filter
Enlace de parámetros
El enlace de parámetros es el proceso de convertir los datos de solicitud en parámetros fuertemente tipados que se expresan mediante controladores de ruta. Un origen de enlace determina desde dónde se enlazan los parámetros. Los orígenes de enlace pueden ser explícitos o inferidos en función del método HTTP y el tipo de parámetro.
Orígenes de enlace admitidos:
- Valores de ruta
- Cadena de consulta
- Encabezado
- Cuerpo (como JSON)
- Servicios proporcionados por la inserción de dependencias
- Personalizado
El enlace desde los valores de formularios no se admite de forma nativa en .NET 6 y 7.
En el siguiente GET
, el controlador de ruta usa algunos de estos orígenes de enlace de parámetros:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
En la tabla siguiente se muestra la relación entre los parámetros utilizados en el ejemplo anterior y los orígenes de enlace asociados.
Parámetro | Origen de enlace |
---|---|
id |
valor de ruta |
page |
cadena de consulta |
customHeader |
header |
service |
Proporcionado por la inserción de dependencias |
Los métodos HTTP GET
, HEAD
, OPTIONS
y DELETE
no se enlazan implícitamente desde el cuerpo. Para enlazar desde el cuerpo (como JSON) para estos métodos HTTP, enlace explícitamente con [FromBody]
o lea desde HttpRequest.
En el siguiente ejemplo, el controlador de ruta POST usa un origen de cuerpo de enlace (como JSON) para el parámetro person
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Los parámetros de los ejemplos anteriores se enlazan automáticamente a partir de los datos de solicitud. Para demostrar la comodidad que proporciona el enlace de parámetros, los siguientes controladores de ruta muestran cómo leer los datos de solicitud directamente desde la solicitud:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
Enlace de parámetros explícitos
Los atributos se pueden usar para declarar explícitamente desde dónde se enlazan los parámetros.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
Parámetro | Origen de enlace |
---|---|
id |
valor de ruta con el nombre id |
page |
cadena de consulta con el nombre "p" |
service |
Proporcionado por la inserción de dependencias |
contentType |
encabezado con el nombre "Content-Type" |
Nota
El enlace desde los valores de formularios no se admite de forma nativa en .NET 6 y 7.
Enlace de parámetros con inserción de dependencias
El enlace de parámetros para las API mínimas enlaza parámetros mediante la inserción de dependencias cuando el tipo está configurado como servicio. No es necesario aplicar explícitamente el atributo [FromServices]
a un parámetro. En el código siguiente, ambas acciones devuelven la hora:
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
var app = builder.Build();
app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
Parámetros opcionales
Los parámetros declarados en controladores de ruta se tratan como obligatorios:
- Si una solicitud coincide con la ruta, el controlador de rutas solo se ejecuta si se proporcionan todos los parámetros necesarios en la solicitud.
- Si no se proporcionan todos los parámetros necesarios, se producirá un error.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
BadHttpRequestException : no se proporcionó el parámetro necesario "int pageNumber" de la cadena de consulta. |
/products/1 |
Error HTTP 404, no se encuentra ninguna ruta coincidente |
Para que pageNumber
sea opcional, defina el tipo como opcional o proporcione un valor predeterminado:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";
app.MapGet("/products2", ListProducts);
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
1 devuelto |
/products2 |
1 devuelto |
El valor predeterminado y que admite un valor NULL anterior se aplica a todos los orígenes:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
El código anterior llama al método con un producto NULL si no se envía ningún cuerpo de la solicitud.
NOTA: Si se proporcionan datos no válidos y el parámetro admite un valor NULL, el controlador de rutas no se ejecuta.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
1 devuelto |
/products?pageNumber=two |
BadHttpRequestException : error al enlazar el parámetro "Nullable<int> pageNumber" a partir de "two". |
/products/two |
Error HTTP 404, no se encuentra ninguna ruta coincidente |
Vea la sección Errores de enlace para más información.
Tipos especiales
Los siguientes tipos se enlazan sin atributos explícitos:
HttpContext: contexto que contiene toda la información sobre la solicitud o respuesta HTTP actual:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest y HttpResponse: solicitud HTTP y respuesta HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken: token de cancelación asociado a la solicitud HTTP actual:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: usuario asociado a la solicitud, enlazado desde HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Enlazar el cuerpo de la solicitud como Stream
o PipeReader
El cuerpo de la solicitud puede enlazarse como Stream
o PipeReader
para admitir sin problemas los escenarios en los que el usuario tiene que procesar datos y:
- Almacenar los datos en Blob Storage o ponerlos en cola en un proveedor de colas.
- Procesar los datos almacenados con un proceso de trabajo o una función en la nube.
Por ejemplo, los datos pueden ponerse en cola en Azure Queue Storage o almacenarse en Azure Blob Storage.
El código siguiente implementa una cola en segundo plano:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
El código siguiente enlaza el cuerpo de la solicitud a un objeto Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
El código siguiente muestra el archivo Program.cs
completo:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- Cuando se leen datos,
Stream
es el mismo objeto queHttpRequest.Body
. - El cuerpo de la solicitud no se almacena en búfer de forma predeterminada. Una vez leído el cuerpo, no se puede rebobinar. La secuencia no se puede leer varias veces.
Stream
yPipeReader
no se pueden usar fuera del controlador de acciones mínimas, ya que los búferes subyacentes se eliminarán o reutilizarán.
Cargas de archivos mediante IFormFile e IFormFileCollection
El código siguiente usa IFormFile y IFormFileCollection para cargar el archivo:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
Las solicitudes de carga de archivos autenticadas se admiten mediante un encabezado de autorización, un certificado de cliente o un cookie encabezado.
No hay compatibilidad integrada con la antifalsificación en ASP.NET Core 7.0. La antifalsificación está disponible en ASP.NET Core 8.0 y versiones posteriores. Sin embargo, se puede implementar mediante el servicioIAntiforgery
.
Enlace de matrices y valores de cadena desde encabezados y cadenas de consulta
En el código siguiente se muestra cómo enlazar cadenas de consulta a una matriz de tipos primitivos, matrices de cadena y StringValues:
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
El enlace de cadenas de consulta o valores de encabezado a una matriz de tipos complejos se admite cuando el tipo tiene TryParse
implementado. El código siguiente se enlaza a una matriz de cadena y devuelve todos los elementos con las etiquetas especificadas:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
En el código siguiente se muestra el modelo y la implementación de TryParse
necesaria:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
El código siguiente se enlaza a una matriz int
:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Para probar el código anterior, agregue el siguiente punto de conexión para rellenar la base de datos con elementos Todo
:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
Use una herramienta de prueba de API como HttpRepl
para pasar los datos siguientes al punto de conexión anterior:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
El código siguiente se enlaza a la clave de encabezado X-Todo-Id
y devuelve los elementos Todo
con valores Id
coincidentes:
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
Nota
Al enlazar un objeto string[]
desde una cadena de consulta, la ausencia de cualquier valor de cadena de consulta coincidente dará como resultado una matriz vacía en lugar de un valor NULL.
Enlace de parámetros para listas de argumentos con [AsParameters]
AsParametersAttribute permite el enlace de parámetros simple a tipos y no a un enlace de modelo complejo o recursivo.
Observe el código siguiente:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
Tenga en cuenta el siguiente punto de conexión GET
:
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
Se puede usar lo siguiente struct
para reemplazar los parámetros resaltados anteriores:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
El punto de conexión GET
refactorizado usa el anterior struct
con el atributo AsParameters:
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
En el código siguiente se muestran puntos de conexión adicionales en la aplicación:
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Las siguientes clases se usan para refactorizar las listas de parámetros:
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
En el código siguiente se muestran los puntos de conexión refactorizados mediante AsParameters
y las clases y anteriores struct
:
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
Los siguientes tipos record
se pueden usar para reemplazar los parámetros anteriores:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
El uso de struct
con AsParameters
puede ser más eficaz que usar un tipo record
.
El código de ejemplo completo en el repositorio AspNetCore.Docs.Samples.
Enlace personalizado
Hay dos maneras de personalizar el enlace de parámetros:
- Para los orígenes de enlace de ruta, consulta y encabezado, enlace tipos personalizados mediante la adición de un método
TryParse
estático para el tipo. - Controle el proceso de enlace mediante la implementación de un método
BindAsync
en un tipo.
TryParse
TryParse
tiene dos API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
El código siguiente muestra Point: 12.3, 10.1
con el URI /map?Point=12.3,10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
tiene las siguientes API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
El código siguiente muestra SortBy:xyz, SortDirection:Desc, CurrentPage:99
con el URI /products?SortBy=xyz&SortDir=Desc&Page=99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
Errores de enlace
Cuando se produce un error en el enlace, el marco registra un mensaje de depuración y devuelve varios códigos de estado al cliente en función del modo de error.
Modo de error | Tipo de parámetro que admite un valor NULL | Origen de enlace | status code |
---|---|---|---|
{ParameterType}.TryParse devuelve false . |
sí | ruta/consulta/encabezado | 400 |
{ParameterType}.BindAsync devuelve null . |
sí | custom | 400 |
{ParameterType}.BindAsync genera |
no importa | custom | 500 |
Error al deserializar el cuerpo JSON | no importa | body | 400 |
Tipo de contenido incorrecto (no application/json ) |
no importa | body | 415 |
Prioridad de enlace
Reglas para determinar un origen de enlace a partir de un parámetro:
- Atributo explícito definido en el parámetro (atributos From*) en el orden siguiente:
- Valores de ruta:
[FromRoute]
- Cadena de consulta:
[FromQuery]
- Encabezado:
[FromHeader]
- Cuerpo:
[FromBody]
- Servicio:
[FromServices]
- Valores de parámetros:
[AsParameters]
- Valores de ruta:
- Tipos especiales
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)IFormFileCollection
(HttpContext.Request.Form.Files
)IFormFile
(HttpContext.Request.Form.Files[paramName]
)Stream
(HttpContext.Request.Body
)PipeReader
(HttpContext.Request.BodyReader
)
- El tipo de parámetro tiene un método
BindAsync
estático válido. - El tipo de parámetro es una cadena o tiene un método
TryParse
estático válido.- Si el nombre del parámetro existe en la plantilla de ruta. En
app.Map("/todo/{id}", (int id) => {});
,id
está enlazado desde la ruta. - Se enlaza a partir de la cadena de consulta.
- Si el nombre del parámetro existe en la plantilla de ruta. En
- Si el tipo de parámetro es un servicio proporcionado por la inserción de dependencias, usa ese servicio como origen.
- El parámetro procede del cuerpo.
Configuración de opciones de deserialización de JSON para enlace de cuerpo
El origen de enlace de cuerpo usa System.Text.Json para la deserialización. No es posible cambiar este valor predeterminado, pero se pueden configurar las opciones de serialización y deserialización de JSON.
Configuración global de las opciones de deserialización de JSON
Las opciones que se aplican globalmente a una aplicación se pueden configurar invocando ConfigureHttpJsonOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
Dado que el código de ejemplo configura la serialización y la deserialización, puede leer NameField
e incluir NameField
en el JSON de salida.
Configuración de las opciones de deserialización de JSON para un punto de conexión
ReadFromJsonAsync tiene sobrecargas que aceptan un objeto JsonSerializerOptions. En el ejemplo siguiente se incluyen campos públicos y se da formato a la salida de JSON.
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
Dado que el código anterior tan solo aplica las opciones personalizadas a la deserialización, el JSON de salida excluye NameField
.
Lectura del cuerpo de la solicitud
Lea el cuerpo de la solicitud directamente mediante un parámetro HttpContext o HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
El código anterior:
- Tiene acceso al cuerpo de la solicitud mediante HttpRequest.BodyReader.
- Copia el cuerpo de la solicitud en un archivo local.
Respuestas
Los controladores de ruta admiten los siguientes tipos de valores devueltos:
- Basado en
IResult
: incluyeTask<IResult>
yValueTask<IResult>
. string
: incluyeTask<string>
yValueTask<string>
.T
(cualquier otro tipo): incluyeTask<T>
yValueTask<T>
.
Valor devuelto | Comportamiento | Content-Type |
---|---|---|
IResult |
El marco llama a IResult.ExecuteAsync | Decidido por la implementación de IResult |
string |
El marco escribe la cadena directamente en la respuesta | text/plain |
T (cualquier otro tipo) |
El marco JSON serializa la respuesta | application/json |
Para obtener una guía más detallada sobre los valores devueltos del controlador de rutas, consulte Creación de respuestas en aplicaciones de API mínimas.
Valores devueltos de ejemplo
valores devueltos de cadena
app.MapGet("/hello", () => "Hello World");
valores devueltos JSON
app.MapGet("/hello", () => new { Message = "Hello World" });
Devolución de TypedResults
El código siguiente devuelve un TypedResults:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
Devolver TypedResults
es preferible a devolver Results. Para más información, consulte TypedResults frente a Results.
valores devueltos IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
En el ejemplo siguiente se usan los tipos de resultados integrados para personalizar la respuesta:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Código de estado personalizado
app.MapGet("/405", () => Results.StatusCode(405));
Texto
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
Consulte Creación de respuestas en aplicaciones de API mínimas para ver más ejemplos.
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Archivo
app.MapGet("/download", () => Results.File("myfile.text"));
Resultados integrados
Existen asistentes de resultados comunes en las clases estáticas Results y TypedResults. Devolver TypedResults
es preferible a devolver Results
. Para más información, consulte TypedResults frente a Results.
Personalización de resultados
Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Resultados con tipo
La interfaz IResult puede representar los valores devueltos de las API mínimas que no usan la compatibilidad implícita con JSON para serializar el objeto devuelto en la respuesta HTTP. La clase estática Results se usa para crear distintos objetos IResult
que representan diferentes tipos de respuestas. Por ejemplo, establecer el código de estado de respuesta o redirigir a otra dirección URL.
Los tipos que implementan IResult
son públicos, lo que permite las aserciones de tipos al realizar pruebas. Por ejemplo:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
Puede ver los tipos de valor devueltos de los métodos correspondientes en la clase TypedResults estática para buscar el tipo público IResult
correcto al que se va a convertir.
Consulte Creación de respuestas en aplicaciones de API mínimas para ver más ejemplos.
Filtros
Consulte Filtros en las aplicaciones de API mínimas.
Autorización
Las rutas se pueden proteger mediante directivas de autorización. Se pueden declarar mediante el atributo [Authorize]
o con el método RequireAuthorization:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
El código anterior se puede escribir con RequireAuthorization:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
En el ejemplo siguiente se usa la autorización basada en directivas:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
Permitir que los usuarios no autenticados accedan a un punto de conexión
[AllowAnonymous]
permite a los usuarios no autenticados acceder a los puntos de conexión:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Las rutas se pueden habilitar para CORS mediante directivas de CORS. CORS se puede declarar mediante el atributo [EnableCors]
o con el método RequireCors. Los ejemplos siguientes habilitan CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
Para obtener más información, vea Habilitar solicitudes entre orígenes (CORS) en ASP.NET Core.
Consulte también
Este documento:
- Proporciona una referencia rápida para las API mínimas.
- Está pensado para desarrolladores experimentados. Para consultar una introducción, vea Tutorial: Creación de una API mínima con ASP.NET Core.
Las API mínimas constan de lo siguiente:
- WebApplication y WebApplicationBuilder
- Controladores de ruta
WebApplication
Una plantilla de ASP.NET Core genera el código siguiente:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
El código anterior se puede crear a través de dotnet new web
en la línea de comandos o seleccionando la plantilla web vacía en Visual Studio.
El código siguiente crea una clase WebApplication (app
) sin crear explícitamente una clase WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
inicializa una nueva instancia de la clase WebApplication con valores predeterminados preconfigurados.
Trabajo con puertos
Cuando se crea una aplicación web con Visual Studio o dotnet new
, se crea un archivo Properties/launchSettings.json
que especifica los puertos a los que responde la aplicación. En los siguientes ejemplos de configuración de los puertos, al ejecutar la aplicación desde Visual Studio se devuelve un cuadro de diálogo de error Unable to connect to web server 'AppName'
. Ejecute los siguientes ejemplos de cambio de puertos desde la línea de comandos.
En las secciones siguientes se establece el puerto al que responde la aplicación.
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
En el código anterior, la aplicación responde al puerto 3000
.
Varios puertos
En el código siguiente, la aplicación responde a los puertos 3000
y 4000
.
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
Establecimiento del puerto desde la línea de comandos
El siguiente comando hace que la aplicación responda al puerto 7777
:
dotnet run --urls="https://localhost:7777"
Si el punto de conexión Kestrel también está configurado en el archivo appsettings.json
, se usa la dirección URL especificada del archivo appsettings.json
. Para más información, vea Configuración del punto de conexión de Kestrel.
Lectura del puerto desde el entorno
El código siguiente lee el puerto desde el entorno:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
La manera preferida de establecer el puerto desde el entorno es usar la variable de entorno ASPNETCORE_URLS
, que se muestra en la sección siguiente.
Establecimiento de los puertos mediante la variable de entorno ASPNETCORE_URLS
La variable de entorno ASPNETCORE_URLS
está disponible para establecer el puerto:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
admite varias direcciones URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
Escucha en todas las interfaces
En los ejemplos siguientes se muestra la escucha en todas las interfaces.
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Escucha en todas las interfaces mediante ASPNETCORE_URLS
Los ejemplos anteriores pueden usar ASPNETCORE_URLS
.
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
Especificación de HTTPS con certificado de desarrollo
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Para más información sobre el certificado de desarrollo, vea Confianza en el certificado de desarrollo HTTPS de ASP.NET Core en Windows y macOS.
Especificación de HTTPS mediante un certificado personalizado
En las secciones siguientes se muestra cómo especificar el certificado personalizado con el archivo appsettings.json
y mediante la configuración.
Especificación del certificado personalizado con appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
Especificación del certificado personalizado mediante la configuración
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Uso de las API de certificado
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
Lectura del entorno
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
Para más información sobre el uso de los entornos, consulte Uso de varios entornos en ASP.NET Core.
Configuración
El código siguiente lee del sistema de configuración:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Hello";
app.MapGet("/", () => message);
app.Run();
Para más información, consulte Configuración en ASP.NET Core.
Registro
El código siguiente escribe un mensaje en el registro al iniciar la aplicación:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
Para obtener más información, vea Registro en .NET Core y ASP.NET Core.
Acceso al contenedor de inserción de dependencias (DI)
En el código siguiente se muestra cómo obtener servicios del contenedor de DI durante el inicio de la aplicación:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
Para más información, consulte Inserción de dependencias en ASP.NET Core.
WebApplicationBuilder
Esta sección contiene código de ejemplo mediante WebApplicationBuilder.
Cambio de la raíz del contenido, el nombre de la aplicación y el entorno
El código siguiente establece la raíz del contenido, el nombre de la aplicación y el entorno:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder inicializa una nueva instancia de la clase WebApplicationBuilder con valores predeterminados preconfigurados.
Para obtener más información, consulte Información general de los conceptos básicos de ASP.NET Core.
Cambio de la raíz del contenido, el nombre de la aplicación y el entorno mediante variables de entorno o la línea de comandos
En la tabla siguiente se muestra la variable de entorno y el argumento de la línea de comandos usados para cambiar la raíz del contenido, el nombre de la aplicación y el entorno:
feature | Variable de entorno | Argumento de línea de comandos |
---|---|---|
Nombre de la aplicación | ASPNETCORE_APPLICATIONNAME | --applicationName |
Nombre del entorno | ASPNETCORE_ENVIRONMENT | --environment |
Raíz del contenido | ASPNETCORE_CONTENTROOT | --contentRoot |
Incorporación de proveedores de configuración
En el ejemplo siguiente se agrega el proveedor de configuración INI:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
Para obtener información detallada, vea Proveedores de configuración de archivos en Configuración en ASP.NET Core.
Lectura de la configuración
De forma predeterminada, WebApplicationBuilder lee la configuración de varios orígenes, incluidos:
appSettings.json
yappSettings.{environment}.json
- Variables de entorno
- Línea de comandos
Para obtener una lista completa de orígenes de configuración leídos, vea Configuración predeterminada en Configuración en ASP.NET Core.
El código siguiente lee HelloKey
de la configuración y muestra el valor en el punto de conexión /
. Si el valor de configuración es null, "Hello" se asigna a message
:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Lectura del entorno
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
Adición de proveedores de registro
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
Agrega servicios
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
Personalización de IHostBuilder
Se puede acceder a los métodos de extensión existentes en IHostBuilder mediante la propiedad de host:
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Personalización de IWebHostBuilder
Se puede acceder a los métodos de extensión en IWebHostBuilder mediante la propiedad WebApplicationBuilder.WebHost.
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
Cambio de la raíz web
De forma predeterminada, la raíz web guarda relación con la raíz de contenido de la carpeta wwwroot
. La raíz web es donde el middleware de archivos estáticos busca archivos estáticos. La raíz web se puede cambiar con WebHostOptions
, la línea de comandos o el método UseWebRoot:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
Contenedor de inserción de dependencias (ID) personalizado
En el ejemplo siguiente se usa Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
Adición de middleware
Cualquier middleware de ASP.NET Core existente se puede configurar en WebApplication
:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
Para obtener más información, consulte Middleware de ASP.NET Core.
Página de excepciones para el desarrollador
WebApplication.CreateBuilder inicializa una nueva instancia de la clase WebApplicationBuilder con valores predeterminados preconfigurados. La página de excepciones para el desarrollador está habilitada en los valores predeterminados preconfigurados. Cuando se ejecuta el código siguiente en el entorno de desarrollo, la navegación a /
representa una página descriptiva que muestra la excepción.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
Middleware de ASP.NET Core
En la tabla siguiente se enumeran algunos de los middleware que se usan con frecuencia mediante API mínimas.
Software intermedio | Descripción | API |
---|---|---|
Autenticación | Proporciona compatibilidad con autenticación. | UseAuthentication |
Autorización | Proporciona compatibilidad con la autorización. | UseAuthorization |
CORS | Configura el uso compartido de recursos entre orígenes. | UseCors |
Controlador de excepciones | Controla globalmente las excepciones generadas por la canalización de middleware. | UseExceptionHandler |
Encabezados reenviados | Reenvía encabezados con proxy a la solicitud actual. | UseForwardedHeaders |
Redireccionamiento de HTTPS | Redirige todas las solicitudes HTTP a HTTPS. | UseHttpsRedirection |
Seguridad de transporte estricta de HTTP (HSTS) | Middleware de mejora de seguridad que agrega un encabezado de respuesta especial. | UseHsts |
Registro de solicitudes | Proporciona compatibilidad con el registro de solicitudes y respuestas HTTP. | UseHttpLogging |
Registro de solicitudes W3C | Proporciona compatibilidad con el registro de solicitudes y respuestas HTTP en el formato W3C. | UseW3CLogging |
Almacenamiento en caché de respuestas | Proporciona compatibilidad con la captura de respuestas. | UseResponseCaching |
Compresión de respuesta | Proporciona compatibilidad con la compresión de respuestas. | UseResponseCompression |
Sesión | Proporciona compatibilidad con la administración de sesiones de usuario. | UseSession |
Archivos estáticos | Proporciona compatibilidad con la proporción de archivos estáticos y la exploración de directorios. | UseStaticFiles, UseFileServer |
WebSockets | Habilita el protocolo WebSockets. | UseWebSockets |
Gestión de solicitudes
En las secciones siguientes se trata el enrutamiento, el enlace de parámetros y las respuestas.
Enrutamiento
Una clase WebApplication
configurada admite Map{Verb}
y MapMethods:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
Controladores de ruta
Los controladores de ruta son métodos que se ejecutan cuando coincide una ruta. Los controladores de ruta pueden ser una función de cualquier forma, incluidas las sincrónicas o asincrónicas. Los controladores de ruta pueden ser una expresión lambda, una función local, un método de instancia o un método estático.
Expresión lambda
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
Función local
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
Método de instancia
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
Método estático
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
Puntos de conexión con nombre y generación de vínculos
Se pueden asignar nombres a los puntos de conexión para generar direcciones URL al punto de conexión. El uso de un punto de conexión con nombre evita tener que codificar las rutas de acceso de forma rígida en una aplicación:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
El código anterior muestra The link to the hello endpoint is /hello
desde el punto de conexión /
.
NOTA: Los nombres de punto de conexión distinguen mayúsculas de minúsculas.
Nombres de punto de conexión:
- Debe ser único globalmente.
- Se usan como identificador de operación de OpenAPI cuando se habilita la compatibilidad con OpenAPI. Para obtener más información, consulte OpenAPI.
Parámetros de ruta
Los parámetros de ruta se pueden capturar como parte de la definición del patrón de ruta:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
El código anterior devuelve The user id is 3 and book id is 7
a partir del URI /users/3/books/7
.
El controlador de rutas puede declarar los parámetros que se capturan. Cuando se realiza una solicitud en una ruta con parámetros declarados para la captura, los parámetros se analizan y se pasan al controlador. Esto facilita la captura de los valores en un método con seguridad de tipos. En el código anterior, userId
y bookId
son int
.
En el código anterior, si alguno de los valores de ruta no se puede convertir en int
, se produce una excepción. La solicitud GET /users/hello/books/3
produce la siguiente excepción:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
Rutas con carácter comodín y de captura total
La siguiente ruta de captura total devuelve Routing to hello
del punto de conexión "/posts/hello":
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
Restricciones de ruta
Las restricciones de ruta restringen el comportamiento de coincidencia de una ruta.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
En la tabla siguiente se muestran las plantillas de ruta anteriores y su comportamiento:
Plantilla de ruta | URI coincidente de ejemplo |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
Para más información, vea Referencia de restricción de ruta en Enrutamiento en ASP.NET Core.
Enlace de parámetros
El enlace de parámetros es el proceso de convertir los datos de solicitud en parámetros fuertemente tipados que se expresan mediante controladores de ruta. Un origen de enlace determina desde dónde se enlazan los parámetros. Los orígenes de enlace pueden ser explícitos o inferidos en función del método HTTP y el tipo de parámetro.
Orígenes de enlace admitidos:
- Valores de ruta
- Cadena de consulta
- Encabezado
- Cuerpo (como JSON)
- Servicios proporcionados por la inserción de dependencias
- Personalizado
Nota
El enlace desde los valores de formularios no es compatible de forma nativa con .NET.
En el siguiente ejemplo, el controlador de ruta GET usa algunos de estos orígenes de enlace de parámetros:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
En la tabla siguiente se muestra la relación entre los parámetros utilizados en el ejemplo anterior y los orígenes de enlace asociados.
Parámetro | Origen de enlace |
---|---|
id |
valor de ruta |
page |
cadena de consulta |
customHeader |
header |
service |
Proporcionado por la inserción de dependencias |
Los métodos HTTP GET
, HEAD
, OPTIONS
y DELETE
no se enlazan implícitamente desde el cuerpo. Para enlazar desde el cuerpo (como JSON) para estos métodos HTTP, enlace explícitamente con [FromBody]
o lea desde HttpRequest.
En el siguiente ejemplo, el controlador de ruta POST usa un origen de cuerpo de enlace (como JSON) para el parámetro person
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
Los parámetros de los ejemplos anteriores se enlazan automáticamente a partir de los datos de solicitud. Para demostrar la comodidad que proporciona el enlace de parámetros, los siguientes controladores de ruta de ejemplo muestran cómo leer los datos de solicitud directamente desde la solicitud:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
Enlace de parámetros explícitos
Los atributos se pueden usar para declarar explícitamente desde dónde se enlazan los parámetros.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
Parámetro | Origen de enlace |
---|---|
id |
valor de ruta con el nombre id |
page |
cadena de consulta con el nombre "p" |
service |
Proporcionado por la inserción de dependencias |
contentType |
encabezado con el nombre "Content-Type" |
Nota
El enlace desde los valores de formularios no es compatible de forma nativa con .NET.
Enlace de parámetros con inserción de dependencias
El enlace de parámetros para las API mínimas enlaza parámetros mediante la inserción de dependencias cuando el tipo está configurado como servicio. No es necesario aplicar explícitamente el atributo [FromServices]
a un parámetro. En el código siguiente, ambas acciones devuelven la hora:
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
var app = builder.Build();
app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
Parámetros opcionales
Los parámetros declarados en controladores de ruta se tratan como obligatorios:
- Si una solicitud coincide con la ruta, el controlador de rutas solo se ejecuta si se proporcionan todos los parámetros necesarios en la solicitud.
- Si no se proporcionan todos los parámetros necesarios, se producirá un error.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
BadHttpRequestException : no se proporcionó el parámetro necesario "int pageNumber" de la cadena de consulta. |
/products/1 |
Error HTTP 404, no se encuentra ninguna ruta coincidente |
Para que pageNumber
sea opcional, defina el tipo como opcional o proporcione un valor predeterminado:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";
app.MapGet("/products2", ListProducts);
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
1 devuelto |
/products2 |
1 devuelto |
El valor predeterminado y que admite un valor NULL anterior se aplica a todos los orígenes:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
El código anterior llama al método con un producto NULL si no se envía ningún cuerpo de la solicitud.
NOTA: Si se proporcionan datos no válidos y el parámetro admite un valor NULL, el controlador de rutas no se ejecuta.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | resultado |
---|---|
/products?pageNumber=3 |
3 devuelto |
/products |
1 devuelto |
/products?pageNumber=two |
BadHttpRequestException : error al enlazar el parámetro "Nullable<int> pageNumber" a partir de "two". |
/products/two |
Error HTTP 404, no se encuentra ninguna ruta coincidente |
Vea la sección Errores de enlace para más información.
Tipos especiales
Los siguientes tipos se enlazan sin atributos explícitos:
HttpContext: contexto que contiene toda la información sobre la solicitud o respuesta HTTP actual:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
HttpRequest y HttpResponse: solicitud HTTP y respuesta HTTP:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));
CancellationToken: token de cancelación asociado a la solicitud HTTP actual:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal: usuario asociado a la solicitud, enlazado desde HttpContext.User:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
Enlace personalizado
Hay dos maneras de personalizar el enlace de parámetros:
- Para los orígenes de enlace de ruta, consulta y encabezado, enlace tipos personalizados mediante la adición de un método
TryParse
estático para el tipo. - Controle el proceso de enlace mediante la implementación de un método
BindAsync
en un tipo.
TryParse
TryParse
tiene dos API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
El código siguiente muestra Point: 12.3, 10.1
con el URI /map?Point=12.3,10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
tiene las siguientes API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
El código siguiente muestra SortBy:xyz, SortDirection:Desc, CurrentPage:99
con el URI /products?SortBy=xyz&SortDir=Desc&Page=99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
Errores de enlace
Cuando se produce un error en el enlace, el marco registra un mensaje de depuración y devuelve varios códigos de estado al cliente en función del modo de error.
Modo de error | Tipo de parámetro que admite un valor NULL | Origen de enlace | status code |
---|---|---|---|
{ParameterType}.TryParse devuelve false . |
sí | ruta/consulta/encabezado | 400 |
{ParameterType}.BindAsync devuelve null . |
sí | custom | 400 |
{ParameterType}.BindAsync genera |
no importa | custom | 500 |
Error al deserializar el cuerpo JSON | no importa | body | 400 |
Tipo de contenido incorrecto (no application/json ) |
no importa | body | 415 |
Prioridad de enlace
Reglas para determinar un origen de enlace a partir de un parámetro:
- Atributo explícito definido en el parámetro (atributos From*) en el orden siguiente:
- Valores de ruta:
[FromRoute]
- Cadena de consulta:
[FromQuery]
- Encabezado:
[FromHeader]
- Cuerpo:
[FromBody]
- Servicio:
[FromServices]
- Valores de ruta:
- Tipos especiales
- El tipo de parámetro tiene un método
BindAsync
válido. - El tipo de parámetro es una cadena o tiene un método
TryParse
válido.- Si el nombre del parámetro existe en la plantilla de ruta. En
app.Map("/todo/{id}", (int id) => {});
,id
está enlazado desde la ruta. - Se enlaza a partir de la cadena de consulta.
- Si el nombre del parámetro existe en la plantilla de ruta. En
- Si el tipo de parámetro es un servicio proporcionado por la inserción de dependencias, usa ese servicio como origen.
- El parámetro procede del cuerpo.
Personalización del enlace JSON
El origen de enlace del cuerpo usa System.Text.Json para la deserialización. No es posible cambiar este valor predeterminado, pero el enlace se puede personalizar mediante otras técnicas descritas anteriormente. Para personalizar las opciones del serializador JSON, use código similar al siguiente:
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/products", (Product product) => product);
app.Run();
class Product
{
// These are public fields, not properties.
public int Id;
public string? Name;
}
El código anterior:
- Configura las opciones JSON predeterminadas de entrada y salida.
- Devuelve el siguiente código JSON
Al publicar{ "id": 1, "name": "Joe Smith" }
{ "Id": 1, "Name": "Joe Smith" }
Lectura del cuerpo de la solicitud
Lea el cuerpo de la solicitud directamente mediante un parámetro HttpContext o HttpRequest:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await request.BodyReader.CopyToAsync(writeStream);
});
app.Run();
El código anterior:
- Tiene acceso al cuerpo de la solicitud mediante HttpRequest.BodyReader.
- Copia el cuerpo de la solicitud en un archivo local.
Respuestas
Los controladores de ruta admiten los siguientes tipos de valores devueltos:
- Basado en
IResult
: incluyeTask<IResult>
yValueTask<IResult>
. string
: incluyeTask<string>
yValueTask<string>
.T
(cualquier otro tipo): incluyeTask<T>
yValueTask<T>
.
Valor devuelto | Comportamiento | Content-Type |
---|---|---|
IResult |
El marco llama a IResult.ExecuteAsync | Decidido por la implementación de IResult |
string |
El marco escribe la cadena directamente en la respuesta | text/plain |
T (cualquier otro tipo) |
El marco serializará la respuesta en JSON | application/json |
Valores devueltos de ejemplo
valores devueltos de cadena
app.MapGet("/hello", () => "Hello World");
valores devueltos JSON
app.MapGet("/hello", () => new { Message = "Hello World" });
valores devueltos IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
En el ejemplo siguiente se usan los tipos de resultados integrados para personalizar la respuesta:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Código de estado personalizado
app.MapGet("/405", () => Results.StatusCode(405));
Texto
app.MapGet("/text", () => Results.Text("This is some text"));
STREAM
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
Redirect
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Archivo
app.MapGet("/download", () => Results.File("myfile.text"));
Resultados integrados
Existen asistentes de resultados comunes en la clase estática Microsoft.AspNetCore.Http.Results
.
Descripción | Tipo de respuesta | Código de estado | API |
---|---|---|---|
Escritura de una respuesta JSON con opciones avanzadas | application/json | 200 | Results.Json |
Escritura de una respuesta JSON | application/json | 200 | Results.Ok |
Escritura de una respuesta de texto | text/plain (valor predeterminado), configurable | 200 | Results.Text |
Escritura de la respuesta como bytes | application/octet-stream (valor predeterminado), configurable | 200 | Results.Bytes |
Escritura de una secuencia de bytes en la respuesta | application/octet-stream (valor predeterminado), configurable | 200 | Results.Stream |
Transmisión de un archivo a la respuesta para su descarga con el encabezado content-disposition | application/octet-stream (valor predeterminado), configurable | 200 | Results.File |
Definición del código de estado en 404, con una respuesta JSON opcional | N/D | 404 | Results.NotFound |
Definición del código de estado en 204 | N/D | 204 | Results.NoContent |
Definición del código de estado en 422, con una respuesta JSON opcional | N/D | 422 | Results.UnprocessableEntity |
Definición del código de estado en 400, con una respuesta JSON opcional | N/D | 400 | Results.BadRequest |
Definición del código de estado en 409, con una respuesta JSON opcional | N/D | 409 | Results.Conflict |
Escritura de un objeto JSON de detalles del problema en la respuesta | N/D | 500 (valor predeterminado), configurable | Results.Problem |
Escritura de un objeto JSON de detalles del problema en la respuesta con errores de validación | N/D | N/A, configurable | Results.ValidationProblem |
Personalización de resultados
Las aplicaciones pueden controlar las respuestas mediante la implementación de un tipo IResult personalizado. El código siguiente es un ejemplo de un tipo de resultado HTML:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
Se recomienda agregar un método de extensión a Microsoft.AspNetCore.Http.IResultExtensions para que estos resultados personalizados sean más detectables.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
Autorización
Las rutas se pueden proteger mediante directivas de autorización. Se pueden declarar mediante el atributo [Authorize]
o con el método RequireAuthorization:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
El código anterior se puede escribir con RequireAuthorization:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
En el ejemplo siguiente se usa la autorización basada en directivas:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
Permitir que los usuarios no autenticados accedan a un punto de conexión
[AllowAnonymous]
permite a los usuarios no autenticados acceder a los puntos de conexión:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
Las rutas se pueden habilitar para CORS mediante directivas de CORS. CORS se puede declarar mediante el atributo [EnableCors]
o con el método RequireCors. Los ejemplos siguientes habilitan CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
Para obtener más información, vea Habilitar solicitudes entre orígenes (CORS) en ASP.NET Core.