Skip to content

Commit

Permalink
Add support for required keyword (#2810)
Browse files Browse the repository at this point in the history
Add support for `required` keyword.
  • Loading branch information
keahpeters committed Apr 18, 2024
1 parent 1b7957b commit 8a23f0a
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,25 @@ private OpenApiSchema GenerateSchemaForMember(
if (dataProperty != null)
{
var requiredAttribute = customAttributes.OfType<RequiredAttribute>().FirstOrDefault();

schema.ReadOnly = dataProperty.IsReadOnly;
schema.WriteOnly = dataProperty.IsWriteOnly;

#if NET7_0_OR_GREATER
var hasRequiredMemberAttribute = customAttributes.OfType<System.Runtime.CompilerServices.RequiredMemberAttribute>().Any();

schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes
? dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute && !memberInfo.IsNonNullableReferenceType()
: dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute;

schema.MinLength = modelType == typeof(string) && (hasRequiredMemberAttribute || requiredAttribute is { AllowEmptyStrings: false }) ? 1 : null;
#else
schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes
? dataProperty.IsNullable && requiredAttribute==null && !memberInfo.IsNonNullableReferenceType()
: dataProperty.IsNullable && requiredAttribute==null;

schema.ReadOnly = dataProperty.IsReadOnly;
schema.WriteOnly = dataProperty.IsWriteOnly;
schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null;
#endif
}

var defaultValueAttribute = customAttributes.OfType<DefaultValueAttribute>().FirstOrDefault();
Expand Down Expand Up @@ -392,7 +404,13 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi
? GenerateSchemaForMember(dataProperty.MemberType, schemaRepository, dataProperty.MemberInfo, dataProperty)
: GenerateSchemaForType(dataProperty.MemberType, schemaRepository);

if ((dataProperty.IsRequired || customAttributes.OfType<RequiredAttribute>().Any())
if ((
dataProperty.IsRequired
|| customAttributes.OfType<RequiredAttribute>().Any()
#if NET7_0_OR_GREATER
|| customAttributes.OfType<System.Runtime.CompilerServices.RequiredMemberAttribute>().Any()
#endif
)
&& !schema.Required.Contains(dataProperty.Name))
{
schema.Required.Add(dataProperty.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ public static class ApiParameterDescriptionExtensions
private static readonly Type[] RequiredAttributeTypes = new[]
{
typeof(BindRequiredAttribute),
typeof(RequiredAttribute)
typeof(RequiredAttribute),
#if NET7_0_OR_GREATER
typeof(System.Runtime.CompilerServices.RequiredMemberAttribute)
#endif
};

public static bool IsRequiredParameter(this ApiParameterDescription apiParameter)
Expand Down Expand Up @@ -109,4 +112,4 @@ internal static bool IsFromForm(this ApiParameterDescription apiParameter)
|| (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ public void ActionWithIntParameterWithRequiredAttribute([Required]int param)
public void ActionWithObjectParameter(XmlAnnotatedType param)
{ }

#if NET7_0_OR_GREATER
public class TypeWithRequiredProperty
{
public required string RequiredProperty { get; set; }
}

public void ActionWithRequiredMember(TypeWithRequiredProperty param)
{ }
#endif

[Consumes("application/someMediaType")]
public void ActionWithConsumesAttribute(string param)
{ }
Expand All @@ -67,4 +77,4 @@ public int ActionWithProducesAttribute()
throw new NotImplementedException();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Dynamic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Xunit;
using Swashbuckle.AspNetCore.TestSupport;
using Microsoft.OpenApi.Any;
using Xunit;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test
{
Expand Down Expand Up @@ -343,6 +344,43 @@ public void GenerateSchema_SetsReadOnlyAndWriteOnlyFlags_IfPropertyIsRestricted(
Assert.True(schema.Properties["WriteOnlyProperty"].WriteOnly);
}

#if NET7_0_OR_GREATER
public class TypeWithRequiredProperty
{
public required string RequiredProperty { get; set; }
}

public class TypeWithRequiredPropertyAndValidationAttribute
{
[MinLength(1)]
public required string RequiredProperty { get; set; }
}

[Fact]
public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeyword()
{
var schemaRepository = new SchemaRepository();

var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredProperty), schemaRepository);

var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray());
}

[Fact]
public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidationAttribute()
{
var schemaRepository = new SchemaRepository();

var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredPropertyAndValidationAttribute), schemaRepository);

var schema = schemaRepository.Schemas[referenceSchema.Reference.Id];
Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength);
Assert.False(schema.Properties["RequiredProperty"].Nullable);
Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray());
}
#endif

[Theory]
[InlineData(typeof(TypeWithParameterizedConstructor), nameof(TypeWithParameterizedConstructor.Id), false)]
[InlineData(typeof(TypeWithParameterlessAndParameterizedConstructor), nameof(TypeWithParameterlessAndParameterizedConstructor.Id), true)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,38 @@ public void GetSwagger_SetsParameterRequired_IfActionParameterHasRequiredOrBindR
Assert.Equal(expectedRequired, parameter.Required);
}

#if NET7_0_OR_GREATER
[Fact]
public void GetSwagger_SetsParameterRequired_IfActionParameterHasRequiredMember()
{
var subject = Subject(
apiDescriptions: new[]
{
ApiDescriptionFactory.Create(
methodInfo: typeof(FakeController).GetMethod(nameof(FakeController.ActionWithRequiredMember)),
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions: new []
{
new ApiParameterDescription
{
Name = "param",
Source = BindingSource.Query,
ModelMetadata = ModelMetadataFactory.CreateForProperty(typeof(FakeController.TypeWithRequiredProperty), "RequiredProperty")
}
})
}
);

var document = subject.GetSwagger("v1");

var operation = document.Paths["/resource"].Operations[OperationType.Post];
var parameter = Assert.Single(operation.Parameters);
Assert.True(parameter.Required);
}
#endif

[Theory]
[InlineData(false)]
[InlineData(true)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public static ApiDescription Create(
ControllerParameterDescriptor;
#endif

if (parameterDescriptorWithParameterInfo != null)
if (parameterDescriptorWithParameterInfo != null && parameter.ModelMetadata == null)
{
parameter.ModelMetadata = ModelMetadataFactory.CreateForParameter(parameterDescriptorWithParameterInfo.ParameterInfo);
}
Expand Down

0 comments on commit 8a23f0a

Please sign in to comment.