Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
fa5cae6
Drop the net9.0 and net8.0 target frameworks
bkoelman Oct 17, 2026
ec16b95
Introduce IAttributeContainer interface and implement on ResourceType
bkoelman Oct 17, 2026
0993e12
Introduce CompoundType descriptor and register in IResourceGraph
bkoelman Oct 17, 2026
8b9459c
Update AttrAttribute with compound metadata
bkoelman Oct 17, 2026
f7287ec
Update ResourceFieldAttribute: drop Type, add ContainingType, update …
bkoelman Oct 17, 2026
d648ccf
ResourceGraphBuilder: Recursive discovery of CompoundType and children
bkoelman Oct 17, 2026
bb71a3d
Validate compound types and their attributes in ResourceGraphBuilder
bkoelman Oct 17, 2026
d2566d0
Implement 6-token grammar (O, M, A, C, P, Q) in FieldTypes & FieldCha…
bkoelman Oct 17, 2026
b7d74f1
Update PatternParser and PatternTextFormatter for compound tokens
bkoelman Oct 17, 2026
70f1fcb
Update PatternMatcher and MatchState to traverse IAttributeContainer
bkoelman Oct 17, 2026
fbcd044
Support compound attribute and collection scoping in FilterParser
bkoelman Oct 17, 2026
b81292e
Introduce ElementValueExpression and parse '_' in primitive collectio…
bkoelman Oct 17, 2026
e558512
SortParser: Enforce [OC]*A and reject sorting on compound attributes …
bkoelman Oct 17, 2026
f5f2ba5
SparseFieldSetParser: Parse hierarchical attribute chains
bkoelman Oct 17, 2026
39d7590
WhereClauseBuilder: Collection Any(), Count, and ElementValueExpressi…
bkoelman Oct 17, 2026
d0036a3
Scaffold CompoundAttributes integration test models, DbContext, and f…
bkoelman Oct 17, 2026
bbdff41
Add CompoundAttributesReadTests covering GET, filters, has with '_', …
bkoelman Oct 17, 2026
7a8dfb4
ResourceObjectAdapter: Recursive deserialization of compound objects …
bkoelman Oct 17, 2026
3feaf15
ResourceChangeTracker: Deep structural equality for compound attributes
bkoelman Oct 17, 2026
58f5ce2
Add CompoundAttributesWriteTests covering POST and PATCH operations
bkoelman Oct 17, 2026
14d4e21
Hierarchical FieldSelection, SparseFieldSetCache, and SelectClauseBui…
bkoelman Oct 17, 2026
7d47658
ResponseModelAdapter: Prune unselected compound properties and collec…
bkoelman Oct 17, 2026
e903bcc
Add CompoundAttributesSparseFieldSetTests
bkoelman Oct 17, 2026
f11a0f0
Add CompoundAttributesValidationTests and reject isType on compound t…
bkoelman Oct 17, 2026
3b21b6e
XML documentation, code style cleanup, and roadmap update
bkoelman Oct 17, 2026
dca7f65
Reformat code with cleanupcode
bkoelman Oct 17, 2026
d3ac808
Add workaround for coverlet bug
bkoelman Oct 17, 2026
8cb9dee
Relocate EF Core 10 projection workaround from QueryLayerComposer to …
bkoelman Oct 18, 2026
71a98f3
Include complex properties in SelectClauseBuilder.IncludeAllScalarPro…
bkoelman Oct 18, 2026
a2b1523
Clean up FieldSelection and FieldSelectors
bkoelman Oct 18, 2026
176c1eb
Rename PrimitiveCollection to ScalarCollection and update token from …
bkoelman Oct 18, 2026
9146e7f
Rename ElementValueExpression to CollectionElementValueExpression and…
bkoelman Oct 18, 2026
694d8af
Add throwing attribute getters to IAttributeContainer
bkoelman Oct 18, 2026
252f00a
Complete generic convenience overloads on IResourceGraph and Resource…
bkoelman Oct 18, 2026
4aabb3a
Assign IsCollection in ResourceGraphBuilder, document unsupported STJ…
bkoelman Oct 18, 2026
2962433
Update InvalidModelStateException with CompoundType lookup and RFC 69…
bkoelman Oct 18, 2026
b9f4a06
Preserve hierarchical field chains in SparseFieldSetExpressionExtensions
bkoelman Oct 18, 2026
c0036cb
Add GetFieldChain lambda resolution to IResourceGraph and ResourceGraph
bkoelman Oct 18, 2026
d36637d
Enforce AllowView, custom PublicName, and null guards during read ser…
bkoelman Oct 18, 2026
bab18d5
Enforce AllowCreate, AllowChange, read-only, and unknown fields durin…
bkoelman Oct 18, 2026
37510fe
Pass SerializerOptions to ResourceChangeTracker for accurate change d…
bkoelman Oct 18, 2026
806be91
Add explanatory comments for non-obvious design choices and document …
bkoelman Oct 18, 2026
af98454
Expand CompoundAttributes test models, removing required keywords and…
bkoelman Oct 18, 2026
786a867
Add integration tests for expanded read scenarios
bkoelman Oct 18, 2026
7930449
Add integration tests for expanded write scenarios and validation
bkoelman Oct 18, 2026
51f3a74
Code cleanup
bkoelman Oct 18, 2026
df24734
Fix ToString/Equals on AttrAttribute
bkoelman Oct 18, 2026
7bea8bd
Rename built-in patterns, remove unused ones
bkoelman Oct 18, 2026
3a58e66
Remove redundant overloads
bkoelman Oct 18, 2026
636663e
Cleanup FilterParser somewhat
bkoelman Oct 18, 2026
2dc66a9
Fix enum member names on FieldTypes
bkoelman Oct 18, 2026
f7047ec
Fixed: don't use IJsonApiOptions.SerializerOptions in change tracker
bkoelman Oct 18, 2026
25e75ac
CompoundType: Make ctor parameter non-optional
bkoelman Oct 19, 2026
1f02d87
Remove redundant public methods
bkoelman Oct 19, 2026
2002527
Use 'S' for scalar attributes, 'A' for any attribute, and support tok…
bkoelman Oct 20, 2026
ec7122f
Refactor FilterParser by making _ a first-class token
bkoelman Oct 21, 2026
07ea02a
Refactor filter parser: single stack, always allow equality compariso…
bkoelman Oct 25, 2026
946cc79
Revert adding public unused constants in FieldChainPattern
bkoelman Oct 25, 2026
743b2b8
Fix crash in ScopesAuthorizationFilter when used with compound/collec…
bkoelman Oct 25, 2026
2e8c455
Query string parsers: provide API to render set of expected parse cho…
bkoelman Oct 26, 2026
d6af3b2
Clarify docs
bkoelman Oct 26, 2026
cc3e7b6
Fix conditional assertion
bkoelman Oct 28, 2026
be10f73
Refactorings to support sparse fieldsets with compound/collection att…
bkoelman Oct 29, 2026
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
6 changes: 0 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.*
9.0.*
10.0.*
- name: Show installed versions
shell: pwsh
Expand Down Expand Up @@ -154,8 +152,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.*
9.0.*
10.0.*
- name: Git checkout
uses: actions/checkout@v7
Expand Down Expand Up @@ -212,8 +208,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.*
9.0.*
10.0.*
- name: Git checkout
uses: actions/checkout@v7
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.*
9.0.*
10.0.*
- name: Git checkout
uses: actions/checkout@v7
Expand Down
12 changes: 12 additions & 0 deletions Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@
<Analyzer Remove="@(Analyzer)" Condition="'%(Filename)' == 'xunit.analyzers'" />
</ItemGroup>
</Target>

<!--
Workaround for Coverlet 8.0.1: NetCoreSharedFrameworkResolver only searches for <ModuleName>.runtimeconfig.json
to resolve shared framework assemblies (Microsoft.AspNetCore.App) for Cecil during instrumentation.
For class libraries, no such file exists by default, causing Cecil to fail resolving Microsoft.Extensions.Logging.Abstractions.
Copying the test project's runtimeconfig.json as JsonApiDotNetCore.runtimeconfig.json provides the resolver with the needed framework paths.
-->
<Target Name="CopyRuntimeConfigForCoverlet"
Condition="($(MSBuildProjectName.EndsWith('Tests')) Or '$(MSBuildProjectName)' == 'TestBuildingBlocks') And '$(GenerateRuntimeConfigurationFiles)' == 'true' And Exists('$(ProjectRuntimeConfigFilePath)')"
AfterTargets="GenerateBuildRuntimeConfigurationFiles">
<Copy SourceFiles="$(ProjectRuntimeConfigFilePath)" DestinationFiles="$(TargetDir)JsonApiDotNetCore.runtimeconfig.json" SkipUnchangedFiles="true" />
</Target>
</Project>
2 changes: 1 addition & 1 deletion JsonApiDotNetCore.slnx.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<s:Int64 x:Key="/Default/CodeEditing/NullCheckPatterns/PatternTypeNamesToPriority/=JetBrains_002EReSharper_002EFeature_002EServices_002ECSharp_002ENullChecking_002EIfThenThrowPattern/@EntryIndexedValue">2000</s:Int64>
<s:Int64 x:Key="/Default/CodeEditing/NullCheckPatterns/PatternTypeNamesToPriority/=JetBrains_002EReSharper_002EFeature_002EServices_002ECSharp_002ENullChecking_002EThrowExpressionNullCheckPattern/@EntryIndexedValue">3000</s:Int64>
<s:Boolean x:Key="/Default/CodeInspection/CodeAnnotations/PropagateAnnotations/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/GeneratedFilesAndFolders/=58FE19B6_002DF84A_002D990C_002DB25F_002DE37C391C3BD3_002Ff_003AReadOnlySet_002Ecs/@EntryIndexedValue">58FE19B6-F84A-990C-B25F-E37C391C3BD3/f:ReadOnlySet.cs</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/AnalysisEnabled/@EntryValue">SOLUTION</s:String>
<s:Boolean x:Key="/Default/CodeInspection/Highlighting/IdentifierHighlightingEnabled/@EntryValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArgumentsStyleAnonymousFunction/@EntryIndexedValue">SUGGESTION</s:String>
Expand Down Expand Up @@ -600,6 +599,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/TreeGridPresentableViewStyle/CompactMode/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=type_0020of_0020a/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=546845333F49494AB6CF8F0477A0F71D/@KeyIndexDefined">True</s:Boolean>
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=546845333F49494AB6CF8F0477A0F71D/Comment/@EntryValue">Replace argument null check using throw expression with ArgumentNullException.ThrowIfNull</s:String>
<s:Boolean x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=546845333F49494AB6CF8F0477A0F71D/CustomPatternPlaceholder/=argument/@KeyIndexDefined">True</s:Boolean>
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This document provides an overview of the direction this project is heading and

We have an interest in the following topics. It's too soon yet to decide whether they'll make it into v5.x or in a later major version.

- Query strings on JSON-mapped columns [#1439](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1439)
- Compound attributes and collections (query strings, JSON columns, complex types) [#1439](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1439) (Implemented)
- Optimistic concurrency [#1119](https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1119)
- Idempotency [#1132](https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1132)
- Fluent API [#776](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/776)
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/Serialization/SerializationBenchmarkBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ protected SerializationBenchmarkBase()
var metaBuilder = new NoMetaBuilder();
IQueryConstraintProvider[] constraintProviders = [];
var resourceDefinitionAccessor = new NeverResourceDefinitionAccessor();
var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor);
var sparseFieldSetEvaluator = new SparseFieldSetEvaluator(constraintProviders, resourceDefinitionAccessor);
var requestQueryStringAccessor = new FakeRequestQueryStringAccessor();

ResponseModelAdapter = new ResponseModelAdapter(request, options, linkBuilder, metaBuilder, resourceDefinitionAccessor, evaluatedIncludeCache,
sparseFieldSetCache, requestQueryStringAccessor);
sparseFieldSetEvaluator, requestQueryStringAccessor);
}

protected abstract JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph);
Expand Down
2 changes: 1 addition & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Commonly used types are listed in the following sections.
- <xref:JsonApiDotNetCore.Serialization.Response.ResponseModelAdapter> implements <xref:JsonApiDotNetCore.Serialization.Response.IResponseModelAdapter>
- <xref:JsonApiDotNetCore.Serialization.Objects.Document>
- <xref:JsonApiDotNetCore.Queries.IEvaluatedIncludeCache>
- <xref:JsonApiDotNetCore.Queries.SparseFieldSetCache> implements <xref:JsonApiDotNetCore.Queries.ISparseFieldSetCache>
- <xref:JsonApiDotNetCore.Queries.SparseFieldSetEvaluator> implements <xref:JsonApiDotNetCore.Queries.ISparseFieldSetEvaluator>

## Error handling

Expand Down
2 changes: 1 addition & 1 deletion docs/usage/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Here are some injectable request-scoped types to be aware of:
- `ITargetedFields`: Lists the attributes and relationships from an incoming POST/PATCH resource request. Any fields missing there should not be stored (partial updates).
- `IEnumerable<IQueryConstraintProvider>`: Provides access to the parsed query string parameters.
- `IEvaluatedIncludeCache`: This tells the response serializer which related resources to render.
- `ISparseFieldSetCache`: This tells the response serializer which fields to render in the `attributes` and `relationships` objects.
- `ISparseFieldSetEvaluator`: This tells the response serializer which fields to render in the `attributes` and `relationships` objects.

You may also want to inject the singletons `IJsonApiOptions` (which contains settings such as default page size) and `IResourceGraph` (the JSON:API model of resources, attributes and relationships).

Expand Down
21 changes: 1 addition & 20 deletions package-versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
<InheritDocVersion>2.0.*</InheritDocVersion>
<MiniValidationVersion>0.10.*</MiniValidationVersion>
<ReadableExpressionsVersion>4.1.*</ReadableExpressionsVersion>
<SystemTextJsonVersion>10.0.*</SystemTextJsonVersion>
<TestSdkVersion>18.11.*</TestSdkVersion>
<XunitVersion>2.9.*</XunitVersion>
<XunitVisualStudioVersion>4.0.*</XunitVisualStudioVersion>
Expand All @@ -28,24 +27,6 @@

<!-- Non-published dependencies (these are safe to update, won't cause a breaking change) -->
<AspNetCoreVersion>10.0.*</AspNetCoreVersion>
<EntityFrameworkCoreVersion>10.0.*-*</EntityFrameworkCoreVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<!-- Published dependencies (only update on major version change) -->
<EntityFrameworkCoreFrozenVersion>N/A</EntityFrameworkCoreFrozenVersion>

<!-- Non-published dependencies (these are safe to update, won't cause a breaking change) -->
<AspNetCoreVersion>9.0.*</AspNetCoreVersion>
<EntityFrameworkCoreVersion>9.0.*</EntityFrameworkCoreVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0'">
<!-- Published dependencies (only update on major version change) -->
<EntityFrameworkCoreFrozenVersion>8.0.0</EntityFrameworkCoreFrozenVersion>

<!-- Non-published dependencies (these are safe to update, won't cause a breaking change) -->
<AspNetCoreVersion>8.0.*</AspNetCoreVersion>
<EntityFrameworkCoreVersion>8.0.*</EntityFrameworkCoreVersion>
<EntityFrameworkCoreVersion>10.0.*</EntityFrameworkCoreVersion>
</PropertyGroup>
</Project>
5 changes: 2 additions & 3 deletions src/Examples/DapperExample/DapperExample.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand All @@ -15,8 +15,7 @@
<PackageReference Include="Dapper" Version="$(DapperVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Microting.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' != 'net9.0' And '$(TargetFramework)' != 'net8.0'" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Microting.EntityFrameworkCore.MySql" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net9.0' Or '$(TargetFramework)' == 'net8.0'" Version="$(EntityFrameworkCoreVersion)" />
</ItemGroup>
</Project>
4 changes: 0 additions & 4 deletions src/Examples/DapperExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,7 @@
}
case DatabaseProvider.MySql:
{
#if NET8_0
ServerVersion serverVersion = ServerVersion.AutoDetect(connectionString);
#else
ServerVersion serverVersion = await ServerVersion.AutoDetectAsync(connectionString);
#endif

builder.Services.AddMySql<AppDbContext>(connectionString, serverVersion, optionsAction: options => SetDbContextDebugOptions(options));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand Down
2 changes: 1 addition & 1 deletion src/Examples/GettingStarted/GettingStarted.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ private void LogFiltersInTopScope()

FilterExpression[] filtersInTopScope = _constraintProviders
.SelectMany(provider => provider.GetConstraints())
.OfType<RelationshipChainExpressionInScope>()
.Where(constraint => constraint.Scope == null)
.Select(constraint => constraint.Expression)
.OfType<FilterExpression>()
Expand Down
2 changes: 1 addition & 1 deletion src/Examples/ReportsExample/ReportsExample.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
Expand Down
102 changes: 102 additions & 0 deletions src/JsonApiDotNetCore.Annotations/Configuration/CompoundType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCore.Configuration;

/// <summary>
/// Metadata about the shape of a compound JSON:API attribute in the resource graph.
/// </summary>
[PublicAPI]
public sealed class CompoundType : IAttributeContainer
{
private readonly Dictionary<string, AttrAttribute> _attributesByPublicName = [];
private readonly Dictionary<string, AttrAttribute> _attributesByPropertyName = [];

/// <inheritdoc />
public Type ClrType { get; }

/// <inheritdoc />
public IReadOnlyCollection<AttrAttribute> Attributes { get; }

public CompoundType(Type clrType, IReadOnlyCollection<AttrAttribute> attributes)
{
ArgumentNullException.ThrowIfNull(clrType);
ArgumentNullException.ThrowIfNull(attributes);

ClrType = clrType;
Attributes = attributes;

foreach (AttrAttribute attribute in Attributes)
{
_attributesByPublicName.Add(attribute.PublicName, attribute);
_attributesByPropertyName.Add(attribute.Property.Name, attribute);
}
}

/// <inheritdoc />
public AttrAttribute GetAttributeByPublicName(string publicName)
{
AttrAttribute? attribute = FindAttributeByPublicName(publicName);
return attribute ?? throw new InvalidOperationException($"Attribute '{publicName}' does not exist on compound type '{ClrType.Name}'.");
}

/// <inheritdoc />
public AttrAttribute? FindAttributeByPublicName(string publicName)
{
ArgumentNullException.ThrowIfNull(publicName);

return _attributesByPublicName.GetValueOrDefault(publicName);
}

/// <inheritdoc />
public AttrAttribute GetAttributeByPropertyName(string propertyName)
{
AttrAttribute? attribute = FindAttributeByPropertyName(propertyName);
return attribute ?? throw new InvalidOperationException($"Attribute for property '{propertyName}' does not exist on compound type '{ClrType.Name}'.");
}

/// <inheritdoc />
public AttrAttribute? FindAttributeByPropertyName(string propertyName)
{
ArgumentNullException.ThrowIfNull(propertyName);

return _attributesByPropertyName.GetValueOrDefault(propertyName);
}

/// <inheritdoc />
public override string ToString()
{
return ClrType.Name;
}

/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}

if (obj is null || GetType() != obj.GetType())
{
return false;
}

var other = (CompoundType)obj;
return ClrType == other.ClrType && Attributes.SequenceEqual(other.Attributes);
}

/// <inheritdoc />
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(ClrType);

foreach (AttrAttribute attribute in Attributes)
{
hashCode.Add(attribute);
}

return hashCode.ToHashCode();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCore.Configuration;

/// <summary>
/// Metadata about the shape of a type that contains JSON:API attributes.
/// </summary>
[PublicAPI]
public interface IAttributeContainer
{
/// <summary>
/// The CLR type of the container.
/// </summary>
Type ClrType { get; }

/// <summary>
/// The attributes in this container.
/// </summary>
IReadOnlyCollection<AttrAttribute> Attributes { get; }

/// <summary>
/// Gets the attribute for the specified publicly exposed name. Throws an <see cref="InvalidOperationException" /> when not found.
/// </summary>
AttrAttribute GetAttributeByPublicName(string publicName);

/// <summary>
/// Attempts to find an attribute by its publicly exposed name. Returns <c>null</c> when not found.
/// </summary>
AttrAttribute? FindAttributeByPublicName(string publicName);

/// <summary>
/// Gets the attribute for the specified property name. Throws an <see cref="InvalidOperationException" /> when not found.
/// </summary>
AttrAttribute GetAttributeByPropertyName(string propertyName);

/// <summary>
/// Attempts to find an attribute by its property name. Returns <c>null</c> when not found.
/// </summary>
AttrAttribute? FindAttributeByPropertyName(string propertyName);
}
Loading
Loading