From 770b848230fd5ec28a4225e866b8b8c62a562bd9 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 31 Jul 2022 10:44:49 +0200 Subject: [PATCH 01/14] Enable running code cleanup locally only on files changed since the specified branch --- .config/dotnet-tools.json | 2 +- .github/CONTRIBUTING.md | 4 ++-- Build.ps1 | 17 ++++++++++------ cleanupcode.ps1 | 43 +++++++++++++++++++++++++++++++-------- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 69e33b978d..3bd80e6084 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,7 +9,7 @@ ] }, "regitlint": { - "version": "6.0.8", + "version": "6.1.1", "commands": [ "regitlint" ] diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5e87f135f0..4205b1ceec 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -58,8 +58,8 @@ Please follow these steps to have your contribution considered by the maintainer We use [CSharpGuidelines](https://csharpcodingguidelines.com/) as our coding standard (with a few minor exceptions). Coding style is validated during PR build, where we inject an extra settings layer that promotes various suggestions to warning level. This ensures a high-quality codebase without interfering too much when editing code. You can run the following [PowerShell scripts](https://github.com/PowerShell/PowerShell/releases) locally: -- `pwsh inspectcode.ps1`: Scans the code for style violations and opens the result in your web browser. -- `pwsh cleanupcode.ps1`: Reformats the entire codebase to match with our configured style. +- `pwsh ./inspectcode.ps1`: Scans the code for style violations and opens the result in your web browser. +- `pwsh ./cleanupcode.ps1 [branch-name-or-commit-hash]`: Reformats the codebase to match with our configured style, optionally only changed files since the specified branch (usually master). Code inspection violations can be addressed in several ways, depending on the situation: - Types that are reported to be never instantiated (because the IoC container creates them dynamically) should be decorated with `[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]`. diff --git a/Build.ps1 b/Build.ps1 index e2e638fafa..4a92feed89 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -40,16 +40,21 @@ function RunCleanupCode { # When running in cibuild for a pull request, this reformats only the files changed in the PR and fails if the reformat produces changes. if ($env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT) { - Write-Output "Running code cleanup on changed files in pull request" - # In the past, we used $env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT for the merge commit hash. That is the pinned hash at the time the build is enqueued. # When a force-push happens after that, while the build hasn't yet started, this hash becomes invalid during the build, resulting in a lookup error. - # To prevent failing the build for unobvious reasons we use HEAD, which is always the latest version. - $mergeCommitHash = git rev-parse "HEAD" - $targetCommitHash = git rev-parse "$env:APPVEYOR_REPO_BRANCH" + # To prevent failing the build for unobvious reasons we use HEAD, which is always a detached head (the PR merge result). + + $headCommitHash = git rev-parse HEAD + CheckLastExitCode - dotnet regitlint -s JsonApiDotNetCore.sln --print-command --disable-jb-path-hack --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN -f commits -a $mergeCommitHash -b $targetCommitHash --fail-on-diff --print-diff + $baseCommitHash = git rev-parse "$env:APPVEYOR_REPO_BRANCH" CheckLastExitCode + + if ($baseCommitHash -ne $headCommitHash) { + Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash in pull request." + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN -f commits -a $headCommitHash -b $baseCommitHash --fail-on-diff --print-diff + CheckLastExitCode + } } } diff --git a/cleanupcode.ps1 b/cleanupcode.ps1 index 6db01a863a..bab8b82af1 100644 --- a/cleanupcode.ps1 +++ b/cleanupcode.ps1 @@ -1,17 +1,44 @@ #Requires -Version 7.0 -# This script reformats the entire codebase to make it compliant with our coding guidelines. +# This script reformats (part of) the codebase to make it compliant with our coding guidelines. -dotnet tool restore +param( + # Git branch name or base commit hash to reformat only the subset of changed files. Omit for all files. + [string] $revision +) -if ($LASTEXITCODE -ne 0) { - throw "Tool restore failed with exit code $LASTEXITCODE" +function VerifySuccessExitCode { + if ($LastExitCode -ne 0) { + throw "Command failed with exit code $LastExitCode." + } } +dotnet tool restore +VerifySuccessExitCode + dotnet restore +VerifySuccessExitCode -if ($LASTEXITCODE -ne 0) { - throw "Package restore failed with exit code $LASTEXITCODE" -} +if ($revision) { + $headCommitHash = git rev-parse HEAD + VerifySuccessExitCode -dotnet regitlint -s JsonApiDotNetCore.sln --print-command --disable-jb-path-hack --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN + $baseCommitHash = git rev-parse $revision + VerifySuccessExitCode + + if ($baseCommitHash -eq $headCommitHash) { + Write-Output "Running code cleanup on staged/unstaged files." + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN -f staged,modified + VerifySuccessExitCode + } + else { + Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash, including staged/unstaged files." + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN -f staged,modified,commits -a $headCommitHash -b $baseCommitHash + VerifySuccessExitCode + } +} +else { + Write-Output "Running code cleanup on all files." + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN + VerifySuccessExitCode +} From 02d19b5ecf71b5cf9652425b87ff61ae659ef773 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Thu, 4 Aug 2022 00:22:05 +0200 Subject: [PATCH 02/14] Adapts marking many-to-many relationships as tracked so it no longer needs the Issue26779 EF Core back-compat switch. The switch is unavailable in EF 7, so this change unblocks us from upgrade. --- .../Configuration/JsonApiOptions.cs | 6 ---- .../EntityFrameworkCoreRepository.cs | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index eda6374acf..aa7ed0d434 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -102,12 +102,6 @@ public sealed class JsonApiOptions : IJsonApiOptions } }; - static JsonApiOptions() - { - // Bug workaround for https://github.com/dotnet/efcore/issues/27436 - AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue26779", true); - } - public JsonApiOptions() { _lazySerializerReadOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly); diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 358f63a2cd..1b807fd24f 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -547,6 +547,34 @@ private void MarkRelationshipAsLoaded(TResource leftResource, RelationshipAttrib EntityEntry leftEntry = _dbContext.Entry(leftResource); CollectionEntry rightCollectionEntry = leftEntry.Collection(relationship.Property.Name); rightCollectionEntry.IsLoaded = true; + + if (rightCollectionEntry.Metadata is ISkipNavigation skipNavigation) + { + MarkManyToManyRelationshipAsLoaded(leftEntry, skipNavigation); + } + } + + private void MarkManyToManyRelationshipAsLoaded(EntityEntry leftEntry, ISkipNavigation skipNavigation) + { + string[] primaryKeyNames = skipNavigation.ForeignKey.PrincipalKey.Properties.Select(property => property.Name).ToArray(); + object?[] primaryKeyValues = GetCurrentKeyValues(leftEntry, primaryKeyNames); + + string[] foreignKeyNames = skipNavigation.ForeignKey.Properties.Select(property => property.Name).ToArray(); + + foreach (EntityEntry joinEntry in _dbContext.ChangeTracker.Entries().Where(entry => entry.Metadata == skipNavigation.JoinEntityType).ToList()) + { + object?[] foreignKeyValues = GetCurrentKeyValues(joinEntry, foreignKeyNames); + + if (primaryKeyValues.SequenceEqual(foreignKeyValues)) + { + joinEntry.State = EntityState.Unchanged; + } + } + } + + private static object?[] GetCurrentKeyValues(EntityEntry entry, IEnumerable keyNames) + { + return keyNames.Select(keyName => entry.Property(keyName).CurrentValue).ToArray(); } protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, TResource leftResource, object? valueToAssign, From 6825c8345a81ed8024e2968bd95f0264c22cf1ee Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 6 Aug 2022 10:16:04 +0200 Subject: [PATCH 03/14] Added test for removing from many-to-many relationship with composite key --- .../IntegrationTests/CompositeKeys/Car.cs | 3 ++ .../CompositeKeys/CompositeDbContext.cs | 4 ++ .../CompositeKeys/CompositeKeyTests.cs | 48 +++++++++++++++++++ .../CompositeKeys/Dealership.cs | 3 ++ 4 files changed, 58 insertions(+) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs index 29213f5e69..d61d378db7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Car.cs @@ -47,4 +47,7 @@ public override string? Id [HasOne] public Dealership? Dealership { get; set; } + + [HasMany] + public ISet PreviousDealerships { get; set; } = new HashSet(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs index 67213057b7..e9e12439e6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs @@ -34,5 +34,9 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(dealership => dealership.Inventory) .WithOne(car => car.Dealership!); + + builder.Entity() + .HasMany(car => car.PreviousDealerships) + .WithMany(dealership => dealership.SoldCars); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs index 185367930d..9e273be36e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs @@ -508,4 +508,52 @@ await _testContext.RunOnDatabaseAsync(async dbContext => carInDatabase.Should().BeNull(); }); } + + [Fact] + public async Task Can_remove_from_ManyToMany_relationship() + { + // Arrange + Dealership existingDealership = _fakers.Dealership.Generate(); + existingDealership.SoldCars = _fakers.Car.Generate(2).ToHashSet(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Dealerships.Add(existingDealership); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new[] + { + new + { + type = "cars", + id = existingDealership.SoldCars.ElementAt(1).StringId + } + } + }; + + string route = $"/dealerships/{existingDealership.StringId}/relationships/soldCars"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.NoContent); + + responseDocument.Should().BeEmpty(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Dealership dealershipInDatabase = await dbContext.Dealerships.Include(dealership => dealership.SoldCars).FirstWithIdAsync(existingDealership.Id); + + dealershipInDatabase.SoldCars.ShouldHaveCount(1); + dealershipInDatabase.SoldCars.Single().Id.Should().Be(existingDealership.SoldCars.ElementAt(0).Id); + + List carsInDatabase = await dbContext.Cars.ToListAsync(); + carsInDatabase.ShouldHaveCount(2); + }); + } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Dealership.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Dealership.cs index 14784cb438..091e7acbe1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Dealership.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/Dealership.cs @@ -13,4 +13,7 @@ public sealed class Dealership : Identifiable [HasMany] public ISet Inventory { get; set; } = new HashSet(); + + [HasMany] + public ISet SoldCars { get; set; } = new HashSet(); } From 0fe0c59d14080184e7ea280e0ffc042f379c1ce7 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Mon, 8 Aug 2022 01:06:28 +0200 Subject: [PATCH 04/14] Use C# 10, which enables static lambdas to guard against accidentally captured variables, potentially resulting in unintended allocations --- JsonApiDotNetCore.sln.DotSettings | 1 + .../ControllerSourceGenerator.cs | 236 +++++------ .../JsonApiDotNetCore.SourceGenerators.csproj | 3 +- .../JsonApiEndpointsCopy.cs | 39 +- .../SourceCodeWriter.cs | 393 +++++++++--------- .../TypeWithAttributeSyntaxReceiver.cs | 62 ++- 6 files changed, 360 insertions(+), 374 deletions(-) diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings index 2a7eb28d9b..95c0c2b7b2 100644 --- a/JsonApiDotNetCore.sln.DotSettings +++ b/JsonApiDotNetCore.sln.DotSettings @@ -54,6 +54,7 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$); WARNING WARNING WARNING + WARNING DO_NOT_SHOW HINT SUGGESTION diff --git a/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs b/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs index 65800bba82..89a511b08e 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/ControllerSourceGenerator.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Text; using Humanizer; using Microsoft.CodeAnalysis; @@ -11,166 +8,163 @@ #pragma warning disable RS2008 // Enable analyzer release tracking -namespace JsonApiDotNetCore.SourceGenerators +namespace JsonApiDotNetCore.SourceGenerators; +// To debug in Visual Studio (requires v17.2 or higher): +// - Set JsonApiDotNetCore.SourceGenerators as startup project +// - Add a breakpoint at the start of the Initialize or Execute method +// - Optional: change targetProject in Properties\launchSettings.json +// - Press F5 + +[Generator(LanguageNames.CSharp)] +public sealed class ControllerSourceGenerator : ISourceGenerator { - // To debug in Visual Studio (requires v17.2 or higher): - // - Set JsonApiDotNetCore.SourceGenerators as startup project - // - Add a breakpoint at the start of the Initialize or Execute method - // - Optional: change targetProject in Properties\launchSettings.json - // - Press F5 - - [Generator(LanguageNames.CSharp)] - public sealed class ControllerSourceGenerator : ISourceGenerator + private const string Category = "JsonApiDotNetCore"; + + private static readonly DiagnosticDescriptor MissingInterfaceWarning = new("JADNC001", "Resource type does not implement IIdentifiable", + "Type '{0}' must implement IIdentifiable when using ResourceAttribute to auto-generate ASP.NET controllers", Category, DiagnosticSeverity.Warning, + true); + + private static readonly DiagnosticDescriptor MissingIndentInTableError = new("JADNC900", "Internal error: Insufficient entries in IndentTable", + "Internal error: Missing entry in IndentTable for depth {0}", Category, DiagnosticSeverity.Warning, true); + + // PERF: Heap-allocate the delegate only once, instead of per compilation. + private static readonly SyntaxReceiverCreator CreateSyntaxReceiver = static () => new TypeWithAttributeSyntaxReceiver(); + + public void Initialize(GeneratorInitializationContext context) { - private const string Category = "JsonApiDotNetCore"; + context.RegisterForSyntaxNotifications(CreateSyntaxReceiver); + } - private static readonly DiagnosticDescriptor MissingInterfaceWarning = new DiagnosticDescriptor("JADNC001", - "Resource type does not implement IIdentifiable", - "Type '{0}' must implement IIdentifiable when using ResourceAttribute to auto-generate ASP.NET controllers", Category, - DiagnosticSeverity.Warning, true); + public void Execute(GeneratorExecutionContext context) + { + var receiver = (TypeWithAttributeSyntaxReceiver?)context.SyntaxReceiver; - private static readonly DiagnosticDescriptor MissingIndentInTableError = new DiagnosticDescriptor("JADNC900", - "Internal error: Insufficient entries in IndentTable", "Internal error: Missing entry in IndentTable for depth {0}", Category, - DiagnosticSeverity.Warning, true); + if (receiver == null) + { + return; + } - // PERF: Heap-allocate the delegate only once, instead of per compilation. - private static readonly SyntaxReceiverCreator CreateSyntaxReceiver = () => new TypeWithAttributeSyntaxReceiver(); + INamedTypeSymbol? resourceAttributeType = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.Annotations.ResourceAttribute"); + INamedTypeSymbol? identifiableOpenInterface = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.IIdentifiable`1"); + INamedTypeSymbol? loggerFactoryInterface = context.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.ILoggerFactory"); - public void Initialize(GeneratorInitializationContext context) + if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null) { - context.RegisterForSyntaxNotifications(CreateSyntaxReceiver); + return; } - public void Execute(GeneratorExecutionContext context) + var controllerNamesInUse = new Dictionary(StringComparer.OrdinalIgnoreCase); + var writer = new SourceCodeWriter(context, MissingIndentInTableError); + + foreach (TypeDeclarationSyntax? typeDeclarationSyntax in receiver.TypeDeclarations) { - var receiver = (TypeWithAttributeSyntaxReceiver)context.SyntaxReceiver; + // PERF: Note that our code runs on every keystroke in the IDE, which makes it critical to provide near-realtime performance. + // This means keeping an eye on memory allocations and bailing out early when compilations are cancelled while the user is still typing. + context.CancellationToken.ThrowIfCancellationRequested(); + + SemanticModel semanticModel = context.Compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree); + INamedTypeSymbol? resourceType = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, context.CancellationToken); - if (receiver == null) + if (resourceType == null) { - return; + continue; } - INamedTypeSymbol resourceAttributeType = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.Annotations.ResourceAttribute"); - INamedTypeSymbol identifiableOpenInterface = context.Compilation.GetTypeByMetadataName("JsonApiDotNetCore.Resources.IIdentifiable`1"); - INamedTypeSymbol loggerFactoryInterface = context.Compilation.GetTypeByMetadataName("Microsoft.Extensions.Logging.ILoggerFactory"); + AttributeData? resourceAttributeData = FirstOrDefault(resourceType.GetAttributes(), resourceAttributeType, + static (data, type) => SymbolEqualityComparer.Default.Equals(data.AttributeClass, type)); - if (resourceAttributeType == null || identifiableOpenInterface == null || loggerFactoryInterface == null) + if (resourceAttributeData == null) { - return; + continue; } - var controllerNamesInUse = new Dictionary(StringComparer.OrdinalIgnoreCase); - var writer = new SourceCodeWriter(context, MissingIndentInTableError); + TypedConstant endpointsArgument = + resourceAttributeData.NamedArguments.FirstOrDefault(static pair => pair.Key == "GenerateControllerEndpoints").Value; - foreach (TypeDeclarationSyntax typeDeclarationSyntax in receiver.TypeDeclarations) + if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None) { - // PERF: Note that our code runs on every keystroke in the IDE, which makes it critical to provide near-realtime performance. - // This means keeping an eye on memory allocations and bailing out early when compilations are cancelled while the user is still typing. - context.CancellationToken.ThrowIfCancellationRequested(); - - SemanticModel semanticModel = context.Compilation.GetSemanticModel(typeDeclarationSyntax.SyntaxTree); - INamedTypeSymbol resourceType = semanticModel.GetDeclaredSymbol(typeDeclarationSyntax, context.CancellationToken); - - if (resourceType == null) - { - continue; - } - - AttributeData resourceAttributeData = FirstOrDefault(resourceType.GetAttributes(), resourceAttributeType, - (data, type) => SymbolEqualityComparer.Default.Equals(data.AttributeClass, type)); - - if (resourceAttributeData == null) - { - continue; - } - - TypedConstant endpointsArgument = resourceAttributeData.NamedArguments.FirstOrDefault(pair => pair.Key == "GenerateControllerEndpoints").Value; - - if (endpointsArgument.Value != null && (JsonApiEndpointsCopy)endpointsArgument.Value == JsonApiEndpointsCopy.None) - { - continue; - } - - TypedConstant controllerNamespaceArgument = - resourceAttributeData.NamedArguments.FirstOrDefault(pair => pair.Key == "ControllerNamespace").Value; - - string controllerNamespace = GetControllerNamespace(controllerNamespaceArgument, resourceType); - - INamedTypeSymbol identifiableClosedInterface = FirstOrDefault(resourceType.AllInterfaces, identifiableOpenInterface, - (@interface, openInterface) => @interface.IsGenericType && - SymbolEqualityComparer.Default.Equals(@interface.ConstructedFrom, openInterface)); + continue; + } - if (identifiableClosedInterface == null) - { - var diagnostic = Diagnostic.Create(MissingInterfaceWarning, typeDeclarationSyntax.GetLocation(), resourceType.Name); - context.ReportDiagnostic(diagnostic); - continue; - } + TypedConstant controllerNamespaceArgument = + resourceAttributeData.NamedArguments.FirstOrDefault(static pair => pair.Key == "ControllerNamespace").Value; - ITypeSymbol idType = identifiableClosedInterface.TypeArguments[0]; - string controllerName = $"{resourceType.Name.Pluralize()}Controller"; - JsonApiEndpointsCopy endpointsToGenerate = (JsonApiEndpointsCopy?)(int?)endpointsArgument.Value ?? JsonApiEndpointsCopy.All; + string? controllerNamespace = GetControllerNamespace(controllerNamespaceArgument, resourceType); - string sourceCode = writer.Write(resourceType, idType, endpointsToGenerate, controllerNamespace, controllerName, loggerFactoryInterface); - SourceText sourceText = SourceText.From(sourceCode, Encoding.UTF8); + INamedTypeSymbol? identifiableClosedInterface = FirstOrDefault(resourceType.AllInterfaces, identifiableOpenInterface, + static (@interface, openInterface) => + @interface.IsGenericType && SymbolEqualityComparer.Default.Equals(@interface.ConstructedFrom, openInterface)); - string fileName = GetUniqueFileName(controllerName, controllerNamesInUse); - context.AddSource(fileName, sourceText); + if (identifiableClosedInterface == null) + { + var diagnostic = Diagnostic.Create(MissingInterfaceWarning, typeDeclarationSyntax.GetLocation(), resourceType.Name); + context.ReportDiagnostic(diagnostic); + continue; } - } - private static TElement FirstOrDefault(ImmutableArray source, TContext context, Func predicate) - { - // PERF: Using this method enables to avoid allocating a closure in the passed lambda expression. - // See https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions. + ITypeSymbol idType = identifiableClosedInterface.TypeArguments[0]; + string controllerName = $"{resourceType.Name.Pluralize()}Controller"; + JsonApiEndpointsCopy endpointsToGenerate = (JsonApiEndpointsCopy?)(int?)endpointsArgument.Value ?? JsonApiEndpointsCopy.All; - foreach (TElement element in source) - { - if (predicate(element, context)) - { - return element; - } - } + string sourceCode = writer.Write(resourceType, idType, endpointsToGenerate, controllerNamespace, controllerName, loggerFactoryInterface); + SourceText sourceText = SourceText.From(sourceCode, Encoding.UTF8); - return default; + string fileName = GetUniqueFileName(controllerName, controllerNamesInUse); + context.AddSource(fileName, sourceText); } + } - private static string GetControllerNamespace(TypedConstant controllerNamespaceArgument, INamedTypeSymbol resourceType) + private static TElement? FirstOrDefault(ImmutableArray source, TContext context, Func predicate) + { + // PERF: Using this method enables to avoid allocating a closure in the passed lambda expression. + // See https://www.jetbrains.com/help/resharper/2021.2/Fixing_Issues_Found_by_DPA.html#closures-in-lambda-expressions. + + foreach (TElement element in source) { - if (!controllerNamespaceArgument.IsNull) + if (predicate(element, context)) { - return (string)controllerNamespaceArgument.Value; + return element; } + } - if (resourceType.ContainingNamespace.IsGlobalNamespace) - { - return null; - } + return default; + } - if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace) - { - return "Controllers"; - } + private static string? GetControllerNamespace(TypedConstant controllerNamespaceArgument, INamedTypeSymbol resourceType) + { + if (!controllerNamespaceArgument.IsNull) + { + return (string?)controllerNamespaceArgument.Value; + } - return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers"; + if (resourceType.ContainingNamespace.IsGlobalNamespace) + { + return null; } - private static string GetUniqueFileName(string controllerName, IDictionary controllerNamesInUse) + if (resourceType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace) { - // We emit unique file names to prevent a failure in the source generator, but also because our test suite - // may contain two resources with the same class name in different namespaces. That works, as long as only - // one of its controllers gets registered. + return "Controllers"; + } - if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex)) - { - lastIndex++; - controllerNamesInUse[controllerName] = lastIndex; + return $"{resourceType.ContainingNamespace.ContainingNamespace}.Controllers"; + } - return $"{controllerName}{lastIndex}.g.cs"; - } + private static string GetUniqueFileName(string controllerName, IDictionary controllerNamesInUse) + { + // We emit unique file names to prevent a failure in the source generator, but also because our test suite + // may contain two resources with the same class name in different namespaces. That works, as long as only + // one of its controllers gets registered. - controllerNamesInUse[controllerName] = 1; - return $"{controllerName}.g.cs"; + if (controllerNamesInUse.TryGetValue(controllerName, out int lastIndex)) + { + lastIndex++; + controllerNamesInUse[controllerName] = lastIndex; + + return $"{controllerName}{lastIndex}.g.cs"; } + + controllerNamesInUse[controllerName] = 1; + return $"{controllerName}.g.cs"; } } diff --git a/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj b/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj index bcd8c06b0a..8bf3e90cf6 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj +++ b/src/JsonApiDotNetCore.SourceGenerators/JsonApiDotNetCore.SourceGenerators.csproj @@ -5,8 +5,7 @@ true false $(NoWarn);NU5128 - disable - disable + latest true diff --git a/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs b/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs index 14134adcfd..911be3f359 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/JsonApiEndpointsCopy.cs @@ -1,26 +1,23 @@ -using System; +namespace JsonApiDotNetCore.SourceGenerators; -namespace JsonApiDotNetCore.SourceGenerators +// IMPORTANT: A copy of this type exists in the JsonApiDotNetCore project. Keep these in sync when making changes. +[Flags] +public enum JsonApiEndpointsCopy { - // IMPORTANT: A copy of this type exists in the JsonApiDotNetCore project. Keep these in sync when making changes. - [Flags] - public enum JsonApiEndpointsCopy - { - None = 0, - GetCollection = 1, - GetSingle = 1 << 1, - GetSecondary = 1 << 2, - GetRelationship = 1 << 3, - Post = 1 << 4, - PostRelationship = 1 << 5, - Patch = 1 << 6, - PatchRelationship = 1 << 7, - Delete = 1 << 8, - DeleteRelationship = 1 << 9, + None = 0, + GetCollection = 1, + GetSingle = 1 << 1, + GetSecondary = 1 << 2, + GetRelationship = 1 << 3, + Post = 1 << 4, + PostRelationship = 1 << 5, + Patch = 1 << 6, + PatchRelationship = 1 << 7, + Delete = 1 << 8, + DeleteRelationship = 1 << 9, - Query = GetCollection | GetSingle | GetSecondary | GetRelationship, - Command = Post | PostRelationship | Patch | PatchRelationship | Delete | DeleteRelationship, + Query = GetCollection | GetSingle | GetSecondary | GetRelationship, + Command = Post | PostRelationship | Patch | PatchRelationship | Delete | DeleteRelationship, - All = Query | Command - } + All = Query | Command } diff --git a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs index e03e3cbad2..bd6f96e82d 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs @@ -1,266 +1,263 @@ -using System.Collections.Generic; using System.Text; using Microsoft.CodeAnalysis; -namespace JsonApiDotNetCore.SourceGenerators +namespace JsonApiDotNetCore.SourceGenerators; + +/// +/// Writes the source code for an ASP.NET controller for a JSON:API resource. +/// +internal sealed class SourceCodeWriter { - /// - /// Writes the source code for an ASP.NET controller for a JSON:API resource. - /// - internal sealed class SourceCodeWriter + private const int SpacesPerIndent = 4; + + private static readonly IDictionary IndentTable = new Dictionary { - private const int SpacesPerIndent = 4; + [0] = string.Empty, + [1] = new(' ', 1 * SpacesPerIndent), + [2] = new(' ', 2 * SpacesPerIndent), + [3] = new(' ', 3 * SpacesPerIndent) + }; + + private static readonly IDictionary AggregateEndpointToServiceNameMap = + new Dictionary + { + [JsonApiEndpointsCopy.All] = ("IResourceService", "resourceService"), + [JsonApiEndpointsCopy.Query] = ("IResourceQueryService", "queryService"), + [JsonApiEndpointsCopy.Command] = ("IResourceCommandService", "commandService") + }; - private static readonly IDictionary IndentTable = new Dictionary + private static readonly IDictionary EndpointToServiceNameMap = + new Dictionary { - [0] = string.Empty, - [1] = new string(' ', 1 * SpacesPerIndent), - [2] = new string(' ', 2 * SpacesPerIndent), - [3] = new string(' ', 3 * SpacesPerIndent) + [JsonApiEndpointsCopy.GetCollection] = ("IGetAllService", "getAll"), + [JsonApiEndpointsCopy.GetSingle] = ("IGetByIdService", "getById"), + [JsonApiEndpointsCopy.GetSecondary] = ("IGetSecondaryService", "getSecondary"), + [JsonApiEndpointsCopy.GetRelationship] = ("IGetRelationshipService", "getRelationship"), + [JsonApiEndpointsCopy.Post] = ("ICreateService", "create"), + [JsonApiEndpointsCopy.PostRelationship] = ("IAddToRelationshipService", "addToRelationship"), + [JsonApiEndpointsCopy.Patch] = ("IUpdateService", "update"), + [JsonApiEndpointsCopy.PatchRelationship] = ("ISetRelationshipService", "setRelationship"), + [JsonApiEndpointsCopy.Delete] = ("IDeleteService", "delete"), + [JsonApiEndpointsCopy.DeleteRelationship] = ("IRemoveFromRelationshipService", "removeFromRelationship") }; - private static readonly IDictionary AggregateEndpointToServiceNameMap = - new Dictionary - { - [JsonApiEndpointsCopy.All] = ("IResourceService", "resourceService"), - [JsonApiEndpointsCopy.Query] = ("IResourceQueryService", "queryService"), - [JsonApiEndpointsCopy.Command] = ("IResourceCommandService", "commandService") - }; + private readonly GeneratorExecutionContext _context; + private readonly DiagnosticDescriptor _missingIndentInTableErrorDescriptor; - private static readonly IDictionary EndpointToServiceNameMap = - new Dictionary - { - [JsonApiEndpointsCopy.GetCollection] = ("IGetAllService", "getAll"), - [JsonApiEndpointsCopy.GetSingle] = ("IGetByIdService", "getById"), - [JsonApiEndpointsCopy.GetSecondary] = ("IGetSecondaryService", "getSecondary"), - [JsonApiEndpointsCopy.GetRelationship] = ("IGetRelationshipService", "getRelationship"), - [JsonApiEndpointsCopy.Post] = ("ICreateService", "create"), - [JsonApiEndpointsCopy.PostRelationship] = ("IAddToRelationshipService", "addToRelationship"), - [JsonApiEndpointsCopy.Patch] = ("IUpdateService", "update"), - [JsonApiEndpointsCopy.PatchRelationship] = ("ISetRelationshipService", "setRelationship"), - [JsonApiEndpointsCopy.Delete] = ("IDeleteService", "delete"), - [JsonApiEndpointsCopy.DeleteRelationship] = ("IRemoveFromRelationshipService", "removeFromRelationship") - }; - - private readonly GeneratorExecutionContext _context; - private readonly DiagnosticDescriptor _missingIndentInTableErrorDescriptor; - - private readonly StringBuilder _sourceBuilder = new StringBuilder(); - private int _depth; - - public SourceCodeWriter(GeneratorExecutionContext context, DiagnosticDescriptor missingIndentInTableErrorDescriptor) + private readonly StringBuilder _sourceBuilder = new(); + private int _depth; + + public SourceCodeWriter(GeneratorExecutionContext context, DiagnosticDescriptor missingIndentInTableErrorDescriptor) + { + _context = context; + _missingIndentInTableErrorDescriptor = missingIndentInTableErrorDescriptor; + } + + public string Write(INamedTypeSymbol resourceType, ITypeSymbol idType, JsonApiEndpointsCopy endpointsToGenerate, string? controllerNamespace, + string controllerName, INamedTypeSymbol loggerFactoryInterface) + { + _sourceBuilder.Clear(); + _depth = 0; + + if (idType.IsReferenceType && idType.NullableAnnotation == NullableAnnotation.Annotated) { - _context = context; - _missingIndentInTableErrorDescriptor = missingIndentInTableErrorDescriptor; + WriteNullableEnable(); } - public string Write(INamedTypeSymbol resourceType, ITypeSymbol idType, JsonApiEndpointsCopy endpointsToGenerate, string controllerNamespace, - string controllerName, INamedTypeSymbol loggerFactoryInterface) + WriteNamespaceImports(loggerFactoryInterface, resourceType); + + if (controllerNamespace != null) { - _sourceBuilder.Clear(); - _depth = 0; + WriteNamespaceDeclaration(controllerNamespace); + } - if (idType.IsReferenceType && idType.NullableAnnotation == NullableAnnotation.Annotated) - { - WriteNullableEnable(); - } + WriteOpenClassDeclaration(controllerName, endpointsToGenerate, resourceType, idType); + _depth++; - WriteNamespaceImports(loggerFactoryInterface, resourceType); + WriteConstructor(controllerName, loggerFactoryInterface, endpointsToGenerate, resourceType, idType); - if (controllerNamespace != null) - { - WriteNamespaceDeclaration(controllerNamespace); - } + _depth--; + WriteCloseCurly(); - WriteOpenClassDeclaration(controllerName, endpointsToGenerate, resourceType, idType); - _depth++; + return _sourceBuilder.ToString(); + } - WriteConstructor(controllerName, loggerFactoryInterface, endpointsToGenerate, resourceType, idType); + private void WriteNullableEnable() + { + _sourceBuilder.AppendLine("#nullable enable"); + _sourceBuilder.AppendLine(); + } - _depth--; - WriteCloseCurly(); + private void WriteNamespaceImports(INamedTypeSymbol loggerFactoryInterface, INamedTypeSymbol resourceType) + { + _sourceBuilder.AppendLine($@"using {loggerFactoryInterface.ContainingNamespace};"); - return _sourceBuilder.ToString(); - } + _sourceBuilder.AppendLine("using JsonApiDotNetCore.Configuration;"); + _sourceBuilder.AppendLine("using JsonApiDotNetCore.Controllers;"); + _sourceBuilder.AppendLine("using JsonApiDotNetCore.Services;"); - private void WriteNullableEnable() + if (!resourceType.ContainingNamespace.IsGlobalNamespace) { - _sourceBuilder.AppendLine("#nullable enable"); - _sourceBuilder.AppendLine(); + _sourceBuilder.AppendLine($"using {resourceType.ContainingNamespace};"); } - private void WriteNamespaceImports(INamedTypeSymbol loggerFactoryInterface, INamedTypeSymbol resourceType) - { - _sourceBuilder.AppendLine($@"using {loggerFactoryInterface.ContainingNamespace};"); + _sourceBuilder.AppendLine(); + } - _sourceBuilder.AppendLine("using JsonApiDotNetCore.Configuration;"); - _sourceBuilder.AppendLine("using JsonApiDotNetCore.Controllers;"); - _sourceBuilder.AppendLine("using JsonApiDotNetCore.Services;"); + private void WriteNamespaceDeclaration(string controllerNamespace) + { + _sourceBuilder.AppendLine($"namespace {controllerNamespace};"); + _sourceBuilder.AppendLine(); + } - if (!resourceType.ContainingNamespace.IsGlobalNamespace) - { - _sourceBuilder.AppendLine($"using {resourceType.ContainingNamespace};"); - } + private void WriteOpenClassDeclaration(string controllerName, JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, ITypeSymbol idType) + { + string baseClassName = GetControllerBaseClassName(endpointsToGenerate); - _sourceBuilder.AppendLine(); - } + WriteIndent(); + _sourceBuilder.AppendLine($@"public sealed partial class {controllerName} : {baseClassName}<{resourceType.Name}, {idType}>"); - private void WriteNamespaceDeclaration(string controllerNamespace) + WriteOpenCurly(); + } + + private static string GetControllerBaseClassName(JsonApiEndpointsCopy endpointsToGenerate) + { + switch (endpointsToGenerate) { - _sourceBuilder.AppendLine($"namespace {controllerNamespace};"); - _sourceBuilder.AppendLine(); + case JsonApiEndpointsCopy.Query: + { + return "JsonApiQueryController"; + } + case JsonApiEndpointsCopy.Command: + { + return "JsonApiCommandController"; + } + default: + { + return "JsonApiController"; + } } + } - private void WriteOpenClassDeclaration(string controllerName, JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, - ITypeSymbol idType) - { - string baseClassName = GetControllerBaseClassName(endpointsToGenerate); + private void WriteConstructor(string controllerName, INamedTypeSymbol loggerFactoryInterface, JsonApiEndpointsCopy endpointsToGenerate, + INamedTypeSymbol resourceType, ITypeSymbol idType) + { + string loggerName = loggerFactoryInterface.Name; - WriteIndent(); - _sourceBuilder.AppendLine($@"public sealed partial class {controllerName} : {baseClassName}<{resourceType.Name}, {idType}>"); + WriteIndent(); + _sourceBuilder.AppendLine($"public {controllerName}(IJsonApiOptions options, IResourceGraph resourceGraph, {loggerName} loggerFactory,"); - WriteOpenCurly(); - } + _depth++; - private static string GetControllerBaseClassName(JsonApiEndpointsCopy endpointsToGenerate) + if (AggregateEndpointToServiceNameMap.TryGetValue(endpointsToGenerate, out (string ServiceName, string ParameterName) value)) { - switch (endpointsToGenerate) - { - case JsonApiEndpointsCopy.Query: - { - return "JsonApiQueryController"; - } - case JsonApiEndpointsCopy.Command: - { - return "JsonApiCommandController"; - } - default: - { - return "JsonApiController"; - } - } + WriteParameterListForShortConstructor(value.ServiceName, value.ParameterName, resourceType, idType); } - - private void WriteConstructor(string controllerName, INamedTypeSymbol loggerFactoryInterface, JsonApiEndpointsCopy endpointsToGenerate, - INamedTypeSymbol resourceType, ITypeSymbol idType) + else { - string loggerName = loggerFactoryInterface.Name; - - WriteIndent(); - _sourceBuilder.AppendLine($"public {controllerName}(IJsonApiOptions options, IResourceGraph resourceGraph, {loggerName} loggerFactory,"); + WriteParameterListForLongConstructor(endpointsToGenerate, resourceType, idType); + } - _depth++; + _depth--; - if (AggregateEndpointToServiceNameMap.TryGetValue(endpointsToGenerate, out (string ServiceName, string ParameterName) value)) - { - WriteParameterListForShortConstructor(value.ServiceName, value.ParameterName, resourceType, idType); - } - else - { - WriteParameterListForLongConstructor(endpointsToGenerate, resourceType, idType); - } - - _depth--; + WriteOpenCurly(); + WriteCloseCurly(); + } - WriteOpenCurly(); - WriteCloseCurly(); - } + private void WriteParameterListForShortConstructor(string serviceName, string parameterName, INamedTypeSymbol resourceType, ITypeSymbol idType) + { + WriteIndent(); + _sourceBuilder.AppendLine($"{serviceName}<{resourceType.Name}, {idType}> {parameterName})"); - private void WriteParameterListForShortConstructor(string serviceName, string parameterName, INamedTypeSymbol resourceType, ITypeSymbol idType) - { - WriteIndent(); - _sourceBuilder.AppendLine($"{serviceName}<{resourceType.Name}, {idType}> {parameterName})"); + WriteIndent(); + _sourceBuilder.AppendLine($": base(options, resourceGraph, loggerFactory, {parameterName})"); + } - WriteIndent(); - _sourceBuilder.AppendLine($": base(options, resourceGraph, loggerFactory, {parameterName})"); - } + private void WriteParameterListForLongConstructor(JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, ITypeSymbol idType) + { + bool isFirstEntry = true; - private void WriteParameterListForLongConstructor(JsonApiEndpointsCopy endpointsToGenerate, INamedTypeSymbol resourceType, ITypeSymbol idType) + foreach (KeyValuePair entry in EndpointToServiceNameMap) { - bool isFirstEntry = true; - - foreach (KeyValuePair entry in EndpointToServiceNameMap) + if ((endpointsToGenerate & entry.Key) == entry.Key) { - if ((endpointsToGenerate & entry.Key) == entry.Key) + if (isFirstEntry) { - if (isFirstEntry) - { - isFirstEntry = false; - } - else - { - _sourceBuilder.AppendLine(Tokens.Comma); - } - - WriteIndent(); - _sourceBuilder.Append($"{entry.Value.ServiceName}<{resourceType.Name}, {idType}> {entry.Value.ParameterName}"); + isFirstEntry = false; } + else + { + _sourceBuilder.AppendLine(Tokens.Comma); + } + + WriteIndent(); + _sourceBuilder.Append($"{entry.Value.ServiceName}<{resourceType.Name}, {idType}> {entry.Value.ParameterName}"); } + } - _sourceBuilder.AppendLine(Tokens.CloseParen); + _sourceBuilder.AppendLine(Tokens.CloseParen); - WriteIndent(); - _sourceBuilder.AppendLine(": base(options, resourceGraph, loggerFactory,"); + WriteIndent(); + _sourceBuilder.AppendLine(": base(options, resourceGraph, loggerFactory,"); - isFirstEntry = true; - _depth++; + isFirstEntry = true; + _depth++; - foreach (KeyValuePair entry in EndpointToServiceNameMap) + foreach (KeyValuePair entry in EndpointToServiceNameMap) + { + if ((endpointsToGenerate & entry.Key) == entry.Key) { - if ((endpointsToGenerate & entry.Key) == entry.Key) + if (isFirstEntry) { - if (isFirstEntry) - { - isFirstEntry = false; - } - else - { - _sourceBuilder.AppendLine(Tokens.Comma); - } - - WriteIndent(); - _sourceBuilder.Append($"{entry.Value.ParameterName}: {entry.Value.ParameterName}"); + isFirstEntry = false; + } + else + { + _sourceBuilder.AppendLine(Tokens.Comma); } - } - _sourceBuilder.AppendLine(Tokens.CloseParen); - _depth--; + WriteIndent(); + _sourceBuilder.Append($"{entry.Value.ParameterName}: {entry.Value.ParameterName}"); + } } - private void WriteOpenCurly() - { - WriteIndent(); - _sourceBuilder.AppendLine(Tokens.OpenCurly); - } + _sourceBuilder.AppendLine(Tokens.CloseParen); + _depth--; + } - private void WriteCloseCurly() - { - WriteIndent(); - _sourceBuilder.AppendLine(Tokens.CloseCurly); - } + private void WriteOpenCurly() + { + WriteIndent(); + _sourceBuilder.AppendLine(Tokens.OpenCurly); + } - private void WriteIndent() - { - // PERF: Reuse pre-calculated indents instead of allocating a new string each time. - if (!IndentTable.TryGetValue(_depth, out string indent)) - { - var diagnostic = Diagnostic.Create(_missingIndentInTableErrorDescriptor, Location.None, _depth.ToString()); - _context.ReportDiagnostic(diagnostic); + private void WriteCloseCurly() + { + WriteIndent(); + _sourceBuilder.AppendLine(Tokens.CloseCurly); + } - indent = new string(' ', _depth * SpacesPerIndent); - } + private void WriteIndent() + { + // PERF: Reuse pre-calculated indents instead of allocating a new string each time. + if (!IndentTable.TryGetValue(_depth, out string? indent)) + { + var diagnostic = Diagnostic.Create(_missingIndentInTableErrorDescriptor, Location.None, _depth.ToString()); + _context.ReportDiagnostic(diagnostic); - _sourceBuilder.Append(indent); + indent = new string(' ', _depth * SpacesPerIndent); } + _sourceBuilder.Append(indent); + } + #pragma warning disable AV1008 // Class should not be static - private static class Tokens - { - public const string OpenCurly = "{"; - public const string CloseCurly = "}"; - public const string CloseParen = ")"; - public const string Comma = ","; - } -#pragma warning restore AV1008 // Class should not be static + private static class Tokens + { + public const string OpenCurly = "{"; + public const string CloseCurly = "}"; + public const string CloseParen = ")"; + public const string Comma = ","; } +#pragma warning restore AV1008 // Class should not be static } diff --git a/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs b/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs index 0fbc18a758..b23de19cc9 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs @@ -1,41 +1,39 @@ -using System.Collections.Generic; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace JsonApiDotNetCore.SourceGenerators +namespace JsonApiDotNetCore.SourceGenerators; + +/// +/// Collects type declarations in the project that have at least one attribute on them. Because this receiver operates at the syntax level, we cannot +/// check for the expected attribute. This must be done during semantic analysis, because source code may contain any of these: +/// { } +/// +/// [ResourceAttribute] +/// public class ExampleResource2 : Identifiable { } +/// +/// [AlternateNamespaceName.Annotations.Resource] +/// public class ExampleResource3 : Identifiable { } +/// +/// [AlternateTypeName] +/// public class ExampleResource4 : Identifiable { } +/// ]]> +/// +/// +internal sealed class TypeWithAttributeSyntaxReceiver : ISyntaxReceiver { - /// - /// Collects type declarations in the project that have at least one attribute on them. Because this receiver operates at the syntax level, we cannot - /// check for the expected attribute. This must be done during semantic analysis, because source code may contain any of these: - /// { } - /// - /// [ResourceAttribute] - /// public class ExampleResource2 : Identifiable { } - /// - /// [AlternateNamespaceName.Annotations.Resource] - /// public class ExampleResource3 : Identifiable { } - /// - /// [AlternateTypeName] - /// public class ExampleResource4 : Identifiable { } - /// ]]> - /// - /// - internal sealed class TypeWithAttributeSyntaxReceiver : ISyntaxReceiver - { - public readonly ISet TypeDeclarations = new HashSet(); + public readonly ISet TypeDeclarations = new HashSet(); - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Any()) { - if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Any()) - { - TypeDeclarations.Add(typeDeclarationSyntax); - } + TypeDeclarations.Add(typeDeclarationSyntax); } } } From 549586b3d32ee8819284ed87491b12e6561f268f Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Tue, 9 Aug 2022 23:36:26 +0200 Subject: [PATCH 05/14] Write "auto-generated" header comment, to prevent 3rd party analyzers from producing diagnostics --- .../SourceCodeWriter.cs | 8 ++++++ .../ControllerGenerationTests.cs | 28 ++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs index bd6f96e82d..13dc91a836 100644 --- a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs +++ b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs @@ -59,6 +59,8 @@ public string Write(INamedTypeSymbol resourceType, ITypeSymbol idType, JsonApiEn _sourceBuilder.Clear(); _depth = 0; + WriteAutoGeneratedComment(); + if (idType.IsReferenceType && idType.NullableAnnotation == NullableAnnotation.Annotated) { WriteNullableEnable(); @@ -82,6 +84,12 @@ public string Write(INamedTypeSymbol resourceType, ITypeSymbol idType, JsonApiEn return _sourceBuilder.ToString(); } + private void WriteAutoGeneratedComment() + { + _sourceBuilder.AppendLine("// "); + _sourceBuilder.AppendLine(); + } + private void WriteNullableEnable() { _sourceBuilder.AppendLine("#nullable enable"); diff --git a/test/SourceGeneratorTests/ControllerGenerationTests.cs b/test/SourceGeneratorTests/ControllerGenerationTests.cs index 614b4d316c..9f5f9f83d3 100644 --- a/test/SourceGeneratorTests/ControllerGenerationTests.cs +++ b/test/SourceGeneratorTests/ControllerGenerationTests.cs @@ -51,7 +51,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -111,7 +113,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -171,7 +175,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -234,7 +240,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -574,7 +582,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -633,7 +643,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; @@ -691,7 +703,9 @@ public sealed class Item : Identifiable GeneratorDriverRunResult runResult = driver.GetRunResult(); runResult.Should().NotHaveDiagnostics(); - runResult.Should().HaveProducedSourceCode(@"using Microsoft.Extensions.Logging; + runResult.Should().HaveProducedSourceCode(@"// + +using Microsoft.Extensions.Logging; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Services; From 826f29089125c74f02cf01ed7bf447fdb230d7bc Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 17 Jul 2022 22:18:33 +0200 Subject: [PATCH 06/14] Minor fixes --- src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs | 9 +++++---- src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs | 4 ++-- .../Queries/Internal/Parsing/IncludeParser.cs | 10 +++++----- .../JsonConverters/SingleOrManyDataConverterFactory.cs | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index eda6374acf..2e7cc54282 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -11,8 +11,8 @@ namespace JsonApiDotNetCore.Configuration; [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - private Lazy _lazySerializerWriteOptions; - private Lazy _lazySerializerReadOptions; + private readonly Lazy _lazySerializerWriteOptions; + private readonly Lazy _lazySerializerReadOptions; /// JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; @@ -110,7 +110,8 @@ static JsonApiOptions() public JsonApiOptions() { - _lazySerializerReadOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly); + _lazySerializerReadOptions = + new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.ExecutionAndPublication); _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) { @@ -119,6 +120,6 @@ public JsonApiOptions() new WriteOnlyDocumentConverter(), new WriteOnlyRelationshipObjectConverter() } - }, LazyThreadSafetyMode.PublicationOnly); + }, LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 94507750da..2e15e6ae9a 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -72,7 +72,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin return; } - SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request); + SetupOperationsRequest((JsonApiRequest)request); httpContext.RegisterJsonApiRequest(); } @@ -280,7 +280,7 @@ private static bool IsRouteForOperations(RouteValueDictionary routeValues) return actionName == "PostOperations"; } - private static void SetupOperationsRequest(JsonApiRequest request, IJsonApiOptions options, HttpRequest httpRequest) + private static void SetupOperationsRequest(JsonApiRequest request) { request.IsReadOnly = false; request.Kind = EndpointKind.AtomicOperations; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index a453921989..14d2f1ec15 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -202,7 +202,7 @@ public string Path var pathBuilder = new StringBuilder(); IncludeTreeNode? parent = this; - while (parent is { Relationship: not HiddenRootRelationship }) + while (parent is { Relationship: not HiddenRootRelationshipAttribute }) { pathBuilder.Insert(0, pathBuilder.Length > 0 ? $"{parent.Relationship.PublicName}." : parent.Relationship.PublicName); parent = parent._parent; @@ -220,7 +220,7 @@ private IncludeTreeNode(RelationshipAttribute relationship, IncludeTreeNode? par public static IncludeTreeNode CreateRoot(ResourceType resourceType) { - var relationship = new HiddenRootRelationship(resourceType); + var relationship = new HiddenRootRelationshipAttribute(resourceType); return new IncludeTreeNode(relationship, null); } @@ -242,7 +242,7 @@ public IncludeExpression ToExpression() { IncludeElementExpression element = ToElementExpression(); - if (element.Relationship is HiddenRootRelationship) + if (element.Relationship is HiddenRootRelationshipAttribute) { return new IncludeExpression(element.Children); } @@ -262,9 +262,9 @@ public override string ToString() return include.ToFullString(); } - private sealed class HiddenRootRelationship : RelationshipAttribute + private sealed class HiddenRootRelationshipAttribute : RelationshipAttribute { - public HiddenRootRelationship(ResourceType rightType) + public HiddenRootRelationshipAttribute(ResourceType rightType) { ArgumentGuard.NotNull(rightType, nameof(rightType)); diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs index 25e497c2c1..3952ca93d8 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs @@ -28,7 +28,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer private sealed class SingleOrManyDataConverter : JsonObjectConverter> where T : class, IResourceIdentity, new() { - public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions serializerOptions) + public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var objects = new List(); bool isManyData = false; @@ -54,7 +54,7 @@ public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToC } case JsonTokenType.StartObject: { - var resourceObject = ReadSubTree(ref reader, serializerOptions); + var resourceObject = ReadSubTree(ref reader, options); objects.Add(resourceObject); break; } From 96340092d6d388ea860ff21e9bbf9fdeaf11c817 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 17 Jul 2022 22:51:49 +0200 Subject: [PATCH 07/14] Reduce duplication in JSON:API type hierarchy by using JsonPropertyOrder (added in .NET 6). Also makes the model a bit more correct, as "ref" elements (used in atomic:operations) cannot occur in "data". --- .../SingleOrManyDataConverterFactory.cs | 2 +- .../Serialization/Objects/AtomicReference.cs | 14 +--------- .../Objects/IResourceIdentity.cs | 8 ------ .../Objects/ResourceIdentifierObject.cs | 15 ++--------- .../Serialization/Objects/ResourceIdentity.cs | 26 +++++++++++++++++++ .../Serialization/Objects/ResourceObject.cs | 21 +++------------ .../Serialization/Objects/SingleOrManyData.cs | 2 +- .../Request/Adapters/BaseAdapter.cs | 6 ++--- .../Adapters/ResourceIdentityAdapter.cs | 20 +++++++------- .../Adapters/ResourceIdentityRequirements.cs | 2 +- 10 files changed, 49 insertions(+), 67 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs index 3952ca93d8..b842cace0e 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs @@ -26,7 +26,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer } private sealed class SingleOrManyDataConverter : JsonObjectConverter> - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs index 01693d1db6..fcc56298c1 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs @@ -7,20 +7,8 @@ namespace JsonApiDotNetCore.Serialization.Objects; /// See "ref" in https://jsonapi.org/ext/atomic/#operation-objects. /// [PublicAPI] -public sealed class AtomicReference : IResourceIdentity +public sealed class AtomicReference : ResourceIdentity { - [JsonPropertyName("type")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string? Type { get; set; } - - [JsonPropertyName("id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Id { get; set; } - - [JsonPropertyName("lid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Lid { get; set; } - [JsonPropertyName("relationship")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Relationship { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs b/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs deleted file mode 100644 index c4b57f535f..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace JsonApiDotNetCore.Serialization.Objects; - -public interface IResourceIdentity -{ - public string? Type { get; } - public string? Id { get; } - public string? Lid { get; } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs index a1b8271cf7..20c30909ed 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs @@ -7,21 +7,10 @@ namespace JsonApiDotNetCore.Serialization.Objects; /// See https://jsonapi.org/format/1.1/#document-resource-identifier-objects. /// [PublicAPI] -public sealed class ResourceIdentifierObject : IResourceIdentity +public class ResourceIdentifierObject : ResourceIdentity { - [JsonPropertyName("type")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string? Type { get; set; } - - [JsonPropertyName("id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Id { get; set; } - - [JsonPropertyName("lid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Lid { get; set; } - [JsonPropertyName("meta")] + [JsonPropertyOrder(100)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? Meta { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs new file mode 100644 index 0000000000..41a3d951e6 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.Objects; + +/// +/// Shared identity information for various JSON:API objects. +/// +[PublicAPI] +public abstract class ResourceIdentity +{ + [JsonPropertyName("type")] + [JsonPropertyOrder(-3)] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string? Type { get; set; } + + [JsonPropertyName("id")] + [JsonPropertyOrder(-2)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Id { get; set; } + + [JsonPropertyName("lid")] + [JsonPropertyOrder(-1)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Lid { get; set; } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs index 43b3b9616a..ed38a40f9a 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs @@ -7,33 +7,20 @@ namespace JsonApiDotNetCore.Serialization.Objects; /// See https://jsonapi.org/format/1.1/#document-resource-objects. /// [PublicAPI] -public sealed class ResourceObject : IResourceIdentity +public sealed class ResourceObject : ResourceIdentifierObject { - [JsonPropertyName("type")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string? Type { get; set; } - - [JsonPropertyName("id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Id { get; set; } - - [JsonPropertyName("lid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Lid { get; set; } - [JsonPropertyName("attributes")] + [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? Attributes { get; set; } [JsonPropertyName("relationships")] + [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? Relationships { get; set; } [JsonPropertyName("links")] + [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ResourceLinks? Links { get; set; } - - [JsonPropertyName("meta")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IDictionary? Meta { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs index 1d2f99e126..1126f84f26 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs @@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Serialization.Objects; public readonly struct SingleOrManyData // The "new()" constraint exists for parity with SingleOrManyDataConverterFactory, which creates empty instances // to ensure ManyValue never contains null items. - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { public object? Value => ManyValue != null ? ManyValue : SingleValue; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs index 64e2f6d53b..fb1111bea1 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs @@ -11,7 +11,7 @@ public abstract class BaseAdapter { [AssertionMethod] protected static void AssertHasData(SingleOrManyData data, RequestAdapterState state) - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { if (!data.IsAssigned) { @@ -21,7 +21,7 @@ protected static void AssertHasData(SingleOrManyData data, RequestAdapterS [AssertionMethod] protected static void AssertDataHasSingleValue(SingleOrManyData data, bool allowNull, RequestAdapterState state) - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { if (data.SingleValue == null) { @@ -44,7 +44,7 @@ protected static void AssertDataHasSingleValue(SingleOrManyData data, bool [AssertionMethod] protected static void AssertDataHasManyValue(SingleOrManyData data, RequestAdapterState state) - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { if (data.ManyValue == null) { diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs index d163eb56d1..61c6cc1857 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs @@ -25,7 +25,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory _resourceFactory = resourceFactory; } - protected (IIdentifiable resource, ResourceType resourceType) ConvertResourceIdentity(IResourceIdentity identity, ResourceIdentityRequirements requirements, + protected (IIdentifiable resource, ResourceType resourceType) ConvertResourceIdentity(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { ArgumentGuard.NotNull(identity, nameof(identity)); @@ -38,7 +38,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory return (resource, resourceType); } - private ResourceType ResolveType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + private ResourceType ResolveType(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { AssertHasType(identity.Type, state); @@ -93,7 +93,7 @@ private static void AssertIsCompatibleResourceType(ResourceType actual, Resource } } - private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceClrType, RequestAdapterState state) + private IIdentifiable CreateResource(ResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceClrType, RequestAdapterState state) { if (state.Request.Kind != EndpointKind.AtomicOperations) { @@ -120,7 +120,7 @@ private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentit return resource; } - private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state) + private static void AssertHasNoLid(ResourceIdentity identity, RequestAdapterState state) { if (identity.Lid != null) { @@ -129,7 +129,7 @@ private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterSta } } - private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapterState state) + private static void AssertNoIdWithLid(ResourceIdentity identity, RequestAdapterState state) { if (identity.Id != null && identity.Lid != null) { @@ -137,7 +137,7 @@ private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapter } } - private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + private static void AssertHasIdOrLid(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { string? message = null; @@ -160,7 +160,7 @@ private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentit } } - private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterState state) + private static void AssertHasNoId(ResourceIdentity identity, RequestAdapterState state) { if (identity.Id != null) { @@ -169,7 +169,7 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat } } - private static void AssertSameIdValue(IResourceIdentity identity, string? expected, RequestAdapterState state) + private static void AssertSameIdValue(ResourceIdentity identity, string? expected, RequestAdapterState state) { if (expected != null && identity.Id != expected) { @@ -180,7 +180,7 @@ private static void AssertSameIdValue(IResourceIdentity identity, string? expect } } - private static void AssertSameLidValue(IResourceIdentity identity, string? expected, RequestAdapterState state) + private static void AssertSameLidValue(ResourceIdentity identity, string? expected, RequestAdapterState state) { if (expected != null && identity.Lid != expected) { @@ -191,7 +191,7 @@ private static void AssertSameLidValue(IResourceIdentity identity, string? expec } } - private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) + private void AssignStringId(ResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) { if (identity.Id != null) { diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs index 11db5e8ee3..d5498397bf 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Serialization.Request.Adapters; /// -/// Defines requirements to validate an instance against. +/// Defines requirements to validate a instance against. /// [PublicAPI] public sealed class ResourceIdentityRequirements From 792ab16e44eb090465bf2e28ef9d016987aab05d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:56:20 +0200 Subject: [PATCH 08/14] Tweak benchmarks --- benchmarks/Benchmarks.csproj | 4 +- .../OperationsDeserializationBenchmarks.cs | 1 + .../ResourceDeserializationBenchmarks.cs | 1 + .../QueryStringParserBenchmarks.cs | 31 +--- .../OperationsSerializationBenchmarks.cs | 1 + .../ResourceSerializationBenchmarks.cs | 1 + .../SerializationBenchmarkBase.cs | 147 +----------------- benchmarks/Tools/FakeLinkBuilder.cs | 39 +++++ .../Tools/FakeRequestQueryStringAccessor.cs | 18 +++ .../Tools/NeverResourceDefinitionAccessor.cs | 103 ++++++++++++ benchmarks/Tools/NoMetaBuilder.cs | 18 +++ 11 files changed, 191 insertions(+), 173 deletions(-) create mode 100644 benchmarks/Tools/FakeLinkBuilder.cs create mode 100644 benchmarks/Tools/FakeRequestQueryStringAccessor.cs create mode 100644 benchmarks/Tools/NeverResourceDefinitionAccessor.cs create mode 100644 benchmarks/Tools/NoMetaBuilder.cs diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index 4bde435c15..f461a4831b 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -1,7 +1,8 @@ - + Exe $(TargetFrameworkName) + true @@ -10,6 +11,5 @@ - diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs index d28684e27b..99adce73cb 100644 --- a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -7,6 +7,7 @@ namespace Benchmarks.Deserialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase { diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index 23a6205bf5..e503a329bb 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -7,6 +7,7 @@ namespace Benchmarks.Deserialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase { diff --git a/benchmarks/QueryString/QueryStringParserBenchmarks.cs b/benchmarks/QueryString/QueryStringParserBenchmarks.cs index efa4f12659..4218c2e3dc 100644 --- a/benchmarks/QueryString/QueryStringParserBenchmarks.cs +++ b/benchmarks/QueryString/QueryStringParserBenchmarks.cs @@ -1,13 +1,12 @@ using System.ComponentModel.Design; using BenchmarkDotNet.Attributes; +using Benchmarks.Tools; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.QueryStrings.Internal; using JsonApiDotNetCore.Resources; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging.Abstractions; namespace Benchmarks.QueryString; @@ -71,31 +70,9 @@ public void DescendingSort() [Benchmark] public void ComplexQuery() { - Run(100, () => - { - const string queryString = - "?filter[alt-attr-name]=abc,eq:abc&sort=-alt-attr-name&include=child&page[size]=1&fields[alt-resource-name]=alt-attr-name"; - - _queryStringAccessor.SetQueryString(queryString); - _queryStringReader.ReadAll(null); - }); - } - - private void Run(int iterations, Action action) - { - for (int index = 0; index < iterations; index++) - { - action(); - } - } - - private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor - { - public IQueryCollection Query { get; private set; } = new QueryCollection(); + const string queryString = "?filter[alt-attr-name]=abc,eq:abc&sort=-alt-attr-name&include=child&page[size]=1&fields[alt-resource-name]=alt-attr-name"; - public void SetQueryString(string queryString) - { - Query = new QueryCollection(QueryHelpers.ParseQuery(queryString)); - } + _queryStringAccessor.SetQueryString(queryString); + _queryStringReader.ReadAll(null); } } diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 7076ca5cb8..471c9604c7 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -9,6 +9,7 @@ namespace Benchmarks.Serialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class OperationsSerializationBenchmarks : SerializationBenchmarkBase { diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index 12f5c2e788..a985bd5936 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -12,6 +12,7 @@ namespace Benchmarks.Serialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class ResourceSerializationBenchmarks : SerializationBenchmarkBase { diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index e1bcb10843..d9cfefd0b6 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -1,18 +1,14 @@ -using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Serialization; +using Benchmarks.Tools; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Serialization.Response; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; namespace Benchmarks.Serialization; @@ -45,9 +41,9 @@ protected SerializationBenchmarkBase() // ReSharper restore VirtualMemberCallInConstructor var linkBuilder = new FakeLinkBuilder(); - var metaBuilder = new FakeMetaBuilder(); + var metaBuilder = new NoMetaBuilder(); IQueryConstraintProvider[] constraintProviders = Array.Empty(); - var resourceDefinitionAccessor = new FakeResourceDefinitionAccessor(); + var resourceDefinitionAccessor = new NeverResourceDefinitionAccessor(); var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); var requestQueryStringAccessor = new FakeRequestQueryStringAccessor(); @@ -122,141 +118,4 @@ public sealed class OutgoingResource : Identifiable [HasMany] public ISet Multi5 { get; set; } = null!; } - - private sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor - { - public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) - { - return existingIncludes; - } - - public FilterExpression? OnApplyFilter(ResourceType resourceType, FilterExpression? existingFilter) - { - return existingFilter; - } - - public SortExpression? OnApplySort(ResourceType resourceType, SortExpression? existingSort) - { - return existingSort; - } - - public PaginationExpression? OnApplyPagination(ResourceType resourceType, PaginationExpression? existingPagination) - { - return existingPagination; - } - - public SparseFieldSetExpression? OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression? existingSparseFieldSet) - { - return existingSparseFieldSet; - } - - public object? GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) - { - return null; - } - - public IDictionary? GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) - { - return null; - } - - public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable? rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.FromResult(rightResourceId); - } - - public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public void OnDeserialize(IIdentifiable resource) - { - } - - public void OnSerialize(IIdentifiable resource) - { - } - } - - private sealed class FakeLinkBuilder : ILinkBuilder - { - public TopLevelLinks GetTopLevelLinks() - { - return new TopLevelLinks - { - Self = "TopLevel:Self" - }; - } - - public ResourceLinks GetResourceLinks(ResourceType resourceType, IIdentifiable resource) - { - return new ResourceLinks - { - Self = "Resource:Self" - }; - } - - public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) - { - return new RelationshipLinks - { - Self = "Relationship:Self", - Related = "Relationship:Related" - }; - } - } - - private sealed class FakeMetaBuilder : IMetaBuilder - { - public void Add(IDictionary values) - { - } - - public IDictionary? Build() - { - return null; - } - } - - private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor - { - public IQueryCollection Query { get; } = new QueryCollection(0); - } } diff --git a/benchmarks/Tools/FakeLinkBuilder.cs b/benchmarks/Tools/FakeLinkBuilder.cs new file mode 100644 index 0000000000..3468237507 --- /dev/null +++ b/benchmarks/Tools/FakeLinkBuilder.cs @@ -0,0 +1,39 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Response; +using Microsoft.AspNetCore.Http; + +namespace Benchmarks.Tools; + +/// +/// Renders hard-coded fake links, without depending on . +/// +internal sealed class FakeLinkBuilder : ILinkBuilder +{ + public TopLevelLinks GetTopLevelLinks() + { + return new TopLevelLinks + { + Self = "TopLevel:Self" + }; + } + + public ResourceLinks GetResourceLinks(ResourceType resourceType, IIdentifiable resource) + { + return new ResourceLinks + { + Self = "Resource:Self" + }; + } + + public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) + { + return new RelationshipLinks + { + Self = "Relationship:Self", + Related = "Relationship:Related" + }; + } +} diff --git a/benchmarks/Tools/FakeRequestQueryStringAccessor.cs b/benchmarks/Tools/FakeRequestQueryStringAccessor.cs new file mode 100644 index 0000000000..8b2b5540a1 --- /dev/null +++ b/benchmarks/Tools/FakeRequestQueryStringAccessor.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.QueryStrings; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; + +namespace Benchmarks.Tools; + +/// +/// Enables to inject a query string, instead of obtaining it from . +/// +internal sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor +{ + public IQueryCollection Query { get; private set; } = new QueryCollection(); + + public void SetQueryString(string queryString) + { + Query = new QueryCollection(QueryHelpers.ParseQuery(queryString)); + } +} diff --git a/benchmarks/Tools/NeverResourceDefinitionAccessor.cs b/benchmarks/Tools/NeverResourceDefinitionAccessor.cs new file mode 100644 index 0000000000..6e93519dae --- /dev/null +++ b/benchmarks/Tools/NeverResourceDefinitionAccessor.cs @@ -0,0 +1,103 @@ +using System.Collections.Immutable; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace Benchmarks.Tools; + +/// +/// Never calls into instances. +/// +internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor +{ + public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) + { + return existingIncludes; + } + + public FilterExpression? OnApplyFilter(ResourceType resourceType, FilterExpression? existingFilter) + { + return existingFilter; + } + + public SortExpression? OnApplySort(ResourceType resourceType, SortExpression? existingSort) + { + return existingSort; + } + + public PaginationExpression? OnApplyPagination(ResourceType resourceType, PaginationExpression? existingPagination) + { + return existingPagination; + } + + public SparseFieldSetExpression? OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression? existingSparseFieldSet) + { + return existingSparseFieldSet; + } + + public object? GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) + { + return null; + } + + public IDictionary? GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) + { + return null; + } + + public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, + IIdentifiable? rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.FromResult(rightResourceId); + } + + public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public void OnDeserialize(IIdentifiable resource) + { + } + + public void OnSerialize(IIdentifiable resource) + { + } +} diff --git a/benchmarks/Tools/NoMetaBuilder.cs b/benchmarks/Tools/NoMetaBuilder.cs new file mode 100644 index 0000000000..db3ed7857e --- /dev/null +++ b/benchmarks/Tools/NoMetaBuilder.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Serialization.Response; + +namespace Benchmarks.Tools; + +/// +/// Doesn't produce any top-level meta. +/// +internal sealed class NoMetaBuilder : IMetaBuilder +{ + public void Add(IDictionary values) + { + } + + public IDictionary? Build() + { + return null; + } +} From a00ab50528ed6ba3db84d536287450d29b49a92d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Mon, 18 Jul 2022 02:01:56 +0200 Subject: [PATCH 09/14] Use System.Text.Json source generator (see https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator) --- .../DeserializationBenchmarkBase.cs | 6 ++-- .../OperationsDeserializationBenchmarks.cs | 2 +- .../ResourceDeserializationBenchmarks.cs | 2 +- .../OperationsSerializationBenchmarks.cs | 2 +- .../ResourceSerializationBenchmarks.cs | 2 +- .../SerializationBenchmarkBase.cs | 6 ++-- .../Configuration/IJsonApiOptions.cs | 17 +++++++++++ .../Configuration/JsonApiOptions.cs | 23 +++++++++------ .../Middleware/JsonApiMiddleware.cs | 28 ++++++++++--------- .../JsonApiSerializationContext.cs | 17 +++++++++++ .../JsonConverters/JsonObjectConverter.cs | 14 +--------- .../Serialization/Objects/Document.cs | 2 ++ .../Serialization/Request/JsonApiReader.cs | 2 +- .../Serialization/Response/JsonApiWriter.cs | 2 +- .../Response/ResponseModelAdapterTests.cs | 6 ++-- 15 files changed, 82 insertions(+), 49 deletions(-) create mode 100644 src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index bbf746d1a8..80a9753597 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -1,10 +1,10 @@ using System.ComponentModel.Design; -using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.Extensions.Logging.Abstractions; @@ -13,7 +13,7 @@ namespace Benchmarks.Deserialization; public abstract class DeserializationBenchmarkBase { - protected readonly JsonSerializerOptions SerializerReadOptions; + protected readonly JsonApiSerializationContext SerializationReadContext; protected readonly DocumentAdapter DocumentAdapter; protected DeserializationBenchmarkBase() @@ -21,7 +21,7 @@ protected DeserializationBenchmarkBase() var options = new JsonApiOptions(); IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); - SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions; + SerializationReadContext = ((IJsonApiOptions)options).SerializationReadContext; var serviceContainer = new ServiceContainer(); var resourceFactory = new ResourceFactory(serviceContainer); diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs index 99adce73cb..efe0ae568f 100644 --- a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -270,7 +270,7 @@ public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeOperationsRequest() { - var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; + Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index e503a329bb..3d2cdd35af 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -133,7 +133,7 @@ public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeResourceRequest() { - var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; + Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 471c9604c7..2be9da5da6 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -116,7 +116,7 @@ private static IEnumerable CreateResponseOperations(IJsonApi public string SerializeOperationsResponse() { Document responseDocument = ResponseModelAdapter.Convert(_responseOperations); - return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); + return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index a985bd5936..f896846ee2 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -107,7 +107,7 @@ private static OutgoingResource CreateResponseResource() public string SerializeResourceResponse() { Document responseDocument = ResponseModelAdapter.Convert(ResponseResource); - return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); + return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index d9cfefd0b6..be1a711ad9 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using System.Text.Json.Serialization; using Benchmarks.Tools; using JetBrains.Annotations; @@ -8,6 +7,7 @@ using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Response; using Microsoft.Extensions.Logging.Abstractions; @@ -15,7 +15,7 @@ namespace Benchmarks.Serialization; public abstract class SerializationBenchmarkBase { - protected readonly JsonSerializerOptions SerializerWriteOptions; + protected readonly JsonApiSerializationContext SerializationWriteContext; protected readonly IResponseModelAdapter ResponseModelAdapter; protected readonly IResourceGraph ResourceGraph; @@ -33,7 +33,7 @@ protected SerializationBenchmarkBase() }; ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - SerializerWriteOptions = ((IJsonApiOptions)options).SerializerWriteOptions; + SerializationWriteContext = ((IJsonApiOptions)options).SerializationWriteContext; // ReSharper disable VirtualMemberCallInConstructor JsonApiRequest request = CreateJsonApiRequest(ResourceGraph); diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 597d22294d..477500d79b 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,6 +1,8 @@ using System.Data; using System.Text.Json; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Configuration; @@ -8,6 +10,7 @@ namespace JsonApiDotNetCore.Configuration; /// /// Global options that configure the behavior of JsonApiDotNetCore. /// +[PublicAPI] public interface IJsonApiOptions { /// @@ -156,13 +159,27 @@ public interface IJsonApiOptions /// JsonSerializerOptions SerializerOptions { get; } + /// + /// Gets the source-generated JSON serialization context used for deserializing request bodies. This value is based on + /// and is intended for internal use. + /// + JsonApiSerializationContext SerializationReadContext { get; } + /// /// Gets the settings used for deserializing request bodies. This value is based on and is intended for internal use. /// + [Obsolete("Use SerializationReadContext.Options instead.")] JsonSerializerOptions SerializerReadOptions { get; } + /// + /// Gets the source-generated JSON serialization context used for serializing response bodies. This value is based on + /// and is intended for internal use. + /// + JsonApiSerializationContext SerializationWriteContext { get; } + /// /// Gets the settings used for serializing response bodies. This value is based on and is intended for internal use. /// + [Obsolete("Use SerializationWriteContext.Options instead.")] JsonSerializerOptions SerializerWriteOptions { get; } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 2e7cc54282..bb167e4db2 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -3,6 +3,7 @@ using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; namespace JsonApiDotNetCore.Configuration; @@ -11,14 +12,20 @@ namespace JsonApiDotNetCore.Configuration; [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - private readonly Lazy _lazySerializerWriteOptions; - private readonly Lazy _lazySerializerReadOptions; + private readonly Lazy _lazySerializerReadContext; + private readonly Lazy _lazySerializerWriteContext; /// - JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; + JsonApiSerializationContext IJsonApiOptions.SerializationReadContext => _lazySerializerReadContext.Value; /// - JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value; + JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => ((IJsonApiOptions)this).SerializationReadContext.Options; + + /// + JsonApiSerializationContext IJsonApiOptions.SerializationWriteContext => _lazySerializerWriteContext.Value; + + /// + JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => ((IJsonApiOptions)this).SerializationWriteContext.Options; /// public string? Namespace { get; set; } @@ -110,16 +117,16 @@ static JsonApiOptions() public JsonApiOptions() { - _lazySerializerReadOptions = - new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.ExecutionAndPublication); + _lazySerializerReadContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions)), + LazyThreadSafetyMode.ExecutionAndPublication); - _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) + _lazySerializerWriteContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions) { Converters = { new WriteOnlyDocumentConverter(), new WriteOnlyRelationshipObjectConverter() } - }, LazyThreadSafetyMode.ExecutionAndPublication); + }), LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 2e15e6ae9a..6ea3853e92 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -44,7 +45,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializationWriteContext)) { return; } @@ -54,8 +55,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin if (primaryResourceType != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializationWriteContext) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializationWriteContext)) { return; } @@ -66,8 +67,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } else if (IsRouteForOperations(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializationWriteContext) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializationWriteContext)) { return; } @@ -91,11 +92,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } } - private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions) + private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonApiSerializationContext serializationContext) { if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { - await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed) + await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.PreconditionFailed) { Title = "Detection of mid-air edit collisions using ETags is not supported.", Source = new ErrorSource @@ -120,13 +121,14 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso : null; } - private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, JsonSerializerOptions serializerOptions) + private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, + JsonApiSerializationContext serializationContext) { string? contentType = httpContext.Request.ContentType; if (contentType != null && contentType != allowedContentType) { - await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.UnsupportedMediaType) + await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.UnsupportedMediaType) { Title = "The specified Content-Type header value is not supported.", Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' for the Content-Type header value.", @@ -143,7 +145,7 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon } private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, - JsonSerializerOptions serializerOptions) + JsonApiSerializationContext serializationContext) { string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept"); @@ -176,7 +178,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a if (!seenCompatibleMediaType) { - await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.NotAcceptable) + await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.NotAcceptable) { Title = "The specified Accept header value does not contain any supported media types.", Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values.", @@ -192,7 +194,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a return true; } - private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error) + private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiSerializationContext serializationContext, ErrorObject error) { httpResponse.ContentType = HeaderConstants.MediaType; httpResponse.StatusCode = (int)error.StatusCode; @@ -202,7 +204,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri Errors = error.AsList() }; - await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions); + await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializationContext.Document); await httpResponse.Body.FlushAsync(); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs new file mode 100644 index 0000000000..38180d9369 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization; + +// Workaround for https://youtrack.jetbrains.com/issue/RSRP-487028 +partial class JsonApiSerializationContext +{ +} + +/// +/// Provides compile-time metadata about the set of JSON:API types used in JSON serialization of request/response bodies. +/// +[JsonSerializable(typeof(Document))] +public sealed partial class JsonApiSerializationContext : JsonSerializerContext +{ +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs index 32e4351e12..97d7589cc6 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs @@ -7,24 +7,12 @@ public abstract class JsonObjectConverter : JsonConverter { protected static TValue? ReadSubTree(ref Utf8JsonReader reader, JsonSerializerOptions options) { - if (typeof(TValue) != typeof(object) && options.GetConverter(typeof(TValue)) is JsonConverter converter) - { - return converter.Read(ref reader, typeof(TValue), options); - } - return JsonSerializer.Deserialize(ref reader, options); } protected static void WriteSubTree(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options) { - if (typeof(TValue) != typeof(object) && options.GetConverter(typeof(TValue)) is JsonConverter converter) - { - converter.Write(writer, value, options); - } - else - { - JsonSerializer.Serialize(writer, value, options); - } + JsonSerializer.Serialize(writer, value, options); } protected static JsonException GetEndOfStreamError() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index 2f40aeb27b..87c3a0acfa 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,10 +1,12 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects; /// /// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure. /// +[PublicAPI] public sealed class Document { [JsonPropertyName("jsonapi")] diff --git a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs index 0942683487..0282ab39bd 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs @@ -80,7 +80,7 @@ private Document DeserializeDocument(string requestBody) using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - var document = JsonSerializer.Deserialize(requestBody, _options.SerializerReadOptions); + Document? document = JsonSerializer.Deserialize(requestBody, _options.SerializationReadContext.Document); AssertHasDocument(document, requestBody); diff --git a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs index 20f4ad242b..2bfefde98b 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs @@ -125,7 +125,7 @@ private string SerializeDocument(Document document) { using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - return JsonSerializer.Serialize(document, _options.SerializerWriteOptions); + return JsonSerializer.Serialize(document, _options.SerializationWriteContext.Document); } private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent) diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs index d9459f7ec1..f0af041de5 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs @@ -38,7 +38,7 @@ public void Resources_in_deeply_nested_circular_chain_are_written_in_relationshi Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); text.Should().BeJson(@"{ ""data"": { @@ -175,7 +175,7 @@ public void Resources_in_deeply_nested_circular_chains_are_written_in_relationsh }); // Assert - string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); text.Should().BeJson(@"{ ""data"": [ @@ -333,7 +333,7 @@ public void Resources_in_overlapping_deeply_nested_circular_chains_are_written_i Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); text.Should().BeJson(@"{ ""data"": { From f760e8f789b044ca35074b3ca5f84e084c422076 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 21 Aug 2022 15:04:02 +0200 Subject: [PATCH 10/14] Revert "Use System.Text.Json source generator (see https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator)" This reverts commit a00ab50528ed6ba3db84d536287450d29b49a92d. --- .../DeserializationBenchmarkBase.cs | 6 ++-- .../OperationsDeserializationBenchmarks.cs | 2 +- .../ResourceDeserializationBenchmarks.cs | 2 +- .../OperationsSerializationBenchmarks.cs | 2 +- .../ResourceSerializationBenchmarks.cs | 2 +- .../SerializationBenchmarkBase.cs | 6 ++-- .../Configuration/IJsonApiOptions.cs | 17 ----------- .../Configuration/JsonApiOptions.cs | 23 ++++++--------- .../Middleware/JsonApiMiddleware.cs | 28 +++++++++---------- .../JsonApiSerializationContext.cs | 17 ----------- .../JsonConverters/JsonObjectConverter.cs | 14 +++++++++- .../Serialization/Objects/Document.cs | 2 -- .../Serialization/Request/JsonApiReader.cs | 2 +- .../Serialization/Response/JsonApiWriter.cs | 2 +- .../Response/ResponseModelAdapterTests.cs | 6 ++-- 15 files changed, 49 insertions(+), 82 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index 80a9753597..bbf746d1a8 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -1,10 +1,10 @@ using System.ComponentModel.Design; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.Extensions.Logging.Abstractions; @@ -13,7 +13,7 @@ namespace Benchmarks.Deserialization; public abstract class DeserializationBenchmarkBase { - protected readonly JsonApiSerializationContext SerializationReadContext; + protected readonly JsonSerializerOptions SerializerReadOptions; protected readonly DocumentAdapter DocumentAdapter; protected DeserializationBenchmarkBase() @@ -21,7 +21,7 @@ protected DeserializationBenchmarkBase() var options = new JsonApiOptions(); IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); - SerializationReadContext = ((IJsonApiOptions)options).SerializationReadContext; + SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions; var serviceContainer = new ServiceContainer(); var resourceFactory = new ResourceFactory(serviceContainer); diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs index efe0ae568f..99adce73cb 100644 --- a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -270,7 +270,7 @@ public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeOperationsRequest() { - Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; + var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index 3d2cdd35af..e503a329bb 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -133,7 +133,7 @@ public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeResourceRequest() { - Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; + var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 2be9da5da6..471c9604c7 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -116,7 +116,7 @@ private static IEnumerable CreateResponseOperations(IJsonApi public string SerializeOperationsResponse() { Document responseDocument = ResponseModelAdapter.Convert(_responseOperations); - return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); + return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index f896846ee2..a985bd5936 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -107,7 +107,7 @@ private static OutgoingResource CreateResponseResource() public string SerializeResourceResponse() { Document responseDocument = ResponseModelAdapter.Convert(ResponseResource); - return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); + return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index be1a711ad9..d9cfefd0b6 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; using Benchmarks.Tools; using JetBrains.Annotations; @@ -7,7 +8,6 @@ using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Response; using Microsoft.Extensions.Logging.Abstractions; @@ -15,7 +15,7 @@ namespace Benchmarks.Serialization; public abstract class SerializationBenchmarkBase { - protected readonly JsonApiSerializationContext SerializationWriteContext; + protected readonly JsonSerializerOptions SerializerWriteOptions; protected readonly IResponseModelAdapter ResponseModelAdapter; protected readonly IResourceGraph ResourceGraph; @@ -33,7 +33,7 @@ protected SerializationBenchmarkBase() }; ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - SerializationWriteContext = ((IJsonApiOptions)options).SerializationWriteContext; + SerializerWriteOptions = ((IJsonApiOptions)options).SerializerWriteOptions; // ReSharper disable VirtualMemberCallInConstructor JsonApiRequest request = CreateJsonApiRequest(ResourceGraph); diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 477500d79b..597d22294d 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,8 +1,6 @@ using System.Data; using System.Text.Json; -using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Configuration; @@ -10,7 +8,6 @@ namespace JsonApiDotNetCore.Configuration; /// /// Global options that configure the behavior of JsonApiDotNetCore. /// -[PublicAPI] public interface IJsonApiOptions { /// @@ -159,27 +156,13 @@ public interface IJsonApiOptions /// JsonSerializerOptions SerializerOptions { get; } - /// - /// Gets the source-generated JSON serialization context used for deserializing request bodies. This value is based on - /// and is intended for internal use. - /// - JsonApiSerializationContext SerializationReadContext { get; } - /// /// Gets the settings used for deserializing request bodies. This value is based on and is intended for internal use. /// - [Obsolete("Use SerializationReadContext.Options instead.")] JsonSerializerOptions SerializerReadOptions { get; } - /// - /// Gets the source-generated JSON serialization context used for serializing response bodies. This value is based on - /// and is intended for internal use. - /// - JsonApiSerializationContext SerializationWriteContext { get; } - /// /// Gets the settings used for serializing response bodies. This value is based on and is intended for internal use. /// - [Obsolete("Use SerializationWriteContext.Options instead.")] JsonSerializerOptions SerializerWriteOptions { get; } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index bb167e4db2..2e7cc54282 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -3,7 +3,6 @@ using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; namespace JsonApiDotNetCore.Configuration; @@ -12,20 +11,14 @@ namespace JsonApiDotNetCore.Configuration; [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - private readonly Lazy _lazySerializerReadContext; - private readonly Lazy _lazySerializerWriteContext; + private readonly Lazy _lazySerializerWriteOptions; + private readonly Lazy _lazySerializerReadOptions; /// - JsonApiSerializationContext IJsonApiOptions.SerializationReadContext => _lazySerializerReadContext.Value; + JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; /// - JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => ((IJsonApiOptions)this).SerializationReadContext.Options; - - /// - JsonApiSerializationContext IJsonApiOptions.SerializationWriteContext => _lazySerializerWriteContext.Value; - - /// - JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => ((IJsonApiOptions)this).SerializationWriteContext.Options; + JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value; /// public string? Namespace { get; set; } @@ -117,16 +110,16 @@ static JsonApiOptions() public JsonApiOptions() { - _lazySerializerReadContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions)), - LazyThreadSafetyMode.ExecutionAndPublication); + _lazySerializerReadOptions = + new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.ExecutionAndPublication); - _lazySerializerWriteContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions) + _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) { Converters = { new WriteOnlyDocumentConverter(), new WriteOnlyRelationshipObjectConverter() } - }), LazyThreadSafetyMode.ExecutionAndPublication); + }, LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 6ea3853e92..2e15e6ae9a 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -4,7 +4,6 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -45,7 +44,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializationWriteContext)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions)) { return; } @@ -55,8 +54,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin if (primaryResourceType != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializationWriteContext) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializationWriteContext)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) { return; } @@ -67,8 +66,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } else if (IsRouteForOperations(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializationWriteContext) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializationWriteContext)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions)) { return; } @@ -92,11 +91,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } } - private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonApiSerializationContext serializationContext) + private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions) { if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { - await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.PreconditionFailed) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed) { Title = "Detection of mid-air edit collisions using ETags is not supported.", Source = new ErrorSource @@ -121,14 +120,13 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso : null; } - private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, - JsonApiSerializationContext serializationContext) + private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, JsonSerializerOptions serializerOptions) { string? contentType = httpContext.Request.ContentType; if (contentType != null && contentType != allowedContentType) { - await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.UnsupportedMediaType) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.UnsupportedMediaType) { Title = "The specified Content-Type header value is not supported.", Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' for the Content-Type header value.", @@ -145,7 +143,7 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon } private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, - JsonApiSerializationContext serializationContext) + JsonSerializerOptions serializerOptions) { string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept"); @@ -178,7 +176,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a if (!seenCompatibleMediaType) { - await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.NotAcceptable) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.NotAcceptable) { Title = "The specified Accept header value does not contain any supported media types.", Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values.", @@ -194,7 +192,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a return true; } - private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiSerializationContext serializationContext, ErrorObject error) + private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error) { httpResponse.ContentType = HeaderConstants.MediaType; httpResponse.StatusCode = (int)error.StatusCode; @@ -204,7 +202,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiS Errors = error.AsList() }; - await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializationContext.Document); + await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions); await httpResponse.Body.FlushAsync(); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs deleted file mode 100644 index 38180d9369..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Serialization; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization; - -// Workaround for https://youtrack.jetbrains.com/issue/RSRP-487028 -partial class JsonApiSerializationContext -{ -} - -/// -/// Provides compile-time metadata about the set of JSON:API types used in JSON serialization of request/response bodies. -/// -[JsonSerializable(typeof(Document))] -public sealed partial class JsonApiSerializationContext : JsonSerializerContext -{ -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs index 97d7589cc6..32e4351e12 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs @@ -7,12 +7,24 @@ public abstract class JsonObjectConverter : JsonConverter { protected static TValue? ReadSubTree(ref Utf8JsonReader reader, JsonSerializerOptions options) { + if (typeof(TValue) != typeof(object) && options.GetConverter(typeof(TValue)) is JsonConverter converter) + { + return converter.Read(ref reader, typeof(TValue), options); + } + return JsonSerializer.Deserialize(ref reader, options); } protected static void WriteSubTree(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options) { - JsonSerializer.Serialize(writer, value, options); + if (typeof(TValue) != typeof(object) && options.GetConverter(typeof(TValue)) is JsonConverter converter) + { + converter.Write(writer, value, options); + } + else + { + JsonSerializer.Serialize(writer, value, options); + } } protected static JsonException GetEndOfStreamError() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index 87c3a0acfa..2f40aeb27b 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,12 +1,10 @@ using System.Text.Json.Serialization; -using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects; /// /// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure. /// -[PublicAPI] public sealed class Document { [JsonPropertyName("jsonapi")] diff --git a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs index 0282ab39bd..0942683487 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs @@ -80,7 +80,7 @@ private Document DeserializeDocument(string requestBody) using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - Document? document = JsonSerializer.Deserialize(requestBody, _options.SerializationReadContext.Document); + var document = JsonSerializer.Deserialize(requestBody, _options.SerializerReadOptions); AssertHasDocument(document, requestBody); diff --git a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs index 2bfefde98b..20f4ad242b 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs @@ -125,7 +125,7 @@ private string SerializeDocument(Document document) { using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - return JsonSerializer.Serialize(document, _options.SerializationWriteContext.Document); + return JsonSerializer.Serialize(document, _options.SerializerWriteOptions); } private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent) diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs index f0af041de5..d9459f7ec1 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs @@ -38,7 +38,7 @@ public void Resources_in_deeply_nested_circular_chain_are_written_in_relationshi Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); text.Should().BeJson(@"{ ""data"": { @@ -175,7 +175,7 @@ public void Resources_in_deeply_nested_circular_chains_are_written_in_relationsh }); // Assert - string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); text.Should().BeJson(@"{ ""data"": [ @@ -333,7 +333,7 @@ public void Resources_in_overlapping_deeply_nested_circular_chains_are_written_i Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); text.Should().BeJson(@"{ ""data"": { From aedb16496dfa8f4e8861e0bac6cc9512bdbd7e31 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 3 Sep 2022 17:40:09 +0200 Subject: [PATCH 11/14] Package updates --- Directory.Build.props | 8 ++++---- benchmarks/Benchmarks.csproj | 7 +++++-- test/TestBuildingBlocks/TestBuildingBlocks.csproj | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 515d7de1ba..380996c725 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ 6.0.* 6.0.* 6.0.* - 4.2.* + 4.3.* 2.14.1 5.0.3 $(MSBuildThisFileDirectory)CodingGuidelines.ruleset @@ -17,7 +17,7 @@ - + @@ -34,7 +34,7 @@ 3.1.2 - 4.18.1 - 17.2.0 + 4.18.2 + 17.3.1 diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index f461a4831b..3958713af4 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -1,4 +1,4 @@ - + Exe $(TargetFrameworkName) @@ -10,6 +10,9 @@ - + + + + diff --git a/test/TestBuildingBlocks/TestBuildingBlocks.csproj b/test/TestBuildingBlocks/TestBuildingBlocks.csproj index 2f383d72e9..ed335f630f 100644 --- a/test/TestBuildingBlocks/TestBuildingBlocks.csproj +++ b/test/TestBuildingBlocks/TestBuildingBlocks.csproj @@ -16,7 +16,7 @@ - + From e4020c629b7165709c5b2bbf598b819f0a68918d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Wed, 7 Sep 2022 10:44:48 +0200 Subject: [PATCH 12/14] Increment version number (used for pre-release builds from ci) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 380996c725..3a3d018a65 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ 6.0.* 4.3.* 2.14.1 - 5.0.3 + 5.0.4 $(MSBuildThisFileDirectory)CodingGuidelines.ruleset 9999 enable From 0413d6dcb5c98dfeb6a11daeaf190263dccf07f1 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Wed, 7 Sep 2022 10:53:10 +0200 Subject: [PATCH 13/14] Fix build warning: duplicate package reference --- test/OpenApiClientTests/OpenApiClientTests.csproj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index 3f54c161b8..2c49707c81 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,10 +27,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - From 47320f0e7732c42b8a6b71051234ca6beed96a3a Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Wed, 7 Sep 2022 11:19:33 +0200 Subject: [PATCH 14/14] Package updates, centralize versions, address breaking changes --- Directory.Build.props | 5 +- .../JsonApiDotNetCoreExampleClient.csproj | 6 +- .../JsonApiSchemaGenerator.cs | 16 ++-- .../LegacyClient/swagger.g.json | 82 +++++++++++------- .../CamelCase/swagger.g.json | 33 ++++++-- .../KebabCase/swagger.g.json | 33 ++++++-- .../PascalCase/swagger.g.json | 33 ++++++-- .../OpenApiClientTests.csproj | 6 +- .../LegacyOpenApiIntegration/swagger.json | 84 ++++++++++++------- test/OpenApiTests/OpenApiTests.csproj | 2 +- 10 files changed, 199 insertions(+), 101 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 92c2d18362..b10fb859f3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,10 @@ 6.0.* 4.3.* 2.14.1 - 6.2.* + 6.4.* + 13.16.* + 6.0.* + 13.0.* 5.0.4 $(MSBuildThisFileDirectory)CodingGuidelines.ruleset 9999 diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj index 17a8f6b436..fc2ebfd8c8 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj +++ b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj @@ -9,12 +9,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index a0e7159a89..e0ad6da30c 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; +using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -42,29 +43,30 @@ public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceG _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor); } - public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository, MemberInfo? memberInfo = null, ParameterInfo? parameterInfo = null) + public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepository, MemberInfo? memberInfo = null, ParameterInfo? parameterInfo = null, + ApiParameterRouteInfo? routeInfo = null) { - ArgumentGuard.NotNull(type, nameof(type)); + ArgumentGuard.NotNull(modelType, nameof(modelType)); ArgumentGuard.NotNull(schemaRepository, nameof(schemaRepository)); _schemaRepositoryAccessor.Current = schemaRepository; - if (schemaRepository.TryLookupByType(type, out OpenApiSchema jsonApiDocumentSchema)) + if (schemaRepository.TryLookupByType(modelType, out OpenApiSchema jsonApiDocumentSchema)) { return jsonApiDocumentSchema; } - if (IsJsonApiDocument(type)) + if (IsJsonApiDocument(modelType)) { - OpenApiSchema schema = GenerateJsonApiDocumentSchema(type); + OpenApiSchema schema = GenerateJsonApiDocumentSchema(modelType); - if (IsDataPropertyNullable(type)) + if (IsDataPropertyNullable(modelType)) { SetDataObjectSchemaToNullable(schema); } } - return _defaultSchemaGenerator.GenerateSchema(type, schemaRepository, memberInfo, parameterInfo); + return _defaultSchemaGenerator.GenerateSchema(modelType, schemaRepository, memberInfo, parameterInfo); } private static bool IsJsonApiDocument(Type type) diff --git a/test/OpenApiClientTests/LegacyClient/swagger.g.json b/test/OpenApiClientTests/LegacyClient/swagger.g.json index 03575fe1d2..f95214e8d4 100644 --- a/test/OpenApiClientTests/LegacyClient/swagger.g.json +++ b/test/OpenApiClientTests/LegacyClient/swagger.g.json @@ -58,7 +58,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -68,7 +68,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -166,7 +166,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -187,7 +187,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -333,7 +333,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -363,7 +363,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -393,7 +393,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -451,7 +451,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -461,7 +461,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -559,7 +559,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -580,7 +580,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -726,7 +726,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -756,7 +756,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -786,7 +786,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -932,7 +932,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -962,7 +962,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -992,7 +992,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1050,7 +1050,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -1060,7 +1060,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -1158,7 +1158,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1179,7 +1179,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1325,7 +1325,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1471,7 +1471,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1501,7 +1501,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1531,7 +1531,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1677,7 +1677,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1707,7 +1707,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1737,7 +1737,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1883,7 +1883,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -2046,6 +2046,7 @@ "$ref": "#/components/schemas/airplane-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2087,6 +2088,7 @@ "$ref": "#/components/schemas/airplane-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2197,6 +2199,7 @@ "type": "object", "properties": { "email-address": { + "minLength": 1, "type": "string", "format": "email" }, @@ -2217,6 +2220,7 @@ "type": "object", "properties": { "email-address": { + "minLength": 1, "type": "string", "format": "email" }, @@ -2237,6 +2241,7 @@ "type": "object", "properties": { "email-address": { + "minLength": 1, "type": "string", "format": "email" }, @@ -2294,6 +2299,7 @@ "$ref": "#/components/schemas/flight-attendant-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2335,6 +2341,7 @@ "$ref": "#/components/schemas/flight-attendant-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2364,6 +2371,7 @@ "$ref": "#/components/schemas/flight-attendant-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, @@ -2619,6 +2627,7 @@ "$ref": "#/components/schemas/flight-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2657,6 +2666,7 @@ "$ref": "#/components/schemas/flight-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2686,6 +2696,7 @@ "$ref": "#/components/schemas/flight-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, @@ -2860,9 +2871,11 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -2876,12 +2889,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -2903,6 +2918,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { @@ -2920,15 +2936,18 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -2951,12 +2970,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -2969,6 +2990,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" } }, @@ -3153,6 +3175,7 @@ "$ref": "#/components/schemas/passenger-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -3179,6 +3202,7 @@ "$ref": "#/components/schemas/passenger-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, diff --git a/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json index 2eaac9303c..d2850c032d 100644 --- a/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json @@ -58,7 +58,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -68,7 +68,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -169,7 +169,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -191,7 +191,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -342,7 +342,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -493,7 +493,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -524,7 +524,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -555,7 +555,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -706,7 +706,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -747,9 +747,11 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -763,12 +765,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -790,6 +794,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { @@ -807,15 +812,18 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -838,12 +846,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -856,6 +866,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" } }, @@ -1040,6 +1051,7 @@ "$ref": "#/components/schemas/staffMemberResourceType" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -1066,6 +1078,7 @@ "$ref": "#/components/schemas/staffMemberResourceType" }, "id": { + "minLength": 1, "type": "string" } }, @@ -1225,6 +1238,7 @@ "$ref": "#/components/schemas/supermarketResourceType" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -1266,6 +1280,7 @@ "$ref": "#/components/schemas/supermarketResourceType" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { diff --git a/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json index 1013e900a8..1d7feb51f0 100644 --- a/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json @@ -58,7 +58,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -68,7 +68,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -169,7 +169,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -191,7 +191,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -342,7 +342,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -493,7 +493,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -524,7 +524,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -555,7 +555,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -706,7 +706,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -747,9 +747,11 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -763,12 +765,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -790,6 +794,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { @@ -807,15 +812,18 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -838,12 +846,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -856,6 +866,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" } }, @@ -1040,6 +1051,7 @@ "$ref": "#/components/schemas/staff-member-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -1066,6 +1078,7 @@ "$ref": "#/components/schemas/staff-member-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, @@ -1225,6 +1238,7 @@ "$ref": "#/components/schemas/supermarket-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -1266,6 +1280,7 @@ "$ref": "#/components/schemas/supermarket-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { diff --git a/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json index 3906897de4..fa5041afad 100644 --- a/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json @@ -58,7 +58,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -68,7 +68,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -169,7 +169,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -191,7 +191,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -342,7 +342,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -493,7 +493,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -524,7 +524,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -555,7 +555,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -706,7 +706,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -747,9 +747,11 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -763,12 +765,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -790,6 +794,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { @@ -807,15 +812,18 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -838,12 +846,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -856,6 +866,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" } }, @@ -1040,6 +1051,7 @@ "$ref": "#/components/schemas/StaffMemberResourceType" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -1066,6 +1078,7 @@ "$ref": "#/components/schemas/StaffMemberResourceType" }, "id": { + "minLength": 1, "type": "string" } }, @@ -1225,6 +1238,7 @@ "$ref": "#/components/schemas/SupermarketResourceType" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -1266,6 +1280,7 @@ "$ref": "#/components/schemas/SupermarketResourceType" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index 2c49707c81..41e6fe07e1 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -18,12 +18,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 3fb80fb856..4b6c140fc4 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -58,7 +58,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -68,7 +68,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -166,7 +166,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -187,7 +187,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -333,7 +333,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -363,7 +363,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -393,7 +393,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -451,7 +451,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -461,7 +461,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -559,7 +559,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -580,7 +580,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -726,7 +726,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -756,7 +756,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -786,7 +786,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -932,7 +932,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -962,7 +962,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -992,7 +992,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1050,7 +1050,7 @@ }, "responses": { "201": { - "description": "Success", + "description": "Created", "content": { "application/vnd.api+json": { "schema": { @@ -1060,7 +1060,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } } @@ -1158,7 +1158,7 @@ } }, "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1179,7 +1179,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1325,7 +1325,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1471,7 +1471,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1501,7 +1501,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1531,7 +1531,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1677,7 +1677,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1707,7 +1707,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } }, @@ -1737,7 +1737,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -1883,7 +1883,7 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -2046,6 +2046,7 @@ "$ref": "#/components/schemas/airplane-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2087,6 +2088,7 @@ "$ref": "#/components/schemas/airplane-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2197,6 +2199,7 @@ "type": "object", "properties": { "email-address": { + "minLength": 1, "type": "string", "format": "email" }, @@ -2217,6 +2220,7 @@ "type": "object", "properties": { "email-address": { + "minLength": 1, "type": "string", "format": "email" }, @@ -2237,6 +2241,7 @@ "type": "object", "properties": { "email-address": { + "minLength": 1, "type": "string", "format": "email" }, @@ -2294,6 +2299,7 @@ "$ref": "#/components/schemas/flight-attendant-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2335,6 +2341,7 @@ "$ref": "#/components/schemas/flight-attendant-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2364,6 +2371,7 @@ "$ref": "#/components/schemas/flight-attendant-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, @@ -2619,6 +2627,7 @@ "$ref": "#/components/schemas/flight-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2657,6 +2666,7 @@ "$ref": "#/components/schemas/flight-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -2686,6 +2696,7 @@ "$ref": "#/components/schemas/flight-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, @@ -2860,9 +2871,11 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -2876,12 +2889,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -2903,6 +2918,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { @@ -2920,15 +2936,18 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" }, "first": { + "minLength": 1, "type": "string" }, "last": { @@ -2951,12 +2970,14 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" }, "describedby": { "type": "string" }, "related": { + "minLength": 1, "type": "string" } }, @@ -2969,6 +2990,7 @@ "type": "object", "properties": { "self": { + "minLength": 1, "type": "string" } }, @@ -3153,6 +3175,7 @@ "$ref": "#/components/schemas/passenger-resource-type" }, "id": { + "minLength": 1, "type": "string" }, "attributes": { @@ -3179,6 +3202,7 @@ "$ref": "#/components/schemas/passenger-resource-type" }, "id": { + "minLength": 1, "type": "string" } }, @@ -3360,4 +3384,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index f5a24feec9..50b96c1d36 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -23,6 +23,6 @@ - +