Skip to content
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