Skip to content

Commit 9720579

Browse files
Update media type for JSON Patch (#62988)
- Use `Content-Type: application/json-patch+json` for JSON patch. - Fix some code analyzer suggestions. - Fix broken `build.cmd` for JsonPatch. Adapted from #62057. Resolves #61956.
1 parent 407f7ce commit 9720579

11 files changed

+145
-25
lines changed

src/Features/JsonPatch.SystemTextJson/src/JsonPatchDocument.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Reflection;
67
using System.Text.Json;
78
using System.Text.Json.Serialization;
9+
using Microsoft.AspNetCore.Builder;
10+
using Microsoft.AspNetCore.Http.Metadata;
811
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
912
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
1013
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Exceptions;
@@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson;
1821
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
1922
// .NET or architecture doesn't contain a shared DTO layer.
2023
[JsonConverter(typeof(JsonPatchDocumentConverter))]
21-
public class JsonPatchDocument : IJsonPatchDocument
24+
public class JsonPatchDocument : IJsonPatchDocument, IEndpointParameterMetadataProvider
2225
{
2326
public List<Operation> Operations { get; private set; }
2427

@@ -27,7 +30,7 @@ public class JsonPatchDocument : IJsonPatchDocument
2730

2831
public JsonPatchDocument()
2932
{
30-
Operations = new List<Operation>();
33+
Operations = [];
3134
SerializerOptions = JsonSerializerOptions.Default;
3235
}
3336

@@ -205,17 +208,27 @@ IList<Operation> IJsonPatchDocument.GetOperations()
205208
{
206209
foreach (var op in Operations)
207210
{
208-
var untypedOp = new Operation();
209-
210-
untypedOp.op = op.op;
211-
untypedOp.value = op.value;
212-
untypedOp.path = op.path;
213-
untypedOp.from = op.from;
211+
var untypedOp = new Operation
212+
{
213+
op = op.op,
214+
value = op.value,
215+
path = op.path,
216+
from = op.from
217+
};
214218

215219
allOps.Add(untypedOp);
216220
}
217221
}
218222

219223
return allOps;
220224
}
225+
226+
/// <inheritdoc/>
227+
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
228+
{
229+
ArgumentNullException.ThrowIfNull(parameter);
230+
ArgumentNullException.ThrowIfNull(builder);
231+
232+
builder.Metadata.Add(new AcceptsMetadata(["application/json-patch+json"], parameter.ParameterType));
233+
}
221234
}

src/Features/JsonPatch.SystemTextJson/src/JsonPatchDocumentOfT.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using System.Reflection;
1010
using System.Text.Json;
1111
using System.Text.Json.Serialization;
12+
using Microsoft.AspNetCore.Builder;
13+
using Microsoft.AspNetCore.Http.Metadata;
1214
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Adapters;
1315
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Converters;
1416
using Microsoft.AspNetCore.JsonPatch.SystemTextJson.Exceptions;
@@ -23,7 +25,7 @@ namespace Microsoft.AspNetCore.JsonPatch.SystemTextJson;
2325
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
2426
// not according to RFC 6902, and would thus break cross-platform compatibility.
2527
[JsonConverter(typeof(JsonPatchDocumentConverterFactory))]
26-
public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
28+
public class JsonPatchDocument<TModel> : IJsonPatchDocument, IEndpointParameterMetadataProvider where TModel : class
2729
{
2830
public List<Operation<TModel>> Operations { get; private set; }
2931

@@ -32,7 +34,7 @@ public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
3234

3335
public JsonPatchDocument()
3436
{
35-
Operations = new List<Operation<TModel>>();
37+
Operations = [];
3638
SerializerOptions = JsonSerializerOptions.Default;
3739
}
3840

@@ -657,11 +659,20 @@ IList<Operation> IJsonPatchDocument.GetOperations()
657659
return allOps;
658660
}
659661

662+
/// <inheritdoc/>
663+
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
664+
{
665+
ArgumentNullException.ThrowIfNull(parameter);
666+
ArgumentNullException.ThrowIfNull(builder);
667+
668+
builder.Metadata.Add(new AcceptsMetadata(["application/json-patch+json"], typeof(TModel)));
669+
}
670+
660671
// Internal for testing
661672
internal string GetPath<TProp>(Expression<Func<TModel, TProp>> expr, string position)
662673
{
663674
var segments = GetPathSegments(expr.Body);
664-
var path = String.Join("/", segments);
675+
var path = string.Join('/', segments);
665676
if (position != null)
666677
{
667678
path += "/" + position;

src/Features/JsonPatch.SystemTextJson/src/Microsoft.AspNetCore.JsonPatch.SystemTextJson.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
<Compile Include="$(SharedSourceRoot)CallerArgument\CallerArgumentExpressionAttribute.cs" LinkBase="Shared" />
1717
</ItemGroup>
1818

19+
<ItemGroup>
20+
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
21+
</ItemGroup>
22+
1923
<ItemGroup>
2024
<InternalsVisibleTo Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson.Tests" />
2125
</ItemGroup>

src/Features/JsonPatch/build.cmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
@ECHO OFF
2-
SET RepoRoot=%~dp0..\..
2+
SET RepoRoot=%~dp0..\..\..
33
%RepoRoot%\eng\build.cmd -projects %~dp0**\*.*proj %*

src/Features/JsonPatch/src/JsonPatchDocument.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Reflection;
67
using Microsoft.AspNetCore.JsonPatch.Adapters;
78
using Microsoft.AspNetCore.JsonPatch.Converters;
89
using Microsoft.AspNetCore.JsonPatch.Exceptions;
@@ -12,13 +13,22 @@
1213
using Newtonsoft.Json;
1314
using Newtonsoft.Json.Serialization;
1415

16+
#if NET
17+
using Microsoft.AspNetCore.Builder;
18+
using Microsoft.AspNetCore.Http.Metadata;
19+
#endif
20+
1521
namespace Microsoft.AspNetCore.JsonPatch;
1622

1723
// Implementation details: the purpose of this type of patch document is to allow creation of such
1824
// documents for cases where there's no class/DTO to work on. Typical use case: backend not built in
1925
// .NET or architecture doesn't contain a shared DTO layer.
2026
[JsonConverter(typeof(JsonPatchDocumentConverter))]
27+
#if NET
28+
public class JsonPatchDocument : IJsonPatchDocument, IEndpointParameterMetadataProvider
29+
#else
2130
public class JsonPatchDocument : IJsonPatchDocument
31+
#endif
2232
{
2333
public List<Operation> Operations { get; private set; }
2434

@@ -27,7 +37,7 @@ public class JsonPatchDocument : IJsonPatchDocument
2737

2838
public JsonPatchDocument()
2939
{
30-
Operations = new List<Operation>();
40+
Operations = [];
3141
ContractResolver = new DefaultContractResolver();
3242
}
3343

@@ -205,17 +215,29 @@ IList<Operation> IJsonPatchDocument.GetOperations()
205215
{
206216
foreach (var op in Operations)
207217
{
208-
var untypedOp = new Operation();
209-
210-
untypedOp.op = op.op;
211-
untypedOp.value = op.value;
212-
untypedOp.path = op.path;
213-
untypedOp.from = op.from;
218+
var untypedOp = new Operation
219+
{
220+
op = op.op,
221+
value = op.value,
222+
path = op.path,
223+
from = op.from
224+
};
214225

215226
allOps.Add(untypedOp);
216227
}
217228
}
218229

219230
return allOps;
220231
}
232+
233+
#if NET
234+
/// <inheritdoc/>
235+
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
236+
{
237+
ArgumentNullException.ThrowIfNull(parameter);
238+
ArgumentNullException.ThrowIfNull(builder);
239+
240+
builder.Metadata.Add(new AcceptsMetadata(["application/json-patch+json"], parameter.ParameterType));
241+
}
242+
#endif
221243
}

src/Features/JsonPatch/src/JsonPatchDocumentOfT.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Globalization;
77
using System.Linq;
88
using System.Linq.Expressions;
9+
using System.Reflection;
910
using Microsoft.AspNetCore.JsonPatch.Adapters;
1011
using Microsoft.AspNetCore.JsonPatch.Converters;
1112
using Microsoft.AspNetCore.JsonPatch.Exceptions;
@@ -15,14 +16,23 @@
1516
using Newtonsoft.Json;
1617
using Newtonsoft.Json.Serialization;
1718

19+
#if NET
20+
using Microsoft.AspNetCore.Builder;
21+
using Microsoft.AspNetCore.Http.Metadata;
22+
#endif
23+
1824
namespace Microsoft.AspNetCore.JsonPatch;
1925

2026
// Implementation details: the purpose of this type of patch document is to ensure we can do type-checking
2127
// when producing a JsonPatchDocument. However, we cannot send this "typed" over the wire, as that would require
2228
// including type data in the JsonPatchDocument serialized as JSON (to allow for correct deserialization) - that's
2329
// not according to RFC 6902, and would thus break cross-platform compatibility.
2430
[JsonConverter(typeof(TypedJsonPatchDocumentConverter))]
31+
#if NET
32+
public class JsonPatchDocument<TModel> : IJsonPatchDocument, IEndpointParameterMetadataProvider where TModel : class
33+
#else
2534
public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
35+
#endif
2636
{
2737
public List<Operation<TModel>> Operations { get; private set; }
2838

@@ -31,7 +41,7 @@ public class JsonPatchDocument<TModel> : IJsonPatchDocument where TModel : class
3141

3242
public JsonPatchDocument()
3343
{
34-
Operations = new List<Operation<TModel>>();
44+
Operations = [];
3545
ContractResolver = new DefaultContractResolver();
3646
}
3747

@@ -656,11 +666,22 @@ IList<Operation> IJsonPatchDocument.GetOperations()
656666
return allOps;
657667
}
658668

669+
#if NET
670+
/// <inheritdoc/>
671+
static void IEndpointParameterMetadataProvider.PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
672+
{
673+
ArgumentNullException.ThrowIfNull(parameter);
674+
ArgumentNullException.ThrowIfNull(builder);
675+
676+
builder.Metadata.Add(new AcceptsMetadata(["application/json-patch+json"], typeof(TModel)));
677+
}
678+
#endif
679+
659680
// Internal for testing
660681
internal string GetPath<TProp>(Expression<Func<TModel, TProp>> expr, string position)
661682
{
662683
var segments = GetPathSegments(expr.Body);
663-
var path = String.Join("/", segments);
684+
var path = string.Join("/", segments);
664685
if (position != null)
665686
{
666687
path += "/" + position;
@@ -712,8 +733,7 @@ private List<string> GetPathSegments(Expression expr)
712733

713734
private string GetPropertyNameFromMemberExpression(MemberExpression memberExpression)
714735
{
715-
var jsonObjectContract = ContractResolver.ResolveContract(memberExpression.Expression.Type) as JsonObjectContract;
716-
if (jsonObjectContract != null)
736+
if (ContractResolver.ResolveContract(memberExpression.Expression.Type) is JsonObjectContract jsonObjectContract)
717737
{
718738
return jsonObjectContract.Properties
719739
.First(jsonProperty => jsonProperty.UnderlyingName == memberExpression.Member.Name)

src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<ItemGroup>
2525
<Reference Include="Microsoft.CSharp" Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'" />
2626
<Reference Include="Newtonsoft.Json" />
27+
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
2728
</ItemGroup>
2829

2930
<ItemGroup>

src/OpenApi/sample/Endpoints/MapSchemasEndpoints.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Immutable;
55
using System.ComponentModel;
66
using Microsoft.AspNetCore.Http.HttpResults;
7+
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
78

89
public static class SchemasEndpointsExtensions
910
{
@@ -36,6 +37,7 @@ public static IEndpointRouteBuilder MapSchemasEndpoints(this IEndpointRouteBuild
3637
schemas.MapPost("/location", (LocationContainer location) => { });
3738
schemas.MapPost("/parent", (ParentObject parent) => Results.Ok(parent));
3839
schemas.MapPost("/child", (ChildObject child) => Results.Ok(child));
40+
schemas.MapPatch("/json-patch", (JsonPatchDocument<ParentObject> patchDoc) => Results.NoContent());
3941

4042
return endpointRouteBuilder;
4143
}

src/OpenApi/sample/Sample.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
<Reference Include="Microsoft.AspNetCore" />
1717
<Reference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
1818
<Reference Include="Microsoft.AspNetCore.Hosting" />
19-
<Reference Include="Microsoft.AspNetCore.OpenApi" />
2019
<Reference Include="Microsoft.AspNetCore.Http" />
2120
<Reference Include="Microsoft.AspNetCore.Http.Results" />
21+
<Reference Include="Microsoft.AspNetCore.JsonPatch.SystemTextJson" />
22+
<Reference Include="Microsoft.AspNetCore.Mvc" />
23+
<Reference Include="Microsoft.AspNetCore.OpenApi" />
2224
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
2325
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
24-
<Reference Include="Microsoft.AspNetCore.Mvc" />
2526
</ItemGroup>
2627

2728
<ItemGroup>

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,28 @@
507507
}
508508
}
509509
}
510+
},
511+
"/schemas-by-ref/json-patch": {
512+
"patch": {
513+
"tags": [
514+
"Sample"
515+
],
516+
"requestBody": {
517+
"content": {
518+
"application/json-patch+json": {
519+
"schema": {
520+
"$ref": "#/components/schemas/JsonPatchDocumentOfParentObject"
521+
}
522+
}
523+
},
524+
"required": true
525+
},
526+
"responses": {
527+
"200": {
528+
"description": "OK"
529+
}
530+
}
531+
}
510532
}
511533
},
512534
"components": {
@@ -599,6 +621,7 @@
599621
}
600622
}
601623
},
624+
"JsonPatchDocumentOfParentObject": { },
602625
"LocationContainer": {
603626
"required": [
604627
"location"

0 commit comments

Comments
 (0)