Skip to content

Commit d168bb8

Browse files
committed
Set readOnly status for properties
1 parent 4c8b5fe commit d168bb8

File tree

5 files changed

+64
-0
lines changed

5 files changed

+64
-0
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,4 +388,22 @@ internal static void ApplyNullabilityContextInfo(this JsonNode schema, JsonPrope
388388
schema[OpenApiSchemaKeywords.NullableKeyword] = true;
389389
}
390390
}
391+
392+
/// <summary>
393+
/// Apply `readOnly` to the schema based on the presence of the <see cref="ReadOnlyAttribute"/> or whether the
394+
/// property exposes a setter.
395+
/// </summary>
396+
/// <param name="schema"></param>
397+
/// <param name="attributeProvider"></param>
398+
internal static void ApplyReadOnly(this JsonNode schema, ICustomAttributeProvider attributeProvider)
399+
{
400+
if (attributeProvider is PropertyInfo jsonPropertyInfo)
401+
{
402+
schema[OpenApiSchemaKeywords.ReadOnlyKeyword] = !jsonPropertyInfo.CanWrite;
403+
}
404+
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<ReadOnlyAttribute>().SingleOrDefault() is { } readOnlyAttribute)
405+
{
406+
schema[OpenApiSchemaKeywords.ReadOnlyKeyword] = readOnlyAttribute.IsReadOnly;
407+
}
408+
}
391409
}

src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
294294
reader.Read();
295295
schema.Extensions.Add(OpenApiConstants.SchemaId, new OpenApiString(reader.GetString()));
296296
break;
297+
case OpenApiSchemaKeywords.ReadOnlyKeyword:
298+
reader.Read();
299+
schema.ReadOnly = reader.GetBoolean();
300+
break;
297301
default:
298302
reader.Skip();
299303
break;

src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ internal class OpenApiSchemaKeywords
2525
public const string MaxItemsKeyword = "maxItems";
2626
public const string RefKeyword = "$ref";
2727
public const string SchemaIdKeyword = "x-schema-id";
28+
public const string ReadOnlyKeyword = "readOnly";
2829
}

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ internal sealed class OpenApiSchemaService(
9393
if (context.PropertyInfo is { AttributeProvider: { } attributeProvider } jsonPropertyInfo)
9494
{
9595
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
96+
schema.ApplyReadOnly(attributeProvider);
9697
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<ValidationAttribute>() is { } validationAttributes)
9798
{
9899
schema.ApplyValidationAttributes(validationAttributes);

src/OpenApi/test/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.RequestBody.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.ComponentModel;
45
using System.IO.Pipelines;
56
using Microsoft.AspNetCore.Builder;
67
using Microsoft.AspNetCore.Http;
@@ -1039,4 +1040,43 @@ static void VerifyDocument(OpenApiDocument document)
10391040
private void ActionWithStream(Stream stream) { }
10401041
[Route("/pipereader")]
10411042
private void ActionWithPipeReader(PipeReader pipeReader) { }
1043+
1044+
[Fact]
1045+
public async Task GetOpenApiRequestBody_AppliesReadOnlyOnProperties()
1046+
{
1047+
// Arrange
1048+
var builder = CreateBuilder();
1049+
1050+
// Act
1051+
builder.MapPost("/", (TypeWithReadOnlyProperties model) => { });
1052+
1053+
// Assert
1054+
await VerifyOpenApiDocument(builder, document =>
1055+
{
1056+
var paths = Assert.Single(document.Paths.Values);
1057+
var operation = paths.Operations[OperationType.Post];
1058+
var content = Assert.Single(operation.RequestBody.Content);
1059+
var schema = content.Value.Schema;
1060+
Assert.NotNull(schema.Properties);
1061+
Assert.Collection(schema.Properties,
1062+
property =>
1063+
{
1064+
Assert.Equal("computedProperty", property.Key);
1065+
Assert.True(property.Value.ReadOnly);
1066+
},
1067+
property =>
1068+
{
1069+
Assert.Equal("readOnlyProperty", property.Key);
1070+
Assert.True(property.Value.ReadOnly);
1071+
});
1072+
});
1073+
}
1074+
1075+
private class TypeWithReadOnlyProperties
1076+
{
1077+
public int ComputedProperty { get; } = 42;
1078+
1079+
[ReadOnly(true)]
1080+
public int ReadOnlyProperty { get; set; }
1081+
}
10421082
}

0 commit comments

Comments
 (0)