Skip to content

Commit 9657947

Browse files
authored
Merge pull request #1624 from json-api-dotnet/merge-master-into-openapi
Merge master into openapi
2 parents 2fd9263 + 32f7515 commit 9657947

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1633
-343
lines changed

src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.AspNetCore.Mvc.Filters;
1010
using Microsoft.AspNetCore.Mvc.Infrastructure;
1111
using Microsoft.AspNetCore.Mvc.Routing;
12+
using Microsoft.Net.Http.Headers;
1213

1314
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
1415

@@ -21,6 +22,8 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
2122
/// </summary>
2223
internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
2324
{
25+
private static readonly string DefaultMediaType = JsonApiMediaType.Default.ToString();
26+
2427
private readonly IActionDescriptorCollectionProvider _defaultProvider;
2528
private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;
2629

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

132-
return produces != null && produces.ContentTypes.Any(contentType =>
133-
contentType is HeaderConstants.MediaType or HeaderConstants.AtomicOperationsMediaType or HeaderConstants.RelaxedAtomicOperationsMediaType);
135+
if (produces != null)
136+
{
137+
foreach (string contentType in produces.ContentTypes)
138+
{
139+
if (MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue? headerValue))
140+
{
141+
if (headerValue.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
142+
{
143+
return true;
144+
}
145+
}
146+
}
147+
}
148+
149+
return false;
134150
}
135151

136152
private static List<ActionDescriptor> Expand(ActionDescriptor genericEndpoint, NonPrimaryEndpointMetadata metadata,

src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRequestFormatMetadataProvider.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
77

88
internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider
99
{
10+
private static readonly string DefaultMediaType = JsonApiMediaType.Default.ToString();
11+
1012
/// <inheritdoc />
1113
public bool CanRead(InputFormatterContext context)
1214
{
@@ -25,12 +27,12 @@ public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type o
2527
ArgumentGuard.NotNullNorEmpty(contentType);
2628
ArgumentGuard.NotNull(objectType);
2729

28-
if (JsonApiSchemaFacts.IsRequestBodySchemaType(objectType) && contentType is HeaderConstants.MediaType or HeaderConstants.AtomicOperationsMediaType or
29-
HeaderConstants.RelaxedAtomicOperationsMediaType)
30+
if (JsonApiSchemaFacts.IsRequestBodySchemaType(objectType) && MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue? headerValue) &&
31+
headerValue.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
3032
{
3133
return new MediaTypeCollection
3234
{
33-
MediaTypeHeaderValue.Parse(contentType)
35+
headerValue
3436
};
3537
}
3638

src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoint endpoint)
129129

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

135135
foreach (HttpStatusCode statusCode in GetSuccessStatusCodesForEndpoint(endpoint))
136136
{
@@ -144,6 +144,11 @@ private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint, R
144144
}
145145
}
146146

147+
private JsonApiMediaType GetMediaTypeForEndpoint(JsonApiEndpoint endpoint)
148+
{
149+
return endpoint == JsonApiEndpoint.PostOperations ? JsonApiMediaType.RelaxedAtomicOperations : JsonApiMediaType.Default;
150+
}
151+
147152
private static HttpStatusCode[] GetSuccessStatusCodesForEndpoint(JsonApiEndpoint endpoint)
148153
{
149154
return endpoint switch
@@ -230,12 +235,12 @@ private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpoint endpoint
230235
};
231236
}
232237

233-
private static void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint)
238+
private void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint)
234239
{
235240
if (RequiresRequestBody(endpoint))
236241
{
237-
string contentType = endpoint == JsonApiEndpoint.PostOperations ? HeaderConstants.RelaxedAtomicOperationsMediaType : HeaderConstants.MediaType;
238-
action.Filters.Add(new ConsumesAttribute(contentType));
242+
JsonApiMediaType mediaType = GetMediaTypeForEndpoint(endpoint);
243+
action.Filters.Add(new ConsumesAttribute(mediaType.ToString()));
239244
}
240245
}
241246

src/JsonApiDotNetCore/CollectionExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ public static bool IsNullOrEmpty<T>([NotNullWhen(false)] this IEnumerable<T>? so
1616
return !source.Any();
1717
}
1818

19+
public static int FindIndex<T>(this IReadOnlyList<T> source, T item)
20+
{
21+
ArgumentGuard.NotNull(source);
22+
23+
for (int index = 0; index < source.Count; index++)
24+
{
25+
if (EqualityComparer<T>.Default.Equals(source[index], item))
26+
{
27+
return index;
28+
}
29+
}
30+
31+
return -1;
32+
}
33+
1934
public static int FindIndex<T>(this IReadOnlyList<T> source, Predicate<T> match)
2035
{
2136
ArgumentGuard.NotNull(source);

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Data;
22
using System.Text.Json;
33
using JetBrains.Annotations;
4+
using JsonApiDotNetCore.Controllers;
5+
using JsonApiDotNetCore.Middleware;
46
using JsonApiDotNetCore.Resources.Annotations;
57
using JsonApiDotNetCore.Serialization.Objects;
68

@@ -172,6 +174,18 @@ public interface IJsonApiOptions
172174
/// </summary>
173175
IsolationLevel? TransactionIsolationLevel { get; }
174176

177+
/// <summary>
178+
/// Lists the JSON:API extensions that are turned on. Empty by default, but if your project contains a controller that derives from
179+
/// <see cref="BaseJsonApiOperationsController" />, the <see cref="JsonApiExtension.AtomicOperations" /> and
180+
/// <see cref="JsonApiExtension.RelaxedAtomicOperations" /> extensions are automatically added.
181+
/// </summary>
182+
/// <remarks>
183+
/// To implement a custom JSON:API extension, add it here and override <see cref="JsonApiContentNegotiator.GetPossibleMediaTypes" /> to indicate which
184+
/// combinations of extensions are available, depending on the current endpoint. Use <see cref="IJsonApiRequest.Extensions" /> to obtain the active
185+
/// extensions when implementing extension-specific logic.
186+
/// </remarks>
187+
IReadOnlySet<JsonApiExtension> Extensions { get; }
188+
175189
/// <summary>
176190
/// Enables to customize the settings that are used by the <see cref="JsonSerializer" />.
177191
/// </summary>

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ private void AddMiddlewareLayer()
184184
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
185185
_services.TryAddSingleton<IControllerResourceMapping>(provider => provider.GetRequiredService<IJsonApiRoutingConvention>());
186186
_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
187+
_services.TryAddSingleton<IJsonApiContentNegotiator, JsonApiContentNegotiator>();
187188
_services.TryAddScoped<IJsonApiRequest, JsonApiRequest>();
188189
_services.TryAddScoped<IJsonApiWriter, JsonApiWriter>();
189190
_services.TryAddScoped<IJsonApiReader, JsonApiReader>();

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Text.Encodings.Web;
33
using System.Text.Json;
44
using JetBrains.Annotations;
5+
using JsonApiDotNetCore.Middleware;
56
using JsonApiDotNetCore.Resources.Annotations;
67
using JsonApiDotNetCore.Serialization.JsonConverters;
78

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

@@ -97,6 +99,9 @@ public bool AllowClientGeneratedIds
9799
/// <inheritdoc />
98100
public IsolationLevel? TransactionIsolationLevel { get; set; }
99101

102+
/// <inheritdoc />
103+
public IReadOnlySet<JsonApiExtension> Extensions { get; set; } = EmptyExtensionSet;
104+
100105
/// <inheritdoc />
101106
public JsonSerializerOptions SerializerOptions { get; } = new()
102107
{
@@ -130,4 +135,27 @@ public JsonApiOptions()
130135
}
131136
}, LazyThreadSafetyMode.ExecutionAndPublication);
132137
}
138+
139+
/// <summary>
140+
/// Adds the specified JSON:API extensions to the existing <see cref="Extensions" /> set.
141+
/// </summary>
142+
/// <param name="extensionsToAdd">
143+
/// The JSON:API extensions to add.
144+
/// </param>
145+
public void IncludeExtensions(params JsonApiExtension[] extensionsToAdd)
146+
{
147+
ArgumentGuard.NotNull(extensionsToAdd);
148+
149+
if (!Extensions.IsSupersetOf(extensionsToAdd))
150+
{
151+
var extensions = new HashSet<JsonApiExtension>(Extensions);
152+
153+
foreach (JsonApiExtension extension in extensionsToAdd)
154+
{
155+
extensions.Add(extension);
156+
}
157+
158+
Extensions = extensions.AsReadOnly();
159+
}
160+
}
133161
}

src/JsonApiDotNetCore/Middleware/HeaderConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ namespace JsonApiDotNetCore.Middleware;
77
[PublicAPI]
88
public static class HeaderConstants
99
{
10+
[Obsolete($"Use {nameof(JsonApiMediaType)}.{nameof(JsonApiMediaType.Default)}.ToString() instead.")]
1011
public const string MediaType = "application/vnd.api+json";
12+
13+
[Obsolete($"Use {nameof(JsonApiMediaType)}.{nameof(JsonApiMediaType.AtomicOperations)}.ToString() instead.")]
1114
public const string AtomicOperationsMediaType = $"{MediaType}; ext=\"https://jsonapi.org/ext/atomic\"";
15+
16+
[Obsolete($"Use {nameof(JsonApiMediaType)}.{nameof(JsonApiMediaType.RelaxedAtomicOperations)}.ToString() instead.")]
1217
public const string RelaxedAtomicOperationsMediaType = $"{MediaType}; ext=atomic-operations";
1318
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Errors;
3+
4+
namespace JsonApiDotNetCore.Middleware;
5+
6+
/// <summary>
7+
/// Performs content negotiation for JSON:API requests.
8+
/// </summary>
9+
public interface IJsonApiContentNegotiator
10+
{
11+
/// <summary>
12+
/// Validates the Content-Type and Accept HTTP headers from the incoming request. Throws a <see cref="JsonApiException" /> if unsupported. Otherwise,
13+
/// returns the list of negotiated JSON:API extensions, which should always be a subset of <see cref="IJsonApiOptions.Extensions" />.
14+
/// </summary>
15+
IReadOnlySet<JsonApiExtension> Negotiate();
16+
}

src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public interface IJsonApiRequest
6060
/// </summary>
6161
string? TransactionId { get; }
6262

63+
/// <summary>
64+
/// The JSON:API extensions enabled for the current request. This is always a subset of <see cref="IJsonApiOptions.Extensions" />.
65+
/// </summary>
66+
IReadOnlySet<JsonApiExtension> Extensions { get; }
67+
6368
/// <summary>
6469
/// Performs a shallow copy.
6570
/// </summary>

0 commit comments

Comments
 (0)