Skip to content

Commit 8323836

Browse files
authored
Merge pull request #1629 from json-api-dotnet/jsonapi-extensions-refactor
Refactorings for JSON:API extensions to unblock OpenAPI support
2 parents 43c1a4b + 3840fcb commit 8323836

23 files changed

+739
-75
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using JsonApiDotNetCore.Serialization.JsonConverters;
2+
3+
namespace JsonApiDotNetCore.Configuration;
4+
5+
internal sealed class DefaultJsonApiApplicationBuilderEvents : IJsonApiApplicationBuilderEvents
6+
{
7+
private readonly IJsonApiOptions _options;
8+
9+
public DefaultJsonApiApplicationBuilderEvents(IJsonApiOptions options)
10+
{
11+
ArgumentGuard.NotNull(options);
12+
13+
_options = options;
14+
}
15+
16+
public void ResourceGraphBuilt(IResourceGraph resourceGraph)
17+
{
18+
_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace JsonApiDotNetCore.Configuration;
2+
3+
internal interface IJsonApiApplicationBuilderEvents
4+
{
5+
void ResourceGraphBuilt(IResourceGraph resourceGraph);
6+
}

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,15 @@ public interface IJsonApiOptions
176176

177177
/// <summary>
178178
/// 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.
179+
/// <see cref="BaseJsonApiOperationsController" />, the <see cref="JsonApiMediaTypeExtension.AtomicOperations" /> and
180+
/// <see cref="JsonApiMediaTypeExtension.RelaxedAtomicOperations" /> extensions are automatically added.
181181
/// </summary>
182182
/// <remarks>
183183
/// To implement a custom JSON:API extension, add it here and override <see cref="JsonApiContentNegotiator.GetPossibleMediaTypes" /> to indicate which
184184
/// combinations of extensions are available, depending on the current endpoint. Use <see cref="IJsonApiRequest.Extensions" /> to obtain the active
185185
/// extensions when implementing extension-specific logic.
186186
/// </remarks>
187-
IReadOnlySet<JsonApiExtension> Extensions { get; }
187+
IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; }
188188

189189
/// <summary>
190190
/// Enables to customize the settings that are used by the <see cref="JsonSerializer" />.

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using JsonApiDotNetCore.QueryStrings;
88
using JsonApiDotNetCore.Repositories;
99
using JsonApiDotNetCore.Resources;
10-
using JsonApiDotNetCore.Serialization.JsonConverters;
1110
using JsonApiDotNetCore.Serialization.Request;
1211
using JsonApiDotNetCore.Serialization.Request.Adapters;
1312
using JsonApiDotNetCore.Serialization.Response;
@@ -74,6 +73,8 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso
7473
_services.TryAddSingleton(serviceProvider =>
7574
{
7675
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
76+
var events = serviceProvider.GetRequiredService<IJsonApiApplicationBuilderEvents>();
77+
7778
var resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
7879

7980
var scanner = new ResourcesAssemblyScanner(_assemblyCache, resourceGraphBuilder);
@@ -93,8 +94,7 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso
9394
configureResourceGraph?.Invoke(resourceGraphBuilder);
9495

9596
IResourceGraph resourceGraph = resourceGraphBuilder.Build();
96-
97-
_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
97+
events.ResourceGraphBuilt(resourceGraph);
9898

9999
return resourceGraph;
100100
});
@@ -169,6 +169,7 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
169169
_services.TryAddScoped<IQueryLayerComposer, QueryLayerComposer>();
170170
_services.TryAddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
171171
_services.TryAddSingleton<IDocumentDescriptionLinkProvider, NoDocumentDescriptionLinkProvider>();
172+
_services.TryAddSingleton<IJsonApiApplicationBuilderEvents, DefaultJsonApiApplicationBuilderEvents>();
172173
}
173174

174175
private void AddMiddlewareLayer()

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace JsonApiDotNetCore.Configuration;
1212
[PublicAPI]
1313
public sealed class JsonApiOptions : IJsonApiOptions
1414
{
15-
private static readonly IReadOnlySet<JsonApiExtension> EmptyExtensionSet = new HashSet<JsonApiExtension>().AsReadOnly();
15+
private static readonly IReadOnlySet<JsonApiMediaTypeExtension> EmptyExtensionSet = new HashSet<JsonApiMediaTypeExtension>().AsReadOnly();
1616
private readonly Lazy<JsonSerializerOptions> _lazySerializerWriteOptions;
1717
private readonly Lazy<JsonSerializerOptions> _lazySerializerReadOptions;
1818

@@ -100,7 +100,7 @@ public bool AllowClientGeneratedIds
100100
public IsolationLevel? TransactionIsolationLevel { get; set; }
101101

102102
/// <inheritdoc />
103-
public IReadOnlySet<JsonApiExtension> Extensions { get; set; } = EmptyExtensionSet;
103+
public IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; set; } = EmptyExtensionSet;
104104

105105
/// <inheritdoc />
106106
public JsonSerializerOptions SerializerOptions { get; } = new()
@@ -142,15 +142,15 @@ public JsonApiOptions()
142142
/// <param name="extensionsToAdd">
143143
/// The JSON:API extensions to add.
144144
/// </param>
145-
public void IncludeExtensions(params JsonApiExtension[] extensionsToAdd)
145+
public void IncludeExtensions(params JsonApiMediaTypeExtension[] extensionsToAdd)
146146
{
147147
ArgumentGuard.NotNull(extensionsToAdd);
148148

149149
if (!Extensions.IsSupersetOf(extensionsToAdd))
150150
{
151-
var extensions = new HashSet<JsonApiExtension>(Extensions);
151+
var extensions = new HashSet<JsonApiMediaTypeExtension>(Extensions);
152152

153-
foreach (JsonApiExtension extension in extensionsToAdd)
153+
foreach (JsonApiMediaTypeExtension extension in extensionsToAdd)
154154
{
155155
extensions.Add(extension);
156156
}

src/JsonApiDotNetCore/Middleware/IJsonApiContentNegotiator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ public interface IJsonApiContentNegotiator
1212
/// Validates the Content-Type and Accept HTTP headers from the incoming request. Throws a <see cref="JsonApiException" /> if unsupported. Otherwise,
1313
/// returns the list of negotiated JSON:API extensions, which should always be a subset of <see cref="IJsonApiOptions.Extensions" />.
1414
/// </summary>
15-
IReadOnlySet<JsonApiExtension> Negotiate();
15+
IReadOnlySet<JsonApiMediaTypeExtension> Negotiate();
1616
}

src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public interface IJsonApiRequest
6363
/// <summary>
6464
/// The JSON:API extensions enabled for the current request. This is always a subset of <see cref="IJsonApiOptions.Extensions" />.
6565
/// </summary>
66-
IReadOnlySet<JsonApiExtension> Extensions { get; }
66+
IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; }
6767

6868
/// <summary>
6969
/// Performs a shallow copy.

src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public JsonApiContentNegotiator(IJsonApiOptions options, IHttpContextAccessor ht
3636
}
3737

3838
/// <inheritdoc />
39-
public IReadOnlySet<JsonApiExtension> Negotiate()
39+
public IReadOnlySet<JsonApiMediaTypeExtension> Negotiate()
4040
{
4141
IReadOnlyList<JsonApiMediaType> possibleMediaTypes = GetPossibleMediaTypes();
4242

@@ -66,7 +66,7 @@ public IReadOnlySet<JsonApiExtension> Negotiate()
6666
return mediaType;
6767
}
6868

69-
private IReadOnlySet<JsonApiExtension> ValidateAcceptHeader(IReadOnlyList<JsonApiMediaType> possibleMediaTypes, JsonApiMediaType? requestMediaType)
69+
private IReadOnlySet<JsonApiMediaTypeExtension> ValidateAcceptHeader(IReadOnlyList<JsonApiMediaType> possibleMediaTypes, JsonApiMediaType? requestMediaType)
7070
{
7171
string[] acceptHeaderValues = HttpContext.Request.Headers.GetCommaSeparatedValues("Accept");
7272
JsonApiMediaType? bestMatch = null;
@@ -166,12 +166,12 @@ protected virtual IReadOnlyList<JsonApiMediaType> GetPossibleMediaTypes()
166166

167167
if (IsOperationsEndpoint())
168168
{
169-
if (_options.Extensions.Contains(JsonApiExtension.AtomicOperations))
169+
if (_options.Extensions.Contains(JsonApiMediaTypeExtension.AtomicOperations))
170170
{
171171
mediaTypes.Add(JsonApiMediaType.AtomicOperations);
172172
}
173173

174-
if (_options.Extensions.Contains(JsonApiExtension.RelaxedAtomicOperations))
174+
if (_options.Extensions.Contains(JsonApiMediaTypeExtension.RelaxedAtomicOperations))
175175
{
176176
mediaTypes.Add(JsonApiMediaType.RelaxedAtomicOperations);
177177
}

src/JsonApiDotNetCore/Middleware/JsonApiMediaType.cs

+10-10
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,23 @@ public sealed class JsonApiMediaType : IEquatable<JsonApiMediaType>
2222
/// <summary>
2323
/// Gets the JSON:API media type with the "https://jsonapi.org/ext/atomic" extension.
2424
/// </summary>
25-
public static readonly JsonApiMediaType AtomicOperations = new([JsonApiExtension.AtomicOperations]);
25+
public static readonly JsonApiMediaType AtomicOperations = new([JsonApiMediaTypeExtension.AtomicOperations]);
2626

2727
/// <summary>
2828
/// Gets the JSON:API media type with the "atomic-operations" extension.
2929
/// </summary>
30-
public static readonly JsonApiMediaType RelaxedAtomicOperations = new([JsonApiExtension.RelaxedAtomicOperations]);
30+
public static readonly JsonApiMediaType RelaxedAtomicOperations = new([JsonApiMediaTypeExtension.RelaxedAtomicOperations]);
3131

32-
public IReadOnlySet<JsonApiExtension> Extensions { get; }
32+
public IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; }
3333

34-
public JsonApiMediaType(IReadOnlySet<JsonApiExtension> extensions)
34+
public JsonApiMediaType(IReadOnlySet<JsonApiMediaTypeExtension> extensions)
3535
{
3636
ArgumentGuard.NotNull(extensions);
3737

3838
Extensions = extensions;
3939
}
4040

41-
public JsonApiMediaType(IEnumerable<JsonApiExtension> extensions)
41+
public JsonApiMediaType(IEnumerable<JsonApiMediaTypeExtension> extensions)
4242
{
4343
ArgumentGuard.NotNull(extensions);
4444

@@ -69,7 +69,7 @@ private static (JsonApiMediaType MediaType, decimal QualityFactor)? TryParse(str
6969

7070
if (isBaseMatch)
7171
{
72-
HashSet<JsonApiExtension> extensions = [];
72+
HashSet<JsonApiMediaTypeExtension> extensions = [];
7373

7474
decimal qualityFactor = 1.0m;
7575

@@ -97,13 +97,13 @@ private static (JsonApiMediaType MediaType, decimal QualityFactor)? TryParse(str
9797
return null;
9898
}
9999

100-
private static void ParseExtensions(NameValueHeaderValue parameter, HashSet<JsonApiExtension> extensions)
100+
private static void ParseExtensions(NameValueHeaderValue parameter, HashSet<JsonApiMediaTypeExtension> extensions)
101101
{
102102
string parameterValue = parameter.GetUnescapedValue().ToString();
103103

104104
foreach (string extValue in parameterValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
105105
{
106-
var extension = new JsonApiExtension(extValue);
106+
var extension = new JsonApiMediaTypeExtension(extValue);
107107
extensions.Add(extension);
108108
}
109109
}
@@ -114,7 +114,7 @@ public override string ToString()
114114
List<NameValueHeaderValue> parameters = [];
115115
bool requiresEscape = false;
116116

117-
foreach (JsonApiExtension extension in Extensions)
117+
foreach (JsonApiMediaTypeExtension extension in Extensions)
118118
{
119119
var extHeaderValue = new NameValueHeaderValue(ExtSegment);
120120
extHeaderValue.SetAndEscapeValue(extension.UnescapedValue);
@@ -178,7 +178,7 @@ public override int GetHashCode()
178178
{
179179
int hashCode = 0;
180180

181-
foreach (JsonApiExtension extension in Extensions)
181+
foreach (JsonApiMediaTypeExtension extension in Extensions)
182182
{
183183
hashCode = HashCode.Combine(hashCode, extension);
184184
}

src/JsonApiDotNetCore/Middleware/JsonApiExtension.cs renamed to src/JsonApiDotNetCore/Middleware/JsonApiMediaTypeExtension.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ namespace JsonApiDotNetCore.Middleware;
66
/// Represents a JSON:API extension (in unescaped format), which occurs as an "ext" parameter inside an HTTP Accept or Content-Type header.
77
/// </summary>
88
[PublicAPI]
9-
public sealed class JsonApiExtension : IEquatable<JsonApiExtension>
9+
public sealed class JsonApiMediaTypeExtension : IEquatable<JsonApiMediaTypeExtension>
1010
{
11-
public static readonly JsonApiExtension AtomicOperations = new("https://jsonapi.org/ext/atomic");
12-
public static readonly JsonApiExtension RelaxedAtomicOperations = new("atomic-operations");
11+
public static readonly JsonApiMediaTypeExtension AtomicOperations = new("https://jsonapi.org/ext/atomic");
12+
public static readonly JsonApiMediaTypeExtension RelaxedAtomicOperations = new("atomic-operations");
1313

1414
public string UnescapedValue { get; }
1515

16-
public JsonApiExtension(string unescapedValue)
16+
public JsonApiMediaTypeExtension(string unescapedValue)
1717
{
1818
ArgumentGuard.NotNullNorEmpty(unescapedValue);
1919

@@ -25,7 +25,7 @@ public override string ToString()
2525
return UnescapedValue;
2626
}
2727

28-
public bool Equals(JsonApiExtension? other)
28+
public bool Equals(JsonApiMediaTypeExtension? other)
2929
{
3030
if (other is null)
3131
{
@@ -42,7 +42,7 @@ public bool Equals(JsonApiExtension? other)
4242

4343
public override bool Equals(object? other)
4444
{
45-
return Equals(other as JsonApiExtension);
45+
return Equals(other as JsonApiMediaTypeExtension);
4646
}
4747

4848
public override int GetHashCode()

src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public async Task InvokeAsync(HttpContext httpContext, IJsonApiRequest request)
6666
try
6767
{
6868
ValidateIfMatchHeader(httpContext.Request);
69-
IReadOnlySet<JsonApiExtension> extensions = _contentNegotiator.Negotiate();
69+
IReadOnlySet<JsonApiMediaTypeExtension> extensions = _contentNegotiator.Negotiate();
7070

7171
if (isResourceRequest)
7272
{
@@ -130,7 +130,7 @@ private void ValidateIfMatchHeader(HttpRequest httpRequest)
130130
}
131131

132132
private static void SetupResourceRequest(JsonApiRequest request, ResourceType primaryResourceType, RouteValueDictionary routeValues,
133-
HttpRequest httpRequest, IReadOnlySet<JsonApiExtension> extensions)
133+
HttpRequest httpRequest, IReadOnlySet<JsonApiMediaTypeExtension> extensions)
134134
{
135135
AssertNoAtomicOperationsExtension(extensions);
136136

@@ -184,9 +184,9 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceType pr
184184
request.Extensions = extensions;
185185
}
186186

187-
private static void AssertNoAtomicOperationsExtension(IReadOnlySet<JsonApiExtension> extensions)
187+
private static void AssertNoAtomicOperationsExtension(IReadOnlySet<JsonApiMediaTypeExtension> extensions)
188188
{
189-
if (extensions.Contains(JsonApiExtension.AtomicOperations) || extensions.Contains(JsonApiExtension.RelaxedAtomicOperations))
189+
if (extensions.Contains(JsonApiMediaTypeExtension.AtomicOperations) || extensions.Contains(JsonApiMediaTypeExtension.RelaxedAtomicOperations))
190190
{
191191
throw new InvalidOperationException("Incorrect content negotiation implementation detected: Unexpected atomic:operations extension found.");
192192
}
@@ -214,7 +214,7 @@ internal static bool IsRouteForOperations(RouteValueDictionary routeValues)
214214
return actionName == "PostOperations";
215215
}
216216

217-
private static void SetupOperationsRequest(JsonApiRequest request, IReadOnlySet<JsonApiExtension> extensions)
217+
private static void SetupOperationsRequest(JsonApiRequest request, IReadOnlySet<JsonApiMediaTypeExtension> extensions)
218218
{
219219
AssertHasAtomicOperationsExtension(extensions);
220220

@@ -223,9 +223,9 @@ private static void SetupOperationsRequest(JsonApiRequest request, IReadOnlySet<
223223
request.Extensions = extensions;
224224
}
225225

226-
private static void AssertHasAtomicOperationsExtension(IReadOnlySet<JsonApiExtension> extensions)
226+
private static void AssertHasAtomicOperationsExtension(IReadOnlySet<JsonApiMediaTypeExtension> extensions)
227227
{
228-
if (!extensions.Contains(JsonApiExtension.AtomicOperations) && !extensions.Contains(JsonApiExtension.RelaxedAtomicOperations))
228+
if (!extensions.Contains(JsonApiMediaTypeExtension.AtomicOperations) && !extensions.Contains(JsonApiMediaTypeExtension.RelaxedAtomicOperations))
229229
{
230230
throw new InvalidOperationException("Incorrect content negotiation implementation detected: Missing atomic:operations extension.");
231231
}

src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Middleware;
88
[PublicAPI]
99
public sealed class JsonApiRequest : IJsonApiRequest
1010
{
11-
private static readonly IReadOnlySet<JsonApiExtension> EmptyExtensionSet = new HashSet<JsonApiExtension>().AsReadOnly();
11+
private static readonly IReadOnlySet<JsonApiMediaTypeExtension> EmptyExtensionSet = new HashSet<JsonApiMediaTypeExtension>().AsReadOnly();
1212

1313
/// <inheritdoc />
1414
public EndpointKind Kind { get; set; }
@@ -38,7 +38,7 @@ public sealed class JsonApiRequest : IJsonApiRequest
3838
public string? TransactionId { get; set; }
3939

4040
/// <inheritdoc />
41-
public IReadOnlySet<JsonApiExtension> Extensions { get; set; } = EmptyExtensionSet;
41+
public IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; set; } = EmptyExtensionSet;
4242

4343
/// <inheritdoc />
4444
public void CopyFrom(IJsonApiRequest other)

src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public void Apply(ApplicationModel application)
113113
else
114114
{
115115
var options = (JsonApiOptions)_options;
116-
options.IncludeExtensions(JsonApiExtension.AtomicOperations, JsonApiExtension.RelaxedAtomicOperations);
116+
options.IncludeExtensions(JsonApiMediaTypeExtension.AtomicOperations, JsonApiMediaTypeExtension.RelaxedAtomicOperations);
117117
}
118118

119119
if (IsRoutingConventionDisabled(controller))

0 commit comments

Comments
 (0)