diff --git a/.editorconfig b/.editorconfig
index 5a036d1797..37fda299fb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,60 +4,119 @@ root = true
[*]
indent_style = space
indent_size = 4
+tab-width = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
-[*.{config,csproj,css,js,json,props,ruleset,xslt,html}]
+[*.{config,csproj,css,js,json,props,targets,xml,ruleset,xsd,xslt,html,yml,yaml}]
indent_size = 2
+tab-width = 2
+max_line_length = 160
+
+[*.{cs,cshtml,ascx,aspx}]
-[*.{cs}]
#### C#/.NET Coding Conventions ####
+# Default severity for IDE* analyzers with category 'Style'
+# Note: specific rules below use severity silent, because Resharper code cleanup auto-fixes them.
+dotnet_analyzer_diagnostic.category-Style.severity = warning
+
# 'using' directive preferences
dotnet_sort_system_directives_first = true
-csharp_using_directive_placement = outside_namespace:suggestion
+csharp_using_directive_placement = outside_namespace:silent
+# IDE0005: Remove unnecessary import
+dotnet_diagnostic.IDE0005.severity = silent
# Namespace declarations
-csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_style_namespace_declarations = file_scoped:silent
+# IDE0160: Use block-scoped namespace
+dotnet_diagnostic.IDE0160.severity = silent
+# IDE0161: Use file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = silent
# this. preferences
-dotnet_style_qualification_for_field = false:suggestion
-dotnet_style_qualification_for_property = false:suggestion
-dotnet_style_qualification_for_method = false:suggestion
-dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+# IDE0003: Remove this or Me qualification
+dotnet_diagnostic.IDE0003.severity = silent
+# IDE0009: Add this or Me qualification
+dotnet_diagnostic.IDE0009.severity = silent
# Language keywords vs BCL types preferences
-dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
-dotnet_style_predefined_type_for_member_access = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+# IDE0049: Use language keywords instead of framework type names for type references
+dotnet_diagnostic.IDE0049.severity = silent
# Modifier preferences
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
-csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion
-csharp_style_pattern_local_over_anonymous_function = false:silent
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+# IDE0040: Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = silent
+csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:silent
+# IDE0036: Order modifiers
+dotnet_diagnostic.IDE0036.severity = silent
# Expression-level preferences
dotnet_style_operator_placement_when_wrapping = end_of_line
-dotnet_style_prefer_auto_properties = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
-dotnet_style_prefer_conditional_expression_over_return = true:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+# IDE0032: Use auto property
+dotnet_diagnostic.IDE0032.severity = silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+# IDE0045: Use conditional expression for assignment
+dotnet_diagnostic.IDE0045.severity = silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+# IDE0046: Use conditional expression for return
+dotnet_diagnostic.IDE0046.severity = silent
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+# IDE0058: Remove unused expression value
+dotnet_diagnostic.IDE0058.severity = silent
+
+# Collection expression preferences (note: partially turned off in Directory.Build.props)
+dotnet_style_prefer_collection_expression = when_types_exactly_match
# Parameter preferences
-dotnet_code_quality_unused_parameters = non_public:suggestion
+dotnet_code_quality_unused_parameters = non_public
+
+# Local functions vs lambdas
+csharp_style_prefer_local_over_anonymous_function = false:silent
+# IDE0039: Use local function instead of lambda
+dotnet_diagnostic.IDE0039.severity = silent
# Expression-bodied members
-csharp_style_expression_bodied_accessors = true:suggestion
-csharp_style_expression_bodied_constructors = false:suggestion
-csharp_style_expression_bodied_indexers = true:suggestion
-csharp_style_expression_bodied_lambdas = true:suggestion
-csharp_style_expression_bodied_local_functions = false:suggestion
-csharp_style_expression_bodied_methods = false:suggestion
-csharp_style_expression_bodied_operators = false:suggestion
-csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_accessors = true:silent
+# IDE0027: Use expression body for accessors
+dotnet_diagnostic.IDE0027.severity = silent
+csharp_style_expression_bodied_constructors = false:silent
+# IDE0021: Use expression body for constructors
+dotnet_diagnostic.IDE0021.severity = silent
+csharp_style_expression_bodied_indexers = true:silent
+# IDE0026: Use expression body for indexers
+dotnet_diagnostic.IDE0026.severity = silent
+csharp_style_expression_bodied_lambdas = true:silent
+# IDE0053: Use expression body for lambdas
+dotnet_diagnostic.IDE0053.severity = silent
+csharp_style_expression_bodied_local_functions = false:silent
+# IDE0061: Use expression body for local functions
+dotnet_diagnostic.IDE0061.severity = silent
+csharp_style_expression_bodied_methods = false:silent
+# IDE0022: Use expression body for methods
+dotnet_diagnostic.IDE0022.severity = silent
+csharp_style_expression_bodied_operators = false:silent
+# IDE0023: Use expression body for conversion operators
+dotnet_diagnostic.IDE0023.severity = silent
+# IDE0024: Use expression body for operators
+dotnet_diagnostic.IDE0024.severity = silent
+csharp_style_expression_bodied_properties = true:silent
+# IDE0025: Use expression body for properties
+dotnet_diagnostic.IDE0025.severity = silent
# Code-block preferences
-csharp_prefer_braces = true:suggestion
+csharp_prefer_braces = true:silent
+# IDE0011: Add braces
+dotnet_diagnostic.IDE0011.severity = silent
# Indentation preferences
csharp_indent_case_contents_when_block = false
@@ -66,19 +125,42 @@ csharp_indent_case_contents_when_block = false
csharp_preserve_single_line_statements = false
# 'var' usage preferences
-csharp_style_var_for_built_in_types = false:none
-csharp_style_var_when_type_is_apparent = true:none
-csharp_style_var_elsewhere = false:none
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = true:silent
+csharp_style_var_elsewhere = false:silent
+# IDE0007: Use var instead of explicit type
+dotnet_diagnostic.IDE0007.severity = silent
+# IDE0008: Use explicit type instead of var
+dotnet_diagnostic.IDE0008.severity = silent
# Parentheses preferences
-dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
-dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion
-
-# Expression value is never used
-dotnet_diagnostic.IDE0058.severity = none
-
-#### Naming Style ####
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent
+# IDE0047: Remove unnecessary parentheses
+dotnet_diagnostic.IDE0047.severity = silent
+# IDE0048: Add parentheses for clarity
+dotnet_diagnostic.IDE0048.severity = silent
+
+# IDE0010: Add missing cases to switch statement
+dotnet_diagnostic.IDE0010.severity = silent
+# IDE0072: Add missing cases to switch expression
+dotnet_diagnostic.IDE0072.severity = silent
+
+# IDE0029: Null check can be simplified
+dotnet_diagnostic.IDE0029.severity = silent
+# IDE0030: Null check can be simplified
+dotnet_diagnostic.IDE0030.severity = silent
+# IDE0270: Null check can be simplified
+dotnet_diagnostic.IDE0270.severity = silent
+
+# JSON002: Probable JSON string detected
+dotnet_diagnostic.JSON002.severity = silent
+
+# CA1062: Validate arguments of public methods
+dotnet_code_quality.CA1062.excluded_symbol_names = Accept|DefaultVisit|Visit*|Apply*
+
+#### .NET Naming Style ####
dotnet_diagnostic.IDE1006.severity = warning
diff --git a/CodingGuidelines.ruleset b/CodingGuidelines.ruleset
index e647ad9e58..b29d7423b4 100644
--- a/CodingGuidelines.ruleset
+++ b/CodingGuidelines.ruleset
@@ -1,32 +1,54 @@
-
+
+
+
-
-
-
-
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 375ee5066f..b06480623f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,4 +1,29 @@
+
+ enable
+ latest
+ enable
+ false
+ false
+ true
+ Recommended
+ $(MSBuildThisFileDirectory)CodingGuidelines.ruleset
+ $(MSBuildThisFileDirectory)tests.runsettings
+ 5.6.1
+
+
+
+
+ IDE0028;IDE0300;IDE0301;IDE0302;IDE0303;IDE0304;IDE0305
+ $(NoWarn);$(UseCollectionExpressionRules)
+
+
$(NoWarn);AV2210
@@ -13,20 +38,23 @@
true
+
+ $(NoWarn);CA1707;CA1062
+
+
+
+ $(NoWarn);CA1062
+
+
+
+
+ $(NoWarn);SYSLIB1006
+
+
-
-
- enable
- latest
- enable
- false
- false
- $(MSBuildThisFileDirectory)CodingGuidelines.ruleset
- $(MSBuildThisFileDirectory)tests.runsettings
- 5.6.1
-
diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs
index bbf746d1a8..4febabba1a 100644
--- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs
+++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs
@@ -11,10 +11,12 @@
namespace Benchmarks.Deserialization;
-public abstract class DeserializationBenchmarkBase
+public abstract class DeserializationBenchmarkBase : IDisposable
{
- protected readonly JsonSerializerOptions SerializerReadOptions;
- protected readonly DocumentAdapter DocumentAdapter;
+ private readonly ServiceContainer _serviceProvider = new();
+
+ protected JsonSerializerOptions SerializerReadOptions { get; }
+ protected DocumentAdapter DocumentAdapter { get; }
protected DeserializationBenchmarkBase()
{
@@ -23,12 +25,11 @@ protected DeserializationBenchmarkBase()
options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions;
- var serviceContainer = new ServiceContainer();
- var resourceFactory = new ResourceFactory(serviceContainer);
- var resourceDefinitionAccessor = new ResourceDefinitionAccessor(resourceGraph, serviceContainer);
+ var resourceFactory = new ResourceFactory(_serviceProvider);
+ var resourceDefinitionAccessor = new ResourceDefinitionAccessor(resourceGraph, _serviceProvider);
- serviceContainer.AddService(typeof(IResourceDefinitionAccessor), resourceDefinitionAccessor);
- serviceContainer.AddService(typeof(IResourceDefinition), new JsonApiResourceDefinition(resourceGraph));
+ _serviceProvider.AddService(typeof(IResourceDefinitionAccessor), resourceDefinitionAccessor);
+ _serviceProvider.AddService(typeof(IResourceDefinition), new JsonApiResourceDefinition(resourceGraph));
// ReSharper disable once VirtualMemberCallInConstructor
JsonApiRequest request = CreateJsonApiRequest(resourceGraph);
@@ -53,6 +54,22 @@ protected DeserializationBenchmarkBase()
protected abstract JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph);
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+#pragma warning disable CA1063 // Implement IDisposable Correctly
+ private void Dispose(bool disposing)
+#pragma warning restore CA1063 // Implement IDisposable Correctly
+ {
+ if (disposing)
+ {
+ _serviceProvider.Dispose();
+ }
+ }
+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class IncomingResource : Identifiable
{
diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs
index b1e3307931..04d5fa1eaa 100644
--- a/benchmarks/Program.cs
+++ b/benchmarks/Program.cs
@@ -3,20 +3,12 @@
using Benchmarks.QueryString;
using Benchmarks.Serialization;
-namespace Benchmarks;
+var switcher = new BenchmarkSwitcher([
+ typeof(ResourceDeserializationBenchmarks),
+ typeof(OperationsDeserializationBenchmarks),
+ typeof(ResourceSerializationBenchmarks),
+ typeof(OperationsSerializationBenchmarks),
+ typeof(QueryStringParserBenchmarks)
+]);
-internal static class Program
-{
- private static void Main(string[] args)
- {
- var switcher = new BenchmarkSwitcher([
- typeof(ResourceDeserializationBenchmarks),
- typeof(OperationsDeserializationBenchmarks),
- typeof(ResourceSerializationBenchmarks),
- typeof(OperationsSerializationBenchmarks),
- typeof(QueryStringParserBenchmarks)
- ]);
-
- switcher.Run(args);
- }
-}
+switcher.Run(args);
diff --git a/benchmarks/QueryString/QueryStringParserBenchmarks.cs b/benchmarks/QueryString/QueryStringParserBenchmarks.cs
index 0b2f88134a..548c08d532 100644
--- a/benchmarks/QueryString/QueryStringParserBenchmarks.cs
+++ b/benchmarks/QueryString/QueryStringParserBenchmarks.cs
@@ -14,8 +14,9 @@ namespace Benchmarks.QueryString;
[MarkdownExporter]
[SimpleJob(3, 10, 20)]
[MemoryDiagnoser]
-public class QueryStringParserBenchmarks
+public class QueryStringParserBenchmarks : IDisposable
{
+ private readonly ServiceContainer _serviceProvider = new();
private readonly FakeRequestQueryStringAccessor _queryStringAccessor = new();
private readonly QueryStringReader _queryStringReader;
@@ -34,7 +35,7 @@ public QueryStringParserBenchmarks()
IsCollection = true
};
- var resourceFactory = new ResourceFactory(new ServiceContainer());
+ var resourceFactory = new ResourceFactory(_serviceProvider);
var includeParser = new IncludeParser(options);
var includeReader = new IncludeQueryStringParameterReader(includeParser, request, resourceGraph);
@@ -92,4 +93,20 @@ public void ComplexQuery()
_queryStringAccessor.SetQueryString(queryString);
_queryStringReader.ReadAll(null);
}
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+#pragma warning disable CA1063 // Implement IDisposable Correctly
+ private void Dispose(bool disposing)
+#pragma warning restore CA1063 // Implement IDisposable Correctly
+ {
+ if (disposing)
+ {
+ _serviceProvider.Dispose();
+ }
+ }
}
diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs
index 458c4eecae..8c4a00b6da 100644
--- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs
+++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs
@@ -13,7 +13,7 @@ namespace Benchmarks.Serialization;
// ReSharper disable once ClassCanBeSealed.Global
public class OperationsSerializationBenchmarks : SerializationBenchmarkBase
{
- private readonly IEnumerable _responseOperations;
+ private readonly List _responseOperations;
public OperationsSerializationBenchmarks()
{
@@ -23,7 +23,7 @@ public OperationsSerializationBenchmarks()
_responseOperations = CreateResponseOperations(request);
}
- private static IEnumerable CreateResponseOperations(IJsonApiRequest request)
+ private static List CreateResponseOperations(IJsonApiRequest request)
{
var resource1 = new OutgoingResource
{
@@ -102,14 +102,14 @@ private static IEnumerable CreateResponseOperations(IJsonApi
var targetedFields = new TargetedFields();
- return new List
- {
- new(resource1, targetedFields, request),
- new(resource2, targetedFields, request),
- new(resource3, targetedFields, request),
- new(resource4, targetedFields, request),
- new(resource5, targetedFields, request)
- };
+ return
+ [
+ new OperationContainer(resource1, targetedFields, request),
+ new OperationContainer(resource2, targetedFields, request),
+ new OperationContainer(resource3, targetedFields, request),
+ new OperationContainer(resource4, targetedFields, request),
+ new OperationContainer(resource5, targetedFields, request)
+ ];
}
[Benchmark]
diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs
index a2d76b87b1..6f979e86b9 100644
--- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs
+++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs
@@ -1,7 +1,6 @@
using System.Collections.Immutable;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
-using JsonApiDotNetCore;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
@@ -97,12 +96,17 @@ private static OutgoingResource CreateResponseResource()
resource1.Single2 = resource2;
resource2.Single3 = resource3;
- resource3.Multi4 = resource4.AsHashSet();
- resource4.Multi5 = resource5.AsHashSet();
+ resource3.Multi4 = ToHashSet(resource4);
+ resource4.Multi5 = ToHashSet(resource5);
return resource1;
}
+ private static HashSet ToHashSet(T element)
+ {
+ return [element];
+ }
+
[Benchmark]
public string SerializeResourceResponse()
{
diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs
index eba222c9d1..c8451835cc 100644
--- a/benchmarks/Serialization/SerializationBenchmarkBase.cs
+++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs
@@ -14,9 +14,9 @@ namespace Benchmarks.Serialization;
public abstract class SerializationBenchmarkBase
{
- protected readonly JsonSerializerOptions SerializerWriteOptions;
- protected readonly IResponseModelAdapter ResponseModelAdapter;
- protected readonly IResourceGraph ResourceGraph;
+ protected JsonSerializerOptions SerializerWriteOptions { get; }
+ protected IResponseModelAdapter ResponseModelAdapter { get; }
+ protected IResourceGraph ResourceGraph { get; }
protected SerializationBenchmarkBase()
{
diff --git a/src/Examples/DapperExample/Program.cs b/src/Examples/DapperExample/Program.cs
index 00ab54ca97..f7bf198af9 100644
--- a/src/Examples/DapperExample/Program.cs
+++ b/src/Examples/DapperExample/Program.cs
@@ -86,7 +86,7 @@
await CreateDatabaseAsync(app.Services);
-app.Run();
+await app.RunAsync();
static DatabaseProvider GetDatabaseProvider(IConfiguration configuration)
{
diff --git a/src/Examples/DapperExample/Repositories/DapperFacade.cs b/src/Examples/DapperExample/Repositories/DapperFacade.cs
index 4d30e430c7..feb0a88e06 100644
--- a/src/Examples/DapperExample/Repositories/DapperFacade.cs
+++ b/src/Examples/DapperExample/Repositories/DapperFacade.cs
@@ -73,7 +73,7 @@ public IReadOnlyCollection BuildSqlCommandsForOneToOneRelatio
}
}
- return sqlCommands;
+ return sqlCommands.AsReadOnly();
}
public IReadOnlyCollection BuildSqlCommandsForChangedRelationshipsHavingForeignKeyAtRightSide(ResourceChangeDetector changeDetector,
@@ -107,20 +107,20 @@ public IReadOnlyCollection BuildSqlCommandsForChangedRelation
object[] rightIdsToRemove = currentRightIds.Except(newRightIds).ToArray();
object[] rightIdsToAdd = newRightIds.Except(currentRightIds).ToArray();
- if (rightIdsToRemove.Any())
+ if (rightIdsToRemove.Length > 0)
{
CommandDefinition sqlCommand = BuildSqlCommandForRemoveFromToMany(foreignKey, rightIdsToRemove, cancellationToken);
sqlCommands.Add(sqlCommand);
}
- if (rightIdsToAdd.Any())
+ if (rightIdsToAdd.Length > 0)
{
CommandDefinition sqlCommand = BuildSqlCommandForAddToToMany(foreignKey, leftId!, rightIdsToAdd, cancellationToken);
sqlCommands.Add(sqlCommand);
}
}
- return sqlCommands;
+ return sqlCommands.AsReadOnly();
}
public CommandDefinition BuildSqlCommandForRemoveFromToMany(RelationshipForeignKey foreignKey, object[] rightResourceIdValues,
@@ -180,7 +180,7 @@ public CommandDefinition BuildSqlCommandForCreate(ResourceChangeDetector changeD
IReadOnlyDictionary columnsToUpdate = changeDetector.GetChangedColumnValues();
- if (columnsToUpdate.Any())
+ if (columnsToUpdate.Count > 0)
{
var updateBuilder = new UpdateResourceStatementBuilder(_dataModelService);
UpdateNode updateNode = updateBuilder.Build(changeDetector.ResourceType, columnsToUpdate, leftId!);
diff --git a/src/Examples/DapperExample/Repositories/DapperRepository.cs b/src/Examples/DapperExample/Repositories/DapperRepository.cs
index 0c54b34353..23e9806139 100644
--- a/src/Examples/DapperExample/Repositories/DapperRepository.cs
+++ b/src/Examples/DapperExample/Repositories/DapperRepository.cs
@@ -1,3 +1,4 @@
+using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using Dapper;
@@ -90,7 +91,7 @@ namespace DapperExample.Repositories;
///
///
///
-public sealed class DapperRepository : IResourceRepository, IRepositorySupportsTransaction
+public sealed partial class DapperRepository : IResourceRepository, IRepositorySupportsTransaction
where TResource : class, IIdentifiable
{
private readonly ITargetedFields _targetedFields;
@@ -216,7 +217,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected > 1)
{
- throw new DataStoreUpdateException(new Exception("Multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Multiple rows found."));
}
}
@@ -233,7 +234,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected == 0)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist."));
}
}
}, cancellationToken);
@@ -313,7 +314,7 @@ public async Task UpdateAsync(TResource resourceFromRequest, TResource resourceF
IReadOnlyCollection postSqlCommands =
_dapperFacade.BuildSqlCommandsForChangedRelationshipsHavingForeignKeyAtRightSide(changeDetector, resourceFromDatabase.Id, cancellationToken);
- if (preSqlCommands.Any() || updateCommand != null || postSqlCommands.Any())
+ if (preSqlCommands.Count > 0 || updateCommand != null || postSqlCommands.Count > 0)
{
await ExecuteInTransactionAsync(async transaction =>
{
@@ -324,7 +325,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected > 1)
{
- throw new DataStoreUpdateException(new Exception("Multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Multiple rows found."));
}
}
@@ -335,7 +336,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected != 1)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist or multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist or multiple rows found."));
}
}
@@ -346,7 +347,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected == 0)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist."));
}
}
}, cancellationToken);
@@ -374,7 +375,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected != 1)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist or multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist or multiple rows found."));
}
}, cancellationToken);
@@ -409,7 +410,7 @@ public async Task SetRelationshipAsync(TResource leftResource, object? rightValu
IReadOnlyCollection postSqlCommands =
_dapperFacade.BuildSqlCommandsForChangedRelationshipsHavingForeignKeyAtRightSide(changeDetector, leftResource.Id, cancellationToken);
- if (preSqlCommands.Any() || updateCommand != null || postSqlCommands.Any())
+ if (preSqlCommands.Count > 0 || updateCommand != null || postSqlCommands.Count > 0)
{
await ExecuteInTransactionAsync(async transaction =>
{
@@ -420,7 +421,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected > 1)
{
- throw new DataStoreUpdateException(new Exception("Multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Multiple rows found."));
}
}
@@ -431,7 +432,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected != 1)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist or multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist or multiple rows found."));
}
}
@@ -442,7 +443,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected == 0)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist."));
}
}
}, cancellationToken);
@@ -467,7 +468,7 @@ public async Task AddToToManyRelationshipAsync(TResource? leftResource, [Disallo
await _resourceDefinitionAccessor.OnWritingAsync(leftPlaceholderResource, WriteOperationKind.AddToRelationship, cancellationToken);
- if (rightResourceIds.Any())
+ if (rightResourceIds.Count > 0)
{
RelationshipForeignKey foreignKey = _dataModelService.GetForeignKey(relationship);
object[] rightResourceIdValues = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray();
@@ -482,7 +483,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected != rightResourceIdValues.Length)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist or multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist or multiple rows found."));
}
}, cancellationToken);
@@ -503,7 +504,7 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet
await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.RemoveFromRelationship, cancellationToken);
- if (rightResourceIds.Any())
+ if (rightResourceIds.Count > 0)
{
RelationshipForeignKey foreignKey = _dataModelService.GetForeignKey(relationship);
object[] rightResourceIdValues = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray();
@@ -516,7 +517,7 @@ await ExecuteInTransactionAsync(async transaction =>
if (rowsAffected != rightResourceIdValues.Length)
{
- throw new DataStoreUpdateException(new Exception("Row does not exist or multiple rows found."));
+ throw new DataStoreUpdateException(new DataException("Row does not exist or multiple rows found."));
}
}, cancellationToken);
@@ -530,19 +531,18 @@ private void LogSqlCommand(CommandDefinition command)
_captureStore.Add(command.CommandText, parameters);
- string message = GetLogText(command.CommandText, parameters);
- _logger.LogInformation(message);
- }
-
- private string GetLogText(string statement, IDictionary? parameters)
- {
- if (parameters?.Any() == true)
+ if (_logger.IsEnabled(LogLevel.Information))
{
- string parametersText = string.Join(", ", parameters.Select(parameter => _parameterFormatter.Format(parameter.Key, parameter.Value)));
- return $"Executing SQL with parameters: {parametersText}{Environment.NewLine}{statement}";
+ if (parameters?.Count > 0)
+ {
+ string parametersText = string.Join(", ", parameters.Select(parameter => _parameterFormatter.Format(parameter.Key, parameter.Value)));
+ LogExecuteWithParameters(Environment.NewLine, command.CommandText, parametersText);
+ }
+ else
+ {
+ LogExecute(Environment.NewLine, command.CommandText);
+ }
}
-
- return $"Executing SQL: {Environment.NewLine}{statement}";
}
private async Task ExecuteQueryAsync(Func> asyncAction, CancellationToken cancellationToken)
@@ -580,4 +580,10 @@ private async Task ExecuteInTransactionAsync(Func asyncActi
throw new DataStoreUpdateException(exception);
}
}
+
+ [LoggerMessage(Level = LogLevel.Information, SkipEnabledCheck = true, Message = "Executing SQL: {LineBreak}{Query}")]
+ private partial void LogExecute(string lineBreak, string query);
+
+ [LoggerMessage(Level = LogLevel.Information, SkipEnabledCheck = true, Message = "Executing SQL with parameters: {Parameters}{LineBreak}{Query}")]
+ private partial void LogExecuteWithParameters(string lineBreak, string query, string parameters);
}
diff --git a/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs b/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs
index 1d9b998340..6b075d6cae 100644
--- a/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs
+++ b/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs
@@ -18,8 +18,8 @@ internal sealed class ResourceChangeDetector
private Dictionary _currentColumnValues = [];
private Dictionary _newColumnValues = [];
- private Dictionary> _currentRightResourcesByRelationship = [];
- private Dictionary> _newRightResourcesByRelationship = [];
+ private Dictionary> _currentRightResourcesByRelationship = [];
+ private Dictionary> _newRightResourcesByRelationship = [];
public ResourceType ResourceType { get; }
@@ -62,9 +62,9 @@ public void CaptureNewValues(IIdentifiable resource)
return columnValues;
}
- private Dictionary> CaptureRightResourcesByRelationship(IIdentifiable resource)
+ private Dictionary> CaptureRightResourcesByRelationship(IIdentifiable resource)
{
- Dictionary> relationshipValues = [];
+ Dictionary> relationshipValues = [];
foreach (RelationshipAttribute relationship in ResourceType.Relationships)
{
@@ -88,7 +88,7 @@ public void AssertIsNotClearingAnyRequiredToOneRelationships(string resourceName
if (!foreignKey.IsNullable)
{
object? currentRightId =
- _currentRightResourcesByRelationship.TryGetValue(hasOneRelationship, out ISet? currentRightResources)
+ _currentRightResourcesByRelationship.TryGetValue(hasOneRelationship, out HashSet? currentRightResources)
? currentRightResources.FirstOrDefault()?.GetTypedId()
: null;
@@ -118,7 +118,7 @@ public void AssertIsNotClearingAnyRequiredToOneRelationships(string resourceName
if (newRightId != null)
{
object? currentRightId =
- _currentRightResourcesByRelationship.TryGetValue(hasOneRelationship, out ISet? currentRightResources)
+ _currentRightResourcesByRelationship.TryGetValue(hasOneRelationship, out HashSet? currentRightResources)
? currentRightResources.FirstOrDefault()?.GetTypedId()
: null;
@@ -130,7 +130,7 @@ public void AssertIsNotClearingAnyRequiredToOneRelationships(string resourceName
}
}
- return changes;
+ return changes.AsReadOnly();
}
public IReadOnlyDictionary GetChangedColumnValues()
@@ -147,7 +147,7 @@ public void AssertIsNotClearingAnyRequiredToOneRelationships(string resourceName
}
}
- return changes;
+ return changes.AsReadOnly();
}
public IReadOnlyDictionary GetChangedToOneRelationshipsWithForeignKeyAtRightSide()
@@ -165,7 +165,7 @@ public void AssertIsNotClearingAnyRequiredToOneRelationships(string resourceName
continue;
}
- object? currentRightId = _currentRightResourcesByRelationship.TryGetValue(hasOneRelationship, out ISet? currentRightResources)
+ object? currentRightId = _currentRightResourcesByRelationship.TryGetValue(hasOneRelationship, out HashSet? currentRightResources)
? currentRightResources.FirstOrDefault()?.GetTypedId()
: null;
@@ -178,7 +178,7 @@ public void AssertIsNotClearingAnyRequiredToOneRelationships(string resourceName
}
}
- return changes;
+ return changes.AsReadOnly();
}
public IReadOnlyDictionary currentRightIds, ISet
/// The reference to t1 in the WHERE clause has become stale and needs to be pulled out into scope, which is t2.
///
-internal sealed class StaleColumnReferenceRewriter : SqlTreeNodeVisitor
+internal sealed partial class StaleColumnReferenceRewriter : SqlTreeNodeVisitor
{
private readonly IReadOnlyDictionary _oldToNewTableAliasMap;
private readonly ILogger _logger;
@@ -59,12 +60,13 @@ public override SqlTreeNode VisitSelect(SelectNode node, ColumnVisitMode mode)
{
IncludeTableAliasInCurrentScope(node);
- using IDisposable scope = EnterSelectScope();
-
- IReadOnlyDictionary> selectors = VisitSelectors(node.Selectors, mode);
- WhereNode? where = TypedVisit(node.Where, mode);
- OrderByNode? orderBy = TypedVisit(node.OrderBy, mode);
- return new SelectNode(selectors, where, orderBy, node.Alias);
+ using (EnterSelectScope())
+ {
+ ReadOnlyDictionary> selectors = VisitSelectors(node.Selectors, mode);
+ WhereNode? where = TypedVisit(node.Where, mode);
+ OrderByNode? orderBy = TypedVisit(node.OrderBy, mode);
+ return new SelectNode(selectors, where, orderBy, node.Alias);
+ }
}
private void IncludeTableAliasInCurrentScope(TableSourceNode tableSource)
@@ -76,7 +78,7 @@ private void IncludeTableAliasInCurrentScope(TableSourceNode tableSource)
}
}
- private IDisposable EnterSelectScope()
+ private PopStackOnDispose> EnterSelectScope()
{
Dictionary newScope = CopyTopStackElement();
_tablesInScopeStack.Push(newScope);
@@ -95,7 +97,7 @@ private Dictionary CopyTopStackElement()
return new Dictionary(topElement);
}
- private IReadOnlyDictionary> VisitSelectors(
+ private ReadOnlyDictionary> VisitSelectors(
IReadOnlyDictionary> selectors, ColumnVisitMode mode)
{
Dictionary> newSelectors = [];
@@ -103,12 +105,12 @@ private IReadOnlyDictionary> Visi
foreach ((TableAccessorNode tableAccessor, IReadOnlyList tableSelectors) in selectors)
{
TableAccessorNode newTableAccessor = TypedVisit(tableAccessor, mode);
- IReadOnlyList newTableSelectors = VisitList(tableSelectors, ColumnVisitMode.Declaration);
+ ReadOnlyCollection newTableSelectors = VisitSequence(tableSelectors, ColumnVisitMode.Declaration);
newSelectors.Add(newTableAccessor, newTableSelectors);
}
- return newSelectors;
+ return newSelectors.AsReadOnly();
}
public override SqlTreeNode VisitTable(TableNode node, ColumnVisitMode mode)
@@ -142,7 +144,7 @@ public override SqlTreeNode VisitColumnInTable(ColumnInTableNode node, ColumnVis
return MapColumnInTable(node, tablesInScope);
}
- private ColumnNode MapColumnInTable(ColumnInTableNode column, IDictionary tablesInScope)
+ private ColumnNode MapColumnInTable(ColumnInTableNode column, Dictionary tablesInScope)
{
if (column.TableAlias != null && !tablesInScope.ContainsKey(column.TableAlias))
{
@@ -159,7 +161,7 @@ private ColumnNode MapColumnInTable(ColumnInTableNode column, IDictionary terms = VisitList(node.Terms, mode);
+ ReadOnlyCollection terms = VisitSequence(node.Terms, mode);
return new LogicalNode(node.Operator, terms);
}
@@ -233,7 +235,7 @@ public override SqlTreeNode VisitLike(LikeNode node, ColumnVisitMode mode)
public override SqlTreeNode VisitIn(InNode node, ColumnVisitMode mode)
{
ColumnNode column = TypedVisit(node.Column, mode);
- IReadOnlyList values = VisitList(node.Values, mode);
+ ReadOnlyCollection values = VisitSequence(node.Values, mode);
return new InNode(column, values);
}
@@ -251,7 +253,7 @@ public override SqlTreeNode VisitCount(CountNode node, ColumnVisitMode mode)
public override SqlTreeNode VisitOrderBy(OrderByNode node, ColumnVisitMode mode)
{
- IReadOnlyList terms = VisitList(node.Terms, mode);
+ ReadOnlyCollection terms = VisitSequence(node.Terms, mode);
return new OrderByNode(terms);
}
@@ -277,19 +279,22 @@ public override SqlTreeNode VisitNullConstant(NullConstantNode node, ColumnVisit
return node;
}
- [return: NotNullIfNotNull("node")]
+ [return: NotNullIfNotNull(nameof(node))]
private T? TypedVisit(T? node, ColumnVisitMode mode)
where T : SqlTreeNode
{
return node != null ? (T)Visit(node, mode) : null;
}
- private IReadOnlyList VisitList(IEnumerable nodes, ColumnVisitMode mode)
+ private ReadOnlyCollection VisitSequence(IEnumerable nodes, ColumnVisitMode mode)
where T : SqlTreeNode
{
- return nodes.Select(element => TypedVisit(element, mode)).ToList();
+ return nodes.Select(element => TypedVisit(element, mode)).ToArray().AsReadOnly();
}
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Mapped inaccessible column {FromColumn} to {ToColumn}.")]
+ private partial void LogColumnMapped(ColumnNode fromColumn, ColumnNode toColumn);
+
private sealed class PopStackOnDispose(Stack stack) : IDisposable
{
private readonly Stack _stack = stack;
diff --git a/src/Examples/DapperExample/TranslationToSql/Transformations/UnusedSelectorsRewriter.cs b/src/Examples/DapperExample/TranslationToSql/Transformations/UnusedSelectorsRewriter.cs
index 7cffc8e29a..08f09da99a 100644
--- a/src/Examples/DapperExample/TranslationToSql/Transformations/UnusedSelectorsRewriter.cs
+++ b/src/Examples/DapperExample/TranslationToSql/Transformations/UnusedSelectorsRewriter.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using DapperExample.TranslationToSql.TreeNodes;
using JsonApiDotNetCore;
@@ -26,7 +27,7 @@ namespace DapperExample.TranslationToSql.Transformations;
///
/// The selectors t1."AccountId" and t1."FirstName" have no references and can be removed.
///
-internal sealed class UnusedSelectorsRewriter : SqlTreeNodeVisitor, SqlTreeNode>
+internal sealed partial class UnusedSelectorsRewriter : SqlTreeNodeVisitor, SqlTreeNode>
{
private readonly ColumnSelectorUsageCollector _usageCollector;
private readonly ILogger _logger;
@@ -52,9 +53,9 @@ public SelectNode RemoveUnusedSelectorsInSubQueries(SelectNode select)
_hasChanged = false;
_usageCollector.Collect(_rootSelect);
- _logger.LogDebug("Started removal of unused selectors.");
+ LogStarted();
_rootSelect = TypedVisit(_rootSelect, _usageCollector.UsedColumns);
- _logger.LogDebug("Finished removal of unused selectors.");
+ LogFinished();
}
while (_hasChanged);
@@ -68,13 +69,13 @@ public override SqlTreeNode DefaultVisit(SqlTreeNode node, ISet used
public override SqlTreeNode VisitSelect(SelectNode node, ISet usedColumns)
{
- IReadOnlyDictionary> selectors = VisitSelectors(node, usedColumns);
+ ReadOnlyDictionary> selectors = VisitSelectors(node, usedColumns);
WhereNode? where = TypedVisit(node.Where, usedColumns);
OrderByNode? orderBy = TypedVisit(node.OrderBy, usedColumns);
return new SelectNode(selectors, where, orderBy, node.Alias);
}
- private IReadOnlyDictionary> VisitSelectors(SelectNode select, ISet usedColumns)
+ private ReadOnlyDictionary> VisitSelectors(SelectNode select, ISet usedColumns)
{
Dictionary> newSelectors = [];
@@ -85,10 +86,10 @@ private IReadOnlyDictionary> Visi
newSelectors.Add(newTableAccessor, newTableSelectors);
}
- return newSelectors;
+ return newSelectors.AsReadOnly();
}
- private List VisitTableSelectors(IEnumerable selectors, ISet usedColumns)
+ private ReadOnlyCollection VisitTableSelectors(IEnumerable selectors, ISet usedColumns)
{
List newTableSelectors = [];
@@ -98,7 +99,7 @@ private List VisitTableSelectors(IEnumerable selecto
{
if (!usedColumns.Contains(columnSelector.Column))
{
- _logger.LogDebug($"Removing unused selector {columnSelector}.");
+ LogSelectorRemoved(columnSelector);
_hasChanged = true;
continue;
}
@@ -107,7 +108,7 @@ private List VisitTableSelectors(IEnumerable selecto
newTableSelectors.Add(selector);
}
- return newTableSelectors;
+ return newTableSelectors.AsReadOnly();
}
public override SqlTreeNode VisitFrom(FromNode node, ISet usedColumns)
@@ -150,7 +151,7 @@ public override SqlTreeNode VisitNot(NotNode node, ISet usedColumns)
public override SqlTreeNode VisitLogical(LogicalNode node, ISet usedColumns)
{
- IReadOnlyList terms = VisitList(node.Terms, usedColumns);
+ ReadOnlyCollection terms = VisitSequence(node.Terms, usedColumns);
return new LogicalNode(node.Operator, terms);
}
@@ -170,7 +171,7 @@ public override SqlTreeNode VisitLike(LikeNode node, ISet usedColumn
public override SqlTreeNode VisitIn(InNode node, ISet usedColumns)
{
ColumnNode column = TypedVisit(node.Column, usedColumns);
- IReadOnlyList values = VisitList(node.Values, usedColumns);
+ ReadOnlyCollection values = VisitSequence(node.Values, usedColumns);
return new InNode(column, values);
}
@@ -188,7 +189,7 @@ public override SqlTreeNode VisitCount(CountNode node, ISet usedColu
public override SqlTreeNode VisitOrderBy(OrderByNode node, ISet usedColumns)
{
- IReadOnlyList terms = VisitList(node.Terms, usedColumns);
+ ReadOnlyCollection terms = VisitSequence(node.Terms, usedColumns);
return new OrderByNode(terms);
}
@@ -204,16 +205,25 @@ public override SqlTreeNode VisitOrderByCount(OrderByCountNode node, ISet(T? node, ISet usedColumns)
where T : SqlTreeNode
{
return node != null ? (T)Visit(node, usedColumns) : null;
}
- private IReadOnlyList VisitList(IEnumerable nodes, ISet usedColumns)
+ private ReadOnlyCollection VisitSequence(IEnumerable nodes, ISet usedColumns)
where T : SqlTreeNode
{
- return nodes.Select(element => TypedVisit(element, usedColumns)).ToList();
+ return nodes.Select(element => TypedVisit(element, usedColumns)).ToArray().AsReadOnly();
}
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Started removal of unused selectors.")]
+ private partial void LogStarted();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Finished removal of unused selectors.")]
+ private partial void LogFinished();
+
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Removing unused selector {Selector}.")]
+ private partial void LogSelectorRemoved(ColumnSelectorNode selector);
}
diff --git a/src/Examples/DapperExample/TranslationToSql/TreeNodes/LogicalNode.cs b/src/Examples/DapperExample/TranslationToSql/TreeNodes/LogicalNode.cs
index 40fc95b88c..a01306d061 100644
--- a/src/Examples/DapperExample/TranslationToSql/TreeNodes/LogicalNode.cs
+++ b/src/Examples/DapperExample/TranslationToSql/TreeNodes/LogicalNode.cs
@@ -14,7 +14,7 @@ internal sealed class LogicalNode : FilterNode
public IReadOnlyList Terms { get; }
public LogicalNode(LogicalOperator @operator, params FilterNode[] terms)
- : this(@operator, terms.ToList())
+ : this(@operator, terms.AsReadOnly())
{
}
diff --git a/src/Examples/DapperExample/TranslationToSql/TreeNodes/ParameterNode.cs b/src/Examples/DapperExample/TranslationToSql/TreeNodes/ParameterNode.cs
index c2a5824f72..a9feaa7836 100644
--- a/src/Examples/DapperExample/TranslationToSql/TreeNodes/ParameterNode.cs
+++ b/src/Examples/DapperExample/TranslationToSql/TreeNodes/ParameterNode.cs
@@ -16,7 +16,7 @@ internal sealed class ParameterNode : SqlValueNode
public ParameterNode(string name, object? value)
{
- ArgumentGuard.NotNull(name);
+ ArgumentGuard.NotNullNorEmpty(name);
if (!name.StartsWith('@') || name.Length < 2)
{
diff --git a/src/Examples/DapperExample/TranslationToSql/TreeNodes/SelectNode.cs b/src/Examples/DapperExample/TranslationToSql/TreeNodes/SelectNode.cs
index 0fc42b1ba0..add1ddc433 100644
--- a/src/Examples/DapperExample/TranslationToSql/TreeNodes/SelectNode.cs
+++ b/src/Examples/DapperExample/TranslationToSql/TreeNodes/SelectNode.cs
@@ -19,7 +19,7 @@ internal sealed class SelectNode : TableSourceNode
public WhereNode? Where { get; }
public OrderByNode? OrderBy { get; }
- public override IReadOnlyList Columns => _columns;
+ public override IReadOnlyList Columns => _columns.AsReadOnly();
public SelectNode(IReadOnlyDictionary> selectors, WhereNode? where, OrderByNode? orderBy, string? alias)
: base(alias)
diff --git a/src/Examples/DapperExample/TranslationToSql/TreeNodes/TableNode.cs b/src/Examples/DapperExample/TranslationToSql/TreeNodes/TableNode.cs
index 31977f1546..6abb913418 100644
--- a/src/Examples/DapperExample/TranslationToSql/TreeNodes/TableNode.cs
+++ b/src/Examples/DapperExample/TranslationToSql/TreeNodes/TableNode.cs
@@ -21,7 +21,7 @@ internal sealed class TableNode : TableSourceNode
public string Name => _resourceType.ClrType.Name.Pluralize();
- public override IReadOnlyList Columns => _columns;
+ public override IReadOnlyList Columns => _columns.AsReadOnly();
public TableNode(ResourceType resourceType, IReadOnlyDictionary columnMappings, string? alias)
: base(alias)
diff --git a/src/Examples/DatabasePerTenantExample/Program.cs b/src/Examples/DatabasePerTenantExample/Program.cs
index 1414e28424..4b88357d78 100644
--- a/src/Examples/DatabasePerTenantExample/Program.cs
+++ b/src/Examples/DatabasePerTenantExample/Program.cs
@@ -38,7 +38,7 @@
await CreateDatabaseAsync("AdventureWorks", app.Services);
await CreateDatabaseAsync("Contoso", app.Services);
-app.Run();
+await app.RunAsync();
[Conditional("DEBUG")]
static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs
index 9ce6beda08..634e130a3f 100644
--- a/src/Examples/GettingStarted/Program.cs
+++ b/src/Examples/GettingStarted/Program.cs
@@ -38,7 +38,7 @@
await CreateDatabaseAsync(app.Services);
-app.Run();
+await app.RunAsync();
[Conditional("DEBUG")]
static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
diff --git a/src/Examples/JsonApiDotNetCoreExample/AppLog.cs b/src/Examples/JsonApiDotNetCoreExample/AppLog.cs
new file mode 100644
index 0000000000..6cb4af1a55
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/AppLog.cs
@@ -0,0 +1,9 @@
+#pragma warning disable AV1008 // Class should not be static
+
+namespace JsonApiDotNetCoreExample;
+
+internal static partial class AppLog
+{
+ [LoggerMessage(Level = LogLevel.Information, SkipEnabledCheck = true, Message = "Measurement results for application startup:{LineBreak}{TimingResults}")]
+ public static partial void LogStartupTimings(ILogger logger, string lineBreak, string timingResults);
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs
index 3c89ac3bcf..aa51110869 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs
@@ -16,7 +16,8 @@ public IActionResult Get()
[HttpPost]
public async Task PostAsync()
{
- string name = await new StreamReader(Request.Body).ReadToEndAsync();
+ using var reader = new StreamReader(Request.Body, leaveOpen: true);
+ string name = await reader.ReadToEndAsync();
if (string.IsNullOrEmpty(name))
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs
index 52b27759e9..4c11a71660 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Program.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs
@@ -3,6 +3,7 @@
using System.Text.Json.Serialization;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Diagnostics;
+using JsonApiDotNetCoreExample;
using JsonApiDotNetCoreExample.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
@@ -17,7 +18,7 @@
await CreateDatabaseAsync(app.Services);
-app.Run();
+await app.RunAsync();
static WebApplication CreateWebApplication(string[] args)
{
@@ -34,10 +35,10 @@ static WebApplication CreateWebApplication(string[] args)
// Configure the HTTP request pipeline.
ConfigurePipeline(app);
- if (CodeTimingSessionManager.IsEnabled)
+ if (CodeTimingSessionManager.IsEnabled && app.Logger.IsEnabled(LogLevel.Information))
{
string timingResults = CodeTimingSessionManager.Current.GetResults();
- app.Logger.LogInformation($"Measurement results for application startup:{Environment.NewLine}{timingResults}");
+ AppLog.LogStartupTimings(app.Logger, Environment.NewLine, timingResults);
}
return app;
diff --git a/src/Examples/MultiDbContextExample/Program.cs b/src/Examples/MultiDbContextExample/Program.cs
index 2cf567b9b5..481e8f7118 100644
--- a/src/Examples/MultiDbContextExample/Program.cs
+++ b/src/Examples/MultiDbContextExample/Program.cs
@@ -52,7 +52,7 @@
await CreateDatabaseAsync(app.Services);
-app.Run();
+await app.RunAsync();
[Conditional("DEBUG")]
static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
diff --git a/src/Examples/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs
index f21d116e5f..8eff35d7a9 100755
--- a/src/Examples/NoEntityFrameworkExample/Program.cs
+++ b/src/Examples/NoEntityFrameworkExample/Program.cs
@@ -35,4 +35,4 @@
app.UseJsonApi();
app.MapControllers();
-app.Run();
+await app.RunAsync();
diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs
index 9d0852ad7f..9eba0b8326 100644
--- a/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs
+++ b/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs
@@ -32,7 +32,11 @@ public Task> GetAsync(QueryLayer queryLayer, Canc
IEnumerable dataSource = GetDataSource();
IEnumerable resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource);
- return Task.FromResult>(resources.ToList());
+#if NET6_0
+ return Task.FromResult>(Array.AsReadOnly(resources.ToArray()));
+#else
+ return Task.FromResult>(resources.ToArray().AsReadOnly());
+#endif
}
///
diff --git a/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs b/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs
index ee9d2196e9..e55b9340b6 100644
--- a/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs
+++ b/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs
@@ -31,7 +31,7 @@ namespace NoEntityFrameworkExample.Services;
///
/// The resource identifier type.
///
-public abstract class InMemoryResourceService(
+public abstract partial class InMemoryResourceService(
IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext,
IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel,
ILoggerFactory loggerFactory) : IResourceQueryService
@@ -40,7 +40,7 @@ public abstract class InMemoryResourceService(
private readonly IJsonApiOptions _options = options;
private readonly IQueryLayerComposer _queryLayerComposer = queryLayerComposer;
private readonly IPaginationContext _paginationContext = paginationContext;
- private readonly IEnumerable _constraintProviders = constraintProviders;
+ private readonly IQueryConstraintProvider[] _constraintProviders = constraintProviders as IQueryConstraintProvider[] ?? constraintProviders.ToArray();
private readonly ILogger> _logger = loggerFactory.CreateLogger>();
private readonly ResourceType _resourceType = resourceGraph.GetResourceType();
private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter = new(entityModel, queryableBuilder);
@@ -58,14 +58,18 @@ public Task> GetAsync(CancellationToken cancellat
QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_resourceType);
IEnumerable dataSource = GetDataSource(_resourceType).Cast();
- List resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource).ToList();
+ TResource[] resources = _queryLayerToLinqConverter.ApplyQueryLayer(queryLayer, dataSource).ToArray();
- if (queryLayer.Pagination?.PageSize?.Value == resources.Count)
+ if (queryLayer.Pagination?.PageSize?.Value == resources.Length)
{
_paginationContext.IsPageFull = true;
}
- return Task.FromResult>(resources);
+#if NET6_0
+ return Task.FromResult>(Array.AsReadOnly(resources));
+#else
+ return Task.FromResult>(resources.AsReadOnly());
+#endif
}
private void LogFiltersInTopScope()
@@ -87,7 +91,7 @@ private void LogFiltersInTopScope()
if (filter != null)
{
- _logger.LogInformation($"Incoming top-level filter from query string: {filter}");
+ LogIncomingFilter(filter);
}
}
@@ -195,4 +199,7 @@ private void SetNonPrimaryTotalCount([DisallowNull] TId id, RelationshipAttribut
}
protected abstract IEnumerable GetDataSource(ResourceType resourceType);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "Incoming top-level filter from query string: {Filter}")]
+ private partial void LogIncomingFilter(FilterExpression filter);
}
diff --git a/src/Examples/ReportsExample/Program.cs b/src/Examples/ReportsExample/Program.cs
index 04920d0068..7f89ad9301 100644
--- a/src/Examples/ReportsExample/Program.cs
+++ b/src/Examples/ReportsExample/Program.cs
@@ -24,4 +24,4 @@
app.UseJsonApi();
app.MapControllers();
-app.Run();
+await app.RunAsync();
diff --git a/src/Examples/ReportsExample/Services/ReportService.cs b/src/Examples/ReportsExample/Services/ReportService.cs
index c04e821347..62bb7c9554 100644
--- a/src/Examples/ReportsExample/Services/ReportService.cs
+++ b/src/Examples/ReportsExample/Services/ReportService.cs
@@ -5,24 +5,19 @@
namespace ReportsExample.Services;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
-public class ReportService(ILoggerFactory loggerFactory) : IGetAllService
+public class ReportService : IGetAllService
{
- private readonly ILogger _logger = loggerFactory.CreateLogger();
-
public Task> GetAsync(CancellationToken cancellationToken)
{
- _logger.LogInformation("GetAsync");
-
- IReadOnlyCollection reports = GetReports();
-
+ IReadOnlyCollection reports = GetReports().AsReadOnly();
return Task.FromResult(reports);
}
- private IReadOnlyCollection GetReports()
+ private List GetReports()
{
- return new List
- {
- new()
+ return
+ [
+ new Report
{
Id = 1,
Title = "Status Report",
@@ -32,6 +27,6 @@ private IReadOnlyCollection GetReports()
HoursSpent = 24
}
}
- };
+ ];
}
}
diff --git a/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs b/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs
index 4264c5db8b..e01b80d776 100644
--- a/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs
+++ b/src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs
@@ -3,20 +3,21 @@
using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute;
#pragma warning disable AV1008 // Class should not be static
+#pragma warning disable format
namespace JsonApiDotNetCore;
internal static class ArgumentGuard
{
[AssertionMethod]
- public static void NotNull([NoEnumeration] [SysNotNull] T? value, [CallerArgumentExpression("value")] string? parameterName = null)
+ public static void NotNull([NoEnumeration] [SysNotNull] T? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
where T : class
{
ArgumentNullException.ThrowIfNull(value, parameterName);
}
[AssertionMethod]
- public static void NotNullNorEmpty([SysNotNull] IEnumerable? value, [CallerArgumentExpression("value")] string? parameterName = null)
+ public static void NotNullNorEmpty([SysNotNull] IEnumerable? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
{
ArgumentNullException.ThrowIfNull(value, parameterName);
@@ -27,13 +28,32 @@ public static void NotNullNorEmpty([SysNotNull] IEnumerable? value, [Calle
}
[AssertionMethod]
- public static void NotNullNorEmpty([SysNotNull] string? value, [CallerArgumentExpression("value")] string? parameterName = null)
+ public static void NotNullNorEmpty([SysNotNull] string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
{
+#if !NET6_0
+ ArgumentException.ThrowIfNullOrEmpty(value, parameterName);
+#else
ArgumentNullException.ThrowIfNull(value, parameterName);
- if (value == string.Empty)
+ if (value.Length == 0)
{
throw new ArgumentException("String cannot be null or empty.", parameterName);
}
+#endif
+ }
+
+ [AssertionMethod]
+ public static void NotNullNorWhitespace([SysNotNull] string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
+ {
+#if !NET6_0
+ ArgumentException.ThrowIfNullOrWhiteSpace(value, parameterName);
+#else
+ ArgumentNullException.ThrowIfNull(value, parameterName);
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("String cannot be null, empty, or whitespace.", parameterName);
+ }
+#endif
}
}
diff --git a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs
index e95d306329..750b896c27 100644
--- a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs
+++ b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs
@@ -5,15 +5,15 @@ namespace JsonApiDotNetCore;
internal sealed class CollectionConverter
{
- private static readonly ISet HashSetCompatibleCollectionTypes = new HashSet
- {
+ private static readonly HashSet HashSetCompatibleCollectionTypes =
+ [
typeof(HashSet<>),
typeof(ISet<>),
typeof(IReadOnlySet<>),
typeof(ICollection<>),
typeof(IReadOnlyCollection<>),
typeof(IEnumerable<>)
- };
+ ];
///
/// Creates a collection instance based on the specified collection type and copies the specified elements into it.
@@ -70,10 +70,10 @@ public IReadOnlyCollection ExtractResources(object? value)
{
return value switch
{
- List resourceList => resourceList,
- HashSet resourceSet => resourceSet,
+ List resourceList => resourceList.AsReadOnly(),
+ HashSet resourceSet => resourceSet.AsReadOnly(),
IReadOnlyCollection resourceCollection => resourceCollection,
- IEnumerable resources => resources.ToList(),
+ IEnumerable resources => resources.ToArray().AsReadOnly(),
IIdentifiable resource => [resource],
_ => Array.Empty()
};
diff --git a/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs b/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
index 47542def56..d8d5d63f3e 100644
--- a/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
+++ b/src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
@@ -9,6 +9,10 @@ namespace JsonApiDotNetCore.Configuration;
[PublicAPI]
public sealed class ResourceType
{
+ private static readonly IReadOnlySet EmptyResourceTypeSet = new HashSet().AsReadOnly();
+ private static readonly IReadOnlySet EmptyAttributeSet = new HashSet().AsReadOnly();
+ private static readonly IReadOnlySet EmptyRelationshipSet = new HashSet().AsReadOnly();
+
private readonly Dictionary _fieldsByPublicName = [];
private readonly Dictionary _fieldsByPropertyName = [];
private readonly Lazy> _lazyAllConcreteDerivedTypes;
@@ -42,7 +46,7 @@ public sealed class ResourceType
///
/// The resource types that directly derive from this one.
///
- public IReadOnlySet DirectlyDerivedTypes { get; internal set; } = new HashSet();
+ public IReadOnlySet DirectlyDerivedTypes { get; internal set; } = EmptyResourceTypeSet;
///
/// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. When using resource inheritance, this
@@ -107,7 +111,7 @@ public ResourceType(string publicName, ClientIdGenerationMode? clientIdGeneratio
IReadOnlyCollection? eagerLoads, LinkTypes topLevelLinks = LinkTypes.NotConfigured,
LinkTypes resourceLinks = LinkTypes.NotConfigured, LinkTypes relationshipLinks = LinkTypes.NotConfigured)
{
- ArgumentGuard.NotNullNorEmpty(publicName);
+ ArgumentGuard.NotNullNorWhitespace(publicName);
ArgumentGuard.NotNull(clrType);
ArgumentGuard.NotNull(identityClrType);
@@ -121,7 +125,7 @@ public ResourceType(string publicName, ClientIdGenerationMode? clientIdGeneratio
TopLevelLinks = topLevelLinks;
ResourceLinks = resourceLinks;
RelationshipLinks = relationshipLinks;
- Fields = Attributes.Cast().Concat(Relationships).ToArray();
+ Fields = Attributes.Cast().Concat(Relationships).ToArray().AsReadOnly();
foreach (ResourceFieldAttribute field in Fields)
{
@@ -134,10 +138,10 @@ public ResourceType(string publicName, ClientIdGenerationMode? clientIdGeneratio
private IReadOnlySet ResolveAllConcreteDerivedTypes()
{
- var allConcreteDerivedTypes = new HashSet();
+ HashSet allConcreteDerivedTypes = [];
AddConcreteDerivedTypes(this, allConcreteDerivedTypes);
- return allConcreteDerivedTypes;
+ return allConcreteDerivedTypes.AsReadOnly();
}
private static void AddConcreteDerivedTypes(ResourceType resourceType, ISet allConcreteDerivedTypes)
@@ -259,7 +263,20 @@ public ResourceType GetTypeOrDerived(Type clrType)
internal IReadOnlySet GetAttributesInTypeOrDerived(string publicName)
{
- return GetAttributesInTypeOrDerived(this, publicName);
+ if (IsPartOfTypeHierarchy())
+ {
+ return GetAttributesInTypeOrDerived(this, publicName);
+ }
+
+ AttrAttribute? attribute = FindAttributeByPublicName(publicName);
+
+ if (attribute == null)
+ {
+ return EmptyAttributeSet;
+ }
+
+ HashSet attributes = [attribute];
+ return attributes.AsReadOnly();
}
private static IReadOnlySet GetAttributesInTypeOrDerived(ResourceType resourceType, string publicName)
@@ -268,7 +285,8 @@ private static IReadOnlySet GetAttributesInTypeOrDerived(Resource
if (attribute != null)
{
- return attribute.AsHashSet();
+ HashSet attributes = [attribute];
+ return attributes.AsReadOnly();
}
// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
@@ -281,12 +299,25 @@ private static IReadOnlySet GetAttributesInTypeOrDerived(Resource
attributesInDerivedTypes.Add(attributeInDerivedType);
}
- return attributesInDerivedTypes;
+ return attributesInDerivedTypes.AsReadOnly();
}
internal IReadOnlySet GetRelationshipsInTypeOrDerived(string publicName)
{
- return GetRelationshipsInTypeOrDerived(this, publicName);
+ if (IsPartOfTypeHierarchy())
+ {
+ return GetRelationshipsInTypeOrDerived(this, publicName);
+ }
+
+ RelationshipAttribute? relationship = FindRelationshipByPublicName(publicName);
+
+ if (relationship == null)
+ {
+ return EmptyRelationshipSet;
+ }
+
+ HashSet relationships = [relationship];
+ return relationships.AsReadOnly();
}
private static IReadOnlySet GetRelationshipsInTypeOrDerived(ResourceType resourceType, string publicName)
@@ -295,7 +326,8 @@ private static IReadOnlySet GetRelationshipsInTypeOrDeriv
if (relationship != null)
{
- return relationship.AsHashSet();
+ HashSet relationships = [relationship];
+ return relationships.AsReadOnly();
}
// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
@@ -309,12 +341,12 @@ private static IReadOnlySet GetRelationshipsInTypeOrDeriv
relationshipsInDerivedTypes.Add(relationshipInDerivedType);
}
- return relationshipsInDerivedTypes;
+ return relationshipsInDerivedTypes.AsReadOnly();
}
internal bool IsPartOfTypeHierarchy()
{
- return BaseType != null || DirectlyDerivedTypes.Any();
+ return BaseType != null || DirectlyDerivedTypes.Count > 0;
}
public override string ToString()
diff --git a/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs b/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs
deleted file mode 100644
index 8aa1e6c165..0000000000
--- a/src/JsonApiDotNetCore.Annotations/ObjectExtensions.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection
-
-namespace JsonApiDotNetCore;
-
-internal static class ObjectExtensions
-{
- public static HashSet AsHashSet(this T element)
- {
- return [element];
- }
-}
diff --git a/src/JsonApiDotNetCore.Annotations/PolyfillCollectionExtensions.cs b/src/JsonApiDotNetCore.Annotations/PolyfillCollectionExtensions.cs
new file mode 100644
index 0000000000..72578e5db2
--- /dev/null
+++ b/src/JsonApiDotNetCore.Annotations/PolyfillCollectionExtensions.cs
@@ -0,0 +1,34 @@
+#if NET6_0
+using System.Collections.ObjectModel;
+#endif
+
+#if NET6_0
+#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection
+#endif
+
+namespace JsonApiDotNetCore;
+
+// These methods provide polyfills for lower .NET versions.
+internal static class PolyfillCollectionExtensions
+{
+ public static IReadOnlySet AsReadOnly(this HashSet source)
+ {
+ // We can't use ReadOnlySet yet, which is being introduced in .NET 9.
+ return source;
+ }
+
+#if NET6_0
+ public static ReadOnlyDictionary AsReadOnly(this IDictionary source)
+ where TKey : notnull
+ {
+ // The AsReadOnly() extension method is unavailable in .NET 6.
+ return new ReadOnlyDictionary(source);
+ }
+
+ public static ReadOnlyCollection AsReadOnly(this T[] source)
+ {
+ // The AsReadOnly() extension method is unavailable in .NET 6.
+ return Array.AsReadOnly(source);
+ }
+#endif
+}
diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs
index 3a3707442c..3b55c4b644 100644
--- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs
+++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/ResourceFieldAttribute.cs
@@ -115,9 +115,11 @@ public virtual void SetValue(object resource, object? newValue)
protected void AssertIsIdentifiable(object? resource)
{
- if (resource != null && resource is not IIdentifiable)
+ if (resource is not null and not IIdentifiable)
{
+#pragma warning disable CA1062 // Validate arguments of public methods
throw new InvalidOperationException($"Resource of type '{resource.GetType()}' does not implement {nameof(IIdentifiable)}.");
+#pragma warning restore CA1062 // Validate arguments of public methods
}
}
diff --git a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs
index 1e68e5afab..3df1092c4b 100644
--- a/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs
+++ b/src/JsonApiDotNetCore.SourceGenerators/SourceCodeWriter.cs
@@ -10,36 +10,34 @@ internal sealed class SourceCodeWriter(GeneratorExecutionContext context, Diagno
{
private const int SpacesPerIndent = 4;
- private static readonly IDictionary IndentTable = new Dictionary
+ private static readonly Dictionary IndentTable = new()
{
[0] = string.Empty,
- [1] = new(' ', 1 * SpacesPerIndent),
- [2] = new(' ', 2 * SpacesPerIndent),
- [3] = new(' ', 3 * SpacesPerIndent)
+ [1] = new string(' ', 1 * SpacesPerIndent),
+ [2] = new string(' ', 2 * SpacesPerIndent),
+ [3] = new string(' ', 3 * SpacesPerIndent)
};
- private static readonly IDictionary AggregateEndpointToServiceNameMap =
- new Dictionary
- {
- [JsonApiEndpointsCopy.All] = ("IResourceService", "resourceService"),
- [JsonApiEndpointsCopy.Query] = ("IResourceQueryService", "queryService"),
- [JsonApiEndpointsCopy.Command] = ("IResourceCommandService", "commandService")
- };
+ private static readonly Dictionary AggregateEndpointToServiceNameMap = new()
+ {
+ [JsonApiEndpointsCopy.All] = ("IResourceService", "resourceService"),
+ [JsonApiEndpointsCopy.Query] = ("IResourceQueryService", "queryService"),
+ [JsonApiEndpointsCopy.Command] = ("IResourceCommandService", "commandService")
+ };
- 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 static readonly Dictionary EndpointToServiceNameMap = new()
+ {
+ [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 = context;
private readonly DiagnosticDescriptor _missingIndentInTableErrorDescriptor = missingIndentInTableErrorDescriptor;
diff --git a/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs b/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs
index ad7b0d6ad5..17c5ffefd0 100644
--- a/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs
+++ b/src/JsonApiDotNetCore.SourceGenerators/TypeWithAttributeSyntaxReceiver.cs
@@ -30,7 +30,7 @@ internal sealed class TypeWithAttributeSyntaxReceiver : ISyntaxReceiver
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
- if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Any())
+ if (syntaxNode is TypeDeclarationSyntax { AttributeLists.Count: > 0 } typeDeclarationSyntax)
{
TypeDeclarations.Add(typeDeclarationSyntax);
}
diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs
index 09ebefaf93..5d22198a72 100644
--- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs
+++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs
@@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.AtomicOperations;
///
public sealed class LocalIdTracker : ILocalIdTracker
{
- private readonly IDictionary _idsTracked = new Dictionary();
+ private readonly Dictionary _idsTracked = new();
///
public void Reset()
diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs
index 28ac16d612..927bbf92d3 100644
--- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs
+++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs
@@ -31,6 +31,8 @@ public OperationProcessorAccessor(IServiceProvider serviceProvider)
protected virtual IOperationProcessor ResolveProcessor(OperationContainer operation)
{
+ ArgumentGuard.NotNull(operation);
+
Type processorInterface = GetProcessorInterface(operation.Request.WriteOperation!.Value);
ResourceType resourceType = operation.Request.PrimaryResourceType!;
diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs
index cf1cdd7b65..ff7a48dc32 100644
--- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs
+++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs
@@ -51,7 +51,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
_localIdValidator.Validate(operations);
_localIdTracker.Reset();
- var results = new List();
+ List results = [];
await using IOperationsTransaction transaction = await _operationsTransactionFactory.BeginTransactionAsync(cancellationToken);
@@ -101,6 +101,8 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
protected virtual async Task ProcessOperationAsync(OperationContainer operation, CancellationToken cancellationToken)
{
+ ArgumentGuard.NotNull(operation);
+
cancellationToken.ThrowIfCancellationRequested();
TrackLocalIdsForOperation(operation);
@@ -113,6 +115,8 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
protected void TrackLocalIdsForOperation(OperationContainer operation)
{
+ ArgumentGuard.NotNull(operation);
+
if (operation.Request.WriteOperation == WriteOperationKind.CreateResource)
{
DeclareLocalId(operation.Resource, operation.Request.PrimaryResourceType!);
diff --git a/src/JsonApiDotNetCore/AtomicOperations/RevertRequestStateOnDispose.cs b/src/JsonApiDotNetCore/AtomicOperations/RevertRequestStateOnDispose.cs
index 1951333d0c..824e69de74 100644
--- a/src/JsonApiDotNetCore/AtomicOperations/RevertRequestStateOnDispose.cs
+++ b/src/JsonApiDotNetCore/AtomicOperations/RevertRequestStateOnDispose.cs
@@ -11,8 +11,8 @@ internal sealed class RevertRequestStateOnDispose : IDisposable
private readonly IJsonApiRequest _sourceRequest;
private readonly ITargetedFields? _sourceTargetedFields;
- private readonly IJsonApiRequest _backupRequest = new JsonApiRequest();
- private readonly ITargetedFields _backupTargetedFields = new TargetedFields();
+ private readonly JsonApiRequest _backupRequest = new();
+ private readonly TargetedFields _backupTargetedFields = new();
public RevertRequestStateOnDispose(IJsonApiRequest request, ITargetedFields? targetedFields)
{
diff --git a/src/JsonApiDotNetCore/CollectionExtensions.cs b/src/JsonApiDotNetCore/CollectionExtensions.cs
index 4e02b60fc5..6e9c74aed4 100644
--- a/src/JsonApiDotNetCore/CollectionExtensions.cs
+++ b/src/JsonApiDotNetCore/CollectionExtensions.cs
@@ -82,7 +82,7 @@ public static bool DictionaryEqual(this IReadOnlyDictionary EmptyIfNull(this IEnumerable? source)
{
- return source ?? [];
+ return source ?? Array.Empty();
}
public static IEnumerable WhereNotNull(this IEnumerable source)
diff --git a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs
index 6e3e2d718c..270444928e 100644
--- a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs
+++ b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs
@@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Configuration;
public sealed class InverseNavigationResolver : IInverseNavigationResolver
{
private readonly IResourceGraph _resourceGraph;
- private readonly IEnumerable _dbContextResolvers;
+ private readonly IDbContextResolver[] _dbContextResolvers;
public InverseNavigationResolver(IResourceGraph resourceGraph, IEnumerable dbContextResolvers)
{
@@ -19,7 +19,7 @@ public InverseNavigationResolver(IResourceGraph resourceGraph, IEnumerable
@@ -34,19 +34,19 @@ public void Resolve()
private void Resolve(DbContext dbContext)
{
- foreach (ResourceType resourceType in _resourceGraph.GetResourceTypes().Where(resourceType => resourceType.Relationships.Any()))
+ foreach (ResourceType resourceType in _resourceGraph.GetResourceTypes().Where(resourceType => resourceType.Relationships.Count > 0))
{
IEntityType? entityType = dbContext.Model.FindEntityType(resourceType.ClrType);
if (entityType != null)
{
- IDictionary navigationMap = GetNavigations(entityType);
+ Dictionary navigationMap = GetNavigations(entityType);
ResolveRelationships(resourceType.Relationships, navigationMap);
}
}
}
- private static IDictionary GetNavigations(IEntityType entityType)
+ private static Dictionary GetNavigations(IEntityType entityType)
{
// @formatter:wrap_chained_method_calls chop_always
@@ -58,7 +58,7 @@ private static IDictionary GetNavigations(IEntityType e
// @formatter:wrap_chained_method_calls restore
}
- private void ResolveRelationships(IReadOnlyCollection relationships, IDictionary navigationMap)
+ private void ResolveRelationships(IReadOnlyCollection relationships, Dictionary navigationMap)
{
foreach (RelationshipAttribute relationship in relationships)
{
diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
index 2f725e8c68..f2a0da8d02 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
@@ -137,7 +137,7 @@ public void ConfigureServiceContainer(ICollection dbContextTypes)
{
ArgumentGuard.NotNull(dbContextTypes);
- if (dbContextTypes.Any())
+ if (dbContextTypes.Count > 0)
{
_services.TryAddScoped(typeof(DbContextResolver<>));
diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs
index 0f9cbf1fd2..2aaa218be7 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs
@@ -31,6 +31,8 @@ public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsPro
///
protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{
+ ArgumentGuard.NotNull(entry);
+
var metadata = (DefaultModelMetadata)base.CreateModelMetadata(entry);
metadata.ValidationMetadata.PropertyValidationFilter = _jsonApiValidationFilter;
diff --git a/src/JsonApiDotNetCore/Configuration/PageNumber.cs b/src/JsonApiDotNetCore/Configuration/PageNumber.cs
index a2d4c0cba0..44732fc404 100644
--- a/src/JsonApiDotNetCore/Configuration/PageNumber.cs
+++ b/src/JsonApiDotNetCore/Configuration/PageNumber.cs
@@ -11,17 +11,21 @@ public sealed class PageNumber : IEquatable
public PageNumber(int oneBasedValue)
{
+#if NET6_0
if (oneBasedValue < 1)
{
throw new ArgumentOutOfRangeException(nameof(oneBasedValue));
}
+#else
+ ArgumentOutOfRangeException.ThrowIfLessThan(oneBasedValue, 1);
+#endif
OneBasedValue = oneBasedValue;
}
public bool Equals(PageNumber? other)
{
- if (ReferenceEquals(null, other))
+ if (other is null)
{
return false;
}
diff --git a/src/JsonApiDotNetCore/Configuration/PageSize.cs b/src/JsonApiDotNetCore/Configuration/PageSize.cs
index 7f926f519e..46beb1419f 100644
--- a/src/JsonApiDotNetCore/Configuration/PageSize.cs
+++ b/src/JsonApiDotNetCore/Configuration/PageSize.cs
@@ -9,17 +9,21 @@ public sealed class PageSize : IEquatable
public PageSize(int value)
{
+#if NET6_0
if (value < 1)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
+#else
+ ArgumentOutOfRangeException.ThrowIfLessThan(value, 1);
+#endif
Value = value;
}
public bool Equals(PageSize? other)
{
- if (ReferenceEquals(null, other))
+ if (other is null)
{
return false;
}
diff --git a/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs
index a220d96e01..78d83425ce 100644
--- a/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs
+++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs
@@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Configuration;
internal sealed class ResourceDescriptorAssemblyCache
{
private readonly TypeLocator _typeLocator = new();
- private readonly Dictionary?> _resourceDescriptorsPerAssembly = [];
+ private readonly Dictionary _resourceDescriptorsPerAssembly = [];
public void RegisterAssembly(Assembly assembly)
{
@@ -19,7 +19,7 @@ public IReadOnlyCollection GetResourceDescriptors()
{
EnsureAssembliesScanned();
- return _resourceDescriptorsPerAssembly.SelectMany(pair => pair.Value!).ToArray();
+ return _resourceDescriptorsPerAssembly.SelectMany(pair => pair.Value!).ToArray().AsReadOnly();
}
public IReadOnlyCollection GetAssemblies()
diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs
index e763ec2ae0..b0eddedc91 100644
--- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs
+++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
@@ -117,12 +118,12 @@ public IReadOnlyCollection GetRelationships(Ex
return FilterFields(selector);
}
- private IReadOnlyCollection FilterFields(Expression> selector)
+ private ReadOnlyCollection FilterFields(Expression> selector)
where TResource : class, IIdentifiable
where TField : ResourceFieldAttribute
{
IReadOnlyCollection source = GetFieldsOfType();
- var matches = new List();
+ List matches = [];
foreach (string memberName in ToMemberNames(selector))
{
@@ -136,7 +137,7 @@ private IReadOnlyCollection FilterFields(Expression GetFieldsOfType()
diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs
index b0ebd8bb60..159ce5be69 100644
--- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs
+++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using System.Reflection;
using JetBrains.Annotations;
using JsonApiDotNetCore.Errors;
@@ -13,7 +14,7 @@ namespace JsonApiDotNetCore.Configuration;
/// Builds and configures the .
///
[PublicAPI]
-public class ResourceGraphBuilder
+public partial class ResourceGraphBuilder
{
private readonly IJsonApiOptions _options;
private readonly ILogger _logger;
@@ -34,11 +35,11 @@ public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactor
///
public IResourceGraph Build()
{
- HashSet resourceTypes = [.. _resourceTypesByClrType.Values];
+ IReadOnlySet resourceTypes = _resourceTypesByClrType.Values.ToHashSet().AsReadOnly();
- if (!resourceTypes.Any())
+ if (resourceTypes.Count == 0)
{
- _logger.LogWarning("The resource graph is empty.");
+ LogResourceGraphIsEmpty();
}
var resourceGraph = new ResourceGraph(resourceTypes);
@@ -91,18 +92,22 @@ private static void SetDirectlyDerivedTypes(ResourceGraph resourceGraph)
{
resourceType.BaseType = baseType;
- if (!directlyDerivedTypesPerBaseType.ContainsKey(baseType))
+ if (!directlyDerivedTypesPerBaseType.TryGetValue(baseType, out HashSet? directlyDerivedTypes))
{
- directlyDerivedTypesPerBaseType[baseType] = [];
+ directlyDerivedTypes = [];
+ directlyDerivedTypesPerBaseType[baseType] = directlyDerivedTypes;
}
- directlyDerivedTypesPerBaseType[baseType].Add(resourceType);
+ directlyDerivedTypes.Add(resourceType);
}
}
foreach ((ResourceType baseType, HashSet directlyDerivedTypes) in directlyDerivedTypesPerBaseType)
{
- baseType.DirectlyDerivedTypes = directlyDerivedTypes;
+ if (directlyDerivedTypes.Count > 0)
+ {
+ baseType.DirectlyDerivedTypes = directlyDerivedTypes.AsReadOnly();
+ }
}
}
@@ -227,8 +232,7 @@ public ResourceGraphBuilder Add(Type resourceClrType, Type? idClrType = null, st
{
if (resourceClrType.GetCustomAttribute() == null)
{
- _logger.LogWarning(
- $"Skipping: Type '{resourceClrType}' does not implement '{nameof(IIdentifiable)}'. Add [NoResource] to suppress this warning.");
+ LogResourceTypeDoesNotImplementInterface(resourceClrType, nameof(IIdentifiable));
}
}
@@ -239,9 +243,9 @@ private ResourceType CreateResourceType(string publicName, Type resourceClrType,
{
ClientIdGenerationMode? clientIdGeneration = GetClientIdGeneration(resourceClrType);
- IReadOnlyCollection attributes = GetAttributes(resourceClrType);
- IReadOnlyCollection relationships = GetRelationships(resourceClrType);
- IReadOnlyCollection eagerLoads = GetEagerLoads(resourceClrType);
+ Dictionary.ValueCollection attributes = GetAttributes(resourceClrType);
+ Dictionary.ValueCollection relationships = GetRelationships(resourceClrType);
+ ReadOnlyCollection eagerLoads = GetEagerLoads(resourceClrType);
AssertNoDuplicatePublicName(attributes, relationships);
@@ -259,7 +263,7 @@ private ResourceType CreateResourceType(string publicName, Type resourceClrType,
return resourceAttribute?.NullableClientIdGeneration;
}
- private IReadOnlyCollection GetAttributes(Type resourceClrType)
+ private Dictionary.ValueCollection GetAttributes(Type resourceClrType)
{
var attributesByName = new Dictionary();
@@ -296,13 +300,13 @@ private IReadOnlyCollection GetAttributes(Type resourceClrType)
if (attributesByName.Count < 2)
{
- _logger.LogWarning($"Type '{resourceClrType}' does not contain any attributes.");
+ LogResourceTypeHasNoAttributes(resourceClrType);
}
return attributesByName.Values;
}
- private IReadOnlyCollection GetRelationships(Type resourceClrType)
+ private Dictionary.ValueCollection GetRelationships(Type resourceClrType)
{
var relationshipsByName = new Dictionary();
PropertyInfo[] properties = resourceClrType.GetProperties();
@@ -376,11 +380,11 @@ private void SetHasManyRelationshipCapabilities(HasManyAttribute hasManyRelation
}
}
- private IReadOnlyCollection GetEagerLoads(Type resourceClrType, int recursionDepth = 0)
+ private ReadOnlyCollection GetEagerLoads(Type resourceClrType, int recursionDepth = 0)
{
AssertNoInfiniteRecursion(recursionDepth);
- var attributes = new List();
+ List eagerLoads = [];
PropertyInfo[] properties = resourceClrType.GetProperties();
foreach (PropertyInfo property in properties)
@@ -396,10 +400,10 @@ private IReadOnlyCollection GetEagerLoads(Type resourceClrTy
eagerLoad.Children = GetEagerLoads(innerType, recursionDepth + 1);
eagerLoad.Property = property;
- attributes.Add(eagerLoad);
+ eagerLoads.Add(eagerLoad);
}
- return attributes;
+ return eagerLoads.AsReadOnly();
}
private static void IncludeField(Dictionary fieldsByName, TField field)
@@ -475,4 +479,14 @@ private string FormatPropertyName(PropertyInfo resourceProperty)
? resourceProperty.Name
: _options.SerializerOptions.PropertyNamingPolicy.ConvertName(resourceProperty.Name);
}
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "The resource graph is empty.")]
+ private partial void LogResourceGraphIsEmpty();
+
+ [LoggerMessage(Level = LogLevel.Warning,
+ Message = "Skipping: Type '{ResourceType}' does not implement '{InterfaceType}'. Add [NoResource] to suppress this warning.")]
+ private partial void LogResourceTypeDoesNotImplementInterface(Type resourceType, string interfaceType);
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Type '{ResourceType}' does not contain any attributes.")]
+ private partial void LogResourceTypeHasNoAttributes(Type resourceType);
}
diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs
index 9f65b46b97..bce31946f1 100644
--- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs
+++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs
@@ -110,33 +110,6 @@ private static (Type implementationType, Type serviceInterface)? GetContainerReg
return null;
}
- ///
- /// Scans for types in the specified assembly that derive from the specified unbound generic type.
- ///
- ///
- /// The assembly to search for derived types.
- ///
- ///
- /// The unbound generic type to match against.
- ///
- ///
- /// Generic type arguments to construct .
- ///
- ///
- /// ), typeof(Article), typeof(int))
- /// ]]>
- ///
- public IReadOnlyCollection GetDerivedTypesForUnboundType(Assembly assembly, Type unboundType, params Type[] typeArguments)
- {
- ArgumentGuard.NotNull(assembly);
- ArgumentGuard.NotNull(unboundType);
- ArgumentGuard.NotNull(typeArguments);
-
- Type closedType = unboundType.MakeGenericType(typeArguments);
- return GetDerivedTypes(assembly, closedType).ToArray();
- }
-
///
/// Gets all derivatives of the specified type.
///
diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs
index eba4b8340c..b659fdc370 100644
--- a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs
+++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs
@@ -27,7 +27,7 @@ public sealed class DisableQueryStringAttribute : Attribute
///
public DisableQueryStringAttribute(JsonApiQueryStringParameters parameters)
{
- var parameterNames = new HashSet();
+ HashSet parameterNames = [];
foreach (JsonApiQueryStringParameters value in Enum.GetValues())
{
@@ -37,7 +37,7 @@ public DisableQueryStringAttribute(JsonApiQueryStringParameters parameters)
}
}
- ParameterNames = parameterNames;
+ ParameterNames = parameterNames.AsReadOnly();
}
///
@@ -48,7 +48,7 @@ public DisableQueryStringAttribute(string parameterNames)
{
ArgumentGuard.NotNullNorEmpty(parameterNames);
- ParameterNames = parameterNames.Split(",").ToHashSet();
+ ParameterNames = parameterNames.Split(",").ToHashSet().AsReadOnly();
}
public bool ContainsParameter(JsonApiQueryStringParameters parameter)
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
index bef962a971..d293a98701 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
@@ -134,7 +134,8 @@ public virtual async Task GetAsync([DisallowNull] TId id, Cancell
/// GET /articles/1/revisions HTTP/1.1
/// ]]>
///
- public virtual async Task GetSecondaryAsync([DisallowNull] TId id, string relationshipName, CancellationToken cancellationToken)
+ public virtual async Task GetSecondaryAsync([DisallowNull] TId id, [PreserveEmptyString] string relationshipName,
+ CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -142,7 +143,7 @@ public virtual async Task GetSecondaryAsync([DisallowNull] TId id
relationshipName
});
- ArgumentGuard.NotNullNorEmpty(relationshipName);
+ ArgumentGuard.NotNull(relationshipName);
if (_getSecondary == null)
{
@@ -163,7 +164,8 @@ public virtual async Task GetSecondaryAsync([DisallowNull] TId id
/// GET /articles/1/relationships/revisions HTTP/1.1
/// ]]>
///
- public virtual async Task GetRelationshipAsync([DisallowNull] TId id, string relationshipName, CancellationToken cancellationToken)
+ public virtual async Task GetRelationshipAsync([DisallowNull] TId id, [PreserveEmptyString] string relationshipName,
+ CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -171,7 +173,7 @@ public virtual async Task GetRelationshipAsync([DisallowNull] TId
relationshipName
});
- ArgumentGuard.NotNullNorEmpty(relationshipName);
+ ArgumentGuard.NotNull(relationshipName);
if (_getRelationship == null)
{
@@ -214,7 +216,7 @@ public virtual async Task PostAsync([FromBody] TResource resource
if (newResource == null)
{
- HttpContext.Response.Headers["Location"] = locationUrl;
+ HttpContext.Response.Headers.Location = locationUrl;
return NoContent();
}
@@ -247,7 +249,7 @@ private string GetLocationUrl(string resourceId)
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task PostRelationshipAsync([DisallowNull] TId id, string relationshipName,
+ public virtual async Task PostRelationshipAsync([DisallowNull] TId id, [PreserveEmptyString] string relationshipName,
[FromBody] ISet rightResourceIds, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
@@ -257,7 +259,7 @@ public virtual async Task PostRelationshipAsync([DisallowNull] TI
rightResourceIds
});
- ArgumentGuard.NotNullNorEmpty(relationshipName);
+ ArgumentGuard.NotNull(relationshipName);
ArgumentGuard.NotNull(rightResourceIds);
if (_addToRelationship == null)
@@ -322,8 +324,8 @@ public virtual async Task PatchAsync([DisallowNull] TId id, [From
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task PatchRelationshipAsync([DisallowNull] TId id, string relationshipName, [FromBody] object? rightValue,
- CancellationToken cancellationToken)
+ public virtual async Task PatchRelationshipAsync([DisallowNull] TId id, [PreserveEmptyString] string relationshipName,
+ [FromBody] object? rightValue, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -332,7 +334,7 @@ public virtual async Task PatchRelationshipAsync([DisallowNull] T
rightValue
});
- ArgumentGuard.NotNullNorEmpty(relationshipName);
+ ArgumentGuard.NotNull(relationshipName);
if (_setRelationship == null)
{
@@ -383,7 +385,7 @@ public virtual async Task DeleteAsync([DisallowNull] TId id, Canc
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task DeleteRelationshipAsync([DisallowNull] TId id, string relationshipName,
+ public virtual async Task DeleteRelationshipAsync([DisallowNull] TId id, [PreserveEmptyString] string relationshipName,
[FromBody] ISet rightResourceIds, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
@@ -393,7 +395,7 @@ public virtual async Task DeleteRelationshipAsync([DisallowNull]
rightResourceIds
});
- ArgumentGuard.NotNullNorEmpty(relationshipName);
+ ArgumentGuard.NotNull(relationshipName);
ArgumentGuard.NotNull(rightResourceIds);
if (_removeFromRelationship == null)
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
index b169bdd005..1ed6afec83 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
@@ -129,6 +129,8 @@ public virtual async Task PostOperationsAsync([FromBody] IList operations)
{
+ ArgumentGuard.NotNull(operations);
+
List errors = [];
for (int operationIndex = 0; operationIndex < operations.Count; operationIndex++)
@@ -191,13 +193,15 @@ private static string GetOperationCodeText(WriteOperationKind operationKind)
protected virtual void ValidateModelState(IList operations)
{
+ ArgumentGuard.NotNull(operations);
+
// We must validate the resource inside each operation manually, because they are typed as IIdentifiable.
// Instead of validating IIdentifiable we need to validate the resource runtime-type.
using IDisposable _ = new RevertRequestStateOnDispose(_request, _targetedFields);
int operationIndex = 0;
- var requestModelState = new List<(string key, ModelStateEntry? entry)>();
+ List<(string key, ModelStateEntry? entry)> requestModelState = [];
int maxErrorsRemaining = ModelState.MaxAllowedErrors;
foreach (OperationContainer operation in operations)
@@ -212,7 +216,7 @@ protected virtual void ValidateModelState(IList operations)
operationIndex++;
}
- if (requestModelState.Any())
+ if (requestModelState.Count > 0)
{
Dictionary modelStateDictionary = requestModelState.ToDictionary(tuple => tuple.key, tuple => tuple.entry);
diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs
index b4626ba031..332a9bf255 100644
--- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Mvc;
@@ -20,17 +21,17 @@ protected IActionResult Error(ErrorObject error)
protected IActionResult Error(IEnumerable errors)
{
- IReadOnlyList? errorList = ToErrorList(errors);
- ArgumentGuard.NotNullNorEmpty(errorList);
+ ReadOnlyCollection? errorCollection = ToCollection(errors);
+ ArgumentGuard.NotNullNorEmpty(errorCollection, nameof(errors));
- return new ObjectResult(errorList)
+ return new ObjectResult(errorCollection)
{
- StatusCode = (int)ErrorObject.GetResponseStatusCode(errorList)
+ StatusCode = (int)ErrorObject.GetResponseStatusCode(errorCollection)
};
}
- private static IReadOnlyList? ToErrorList(IEnumerable? errors)
+ private static ReadOnlyCollection? ToCollection(IEnumerable? errors)
{
- return errors?.ToArray();
+ return errors?.ToArray().AsReadOnly();
}
}
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs
index 290487cb76..cd58e2d18d 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs
@@ -15,16 +15,8 @@ namespace JsonApiDotNetCore.Controllers;
///
/// The resource identifier type.
///
-public abstract class JsonApiCommandController : JsonApiController
- where TResource : class, IIdentifiable
-{
- ///
- /// Creates an instance from a write-only service.
- ///
- protected JsonApiCommandController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
- IResourceCommandService commandService)
- : base(options, resourceGraph, loggerFactory, null, null, null, null, commandService, commandService, commandService, commandService, commandService,
- commandService)
- {
- }
-}
+public abstract class JsonApiCommandController(
+ IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceCommandService commandService)
+ : JsonApiController(options, resourceGraph, loggerFactory, null, null, null, null, commandService, commandService, commandService,
+ commandService, commandService, commandService)
+ where TResource : class, IIdentifiable;
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
index 846fedb28b..84e4dad3b5 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
@@ -6,6 +6,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
+#pragma warning disable format
+
namespace JsonApiDotNetCore.Controllers;
///
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs
index fa101c2118..6db14e9fde 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs
@@ -15,15 +15,7 @@ namespace JsonApiDotNetCore.Controllers;
///
/// The resource identifier type.
///
-public abstract class JsonApiQueryController : JsonApiController
- where TResource : class, IIdentifiable
-{
- ///
- /// Creates an instance from a read-only service.
- ///
- protected JsonApiQueryController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
- IResourceQueryService queryService)
- : base(options, resourceGraph, loggerFactory, queryService, queryService, queryService, queryService)
- {
- }
-}
+public abstract class JsonApiQueryController(
+ IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceQueryService queryService)
+ : JsonApiController(options, resourceGraph, loggerFactory, queryService, queryService, queryService, queryService)
+ where TResource : class, IIdentifiable;
diff --git a/src/JsonApiDotNetCore/Controllers/PreserveEmptyStringAttribute.cs b/src/JsonApiDotNetCore/Controllers/PreserveEmptyStringAttribute.cs
new file mode 100644
index 0000000000..d76f94ace1
--- /dev/null
+++ b/src/JsonApiDotNetCore/Controllers/PreserveEmptyStringAttribute.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+using JetBrains.Annotations;
+
+namespace JsonApiDotNetCore.Controllers;
+
+[PublicAPI]
+[AttributeUsage(AttributeTargets.Parameter)]
+public sealed class PreserveEmptyStringAttribute : DisplayFormatAttribute
+{
+ public PreserveEmptyStringAttribute()
+ {
+ // Workaround for https://github.com/dotnet/aspnetcore/issues/29948#issuecomment-1898216682
+ ConvertEmptyStringToNull = false;
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs
index 8fc75dad4e..48109b4c98 100644
--- a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs
+++ b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs
@@ -3,6 +3,8 @@
using System.Runtime.InteropServices;
using System.Text;
+#pragma warning disable CA2000 // Dispose objects before losing scope
+
namespace JsonApiDotNetCore.Diagnostics;
///
@@ -65,7 +67,7 @@ private void Close(MeasureScope scope)
_activeScopeStack.Pop();
- if (!_activeScopeStack.Any())
+ if (_activeScopeStack.Count == 0)
{
_completedScopes.Add(scope);
}
@@ -92,7 +94,7 @@ private int GetPaddingLength()
maxLength = Math.Max(maxLength, nextLength);
}
- if (_activeScopeStack.Any())
+ if (_activeScopeStack.Count > 0)
{
MeasureScope scope = _activeScopeStack.Peek();
int nextLength = scope.GetPaddingLength();
@@ -109,7 +111,7 @@ private void WriteResult(StringBuilder builder, int paddingLength)
scope.WriteResult(builder, 0, paddingLength);
}
- if (_activeScopeStack.Any())
+ if (_activeScopeStack.Count > 0)
{
MeasureScope scope = _activeScopeStack.Peek();
scope.WriteResult(builder, 0, paddingLength);
@@ -130,7 +132,7 @@ public void Dispose()
private sealed class MeasureScope : IDisposable
{
private readonly CascadingCodeTimer _owner;
- private readonly IList _children = new List();
+ private readonly List _children = [];
private readonly bool _excludeInRelativeCost;
private readonly TimeSpan _startedAt;
private TimeSpan? _stoppedAt;
diff --git a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs
index 5a862409bc..47f4007db0 100644
--- a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs
+++ b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs
@@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Diagnostics;
///
public static class CodeTimingSessionManager
{
- public static readonly bool IsEnabled;
+ public static readonly bool IsEnabled = GetDefaultIsEnabled();
private static ICodeTimerSession? _session;
public static ICodeTimer Current
@@ -28,12 +28,12 @@ public static ICodeTimer Current
}
}
- static CodeTimingSessionManager()
+ private static bool GetDefaultIsEnabled()
{
#if DEBUG
- IsEnabled = !IsRunningInTest() && !IsRunningInBenchmark();
+ return !IsRunningInTest() && !IsRunningInBenchmark();
#else
- IsEnabled = false;
+ return false;
#endif
}
diff --git a/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs
index a1662095cb..5737dbe3f2 100644
--- a/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs
+++ b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs
@@ -27,10 +27,14 @@ public DefaultCodeTimerSession()
private void AssertNotDisposed()
{
+#if NET6_0
if (_codeTimerInContext.Value == null)
{
throw new ObjectDisposedException(nameof(DefaultCodeTimerSession));
}
+#else
+ ObjectDisposedException.ThrowIf(_codeTimerInContext.Value == null, this);
+#endif
}
public void Dispose()
diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
index 396694b50a..478553f16b 100644
--- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
+++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
@@ -20,7 +20,7 @@ public sealed class InvalidModelStateException(
Func? getCollectionElementTypeCallback = null) : JsonApiException(FromModelStateDictionary(modelState, modelType, resourceGraph,
includeExceptionStackTraceInErrors, getCollectionElementTypeCallback))
{
- private static IEnumerable FromModelStateDictionary(IReadOnlyDictionary modelState, Type modelType,
+ private static List FromModelStateDictionary(IReadOnlyDictionary modelState, Type modelType,
IResourceGraph resourceGraph, bool includeExceptionStackTraceInErrors, Func? getCollectionElementTypeCallback)
{
ArgumentGuard.NotNull(modelState);
@@ -186,7 +186,7 @@ private static ErrorObject FromModelError(ModelError modelError, string? sourceP
Exception exception = modelError.Exception.Demystify();
string[] stackTraceLines = exception.ToString().Split(Environment.NewLine);
- if (stackTraceLines.Any())
+ if (stackTraceLines.Length > 0)
{
error.Meta ??= new Dictionary();
error.Meta["StackTrace"] = stackTraceLines;
@@ -246,7 +246,7 @@ protected ModelStateKeySegment(Type modelType, bool isInComplexType, string next
{
ArgumentGuard.NotNull(modelType);
- return _nextKey == string.Empty ? null : CreateSegment(modelType, _nextKey, isInComplexType, this, sourcePointer, GetCollectionElementTypeCallback);
+ return _nextKey.Length == 0 ? null : CreateSegment(modelType, _nextKey, isInComplexType, this, sourcePointer, GetCollectionElementTypeCallback);
}
public static ModelStateKeySegment Create(Type modelType, string key, Func? getCollectionElementTypeCallback)
@@ -271,7 +271,7 @@ private static ModelStateKeySegment CreateSegment(Type modelType, string key, bo
if (bracketCloseIndex != -1)
{
- segmentValue = key[1.. bracketCloseIndex];
+ segmentValue = key[1..bracketCloseIndex];
int nextKeyStartIndex = key.Length > bracketCloseIndex + 1 && key[bracketCloseIndex + 1] == Dot
? bracketCloseIndex + 2
diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs
index 25297a4056..0b667d532e 100644
--- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs
+++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs
@@ -2,6 +2,8 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
+#pragma warning disable format
+
namespace JsonApiDotNetCore.Errors;
///
diff --git a/src/JsonApiDotNetCore/Errors/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs
index 097b972089..1f15a127b4 100644
--- a/src/JsonApiDotNetCore/Errors/JsonApiException.cs
+++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -32,15 +33,15 @@ public JsonApiException(ErrorObject error, Exception? innerException = null)
public JsonApiException(IEnumerable errors, Exception? innerException = null)
: base(null, innerException)
{
- IReadOnlyList? errorList = ToErrorList(errors);
- ArgumentGuard.NotNullNorEmpty(errorList);
+ ReadOnlyCollection? errorCollection = ToCollection(errors);
+ ArgumentGuard.NotNullNorEmpty(errorCollection, nameof(errors));
- Errors = errorList;
+ Errors = errorCollection;
}
- private static IReadOnlyList? ToErrorList(IEnumerable? errors)
+ private static ReadOnlyCollection? ToCollection(IEnumerable? errors)
{
- return errors?.ToList();
+ return errors?.ToArray().AsReadOnly();
}
public string GetSummary()
diff --git a/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs b/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs
index 42082d6126..c2afd80cbf 100644
--- a/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs
+++ b/src/JsonApiDotNetCore/Errors/MissingResourceInRelationship.cs
@@ -11,9 +11,9 @@ public sealed class MissingResourceInRelationship
public MissingResourceInRelationship(string relationshipName, string resourceType, string resourceId)
{
- ArgumentGuard.NotNullNorEmpty(relationshipName);
- ArgumentGuard.NotNullNorEmpty(resourceType);
- ArgumentGuard.NotNullNorEmpty(resourceId);
+ ArgumentGuard.NotNull(relationshipName);
+ ArgumentGuard.NotNull(resourceType);
+ ArgumentGuard.NotNull(resourceId);
RelationshipName = relationshipName;
ResourceType = resourceType;
diff --git a/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs
index b5ab3859cc..16f79db822 100644
--- a/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs
+++ b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs
@@ -31,7 +31,7 @@ private static IEnumerable ToErrorObjects(ProblemDetails problemDet
HttpStatusCode status = problemDetails.Status != null ? (HttpStatusCode)problemDetails.Status.Value : HttpStatusCode.InternalServerError;
- if (problemDetails is HttpValidationProblemDetails validationProblemDetails && validationProblemDetails.Errors.Any())
+ if (problemDetails is HttpValidationProblemDetails { Errors.Count: > 0 } validationProblemDetails)
{
foreach (string errorMessage in validationProblemDetails.Errors.SelectMany(pair => pair.Value))
{
diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs
index 816c5ffbb7..0712f659ac 100644
--- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs
+++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs
@@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Middleware;
///
[PublicAPI]
-public class ExceptionHandler : IExceptionHandler
+public partial class ExceptionHandler : IExceptionHandler
{
private readonly IJsonApiOptions _options;
private readonly ILogger _logger;
@@ -40,7 +40,7 @@ private void LogException(Exception exception)
LogLevel level = GetLogLevel(exception);
string message = GetLogMessage(exception);
- _logger.Log(level, exception, message);
+ LogException(level, exception, message);
}
protected virtual LogLevel GetLogLevel(Exception exception)
@@ -74,21 +74,21 @@ protected virtual IReadOnlyList CreateErrorResponse(Exception excep
IReadOnlyList errors = exception switch
{
JsonApiException jsonApiException => jsonApiException.Errors,
- OperationCanceledException =>
- [
+ OperationCanceledException => new[]
+ {
new ErrorObject((HttpStatusCode)499)
{
Title = "Request execution was canceled."
}
- ],
- _ =>
- [
+ }.AsReadOnly(),
+ _ => new[]
+ {
new ErrorObject(HttpStatusCode.InternalServerError)
{
Title = "An unhandled error occurred while processing this request.",
Detail = exception.Message
}
- ]
+ }.AsReadOnly()
};
if (_options.IncludeExceptionStackTraceInErrors && exception is not InvalidModelStateException)
@@ -103,7 +103,7 @@ private void IncludeStackTraces(Exception exception, IReadOnlyList
{
string[] stackTraceLines = exception.ToString().Split(Environment.NewLine);
- if (stackTraceLines.Any())
+ if (stackTraceLines.Length > 0)
{
foreach (ErrorObject error in errors)
{
@@ -112,4 +112,7 @@ private void IncludeStackTraces(Exception exception, IReadOnlyList
}
}
}
+
+ [LoggerMessage(Message = "{Message}")]
+ private partial void LogException(LogLevel level, Exception exception, string message);
}
diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
index 4d9896ce8a..8983c00a6f 100644
--- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
+++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
@@ -18,7 +18,7 @@ namespace JsonApiDotNetCore.Middleware;
/// Intercepts HTTP requests to populate injected instance for JSON:API requests.
///
[PublicAPI]
-public sealed class JsonApiMiddleware
+public sealed partial class JsonApiMiddleware
{
private static readonly string[] NonOperationsContentTypes = [HeaderConstants.MediaType];
private static readonly MediaTypeHeaderValue[] NonOperationsMediaTypes = [MediaTypeHeaderValue.Parse(HeaderConstants.MediaType)];
@@ -36,40 +36,48 @@ public sealed class JsonApiMiddleware
];
private readonly RequestDelegate? _next;
+ private readonly IControllerResourceMapping _controllerResourceMapping;
+ private readonly IJsonApiOptions _options;
+ private readonly ILogger _logger;
- public JsonApiMiddleware(RequestDelegate? next, IHttpContextAccessor httpContextAccessor)
+ public JsonApiMiddleware(RequestDelegate? next, IHttpContextAccessor httpContextAccessor, IControllerResourceMapping controllerResourceMapping,
+ IJsonApiOptions options, ILogger logger)
{
ArgumentGuard.NotNull(httpContextAccessor);
+ ArgumentGuard.NotNull(controllerResourceMapping);
+ ArgumentGuard.NotNull(options);
+ ArgumentGuard.NotNull(logger);
_next = next;
+ _controllerResourceMapping = controllerResourceMapping;
+ _options = options;
+ _logger = logger;
+#pragma warning disable CA2000 // Dispose objects before losing scope
var session = new AspNetCodeTimerSession(httpContextAccessor);
+#pragma warning restore CA2000 // Dispose objects before losing scope
CodeTimingSessionManager.Capture(session);
}
- public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options,
- IJsonApiRequest request, ILogger logger)
+ public async Task InvokeAsync(HttpContext httpContext, IJsonApiRequest request)
{
ArgumentGuard.NotNull(httpContext);
- ArgumentGuard.NotNull(controllerResourceMapping);
- ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(request);
- ArgumentGuard.NotNull(logger);
using (CodeTimingSessionManager.Current.Measure("JSON:API middleware"))
{
- if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions))
+ if (!await ValidateIfMatchHeaderAsync(httpContext, _options.SerializerWriteOptions))
{
return;
}
RouteValueDictionary routeValues = httpContext.GetRouteData().Values;
- ResourceType? primaryResourceType = CreatePrimaryResourceType(httpContext, controllerResourceMapping);
+ ResourceType? primaryResourceType = CreatePrimaryResourceType(httpContext, _controllerResourceMapping);
if (primaryResourceType != null)
{
- if (!await ValidateContentTypeHeaderAsync(NonOperationsContentTypes, httpContext, options.SerializerWriteOptions) ||
- !await ValidateAcceptHeaderAsync(NonOperationsMediaTypes, httpContext, options.SerializerWriteOptions))
+ if (!await ValidateContentTypeHeaderAsync(NonOperationsContentTypes, httpContext, _options.SerializerWriteOptions) ||
+ !await ValidateAcceptHeaderAsync(NonOperationsMediaTypes, httpContext, _options.SerializerWriteOptions))
{
return;
}
@@ -80,8 +88,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
}
else if (IsRouteForOperations(routeValues))
{
- if (!await ValidateContentTypeHeaderAsync(OperationsContentTypes, httpContext, options.SerializerWriteOptions) ||
- !await ValidateAcceptHeaderAsync(OperationsMediaTypes, httpContext, options.SerializerWriteOptions))
+ if (!await ValidateContentTypeHeaderAsync(OperationsContentTypes, httpContext, _options.SerializerWriteOptions) ||
+ !await ValidateAcceptHeaderAsync(OperationsMediaTypes, httpContext, _options.SerializerWriteOptions))
{
return;
}
@@ -100,12 +108,12 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
}
}
- if (CodeTimingSessionManager.IsEnabled)
+ if (CodeTimingSessionManager.IsEnabled && _logger.IsEnabled(LogLevel.Information))
{
string timingResults = CodeTimingSessionManager.Current.GetResults();
- string url = httpContext.Request.GetDisplayUrl();
- string method = httpContext.Request.Method.Replace(Environment.NewLine, "");
- logger.LogInformation($"Measurement results for {method} {url}:{Environment.NewLine}{timingResults}");
+ string requestMethod = httpContext.Request.Method.Replace(Environment.NewLine, "");
+ string requestUrl = httpContext.Request.GetEncodedUrl();
+ LogMeasurement(requestMethod, requestUrl, Environment.NewLine, timingResults);
}
}
@@ -138,7 +146,7 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso
: null;
}
- private static async Task ValidateContentTypeHeaderAsync(ICollection allowedContentTypes, HttpContext httpContext,
+ private static async Task ValidateContentTypeHeaderAsync(string[] allowedContentTypes, HttpContext httpContext,
JsonSerializerOptions serializerOptions)
{
string? contentType = httpContext.Request.ContentType;
@@ -163,12 +171,12 @@ private static async Task ValidateContentTypeHeaderAsync(ICollection ValidateAcceptHeaderAsync(ICollection allowedMediaTypes, HttpContext httpContext,
+ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue[] allowedMediaTypes, HttpContext httpContext,
JsonSerializerOptions serializerOptions)
{
string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept");
- if (!acceptHeaders.Any())
+ if (acceptHeaders.Length == 0)
{
return true;
}
@@ -308,4 +316,8 @@ private static void SetupOperationsRequest(JsonApiRequest request)
request.IsReadOnly = false;
request.Kind = EndpointKind.AtomicOperations;
}
+
+ [LoggerMessage(Level = LogLevel.Information, SkipEnabledCheck = true,
+ Message = "Measurement results for {RequestMethod} {RequestUrl}:{LineBreak}{TimingResults}")]
+ private partial void LogMeasurement(string requestMethod, string requestUrl, string lineBreak, string timingResults);
}
diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs
index c2df736e96..d0b4ec13b4 100644
--- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs
+++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs
@@ -27,7 +27,7 @@ namespace JsonApiDotNetCore.Middleware;
/// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustoms/relationship/relatedResource
/// ]]>
[PublicAPI]
-public sealed class JsonApiRoutingConvention : IJsonApiRoutingConvention
+public sealed partial class JsonApiRoutingConvention : IJsonApiRoutingConvention
{
private readonly IJsonApiOptions _options;
private readonly IResourceGraph _resourceGraph;
@@ -83,8 +83,7 @@ public void Apply(ApplicationModel application)
// ProblemDetails, where the origin of the error gets lost. As a result, we can't populate the source pointer in JSON:API error responses.
// For backwards-compatibility, we log a warning instead of throwing. But we can't think of any use cases where having [ApiController] makes sense.
- _logger.LogWarning(
- $"Found JSON:API controller '{controller.ControllerType}' with [ApiController]. Please remove this attribute for optimal JSON:API compliance.");
+ LogApiControllerAttributeFound(controller.ControllerType);
}
if (!IsOperationsController(controller.ControllerType))
@@ -214,4 +213,8 @@ private static bool IsOperationsController(Type type)
Type baseControllerType = typeof(BaseJsonApiOperationsController);
return baseControllerType.IsAssignableFrom(type);
}
+
+ [LoggerMessage(Level = LogLevel.Warning,
+ Message = "Found JSON:API controller '{ControllerType}' with [ApiController]. Please remove this attribute for optimal JSON:API compliance.")]
+ private partial void LogApiControllerAttributeFound(TypeInfo controllerType);
}
diff --git a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs
index e17f02ed21..b78670286e 100644
--- a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs
+++ b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs
@@ -118,43 +118,29 @@ public override void Write(Utf8JsonWriter writer, TWrapper value, JsonSerializer
}
}
-internal sealed class TraceLogWriter(ILoggerFactory loggerFactory) : TraceLogWriter
+internal sealed partial class TraceLogWriter(ILoggerFactory loggerFactory) : TraceLogWriter
{
private readonly ILogger _logger = loggerFactory.CreateLogger(typeof(T));
- private bool IsEnabled => _logger.IsEnabled(LogLevel.Trace);
-
public void LogMethodStart(object? parameters = null, [CallerMemberName] string memberName = "")
{
- if (IsEnabled)
+ if (_logger.IsEnabled(LogLevel.Trace))
{
- string message = FormatMessage(memberName, parameters);
- WriteMessageToLog(message);
- }
- }
+ var builder = new StringBuilder();
+ WriteProperties(builder, parameters);
+ string parameterValues = builder.ToString();
- public void LogMessage(Func messageFactory)
- {
- if (IsEnabled)
- {
- string message = messageFactory();
- WriteMessageToLog(message);
+ if (parameterValues.Length == 0)
+ {
+ LogEnteringMember(memberName);
+ }
+ else
+ {
+ LogEnteringMemberWithParameters(memberName, parameterValues);
+ }
}
}
- private static string FormatMessage(string memberName, object? parameters)
- {
- var builder = new StringBuilder();
-
- builder.Append("Entering ");
- builder.Append(memberName);
- builder.Append('(');
- WriteProperties(builder, parameters);
- builder.Append(')');
-
- return builder.ToString();
- }
-
private static void WriteProperties(StringBuilder builder, object? propertyContainer)
{
if (propertyContainer != null)
@@ -218,8 +204,9 @@ private static string SerializeObject(object? value)
}
}
- private void WriteMessageToLog(string message)
- {
- _logger.LogTrace(message);
- }
+ [LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "Entering {MemberName}({ParameterValues})")]
+ private partial void LogEnteringMemberWithParameters(string memberName, string parameterValues);
+
+ [LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "Entering {MemberName}()")]
+ private partial void LogEnteringMember(string memberName);
}
diff --git a/src/JsonApiDotNetCore/Queries/EvaluatedIncludeCache.cs b/src/JsonApiDotNetCore/Queries/EvaluatedIncludeCache.cs
index 7c9d983d63..ea54da96af 100644
--- a/src/JsonApiDotNetCore/Queries/EvaluatedIncludeCache.cs
+++ b/src/JsonApiDotNetCore/Queries/EvaluatedIncludeCache.cs
@@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Queries;
///
internal sealed class EvaluatedIncludeCache : IEvaluatedIncludeCache
{
- private readonly IEnumerable _constraintProviders;
+ private readonly IQueryConstraintProvider[] _constraintProviders;
private IncludeExpression? _include;
private bool _isAssigned;
@@ -13,7 +13,7 @@ public EvaluatedIncludeCache(IEnumerable constraintPro
{
ArgumentGuard.NotNull(constraintProviders);
- _constraintProviders = constraintProviders;
+ _constraintProviders = constraintProviders as IQueryConstraintProvider[] ?? constraintProviders.ToArray();
}
///
diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs
index f88cb60a86..5915886e72 100644
--- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs
+++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs
@@ -31,7 +31,7 @@ public IReadOnlyCollection GetRelationshipChains(I
{
ArgumentGuard.NotNull(include);
- if (!include.Elements.Any())
+ if (include.Elements.Count == 0)
{
return Array.Empty();
}
@@ -39,7 +39,7 @@ public IReadOnlyCollection GetRelationshipChains(I
var converter = new IncludeToChainsConverter();
converter.Visit(include, null);
- return converter.Chains;
+ return converter.Chains.AsReadOnly();
}
private sealed class IncludeToChainsConverter : QueryExpressionVisitor
@@ -60,7 +60,7 @@ private sealed class IncludeToChainsConverter : QueryExpressionVisitor 0)
{
builder.Append('{');
builder.Append(string.Join(",", Children.Select(child => toFullString ? child.ToFullString() : child.ToString()).OrderBy(name => name)));
diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs
index 7e39ca5735..b6e182a714 100644
--- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs
+++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs
@@ -29,7 +29,7 @@ public class PaginationElementQueryStringValueExpression(ResourceFieldChainExpre
public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument)
{
- return visitor.PaginationElementQueryStringValue(this, argument);
+ return visitor.VisitPaginationElementQueryStringValue(this, argument);
}
public override string ToString()
diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs
index f16ca0cbd6..2a70ea7d8c 100644
--- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs
+++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs
@@ -27,7 +27,7 @@ public PaginationQueryStringValueExpression(IImmutableList(QueryExpressionVisitor visitor, TArgument argument)
{
- return visitor.PaginationQueryStringValue(this, argument);
+ return visitor.VisitPaginationQueryStringValue(this, argument);
}
public override string ToString()
diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs
index 4439e37c21..173c77503c 100644
--- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs
+++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs
@@ -2,6 +2,8 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+#pragma warning disable IDE0019 // Use pattern matching
+
namespace JsonApiDotNetCore.Queries.Expressions;
///
@@ -214,7 +216,7 @@ public override QueryExpression VisitSparseFieldSet(SparseFieldSetExpression exp
return null;
}
- public override QueryExpression PaginationQueryStringValue(PaginationQueryStringValueExpression expression, TArgument argument)
+ public override QueryExpression VisitPaginationQueryStringValue(PaginationQueryStringValueExpression expression, TArgument argument)
{
IImmutableList newElements = VisitList(expression.Elements, argument);
@@ -222,7 +224,7 @@ public override QueryExpression PaginationQueryStringValue(PaginationQueryString
return newExpression.Equals(expression) ? expression : newExpression;
}
- public override QueryExpression PaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, TArgument argument)
+ public override QueryExpression VisitPaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, TArgument argument)
{
ResourceFieldChainExpression? newScope = expression.Scope != null ? Visit(expression.Scope, argument) as ResourceFieldChainExpression : null;
diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs
index 12242c29ff..a0472306f7 100644
--- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs
+++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs
@@ -109,12 +109,12 @@ public virtual TResult VisitQueryStringParameterScope(QueryStringParameterScopeE
return DefaultVisit(expression, argument);
}
- public virtual TResult PaginationQueryStringValue(PaginationQueryStringValueExpression expression, TArgument argument)
+ public virtual TResult VisitPaginationQueryStringValue(PaginationQueryStringValueExpression expression, TArgument argument)
{
return DefaultVisit(expression, argument);
}
- public virtual TResult PaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, TArgument argument)
+ public virtual TResult VisitPaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, TArgument argument)
{
return DefaultVisit(expression, argument);
}
diff --git a/src/JsonApiDotNetCore/Queries/FieldSelection.cs b/src/JsonApiDotNetCore/Queries/FieldSelection.cs
index 7f62db1fcf..53cf38268e 100644
--- a/src/JsonApiDotNetCore/Queries/FieldSelection.cs
+++ b/src/JsonApiDotNetCore/Queries/FieldSelection.cs
@@ -16,7 +16,7 @@ public sealed class FieldSelection : Dictionary
public IReadOnlySet