Skip to content

Commit

Permalink
Merge pull request #1629 from json-api-dotnet/jsonapi-extensions-refa…
Browse files Browse the repository at this point in the history
…ctor

Refactorings for JSON:API extensions to unblock OpenAPI support
  • Loading branch information
bkoelman authored Nov 11, 2024
2 parents 43c1a4b + 3840fcb commit 8323836
Show file tree
Hide file tree
Showing 23 changed files with 739 additions and 75 deletions.
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

0 comments on commit 8323836

Please sign in to comment.