Skip to content

Refactorings for JSON:API extensions to unblock OpenAPI support #1629

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 3 commits into from
Nov 11, 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
@@ -0,0 +1,20 @@
using JsonApiDotNetCore.Serialization.JsonConverters;

namespace JsonApiDotNetCore.Configuration;

internal sealed class DefaultJsonApiApplicationBuilderEvents : IJsonApiApplicationBuilderEvents
{
private readonly IJsonApiOptions _options;

public DefaultJsonApiApplicationBuilderEvents(IJsonApiOptions options)
{
ArgumentGuard.NotNull(options);

_options = options;
}

public void ResourceGraphBuilt(IResourceGraph resourceGraph)
{
_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JsonApiDotNetCore.Configuration;

internal interface IJsonApiApplicationBuilderEvents
{
void ResourceGraphBuilt(IResourceGraph resourceGraph);
}
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,15 @@ public interface IJsonApiOptions

/// <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.
/// <see cref="BaseJsonApiOperationsController" />, the <see cref="JsonApiMediaTypeExtension.AtomicOperations" /> and
/// <see cref="JsonApiMediaTypeExtension.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; }
IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; }

/// <summary>
/// Enables to customize the settings that are used by the <see cref="JsonSerializer" />.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.JsonConverters;
using JsonApiDotNetCore.Serialization.Request;
using JsonApiDotNetCore.Serialization.Request.Adapters;
using JsonApiDotNetCore.Serialization.Response;
Expand Down Expand Up @@ -74,6 +73,8 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso
_services.TryAddSingleton(serviceProvider =>
{
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var events = serviceProvider.GetRequiredService<IJsonApiApplicationBuilderEvents>();

var resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);

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

IResourceGraph resourceGraph = resourceGraphBuilder.Build();

_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
events.ResourceGraphBuilt(resourceGraph);

return resourceGraph;
});
Expand Down Expand Up @@ -169,6 +169,7 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
_services.TryAddScoped<IQueryLayerComposer, QueryLayerComposer>();
_services.TryAddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
_services.TryAddSingleton<IDocumentDescriptionLinkProvider, NoDocumentDescriptionLinkProvider>();
_services.TryAddSingleton<IJsonApiApplicationBuilderEvents, DefaultJsonApiApplicationBuilderEvents>();
}

private void AddMiddlewareLayer()
Expand Down
10 changes: 5 additions & 5 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace JsonApiDotNetCore.Configuration;
[PublicAPI]
public sealed class JsonApiOptions : IJsonApiOptions
{
private static readonly IReadOnlySet<JsonApiExtension> EmptyExtensionSet = new HashSet<JsonApiExtension>().AsReadOnly();
private static readonly IReadOnlySet<JsonApiMediaTypeExtension> EmptyExtensionSet = new HashSet<JsonApiMediaTypeExtension>().AsReadOnly();
private readonly Lazy<JsonSerializerOptions> _lazySerializerWriteOptions;
private readonly Lazy<JsonSerializerOptions> _lazySerializerReadOptions;

Expand Down Expand Up @@ -100,7 +100,7 @@ public bool AllowClientGeneratedIds
public IsolationLevel? TransactionIsolationLevel { get; set; }

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

/// <inheritdoc />
public JsonSerializerOptions SerializerOptions { get; } = new()
Expand Down Expand Up @@ -142,15 +142,15 @@ public JsonApiOptions()
/// <param name="extensionsToAdd">
/// The JSON:API extensions to add.
/// </param>
public void IncludeExtensions(params JsonApiExtension[] extensionsToAdd)
public void IncludeExtensions(params JsonApiMediaTypeExtension[] extensionsToAdd)
{
ArgumentGuard.NotNull(extensionsToAdd);

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

foreach (JsonApiExtension extension in extensionsToAdd)
foreach (JsonApiMediaTypeExtension extension in extensionsToAdd)
{
extensions.Add(extension);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ public interface IJsonApiContentNegotiator
/// 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();
IReadOnlySet<JsonApiMediaTypeExtension> Negotiate();
}
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public interface IJsonApiRequest
/// <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; }
IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; }

/// <summary>
/// Performs a shallow copy.
Expand Down
8 changes: 4 additions & 4 deletions src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public JsonApiContentNegotiator(IJsonApiOptions options, IHttpContextAccessor ht
}

/// <inheritdoc />
public IReadOnlySet<JsonApiExtension> Negotiate()
public IReadOnlySet<JsonApiMediaTypeExtension> Negotiate()
{
IReadOnlyList<JsonApiMediaType> possibleMediaTypes = GetPossibleMediaTypes();

Expand Down Expand Up @@ -66,7 +66,7 @@ public IReadOnlySet<JsonApiExtension> Negotiate()
return mediaType;
}

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

if (IsOperationsEndpoint())
{
if (_options.Extensions.Contains(JsonApiExtension.AtomicOperations))
if (_options.Extensions.Contains(JsonApiMediaTypeExtension.AtomicOperations))
{
mediaTypes.Add(JsonApiMediaType.AtomicOperations);
}

if (_options.Extensions.Contains(JsonApiExtension.RelaxedAtomicOperations))
if (_options.Extensions.Contains(JsonApiMediaTypeExtension.RelaxedAtomicOperations))
{
mediaTypes.Add(JsonApiMediaType.RelaxedAtomicOperations);
}
Expand Down
20 changes: 10 additions & 10 deletions src/JsonApiDotNetCore/Middleware/JsonApiMediaType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ public sealed class JsonApiMediaType : IEquatable<JsonApiMediaType>
/// <summary>
/// Gets the JSON:API media type with the "https://jsonapi.org/ext/atomic" extension.
/// </summary>
public static readonly JsonApiMediaType AtomicOperations = new([JsonApiExtension.AtomicOperations]);
public static readonly JsonApiMediaType AtomicOperations = new([JsonApiMediaTypeExtension.AtomicOperations]);

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

public IReadOnlySet<JsonApiExtension> Extensions { get; }
public IReadOnlySet<JsonApiMediaTypeExtension> Extensions { get; }

public JsonApiMediaType(IReadOnlySet<JsonApiExtension> extensions)
public JsonApiMediaType(IReadOnlySet<JsonApiMediaTypeExtension> extensions)
{
ArgumentGuard.NotNull(extensions);

Extensions = extensions;
}

public JsonApiMediaType(IEnumerable<JsonApiExtension> extensions)
public JsonApiMediaType(IEnumerable<JsonApiMediaTypeExtension> extensions)
{
ArgumentGuard.NotNull(extensions);

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

if (isBaseMatch)
{
HashSet<JsonApiExtension> extensions = [];
HashSet<JsonApiMediaTypeExtension> extensions = [];

decimal qualityFactor = 1.0m;

Expand Down Expand Up @@ -97,13 +97,13 @@ private static (JsonApiMediaType MediaType, decimal QualityFactor)? TryParse(str
return null;
}

private static void ParseExtensions(NameValueHeaderValue parameter, HashSet<JsonApiExtension> extensions)
private static void ParseExtensions(NameValueHeaderValue parameter, HashSet<JsonApiMediaTypeExtension> extensions)
{
string parameterValue = parameter.GetUnescapedValue().ToString();

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

foreach (JsonApiExtension extension in Extensions)
foreach (JsonApiMediaTypeExtension extension in Extensions)
{
var extHeaderValue = new NameValueHeaderValue(ExtSegment);
extHeaderValue.SetAndEscapeValue(extension.UnescapedValue);
Expand Down Expand Up @@ -178,7 +178,7 @@ public override int GetHashCode()
{
int hashCode = 0;

foreach (JsonApiExtension extension in Extensions)
foreach (JsonApiMediaTypeExtension extension in Extensions)
{
hashCode = HashCode.Combine(hashCode, extension);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ namespace JsonApiDotNetCore.Middleware;
/// Represents a JSON:API extension (in unescaped format), which occurs as an "ext" parameter inside an HTTP Accept or Content-Type header.
/// </summary>
[PublicAPI]
public sealed class JsonApiExtension : IEquatable<JsonApiExtension>
public sealed class JsonApiMediaTypeExtension : IEquatable<JsonApiMediaTypeExtension>
{
public static readonly JsonApiExtension AtomicOperations = new("https://jsonapi.org/ext/atomic");
public static readonly JsonApiExtension RelaxedAtomicOperations = new("atomic-operations");
public static readonly JsonApiMediaTypeExtension AtomicOperations = new("https://jsonapi.org/ext/atomic");
public static readonly JsonApiMediaTypeExtension RelaxedAtomicOperations = new("atomic-operations");

public string UnescapedValue { get; }

public JsonApiExtension(string unescapedValue)
public JsonApiMediaTypeExtension(string unescapedValue)
{
ArgumentGuard.NotNullNorEmpty(unescapedValue);

Expand All @@ -25,7 +25,7 @@ public override string ToString()
return UnescapedValue;
}

public bool Equals(JsonApiExtension? other)
public bool Equals(JsonApiMediaTypeExtension? other)
{
if (other is null)
{
Expand All @@ -42,7 +42,7 @@ public bool Equals(JsonApiExtension? other)

public override bool Equals(object? other)
{
return Equals(other as JsonApiExtension);
return Equals(other as JsonApiMediaTypeExtension);
}

public override int GetHashCode()
Expand Down
14 changes: 7 additions & 7 deletions src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public async Task InvokeAsync(HttpContext httpContext, IJsonApiRequest request)
try
{
ValidateIfMatchHeader(httpContext.Request);
IReadOnlySet<JsonApiExtension> extensions = _contentNegotiator.Negotiate();
IReadOnlySet<JsonApiMediaTypeExtension> extensions = _contentNegotiator.Negotiate();

if (isResourceRequest)
{
Expand Down Expand Up @@ -130,7 +130,7 @@ private void ValidateIfMatchHeader(HttpRequest httpRequest)
}

private static void SetupResourceRequest(JsonApiRequest request, ResourceType primaryResourceType, RouteValueDictionary routeValues,
HttpRequest httpRequest, IReadOnlySet<JsonApiExtension> extensions)
HttpRequest httpRequest, IReadOnlySet<JsonApiMediaTypeExtension> extensions)
{
AssertNoAtomicOperationsExtension(extensions);

Expand Down Expand Up @@ -184,9 +184,9 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceType pr
request.Extensions = extensions;
}

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

private static void SetupOperationsRequest(JsonApiRequest request, IReadOnlySet<JsonApiExtension> extensions)
private static void SetupOperationsRequest(JsonApiRequest request, IReadOnlySet<JsonApiMediaTypeExtension> extensions)
{
AssertHasAtomicOperationsExtension(extensions);

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

private static void AssertHasAtomicOperationsExtension(IReadOnlySet<JsonApiExtension> extensions)
private static void AssertHasAtomicOperationsExtension(IReadOnlySet<JsonApiMediaTypeExtension> extensions)
{
if (!extensions.Contains(JsonApiExtension.AtomicOperations) && !extensions.Contains(JsonApiExtension.RelaxedAtomicOperations))
if (!extensions.Contains(JsonApiMediaTypeExtension.AtomicOperations) && !extensions.Contains(JsonApiMediaTypeExtension.RelaxedAtomicOperations))
{
throw new InvalidOperationException("Incorrect content negotiation implementation detected: Missing atomic:operations extension.");
}
Expand Down
4 changes: 2 additions & 2 deletions src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Middleware;
[PublicAPI]
public sealed class JsonApiRequest : IJsonApiRequest
{
private static readonly IReadOnlySet<JsonApiExtension> EmptyExtensionSet = new HashSet<JsonApiExtension>().AsReadOnly();
private static readonly IReadOnlySet<JsonApiMediaTypeExtension> EmptyExtensionSet = new HashSet<JsonApiMediaTypeExtension>().AsReadOnly();

/// <inheritdoc />
public EndpointKind Kind { get; set; }
Expand Down Expand Up @@ -38,7 +38,7 @@ public sealed class JsonApiRequest : IJsonApiRequest
public string? TransactionId { get; set; }

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

/// <inheritdoc />
public void CopyFrom(IJsonApiRequest other)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void Apply(ApplicationModel application)
else
{
var options = (JsonApiOptions)_options;
options.IncludeExtensions(JsonApiExtension.AtomicOperations, JsonApiExtension.RelaxedAtomicOperations);
options.IncludeExtensions(JsonApiMediaTypeExtension.AtomicOperations, JsonApiMediaTypeExtension.RelaxedAtomicOperations);
}

if (IsRoutingConventionDisabled(controller))
Expand Down
Loading
Loading