Skip to content

Add public metadata types. #49068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,21 @@

#nullable enable

using System;
using System.Collections.Generic;

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Metadata that specifies the supported request content types.
/// </summary>
internal sealed class AcceptsMetadata : IAcceptsMetadata
public sealed class AcceptsMetadata : IAcceptsMetadata
{
/// <summary>
/// Creates a new instance of <see cref="AcceptsMetadata"/>.
/// </summary>
public AcceptsMetadata(string[] contentTypes)
{
ArgumentNullException.ThrowIfNull(contentTypes);

ContentTypes = contentTypes;
}

/// <summary>
/// Creates a new instance of <see cref="AcceptsMetadata"/> with a type.
/// </summary>
public AcceptsMetadata(Type? type, bool isOptional, string[] contentTypes)
/// <param name="contentTypes">Content types that are accepted by endpoint.</param>
/// <param name="type">The type being read from the request.</param>
/// <param name="isOptional">Whether the request body is optional.</param>
public AcceptsMetadata(string[] contentTypes, Type? type = null, bool isOptional = false)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(contentTypes);

RequestType = type;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// Specifies the type of the value and status code returned by the action.
/// </summary>
public sealed class ProducesResponseTypeMetadata : IProducesResponseTypeMetadata
{
/// <summary>
/// Initializes an instance of <see cref="ProducesResponseTypeMetadata"/>.
/// </summary>
/// <param name="statusCode">The HTTP response status code.</param>
/// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param>
/// <param name="contentTypes">Content types supported by the response.</param>
public ProducesResponseTypeMetadata(int statusCode, Type? type = null, string[]? contentTypes = null)
{
this.StatusCode = statusCode;
this.Type = type;

if (contentTypes is null || contentTypes.Length == 0)
{
this.ContentTypes = Enumerable.Empty<string>();
}
else
{
for (var i = 0; i < contentTypes.Length; i++)
{
MediaTypeHeaderValue.Parse(contentTypes[i]);
ValidateContentType(contentTypes[i]);
}

this.ContentTypes = contentTypes;
}

static void ValidateContentType(string type)
{
if (type.Contains('*', StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Could not parse '{type}'. Content types with wildcards are not supported.");
}
}
}

// Only for internal use where validation is unnecessary.
private ProducesResponseTypeMetadata(int statusCode, Type? type, IEnumerable<string> contentTypes)
{
Type = type;
StatusCode = statusCode;
ContentTypes = contentTypes;
}

/// <summary>
/// Gets or sets the type of the value returned by an action.
/// </summary>
public Type? Type { get; private set; }

/// <summary>
/// Gets or sets the HTTP status code of the response.
/// </summary>
public int StatusCode { get; private set; }

/// <summary>
/// Gets or sets the content types associated with the response.
/// </summary>
public IEnumerable<string> ContentTypes { get; private set; }

internal static ProducesResponseTypeMetadata CreateUnvalidated(Type? type, int statusCode, IEnumerable<string> contentTypes) => new(statusCode, type, contentTypes);
}
10 changes: 10 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult
Microsoft.AspNetCore.Http.HttpResults.EmptyHttpResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Http.HttpValidationProblemDetails.Errors.set -> void
Microsoft.AspNetCore.Http.IProblemDetailsService.TryWriteAsync(Microsoft.AspNetCore.Http.ProblemDetailsContext! context) -> System.Threading.Tasks.ValueTask<bool>
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! contentTypes, System.Type? type = null, bool isOptional = false) -> void
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList<string!>!
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.IsOptional.get -> bool
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.RequestType.get -> System.Type?
Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata
Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata.Route.get -> string!
Microsoft.AspNetCore.Http.ProblemDetailsContext.Exception.get -> System.Exception?
Microsoft.AspNetCore.Http.ProblemDetailsContext.Exception.init -> void
*REMOVED*Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.init -> void
Microsoft.AspNetCore.Http.ProblemDetailsContext.ProblemDetails.set -> void
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ContentTypes.get -> System.Collections.Generic.IEnumerable<string!>!
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.ProducesResponseTypeMetadata(int statusCode, System.Type? type = null, string![]? contentTypes = null) -> void
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.StatusCode.get -> int
Microsoft.AspNetCore.Http.ProducesResponseTypeMetadata.Type.get -> System.Type?
Microsoft.AspNetCore.Mvc.ProblemDetails.Extensions.set -> void
static Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.Create(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.AspNetCore.Http.EndpointFilterInvocationContext!
static Microsoft.AspNetCore.Http.EndpointFilterInvocationContext.Create<T1, T2, T3, T4, T5, T6, T7, T8>(Microsoft.AspNetCore.Http.HttpContext! httpContext, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) -> Microsoft.AspNetCore.Http.EndpointFilterInvocationContext!
Expand Down
10 changes: 0 additions & 10 deletions src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,16 +247,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
using var codeWriter = new CodeWriter(stringWriter, baseIndent: 0);

if (hasFormBody || hasJsonBody)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.AcceptsMetadataType);
}

if (hasResponseMetadata)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.ProducesResponseTypeMetadataType);
}

if (hasFormBody || hasJsonBody || hasResponseMetadata)
{
codeWriter.WriteLine(RequestDelegateGeneratorSources.ContentTypeConstantsType);
Expand Down
49 changes: 0 additions & 49 deletions src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,55 +30,6 @@ file static class GeneratedMetadataConstants
public static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" };
}

""";

public static string ProducesResponseTypeMetadataType => $$"""
{{GeneratedCodeAttribute}}
file sealed class GeneratedProducesResponseTypeMetadata : IProducesResponseTypeMetadata
{
public GeneratedProducesResponseTypeMetadata(Type? type, int statusCode, string[] contentTypes)
{
Type = type;
StatusCode = statusCode;
ContentTypes = contentTypes;
}

public Type? Type { get; }

public int StatusCode { get; }

public IEnumerable<string> ContentTypes { get; }
}

""";

public static string AcceptsMetadataType => $$"""
{{GeneratedCodeAttribute}}
file sealed class GeneratedAcceptsMetadata : IAcceptsMetadata
{
public GeneratedAcceptsMetadata(string[] contentTypes)
{
ArgumentNullException.ThrowIfNull(contentTypes);

ContentTypes = contentTypes;
}

public GeneratedAcceptsMetadata(Type? type, bool isOptional, string[] contentTypes)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(contentTypes);

RequestType = type;
ContentTypes = contentTypes;
IsOptional = isOptional;
}

public IReadOnlyList<string> ContentTypes { get; }

public Type? RequestType { get; }

public bool IsOptional { get; }
}
""";

public static string PopulateEndpointMetadataMethod => """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ private static void EmitBuiltinResponseTypeMetadata(this Endpoint endpoint, Code

if (responseType.SpecialType == SpecialType.System_String)
{
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new GeneratedProducesResponseTypeMetadata(type: null, statusCode: StatusCodes.Status200OK, contentTypes: GeneratedMetadataConstants.PlaintextContentType));");
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, contentTypes: GeneratedMetadataConstants.PlaintextContentType));");
}
else
{
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new GeneratedProducesResponseTypeMetadata(type: typeof({responseType.ToDisplayString(EmitterConstants.DisplayFormatWithoutNullability)}), statusCode: StatusCodes.Status200OK, contentTypes: GeneratedMetadataConstants.JsonContentType));");
codeWriter.WriteLine($$"""options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(statusCode: StatusCodes.Status200OK, type: typeof({{responseType.ToDisplayString(EmitterConstants.DisplayFormatWithoutNullability)}}), contentTypes: GeneratedMetadataConstants.JsonContentType));""");
}
}

Expand Down Expand Up @@ -264,11 +264,11 @@ public static void EmitFormAcceptsMetadata(this Endpoint endpoint, CodeWriter co

if (hasFormFiles)
{
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new GeneratedAcceptsMetadata(contentTypes: GeneratedMetadataConstants.FormFileContentType));");
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.FormFileContentType));");
}
else
{
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new GeneratedAcceptsMetadata(contentTypes: GeneratedMetadataConstants.FormContentType));");
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.FormContentType));");
}
}

Expand All @@ -292,7 +292,7 @@ public static void EmitJsonAcceptsMetadata(this Endpoint endpoint, CodeWriter co

if (explicitBodyParameter != null)
{
codeWriter.WriteLine($$"""options.EndpointBuilder.Metadata.Add(new GeneratedAcceptsMetadata(type: typeof({{explicitBodyParameter.Type.ToDisplayString(EmitterConstants.DisplayFormatWithoutNullability)}}), isOptional: {{(explicitBodyParameter.IsOptional ? "true" : "false")}}, contentTypes: GeneratedMetadataConstants.JsonContentType));""");
codeWriter.WriteLine($$"""options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: typeof({{explicitBodyParameter.Type.ToDisplayString(EmitterConstants.DisplayFormatWithoutNullability)}}), isOptional: {{(explicitBodyParameter.IsOptional ? "true" : "false")}}, contentTypes: GeneratedMetadataConstants.JsonContentType));""");
}
else if (potentialImplicitBodyParameters.Count > 0)
{
Expand All @@ -311,14 +311,14 @@ public static void EmitJsonAcceptsMetadata(this Endpoint endpoint, CodeWriter co
codeWriter.StartBlock();
codeWriter.WriteLine("if (!serviceProviderIsService.IsService(type))");
codeWriter.StartBlock();
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new GeneratedAcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType));");
codeWriter.WriteLine("options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type: type, isOptional: isOptional, contentTypes: GeneratedMetadataConstants.JsonContentType));");
codeWriter.WriteLine("break;");
codeWriter.EndBlock();
codeWriter.EndBlock();
}
else
{
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new GeneratedAcceptsMetadata(contentTypes: GeneratedMetadataConstants.JsonContentType));");
codeWriter.WriteLine($"options.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes: GeneratedMetadataConstants.JsonContentType));");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<Compile Include="$(SharedSourceRoot)PropertyAsParameterInfo.cs" LinkBase="Shared" />
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)ApiExplorerTypes\*.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoutingMetadata\AcceptsMetadata.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)TypeNameHelper\TypeNameHelper.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)ProblemDetails\ProblemDetailsDefaults.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)ValueStringBuilder\**\*.cs" LinkBase="Shared" />
Expand Down
2 changes: 1 addition & 1 deletion src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1915,7 +1915,7 @@ private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext fac
return;
}

factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes));
factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(contentTypes, type, factoryContext.AllowEmptyRequestBody));
}

private static void InferFormAcceptsMetadata(RequestDelegateFactoryContext factoryContext)
Expand Down
Loading