Add middleware and authz support for server-side handlers (#733)
This commit is contained in:
parent
0f0aad020b
commit
d344c65120
5 changed files with 2871 additions and 435 deletions
359
src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs
Normal file
359
src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates authorization policies from endpoint metadata.
|
||||
/// </summary>
|
||||
internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? policyProvider = null) : IConfigureOptions<McpServerOptions>, IPostConfigureOptions<McpServerOptions>
|
||||
{
|
||||
private static readonly string AuthorizationFilterInvokedKey = "ModelContextProtocol.AspNetCore.AuthorizationFilter.Invoked";
|
||||
|
||||
public void Configure(McpServerOptions options)
|
||||
{
|
||||
ConfigureListToolsFilter(options);
|
||||
ConfigureCallToolFilter(options);
|
||||
|
||||
ConfigureListResourcesFilter(options);
|
||||
ConfigureListResourceTemplatesFilter(options);
|
||||
ConfigureReadResourceFilter(options);
|
||||
|
||||
ConfigureListPromptsFilter(options);
|
||||
ConfigureGetPromptFilter(options);
|
||||
}
|
||||
|
||||
public void PostConfigure(string? name, McpServerOptions options)
|
||||
{
|
||||
CheckListToolsFilter(options);
|
||||
CheckCallToolFilter(options);
|
||||
|
||||
CheckListResourcesFilter(options);
|
||||
CheckListResourceTemplatesFilter(options);
|
||||
CheckReadResourceFilter(options);
|
||||
|
||||
CheckListPromptsFilter(options);
|
||||
CheckGetPromptFilter(options);
|
||||
}
|
||||
|
||||
private void ConfigureListToolsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.Tools, static tool => tool.McpServerTool,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListToolsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.Tools.Select(static tool => tool.McpServerTool))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureCallToolFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new McpException("Access forbidden: This tool requires authorization.", McpErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckCallToolFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (HasAuthorizationMetadata(context.MatchedPrimitive)
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for tools/call operation, but authorization metadata was found on the tool. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureListResourcesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.Resources, static resource => resource.McpServerResource,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListResourcesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.Resources.Select(static resource => resource.McpServerResource))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureListResourceTemplatesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.ResourceTemplates, static resourceTemplate => resourceTemplate.McpServerResource,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListResourceTemplatesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.ResourceTemplates.Select(static resourceTemplate => resourceTemplate.McpServerResource))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureReadResourceFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new McpException("Access forbidden: This resource requires authorization.", McpErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckReadResourceFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (HasAuthorizationMetadata(context.MatchedPrimitive)
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for resources/read operation, but authorization metadata was found on the resource. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureListPromptsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.Prompts, static prompt => prompt.McpServerPrompt,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListPromptsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.Prompts.Select(static prompt => prompt.McpServerPrompt))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureGetPromptFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new McpException("Access forbidden: This prompt requires authorization.", McpErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckGetPromptFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (HasAuthorizationMetadata(context.MatchedPrimitive)
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for prompts/get operation, but authorization metadata was found on the prompt. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters a collection of items based on authorization policies in their metadata.
|
||||
/// For list operations where we need to filter results by authorization.
|
||||
/// </summary>
|
||||
private async ValueTask FilterAuthorizedItemsAsync<T>(IList<T> items, Func<T, IMcpServerPrimitive?> primitiveSelector,
|
||||
ClaimsPrincipal? user, IServiceProvider? requestServices, object context)
|
||||
{
|
||||
for (int i = items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var authorizationResult = await GetAuthorizationResultAsync(
|
||||
user, primitiveSelector(items[i]), requestServices, context);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
items.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<AuthorizationResult> GetAuthorizationResultAsync(
|
||||
ClaimsPrincipal? user, IMcpServerPrimitive? primitive, IServiceProvider? requestServices, object context)
|
||||
{
|
||||
if (!HasAuthorizationMetadata(primitive))
|
||||
{
|
||||
return AuthorizationResult.Success();
|
||||
}
|
||||
|
||||
if (policyProvider is null)
|
||||
{
|
||||
throw new InvalidOperationException($"You must call AddAuthorization() because an authorization related attribute was found on {primitive.Id}");
|
||||
}
|
||||
|
||||
var policy = await CombineAsync(policyProvider, primitive.Metadata);
|
||||
if (policy is null)
|
||||
{
|
||||
return AuthorizationResult.Success();
|
||||
}
|
||||
|
||||
if (requestServices is null)
|
||||
{
|
||||
// The IAuthorizationPolicyProvider service must be non-null to get to this line, so it's very unexpected for RequestContext.Services to not be set.
|
||||
throw new InvalidOperationException("RequestContext.Services is not set! The IMcpServer must be initialized with a non-null IServiceProvider.");
|
||||
}
|
||||
|
||||
// ASP.NET Core's AuthorizationMiddleware resolves the IAuthorizationService from scoped request services, so we do the same.
|
||||
var authService = requestServices.GetRequiredService<IAuthorizationService>();
|
||||
return await authService.AuthorizeAsync(user ?? new ClaimsPrincipal(new ClaimsIdentity()), context, policy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines authorization policies and requirements from endpoint metadata without considering <see cref="IAllowAnonymous"/>.
|
||||
/// </summary>
|
||||
/// <param name="policyProvider">The authorization policy provider.</param>
|
||||
/// <param name="endpointMetadata">The endpoint metadata collection.</param>
|
||||
/// <returns>The combined authorization policy, or null if no authorization is required.</returns>
|
||||
private static async ValueTask<AuthorizationPolicy?> CombineAsync(IAuthorizationPolicyProvider policyProvider, IReadOnlyList<object> endpointMetadata)
|
||||
{
|
||||
// https://github.com/dotnet/aspnetcore/issues/63365 tracks adding this as public API to AuthorizationPolicy itself.
|
||||
// Copied from https://github.com/dotnet/aspnetcore/blob/9f2977bf9cfb539820983bda3bedf81c8cda9f20/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs#L116-L138
|
||||
var authorizeData = endpointMetadata.OfType<IAuthorizeData>();
|
||||
var policies = endpointMetadata.OfType<AuthorizationPolicy>();
|
||||
|
||||
var policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies);
|
||||
|
||||
AuthorizationPolicyBuilder? reqPolicyBuilder = null;
|
||||
|
||||
foreach (var m in endpointMetadata)
|
||||
{
|
||||
if (m is not IAuthorizationRequirementData requirementData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
reqPolicyBuilder ??= new AuthorizationPolicyBuilder();
|
||||
foreach (var requirement in requirementData.GetRequirements())
|
||||
{
|
||||
reqPolicyBuilder.AddRequirements(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
if (reqPolicyBuilder is null)
|
||||
{
|
||||
return policy;
|
||||
}
|
||||
|
||||
// Combine policy with requirements or just use requirements if no policy
|
||||
return (policy is null)
|
||||
? reqPolicyBuilder.Build()
|
||||
: AuthorizationPolicy.Combine(policy, reqPolicyBuilder.Build());
|
||||
}
|
||||
|
||||
private static bool HasAuthorizationMetadata([NotNullWhen(true)] IMcpServerPrimitive? primitive)
|
||||
{
|
||||
// If no primitive was found for this request or there is IAllowAnonymous metadata anywhere on the class or method,
|
||||
// the request should go through as normal.
|
||||
if (primitive is null || primitive.Metadata.Any(static m => m is IAllowAnonymous))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return primitive.Metadata.Any(static m => m is IAuthorizeData or AuthorizationPolicy or IAuthorizationRequirementData);
|
||||
}
|
||||
|
||||
private static bool HasAuthorizationMetadata(IEnumerable<IMcpServerPrimitive?> primitives)
|
||||
=> primitives.Any(HasAuthorizationMetadata);
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.AspNetCore;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
|
|
@ -29,6 +31,8 @@ public static class HttpMcpServerBuilderExtensions
|
|||
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
|
||||
builder.Services.AddDataProtection();
|
||||
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>());
|
||||
|
||||
if (configureOptions is not null)
|
||||
{
|
||||
builder.Services.Configure(configureOptions);
|
||||
|
|
@ -36,4 +40,27 @@ public static class HttpMcpServerBuilderExtensions
|
|||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds authorization filters to support <see cref="AuthorizeAttribute"/>
|
||||
/// on MCP server tools, prompts, and resources. This method should always be called when using
|
||||
/// ASP.NET Core integration to ensure proper authorization support.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This method automatically configures authorization filters for all MCP server handlers. These filters respect
|
||||
/// authorization attributes such as <see cref="AuthorizeAttribute"/>
|
||||
/// and <see cref="AllowAnonymousAttribute"/>.
|
||||
/// </remarks>
|
||||
public static IMcpServerBuilder AddAuthorizationFilters(this IMcpServerBuilder builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
// Allow the authorization filters to get added multiple times in case other middleware changes the matched primitive.
|
||||
builder.Services.AddTransient<IConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -97,7 +96,7 @@ internal sealed class SseHandler(
|
|||
return;
|
||||
}
|
||||
|
||||
var message = (JsonRpcMessage?)await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), context.RequestAborted);
|
||||
var message = await StreamableHttpHandler.ReadJsonRpcMessageAsync(context);
|
||||
if (message is null)
|
||||
{
|
||||
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using Microsoft.Net.Http.Headers;
|
|||
using ModelContextProtocol.AspNetCore.Stateless;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.IO.Pipelines;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
|
@ -26,6 +25,8 @@ internal sealed class StreamableHttpHandler(
|
|||
IServiceProvider applicationServices)
|
||||
{
|
||||
private const string McpSessionIdHeaderName = "Mcp-Session-Id";
|
||||
|
||||
private static readonly JsonTypeInfo<JsonRpcMessage> s_messageTypeInfo = GetRequiredJsonTypeInfo<JsonRpcMessage>();
|
||||
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
|
||||
|
||||
public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions.Value;
|
||||
|
|
@ -55,8 +56,17 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
await using var _ = await session.AcquireReferenceAsync(context.RequestAborted);
|
||||
|
||||
var message = await ReadJsonRpcMessageAsync(context);
|
||||
if (message is null)
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The POST body did not contain a valid JSON-RPC message.",
|
||||
StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeSseResponse(context);
|
||||
var wroteResponse = await session.Transport.HandlePostRequest(new HttpDuplexPipe(context), context.RequestAborted);
|
||||
var wroteResponse = await session.Transport.HandlePostRequest(message, context.Response.Body, context.RequestAborted);
|
||||
if (!wroteResponse)
|
||||
{
|
||||
// We wound up writing nothing, so there should be no Content-Type response header.
|
||||
|
|
@ -264,6 +274,22 @@ internal sealed class StreamableHttpHandler(
|
|||
return WebEncoders.Base64UrlEncode(buffer);
|
||||
}
|
||||
|
||||
internal static async Task<JsonRpcMessage?> ReadJsonRpcMessageAsync(HttpContext context)
|
||||
{
|
||||
// Implementation for reading a JSON-RPC message from the request body
|
||||
var message = await context.Request.ReadFromJsonAsync(s_messageTypeInfo, context.RequestAborted);
|
||||
|
||||
if (context.User?.Identity?.IsAuthenticated == true && message is not null)
|
||||
{
|
||||
message.Context = new()
|
||||
{
|
||||
User = context.User,
|
||||
};
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private void ScheduleStatelessSessionIdWrite(HttpContext context, StreamableHttpServerTransport transport)
|
||||
{
|
||||
transport.OnInitRequestReceived = initRequestParams =>
|
||||
|
|
@ -304,17 +330,11 @@ internal sealed class StreamableHttpHandler(
|
|||
return null;
|
||||
}
|
||||
|
||||
private static JsonTypeInfo<T> GetRequiredJsonTypeInfo<T>() => (JsonTypeInfo<T>)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));
|
||||
internal static JsonTypeInfo<T> GetRequiredJsonTypeInfo<T>() => (JsonTypeInfo<T>)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));
|
||||
|
||||
private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||
=> acceptHeaderValue.MatchesMediaType("application/json");
|
||||
|
||||
private static bool MatchesTextEventStreamMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||
=> acceptHeaderValue.MatchesMediaType("text/event-stream");
|
||||
|
||||
private sealed class HttpDuplexPipe(HttpContext context) : IDuplexPipe
|
||||
{
|
||||
public PipeReader Input => context.Request.BodyReader;
|
||||
public PipeWriter Output => context.Response.BodyWriter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,15 +100,17 @@ internal sealed class StreamableHttpSession(
|
|||
|
||||
try
|
||||
{
|
||||
await _disposeCts.CancelAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Dispose transport first to complete the incoming MessageReader gracefully and avoid a potentially unnecessary OCE.
|
||||
await transport.DisposeAsync();
|
||||
await _disposeCts.CancelAsync();
|
||||
|
||||
await ServerRunTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DisposeServerThenTransportAsync();
|
||||
await server.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
@ -124,18 +126,6 @@ internal sealed class StreamableHttpSession(
|
|||
}
|
||||
}
|
||||
|
||||
private async ValueTask DisposeServerThenTransportAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await server.DisposeAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await transport.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class UnreferenceDisposable(StreamableHttpSession session) : IAsyncDisposable
|
||||
{
|
||||
public ValueTask DisposeAsync()
|
||||
|
|
|
|||
Loading…
Reference in a new issue