Skip to content

Commit dbe3bae

Browse files
committed
Set readOnly status for properties
1 parent ecdc518 commit dbe3bae

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
@@ -394,4 +394,22 @@ internal static void ApplyNullabilityContextInfo(this JsonNode schema, JsonPrope
394394
schema[OpenApiSchemaKeywords.NullableKeyword] = true;
395395
}
396396
}
397+
398+
/// <summary>
399+
/// Apply `readOnly` to the schema based on the presence of the <see cref="ReadOnlyAttribute"/> or whether the
400+
/// property exposes a setter.
401+
/// </summary>
402+
/// <param name="schema"></param>
403+
/// <param name="attributeProvider"></param>
404+
internal static void ApplyReadOnly(this JsonNode schema, ICustomAttributeProvider attributeProvider)
405+
{
406+
if (attributeProvider is PropertyInfo jsonPropertyInfo)
407+
{
408+
schema[OpenApiSchemaKeywords.ReadOnlyKeyword] = !jsonPropertyInfo.CanWrite;
409+
}
410+
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<ReadOnlyAttribute>().SingleOrDefault() is { } readOnlyAttribute)
411+
{
412+
schema[OpenApiSchemaKeywords.ReadOnlyKeyword] = readOnlyAttribute.IsReadOnly;
413+
}
414+
}
397415
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName,
313313
schema.Enum = [ReadOpenApiAny(ref reader, out var constType)];
314314
schema.Type = constType;
315315
break;
316+
case OpenApiSchemaKeywords.ReadOnlyKeyword:
317+
reader.Read();
318+
schema.ReadOnly = reader.GetBoolean();
319+
break;
316320
default:
317321
reader.Skip();
318322
break;

src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ internal class OpenApiSchemaKeywords
2626
public const string RefKeyword = "$ref";
2727
public const string SchemaIdKeyword = "x-schema-id";
2828
public const string ConstKeyword = "const";
29+
public const string ReadOnlyKeyword = "readOnly";
2930
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ internal sealed class OpenApiSchemaService(
9494
if (context.PropertyInfo is { AttributeProvider: { } attributeProvider } jsonPropertyInfo)
9595
{
9696
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
97+
schema.ApplyReadOnly(attributeProvider);
9798
if (attributeProvider.GetCustomAttributes(inherit: false).OfType<ValidationAttribute>() is { } validationAttributes)
9899
{
99100
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)