Skip to content
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,15 @@
namespace JsonApiDotNetCore.OpenApi.Swashbuckle.Annotations;

/// <summary>
/// Hides the underlying resource ID type in OpenAPI documents.
/// </summary>
/// <remarks>
/// For example, when used on a resource type that implements <c><![CDATA[IIdentifiable<long>]]></c>, excludes the <c>format</c> property on the ID
/// schema. As a result, the ID type is displayed as <c>string</c> instead of
/// <c>
/// string($int64)
/// </c>
/// in SwaggerUI.
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct)]
public sealed class HideResourceIdTypeInOpenApiAttribute : Attribute;
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.OpenApi.Swashbuckle.Annotations;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

Expand All @@ -7,32 +10,48 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Components;
internal sealed class ResourceIdSchemaGenerator
{
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly IControllerResourceMapping _controllerResourceMapping;

public ResourceIdSchemaGenerator(SchemaGenerator defaultSchemaGenerator)
public ResourceIdSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IControllerResourceMapping controllerResourceMapping)
{
ArgumentNullException.ThrowIfNull(defaultSchemaGenerator);
ArgumentNullException.ThrowIfNull(controllerResourceMapping);

_defaultSchemaGenerator = defaultSchemaGenerator;
_controllerResourceMapping = controllerResourceMapping;
}

public OpenApiSchema GenerateSchema(ResourceType resourceType, SchemaRepository schemaRepository)
public OpenApiSchema GenerateSchema(ParameterInfo parameter, SchemaRepository schemaRepository)
{
ArgumentNullException.ThrowIfNull(resourceType);
ArgumentNullException.ThrowIfNull(parameter);
ArgumentNullException.ThrowIfNull(schemaRepository);

Type? controllerType = parameter.Member.ReflectedType;
ConsistencyGuard.ThrowIf(controllerType == null);

ResourceType? resourceType = _controllerResourceMapping.GetResourceTypeForController(controllerType);
ConsistencyGuard.ThrowIf(resourceType == null);

return GenerateSchema(resourceType.IdentityClrType, schemaRepository);
return GenerateSchema(resourceType, schemaRepository);
}

public OpenApiSchema GenerateSchema(Type resourceIdClrType, SchemaRepository schemaRepository)
public OpenApiSchema GenerateSchema(ResourceType resourceType, SchemaRepository schemaRepository)
{
ArgumentNullException.ThrowIfNull(resourceIdClrType);
ArgumentNullException.ThrowIfNull(resourceType);
ArgumentNullException.ThrowIfNull(schemaRepository);

OpenApiSchema idSchema = _defaultSchemaGenerator.GenerateSchema(resourceIdClrType, schemaRepository);
OpenApiSchema idSchema = _defaultSchemaGenerator.GenerateSchema(resourceType.IdentityClrType, schemaRepository);
ConsistencyGuard.ThrowIf(idSchema.Reference != null);

idSchema.Type = "string";

if (resourceIdClrType != typeof(string))
var hideIdTypeAttribute = resourceType.ClrType.GetCustomAttribute<HideResourceIdTypeInOpenApiAttribute>();

if (hideIdTypeAttribute != null)
{
idSchema.Format = null;
}
else if (resourceType.IdentityClrType != typeof(string))
{
// When using string IDs, it's discouraged (but possible) to use an empty string as primary key value, because
// some things won't work: get-by-id, update and delete resource are impossible, and rendered links are unusable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public OpenApiSchema GenerateSchema(Type schemaType, SchemaRepository schemaRepo

if (parameterInfo is { Name: "id" } && IsJsonApiParameter(parameterInfo))
{
return _resourceIdSchemaGenerator.GenerateSchema(schemaType, schemaRepository);
return _resourceIdSchemaGenerator.GenerateSchema(parameterInfo, schemaRepository);
}

DocumentSchemaGenerator? schemaGenerator = GetDocumentSchemaGenerator(schemaType);
Expand Down
26 changes: 26 additions & 0 deletions src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,32 @@ private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType)
return entityType is { IsPropertyBag: true, HasSharedClrType: true };
}

/// <summary>
/// Removes a JSON:API resource.
/// </summary>
/// <typeparam name="TResource">
/// The resource CLR type.
/// </typeparam>
public ResourceGraphBuilder Remove<TResource>()
where TResource : class, IIdentifiable
{
return Remove(typeof(TResource));
}

/// <summary>
/// Removes a JSON:API resource.
/// </summary>
/// <param name="resourceClrType">
/// The resource CLR type.
/// </param>
public ResourceGraphBuilder Remove(Type resourceClrType)
{
ArgumentNullException.ThrowIfNull(resourceClrType);

_resourceTypesByClrType.Remove(resourceClrType);
return this;
}

/// <summary>
/// Adds a JSON:API resource.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
namespace JsonApiDotNetCoreTests.IntegrationTests.IdObfuscation;

public sealed class BankAccountsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<BankAccount, int> resourceService)
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<BankAccount, long> resourceService)
: ObfuscatedIdentifiableController<BankAccount>(options, resourceGraph, loggerFactory, resourceService);
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
namespace JsonApiDotNetCoreTests.IntegrationTests.IdObfuscation;

public sealed class DebitCardsController(
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<DebitCard, int> resourceService)
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<DebitCard, long> resourceService)
: ObfuscatedIdentifiableController<DebitCard>(options, resourceGraph, loggerFactory, resourceService);
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.IdObfuscation;

internal sealed class HexadecimalCodec
{
public int Decode(string? value)
// This implementation is deliberately simple for demonstration purposes.
// Consider using something more robust, such as https://github.com/sqids/sqids-dotnet.
public static HexadecimalCodec Instance { get; } = new();

private HexadecimalCodec()
{
}

public long Decode(string? value)
{
if (value == null)
{
Expand All @@ -25,7 +33,7 @@ public int Decode(string? value)
}

string stringValue = FromHexString(value[1..]);
return int.Parse(stringValue);
return long.Parse(stringValue, CultureInfo.InvariantCulture);
}

private static string FromHexString(string hexString)
Expand All @@ -35,22 +43,22 @@ private static string FromHexString(string hexString)
for (int index = 0; index < hexString.Length; index += 2)
{
string hexChar = hexString.Substring(index, 2);
byte bt = byte.Parse(hexChar, NumberStyles.HexNumber);
byte bt = byte.Parse(hexChar, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
bytes.Add(bt);
}

char[] chars = Encoding.ASCII.GetChars([.. bytes]);
return new string(chars);
}

public string? Encode(int value)
public string? Encode(long value)
{
if (value == 0)
{
return null;
}

string stringValue = value.ToString();
string stringValue = value.ToString(CultureInfo.InvariantCulture);
return $"x{ToHexString(stringValue)}";
}

Expand All @@ -60,7 +68,7 @@ private static string ToHexString(string value)

foreach (byte bt in Encoding.ASCII.GetBytes(value))
{
builder.Append(bt.ToString("X2"));
builder.Append(bt.ToString("X2", CultureInfo.InvariantCulture));
}

return builder.ToString();
Expand Down
Loading
Loading