Back to "Comprensión del tubo de solicitud y respuesta del núcleo de ASP.NET - Parte 6: Ganchos avanzados del tubo y extensibilidad"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI-Article ASP.NET ASP.NET Lifecycle

Comprensión del tubo de solicitud y respuesta del núcleo de ASP.NET - Parte 6: Ganchos avanzados del tubo y extensibilidad

Sunday, 09 November 2025

Introducción

A lo largo de esta serie, hemos explorado el gasoducto ASP.NET Core desde sus cimientos hasta los modelos de aplicación.

Ahora nos aventuraremos en territorio avanzado: los puntos de extensión y ganchos que le permiten personalizar la tubería en un nivel profundo.

  • Estas técnicas avanzadas le permiten:
  • Modificar la tubería antes de que comience la aplicación
  • Añada servicios de fondo que se ejecuten junto a su aplicación
  • Crear fuentes de datos de endpoint personalizadas
  • Enganche a eventos de la vida útil de la aplicación

Construir componentes reutilizables que se integren perfectamente con ASP.NET Core

Entender estos puntos de extensión te transforma de un consumidor del marco a alguien que puede extenderlo para cumplir con requisitos únicos.

NOTA: Esto es parte de mis experimentos con IA / una manera de gastar $1000 Créditos Web Código Calude.

graph TB
    subgraph "Application Startup"
        A1[IStartupFilter]
        A2[Configure Services]
        A3[Configure Pipeline]
    end

    subgraph "Background Processing"
        B1[IHostedService]
        B2[BackgroundService]
        B3[IHostApplicationLifetime]
    end

    subgraph "Endpoint Discovery"
        C1[EndpointDataSource]
        C2[IEndpointConventionBuilder]
        C3[IEndpointRouteBuilder]
    end

    subgraph "Request Processing"
        D1[IMiddleware]
        D2[IApplicationModelProvider]
        D3[IActionDescriptorProvider]
    end

    Start[Application Start] --> A1
    A1 --> A2
    A2 --> A3
    A3 --> B1
    B1 --> C1
    C1 --> D1


He alimentado esto un montón de papeles, mi comprensión, preguntas que tuve que generar este artículo.

IStartupFilterEs divertido y llena un hueco que no he visto en ningún otro lugar.

Información general sobre los puntos de extensión

public class RequestTimingStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app =>
        {
            // This middleware runs BEFORE your normal pipeline configuration
            app.Use(async (context, next) =>
            {
                var sw = Stopwatch.StartNew();
                context.Items["RequestStartTime"] = DateTime.UtcNow;

                await next(context);

                sw.Stop();
                context.Response.Headers["X-Response-Time-Ms"] = sw.ElapsedMilliseconds.ToString();
            });

            // Call the next startup filter
            next(app);

            // This middleware runs AFTER your normal pipeline configuration
            app.Use(async (context, next) =>
            {
                context.Response.Headers["X-Pipeline-End"] = "true";
                await next(context);
            });
        };
    }
}

// Register the startup filter
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IStartupFilter, RequestTimingStartupFilter>();

var app = builder.Build();

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

app.Run();

IStartupFilter: Modificar el oleoducto en Startup

sequenceDiagram
    participant Request
    participant SF1 as Startup Filter 1 (Before)
    participant App as Application Middleware
    participant SF1A as Startup Filter 1 (After)
    participant Endpoint

    Request->>SF1: Enter
    Note over SF1: Added before app.Build()
    SF1->>App: Enter
    Note over App: Your middleware (app.Use...)
    App->>Endpoint: Execute endpoint
    Endpoint-->>App: Return
    App-->>SF1A: Return
    Note over SF1A: Added after next(app)
    SF1A-->>Request: Response

permite configurar middleware antes o después de la configuración normal de la aplicación.

public class HttpsEnforcementStartupFilter : IStartupFilter
{
    private readonly IWebHostEnvironment _env;

    public HttpsEnforcementStartupFilter(IWebHostEnvironment env)
    {
        _env = env;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app =>
        {
            // Only enforce HTTPS in production
            if (_env.IsProduction())
            {
                app.Use(async (context, next) =>
                {
                    if (!context.Request.IsHttps)
                    {
                        var httpsUrl = $"https://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
                        context.Response.Redirect(httpsUrl, permanent: true);
                        return;
                    }

                    await next(context);
                });

                // Add HSTS header
                app.UseHsts();
            }

            next(app);
        };
    }
}

Uso básico

// Filter 1: Logging
public class LoggingStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app =>
        {
            app.Use(async (context, next) =>
            {
                Console.WriteLine($"[LoggingFilter] Before: {context.Request.Path}");
                await next(context);
                Console.WriteLine($"[LoggingFilter] After: {context.Response.StatusCode}");
            });

            next(app);
        };
    }
}

// Filter 2: Security headers
public class SecurityHeadersStartupFilter : IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app =>
        {
            app.Use(async (context, next) =>
            {
                context.Response.Headers["X-Content-Type-Options"] = "nosniff";
                context.Response.Headers["X-Frame-Options"] = "DENY";
                context.Response.Headers["X-XSS-Protection"] = "1; mode=block";

                await next(context);
            });

            next(app);
        };
    }
}

// Register both filters
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IStartupFilter, LoggingStartupFilter>();
builder.Services.AddSingleton<IStartupFilter, SecurityHeadersStartupFilter>();

var app = builder.Build();

// Filters execute in registration order

Orden de ejecución:

IHostedServiceEjemplo práctico: Aplicación Auto-HTTPS

Componiendo múltiples filtros de inicio

public class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer;
    private int _executionCount = 0;

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Hosted Service is starting");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref _executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}",
            count);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

// Register
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<TimedHostedService>();
var app = builder.Build();

IHostedService: Tareas de fondo

public class QueueProcessorService : BackgroundService
{
    private readonly ILogger<QueueProcessorService> _logger;
    private readonly IServiceProvider _serviceProvider;

    public QueueProcessorService(
        ILogger<QueueProcessorService> logger,
        IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queue Processor Service is starting");

        // Run until cancellation is requested
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await ProcessQueueAsync(stoppingToken);

                // Wait before next iteration
                await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Expected when stopping
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing queue");

                // Wait before retrying
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }

        _logger.LogInformation("Queue Processor Service is stopping");
    }

    private async Task ProcessQueueAsync(CancellationToken cancellationToken)
    {
        // Create a scope for scoped services
        using var scope = _serviceProvider.CreateScope();
        var queueService = scope.ServiceProvider.GetRequiredService<IQueueService>();

        var item = await queueService.DequeueAsync(cancellationToken);

        if (item != null)
        {
            _logger.LogInformation("Processing item: {Item}", item);
            await queueService.ProcessAsync(item, cancellationToken);
        }
    }
}

permite tareas de fondo que se ejecutan durante la vida útil de la aplicación.

public class StartupTasksService : BackgroundService
{
    private readonly ILogger<StartupTasksService> _logger;
    private readonly IHostApplicationLifetime _lifetime;
    private readonly IServiceProvider _serviceProvider;

    public StartupTasksService(
        ILogger<StartupTasksService> logger,
        IHostApplicationLifetime lifetime,
        IServiceProvider serviceProvider)
    {
        _logger = logger;
        _lifetime = lifetime;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Wait for application to fully start
        await Task.Delay(100, stoppingToken);

        try
        {
            using var scope = _serviceProvider.CreateScope();

            // Run startup tasks
            await WarmUpCacheAsync(scope, stoppingToken);
            await ValidateDatabaseAsync(scope, stoppingToken);
            await LoadConfigurationAsync(scope, stoppingToken);

            _logger.LogInformation("All startup tasks completed successfully");
        }
        catch (Exception ex)
        {
            _logger.LogCritical(ex, "Startup tasks failed");

            // Stop the application if startup tasks fail
            _lifetime.StopApplication();
            return;
        }

        // Continue running for the lifetime of the application
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }

    private async Task WarmUpCacheAsync(IServiceScopeScope, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Warming up cache...");
        await Task.Delay(500, cancellationToken);
        _logger.LogInformation("Cache warmed up");
    }

    private async Task ValidateDatabaseAsync(IServiceScope scope, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Validating database...");
        await Task.Delay(300, cancellationToken);
        _logger.LogInformation("Database validated");
    }

    private async Task LoadConfigurationAsync(IServiceScope scope, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Loading configuration...");
        await Task.Delay(200, cancellationToken);
        _logger.LogInformation("Configuration loaded");
    }
}

Servicio de alojamiento básico

sequenceDiagram
    participant App as Application
    participant Lifetime as IHostApplicationLifetime
    participant Service as BackgroundService

    App->>Lifetime: Application starting
    Lifetime->>Service: StartAsync()
    Service->>Service: ExecuteAsync() begins

    Note over App,Service: Application running

    Lifetime->>Lifetime: ApplicationStarted event

    Note over Service: Background work continues

    App->>Lifetime: Shutdown requested
    Lifetime->>Lifetime: ApplicationStopping event
    Lifetime->>Service: StopAsync()
    Service->>Service: Cancel ExecuteAsync()
    Service-->>Lifetime: Stopped

    Lifetime->>Lifetime: ApplicationStopped event
    App->>App: Exit

BackgroundService: Tareas simplificadas de background

Coordinación con la aplicación Lifetime

public class PluginEndpointDataSource : EndpointDataSource
{
    private readonly List<Endpoint> _endpoints = new();
    private readonly IChangeToken _changeToken = NullChangeToken.Singleton;

    public PluginEndpointDataSource()
    {
        // Discover plugins and create endpoints
        DiscoverPlugins();
    }

    public override IReadOnlyList<Endpoint> Endpoints => _endpoints;

    public override IChangeToken GetChangeToken() => _changeToken;

    private void DiscoverPlugins()
    {
        // Example: Create endpoints for each discovered plugin
        var plugins = new[]
        {
            new { Name = "Plugin1", Path = "/plugins/plugin1" },
            new { Name = "Plugin2", Path = "/plugins/plugin2" }
        };

        foreach (var plugin in plugins)
        {
            var requestDelegate = CreatePluginDelegate(plugin.Name);

            var routeEndpoint = new RouteEndpoint(
                requestDelegate,
                RoutePatternFactory.Parse(plugin.Path),
                order: 0,
                new EndpointMetadataCollection(
                    new DisplayNameMetadata(plugin.Name)),
                displayName: plugin.Name);

            _endpoints.Add(routeEndpoint);
        }
    }

    private RequestDelegate CreatePluginDelegate(string pluginName)
    {
        return async context =>
        {
            await context.Response.WriteAsJsonAsync(new
            {
                plugin = pluginName,
                message = $"Response from {pluginName}",
                timestamp = DateTime.UtcNow
            });
        };
    }
}

// Register the endpoint data source
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<EndpointDataSource, PluginEndpointDataSource>();

var app = builder.Build();

// Endpoints from PluginEndpointDataSource are automatically available

app.Run();

Visualización del ciclo de vida:

public class DatabaseEndpointDataSource : EndpointDataSource
{
    private readonly List<Endpoint> _endpoints = new();
    private CancellationTokenSource _cts = new();
    private IChangeToken _changeToken;

    public DatabaseEndpointDataSource(IServiceProvider serviceProvider)
    {
        _changeToken = new CancellationChangeToken(_cts.Token);

        // Load endpoints from database
        LoadEndpointsFromDatabase(serviceProvider);

        // Set up periodic refresh
        Task.Run(async () =>
        {
            while (true)
            {
                await Task.Delay(TimeSpan.FromMinutes(5));
                ReloadEndpoints(serviceProvider);
            }
        });
    }

    public override IReadOnlyList<Endpoint> Endpoints => _endpoints;

    public override IChangeToken GetChangeToken() => _changeToken;

    private void LoadEndpointsFromDatabase(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();

        // Simulate loading from database
        var routes = new[]
        {
            new { Path = "/dynamic/users", Handler = "GetUsers" },
            new { Path = "/dynamic/products", Handler = "GetProducts" }
        };

        _endpoints.Clear();

        foreach (var route in routes)
        {
            var requestDelegate = CreateDynamicDelegate(route.Handler);

            var endpoint = new RouteEndpoint(
                requestDelegate,
                RoutePatternFactory.Parse(route.Path),
                order: 0,
                new EndpointMetadataCollection(),
                displayName: route.Handler);

            _endpoints.Add(endpoint);
        }
    }

    private void ReloadEndpoints(IServiceProvider serviceProvider)
    {
        LoadEndpointsFromDatabase(serviceProvider);

        // Notify of changes
        var oldCts = _cts;
        _cts = new CancellationTokenSource();
        _changeToken = new CancellationChangeToken(_cts.Token);
        oldCts.Cancel();
    }

    private RequestDelegate CreateDynamicDelegate(string handler)
    {
        return async context =>
        {
            await context.Response.WriteAsJsonAsync(new
            {
                handler,
                message = $"Dynamic endpoint: {handler}",
                path = context.Request.Path.Value
            });
        };
    }
}

Fuentes de datos de punto final personalizadas

Crear puntos finales dinámicos que se descubren en tiempo de ejecución:IMiddlewareGeneración dinámica de puntos finales

public class DatabaseHealthCheckMiddleware : IMiddleware
{
    private readonly ILogger<DatabaseHealthCheckMiddleware> _logger;

    // Can inject scoped services
    public DatabaseHealthCheckMiddleware(ILogger<DatabaseHealthCheckMiddleware> logger)
    {
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (context.Request.Path == "/health/db")
        {
            // Can resolve scoped services from context
            var dbContext = context.RequestServices.GetRequiredService<MyDbContext>();

            try
            {
                await dbContext.Database.CanConnectAsync();

                context.Response.StatusCode = 200;
                await context.Response.WriteAsJsonAsync(new
                {
                    status = "healthy",
                    database = "connected",
                    timestamp = DateTime.UtcNow
                });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Database health check failed");

                context.Response.StatusCode = 503;
                await context.Response.WriteAsJsonAsync(new
                {
                    status = "unhealthy",
                    database = "disconnected",
                    error = ex.Message
                });
            }

            return;
        }

        await next(context);
    }
}

// Register
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<DatabaseHealthCheckMiddleware>();

var app = builder.Build();

app.UseMiddleware<DatabaseHealthCheckMiddleware>();

app.Run();

IMiddleware: Middleware basado en clases con DI

A diferencia de middleware basado en convenciones,

public class CustomApplicationModelProvider : IApplicationModelProvider
{
    public int Order => -1000 + 10; // Run after default provider

    public void OnProvidersExecuting(ApplicationModelProviderContext context)
    {
        // Runs before the default provider
        foreach (var controller in context.Result.Controllers)
        {
            // Add custom route prefix to all controllers
            controller.Selectors.Add(new SelectorModel
            {
                AttributeRouteModel = new AttributeRouteModel
                {
                    Template = "api/v2/[controller]"
                }
            });
        }
    }

    public void OnProvidersExecuted(ApplicationModelProviderContext context)
    {
        // Runs after the default provider
        foreach (var controller in context.Result.Controllers)
        {
            // Add custom metadata to all actions
            foreach (var action in controller.Actions)
            {
                action.Properties["CustomProperty"] = "CustomValue";
            }
        }
    }
}

// Register
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    options.ModelMetadataDetailsProviders.Add(
        new CustomApplicationModelProvider());
});

apoya plenamente la inyección de dependencia:

Proveedores de modelos de aplicaciones

public class DynamicActionDescriptorProvider : IActionDescriptorProvider
{
    public int Order => -1000; // Run early

    public void OnProvidersExecuting(ActionDescriptorProviderContext context)
    {
        // Add dynamic actions
        context.Results.Add(new ActionDescriptor
        {
            RouteValues = new Dictionary<string, string>
            {
                ["controller"] = "Dynamic",
                ["action"] = "Generated"
            },
            DisplayName = "Dynamic Generated Action",
            AttributeRouteInfo = new AttributeRouteInfo
            {
                Template = "dynamic/generated"
            }
        });
    }

    public void OnProvidersExecuted(ActionDescriptorProviderContext context)
    {
        // Modify existing actions
        foreach (var action in context.Results)
        {
            // Add custom metadata
            action.Properties["Timestamp"] = DateTime.UtcNow;
        }
    }
}

Personaliza cómo MVC descubre y configura controladores y acciones:

Proveedores de descriptores de acción

public class PluginFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
    public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
    {
        // Load plugin assemblies
        var pluginPath = Path.Combine(AppContext.BaseDirectory, "Plugins");

        if (!Directory.Exists(pluginPath))
            return;

        var pluginAssemblies = Directory.GetFiles(pluginPath, "*.dll")
            .Select(Assembly.LoadFrom);

        foreach (var assembly in pluginAssemblies)
        {
            var controllers = assembly.GetTypes()
                .Where(t => typeof(Controller).IsAssignableFrom(t) && !t.IsAbstract);

            foreach (var controller in controllers)
            {
                feature.Controllers.Add(controller.GetTypeInfo());
            }
        }
    }
}

// Register
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews()
    .ConfigureApplicationPartManager(manager =>
    {
        manager.FeatureProviders.Add(new PluginFeatureProvider());
    });

Añadir o modificar dinámicamente descriptores de acción:

Proveedores de funciones

public class RequireApiKeyConvention : IEndpointConventionBuilder
{
    private readonly List<Action<EndpointBuilder>> _conventions = new();

    public void Add(Action<EndpointBuilder> convention)
    {
        _conventions.Add(convention);
    }

    public void ApplyConventions(EndpointBuilder builder)
    {
        foreach (var convention in _conventions)
        {
            convention(builder);
        }
    }
}

// Usage
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var apiGroup = app.MapGroup("/api")
    .WithMetadata(new RequiresApiKeyMetadata())
    .AddEndpointFilter(async (context, next) =>
    {
        var apiKey = context.HttpContext.Request.Headers["X-API-Key"].FirstOrDefault();

        if (string.IsNullOrEmpty(apiKey))
        {
            return Results.Unauthorized();
        }

        return await next(context);
    });

apiGroup.MapGet("/data", () => new { data = "Protected data" });

app.Run();

class RequiresApiKeyMetadata { }

Control de qué conjuntos y tipos se escanean para controladores:

Convenios sobre los puntos finales

// Plugin interface
public interface IPlugin
{
    string Name { get; }
    string Version { get; }
    void ConfigureServices(IServiceCollection services);
    void ConfigureRoutes(IEndpointRouteBuilder endpoints);
}

// Plugin discovery service
public class PluginLoader : BackgroundService
{
    private readonly ILogger<PluginLoader> _logger;
    private readonly IServiceProvider _serviceProvider;
    private readonly List<IPlugin> _plugins = new();

    public PluginLoader(ILogger<PluginLoader> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Loading plugins...");

        var pluginPath = Path.Combine(AppContext.BaseDirectory, "Plugins");

        if (Directory.Exists(pluginPath))
        {
            var pluginAssemblies = Directory.GetFiles(pluginPath, "*.dll")
                .Select(Assembly.LoadFrom);

            foreach (var assembly in pluginAssemblies)
            {
                var pluginTypes = assembly.GetTypes()
                    .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract);

                foreach (var type in pluginTypes)
                {
                    var plugin = (IPlugin)Activator.CreateInstance(type)!;
                    _plugins.Add(plugin);

                    _logger.LogInformation(
                        "Loaded plugin: {Name} v{Version}",
                        plugin.Name,
                        plugin.Version);
                }
            }
        }

        _logger.LogInformation("Loaded {Count} plugins", _plugins.Count);

        await Task.CompletedTask;
    }

    public IReadOnlyList<IPlugin> GetPlugins() => _plugins;
}

// Startup filter that configures plugins
public class PluginStartupFilter : IStartupFilter
{
    private readonly PluginLoader _pluginLoader;

    public PluginStartupFilter(PluginLoader pluginLoader)
    {
        _pluginLoader = pluginLoader;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app =>
        {
            next(app);

            // Configure plugin routes after main application
            var plugins = _pluginLoader.GetPlugins();

            foreach (var plugin in plugins)
            {
                app.UseRouting();
                app.UseEndpoints(endpoints =>
                {
                    plugin.ConfigureRoutes(endpoints);
                });
            }
        };
    }
}

// Register everything
var builder = WebApplication.CreateBuilder(args);

// Register plugin system
var pluginLoader = new PluginLoader(
    builder.Services.BuildServiceProvider().GetRequiredService<ILogger<PluginLoader>>(),
    builder.Services.BuildServiceProvider());

builder.Services.AddSingleton(pluginLoader);
builder.Services.AddHostedService(sp => sp.GetRequiredService<PluginLoader>());
builder.Services.AddSingleton<IStartupFilter, PluginStartupFilter>();

var app = builder.Build();

app.MapGet("/plugins", (PluginLoader loader) =>
{
    var plugins = loader.GetPlugins();

    return plugins.Select(p => new
    {
        p.Name,
        p.Version
    });
});

app.Run();

Aplicar metadatos y configuración a los puntos finales:

  • Ejemplo práctico: Sistema de extensión completoConstruyamos un sistema de plugin completo:
  • Llaves para llevarIStartupFilter
  • modifica la tubería antes y después de la configuración normalIHostedService/AntecedentesService
  • permite tareas de fondo durante toda la vida útil de la aplicaciónEndpointDataSource
  • crea endpoints dinámicos y detectablesIMiddleware
  • proporciona soporte DI completo para middlewareProveedores de modelos de aplicaciones
  • personalizar controlador MVC / descubrimiento de acciónProveedores de funciones
  • control de qué tipos se escanean como controladores

Convenios sobre los puntos finales


aplicar metadatos y configuración a los puntos finales

Estos puntos de extensión trabajan juntos para crear arquitecturas sofisticadas y conectables

  1. **Entender estos ganchos avanzados le permite construir marcos en la parte superior de ASP.NET Core, crear sistemas de plugins y personalizar la tubería para cumplir con los requisitos únicos.**Conclusión de la serie
  2. **A lo largo de esta serie de seis partes, hemos viajado a través de cada capa de la solicitud ASP.NET Core y la tubería de respuesta:**Parte 1
  3. : Estableció la fundación y el modelo mentalParte 2
  4. : Kestrel explorado y la capa de alojamientoParte 3
  5. : Middleware masterizado y la tubería de procesamientoParte 4
  6. : Encaminamiento aprendido y coincidencia de endpointParte 5

: MVC examinado, Páginas Razor, y APIs Mínimas

Parte 6

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.