Skip to content
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

Merge master into openapi #1624

Merged
merged 3 commits into from
Oct 21, 2024
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 @@ -9,6 +9,7 @@
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Net.Http.Headers;

namespace JsonApiDotNetCore.OpenApi.Swashbuckle;

Expand All @@ -21,6 +22,8 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
/// </summary>
internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
private static readonly string DefaultMediaType = JsonApiMediaType.Default.ToString();

private readonly IActionDescriptorCollectionProvider _defaultProvider;
private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;

Expand Down Expand Up @@ -129,8 +132,21 @@ private static bool ProducesJsonApiResponseDocument(ActionDescriptor endpoint)
{
var produces = endpoint.GetFilterMetadata<ProducesAttribute>();

return produces != null && produces.ContentTypes.Any(contentType =>
contentType is HeaderConstants.MediaType or HeaderConstants.AtomicOperationsMediaType or HeaderConstants.RelaxedAtomicOperationsMediaType);
if (produces != null)
{
foreach (string contentType in produces.ContentTypes)
{
if (MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue? headerValue))
{
if (headerValue.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
}

return false;
}

private static List<ActionDescriptor> Expand(ActionDescriptor genericEndpoint, NonPrimaryEndpointMetadata metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;

internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider
{
private static readonly string DefaultMediaType = JsonApiMediaType.Default.ToString();

/// <inheritdoc />
public bool CanRead(InputFormatterContext context)
{
Expand All @@ -25,12 +27,12 @@ public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type o
ArgumentGuard.NotNullNorEmpty(contentType);
ArgumentGuard.NotNull(objectType);

if (JsonApiSchemaFacts.IsRequestBodySchemaType(objectType) && contentType is HeaderConstants.MediaType or HeaderConstants.AtomicOperationsMediaType or
HeaderConstants.RelaxedAtomicOperationsMediaType)
if (JsonApiSchemaFacts.IsRequestBodySchemaType(objectType) && MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue? headerValue) &&
headerValue.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
{
return new MediaTypeCollection
{
MediaTypeHeaderValue.Parse(contentType)
headerValue
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoint endpoint)

private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint, ResourceType? resourceType)
{
string contentType = endpoint == JsonApiEndpoint.PostOperations ? HeaderConstants.RelaxedAtomicOperationsMediaType : HeaderConstants.MediaType;
action.Filters.Add(new ProducesAttribute(contentType));
JsonApiMediaType mediaType = GetMediaTypeForEndpoint(endpoint);
action.Filters.Add(new ProducesAttribute(mediaType.ToString()));

foreach (HttpStatusCode statusCode in GetSuccessStatusCodesForEndpoint(endpoint))
{
Expand All @@ -144,6 +144,11 @@ private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint, R
}
}

private JsonApiMediaType GetMediaTypeForEndpoint(JsonApiEndpoint endpoint)
{
return endpoint == JsonApiEndpoint.PostOperations ? JsonApiMediaType.RelaxedAtomicOperations : JsonApiMediaType.Default;
}

private static HttpStatusCode[] GetSuccessStatusCodesForEndpoint(JsonApiEndpoint endpoint)
{
return endpoint switch
Expand Down Expand Up @@ -230,12 +235,12 @@ private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpoint endpoint
};
}

private static void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint)
private void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint)
{
if (RequiresRequestBody(endpoint))
{
string contentType = endpoint == JsonApiEndpoint.PostOperations ? HeaderConstants.RelaxedAtomicOperationsMediaType : HeaderConstants.MediaType;
action.Filters.Add(new ConsumesAttribute(contentType));
JsonApiMediaType mediaType = GetMediaTypeForEndpoint(endpoint);
action.Filters.Add(new ConsumesAttribute(mediaType.ToString()));
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/JsonApiDotNetCore/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ public static bool IsNullOrEmpty<T>([NotNullWhen(false)] this IEnumerable<T>? so
return !source.Any();
}

public static int FindIndex<T>(this IReadOnlyList<T> source, T item)
{
ArgumentGuard.NotNull(source);

for (int index = 0; index < source.Count; index++)
{
if (EqualityComparer<T>.Default.Equals(source[index], item))
{
return index;
}
}

return -1;
}

public static int FindIndex<T>(this IReadOnlyList<T> source, Predicate<T> match)
{
ArgumentGuard.NotNull(source);
Expand Down
14 changes: 14 additions & 0 deletions src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Data;
using System.Text.Json;
using JetBrains.Annotations;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;

Expand Down Expand Up @@ -172,6 +174,18 @@ public interface IJsonApiOptions
/// </summary>
IsolationLevel? TransactionIsolationLevel { get; }

/// <summary>
/// Lists the JSON:API extensions that are turned on. Empty by default, but if your project contains a controller that derives from
/// <see cref="BaseJsonApiOperationsController" />, the <see cref="JsonApiExtension.AtomicOperations" /> and
/// <see cref="JsonApiExtension.RelaxedAtomicOperations" /> extensions are automatically added.
/// </summary>
/// <remarks>
/// To implement a custom JSON:API extension, add it here and override <see cref="JsonApiContentNegotiator.GetPossibleMediaTypes" /> to indicate which
/// combinations of extensions are available, depending on the current endpoint. Use <see cref="IJsonApiRequest.Extensions" /> to obtain the active
/// extensions when implementing extension-specific logic.
/// </remarks>
IReadOnlySet<JsonApiExtension> Extensions { get; }

/// <summary>
/// Enables to customize the settings that are used by the <see cref="JsonSerializer" />.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ private void AddMiddlewareLayer()
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
_services.TryAddSingleton<IControllerResourceMapping>(provider => provider.GetRequiredService<IJsonApiRoutingConvention>());
_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
_services.TryAddSingleton<IJsonApiContentNegotiator, JsonApiContentNegotiator>();
_services.TryAddScoped<IJsonApiRequest, JsonApiRequest>();
_services.TryAddScoped<IJsonApiWriter, JsonApiWriter>();
_services.TryAddScoped<IJsonApiReader, JsonApiReader>();
Expand Down
28 changes: 28 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text.Encodings.Web;
using System.Text.Json;
using JetBrains.Annotations;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.JsonConverters;

Expand All @@ -11,6 +12,7 @@ namespace JsonApiDotNetCore.Configuration;
[PublicAPI]
public sealed class JsonApiOptions : IJsonApiOptions
{
private static readonly IReadOnlySet<JsonApiExtension> EmptyExtensionSet = new HashSet<JsonApiExtension>().AsReadOnly();
private readonly Lazy<JsonSerializerOptions> _lazySerializerWriteOptions;
private readonly Lazy<JsonSerializerOptions> _lazySerializerReadOptions;

Expand Down Expand Up @@ -97,6 +99,9 @@ public bool AllowClientGeneratedIds
/// <inheritdoc />
public IsolationLevel? TransactionIsolationLevel { get; set; }

/// <inheritdoc />
public IReadOnlySet<JsonApiExtension> Extensions { get; set; } = EmptyExtensionSet;

/// <inheritdoc />
public JsonSerializerOptions SerializerOptions { get; } = new()
{
Expand Down Expand Up @@ -130,4 +135,27 @@ public JsonApiOptions()
}
}, LazyThreadSafetyMode.ExecutionAndPublication);
}

/// <summary>
/// Adds the specified JSON:API extensions to the existing <see cref="Extensions" /> set.
/// </summary>
/// <param name="extensionsToAdd">
/// The JSON:API extensions to add.
/// </param>
public void IncludeExtensions(params JsonApiExtension[] extensionsToAdd)
{
ArgumentGuard.NotNull(extensionsToAdd);

if (!Extensions.IsSupersetOf(extensionsToAdd))
{
var extensions = new HashSet<JsonApiExtension>(Extensions);

foreach (JsonApiExtension extension in extensionsToAdd)
{
extensions.Add(extension);
}

Extensions = extensions.AsReadOnly();
}
}
}
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Middleware/HeaderConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ namespace JsonApiDotNetCore.Middleware;
[PublicAPI]
public static class HeaderConstants
{
[Obsolete($"Use {nameof(JsonApiMediaType)}.{nameof(JsonApiMediaType.Default)}.ToString() instead.")]
public const string MediaType = "application/vnd.api+json";

[Obsolete($"Use {nameof(JsonApiMediaType)}.{nameof(JsonApiMediaType.AtomicOperations)}.ToString() instead.")]
public const string AtomicOperationsMediaType = $"{MediaType}; ext=\"https://jsonapi.org/ext/atomic\"";

[Obsolete($"Use {nameof(JsonApiMediaType)}.{nameof(JsonApiMediaType.RelaxedAtomicOperations)}.ToString() instead.")]
public const string RelaxedAtomicOperationsMediaType = $"{MediaType}; ext=atomic-operations";
}
16 changes: 16 additions & 0 deletions src/JsonApiDotNetCore/Middleware/IJsonApiContentNegotiator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;

namespace JsonApiDotNetCore.Middleware;

/// <summary>
/// Performs content negotiation for JSON:API requests.
/// </summary>
public interface IJsonApiContentNegotiator
{
/// <summary>
/// Validates the Content-Type and Accept HTTP headers from the incoming request. Throws a <see cref="JsonApiException" /> if unsupported. Otherwise,
/// returns the list of negotiated JSON:API extensions, which should always be a subset of <see cref="IJsonApiOptions.Extensions" />.
/// </summary>
IReadOnlySet<JsonApiExtension> Negotiate();
}
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public interface IJsonApiRequest
/// </summary>
string? TransactionId { get; }

/// <summary>
/// The JSON:API extensions enabled for the current request. This is always a subset of <see cref="IJsonApiOptions.Extensions" />.
/// </summary>
IReadOnlySet<JsonApiExtension> Extensions { get; }

/// <summary>
/// Performs a shallow copy.
/// </summary>
Expand Down
Loading
Loading