Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"rollForward": false
},
"swashbuckle.aspnetcore.cli": {
"version": "7.0.0",
"version": "10.2.1",
"commands": ["swagger"],
"rollForward": false
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

Expand All @@ -10,19 +10,21 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" VersionOverride="9.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" VersionOverride="10.2.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\OptionalValues.DataAnnotations\OptionalValues.DataAnnotations.csproj" />
<ProjectReference Include="..\..\src\OptionalValues.Swashbuckle\OptionalValues.Swashbuckle.csproj" />
<ProjectReference Include="..\..\src\OptionalValues\OptionalValues.csproj"/>
<ProjectReference Include="..\..\src\OptionalValues\OptionalValues.csproj" />
</ItemGroup>


<Target Name="openapi" AfterTargets="Build">
<Message Text="generating openapi" Importance="high"/>
<Exec Command="dotnet tool run swagger tofile --yaml --output openapi.yaml $(OutputPath)$(AssemblyName).dll v1" EnvironmentVariables="DOTNET_ROLL_FORWARD=LatestMajor"/>
<Message Text="generating openapi" Importance="high" />
<Exec
Command="dotnet tool run swagger tofile --yaml --openapiversion 3.1 --output openapi.yaml $(OutputPath)$(AssemblyName).dll v1"
EnvironmentVariables="DOTNET_ROLL_FORWARD=LatestMajor" />
</Target>

</Project>
</Project>
3 changes: 2 additions & 1 deletion examples/OptionalValues.Examples.Swashbuckle/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

using OptionalValues;
using OptionalValues.DataAnnotations;
Expand All @@ -9,6 +9,7 @@
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "OptionalValues.Examples.Swashbuckle", Version = "1.0" });
options.SchemaGeneratorOptions.SupportNonNullableReferenceTypes = true;
});
// Add OptionalValue support to Swashbuckle
builder.Services.AddSwaggerGenOptionalValueSupport();
Expand Down
10 changes: 4 additions & 6 deletions examples/OptionalValues.Examples.Swashbuckle/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: 3.0.4
openapi: '3.1.1'
info:
title: OptionalValues.Examples.Swashbuckle
version: '1.0'
Expand Down Expand Up @@ -31,15 +31,13 @@ components:
properties:
street:
type: string
nullable: true
city:
type: string
state:
pattern: '^[a-zA-Z ]+$'
type: string
zip:
type: string
nullable: true
additionalProperties: false
Company:
required:
Expand All @@ -55,7 +53,6 @@ components:
maxLength: 50
minLength: 0
type: string
nullable: true
contact:
$ref: '#/components/schemas/Person'
additionalProperties: false
Expand All @@ -64,12 +61,13 @@ components:
properties:
name:
type: string
nullable: true
age:
maximum: 120
minimum: 0
type: integer
format: int32
address:
$ref: '#/components/schemas/Address'
additionalProperties: false
additionalProperties: false
tags:
- name: Example
70 changes: 43 additions & 27 deletions src/OptionalValues.Swashbuckle/OptionalValueDataContractResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,60 +44,76 @@ public virtual DataContract GetDataContractForType(Type type)
}

DataContract? dataContract = _inner.GetDataContractForType(effectiveType);
if (dataContract is { DataType: DataType.Object })
if (dataContract is not
{ DataType: DataType.Object })
{
var effectiveProperties = new List<DataProperty>();
foreach (DataProperty property in dataContract.ObjectProperties)
{
DataProperty effectiveProperty = property;
if (OptionalValue.IsOptionalValueType(property.MemberType))
{
Type underLyingType = OptionalValue.GetUnderlyingType(property.MemberType);
var isNullable = Nullable.GetUnderlyingType(underLyingType) != null || GetNullabilityFromRuntimeInformationFlags(property.MemberInfo);
return dataContract;
}

var isRequired = property.MemberInfo.GetCustomAttributes(true).Any(a => a.GetType().FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute");
effectiveProperty = new DataProperty(property.Name, property.MemberType, isRequired, isNullable, property.IsReadOnly, property.IsWriteOnly, property.MemberInfo);
}
var effectiveProperties = new List<DataProperty>();
foreach (DataProperty property in dataContract.ObjectProperties)
{
DataProperty effectiveProperty = property;
if (OptionalValue.IsOptionalValueType(property.MemberType))
{
Type underLyingType = OptionalValue.GetUnderlyingType(property.MemberType);
var isNullable = Nullable.GetUnderlyingType(underLyingType) != null
|| GetOptionalValueIsNullable(property.MemberInfo);

effectiveProperties.Add(effectiveProperty);
var isRequired = property.MemberInfo.GetCustomAttributes(true).Any(a => a.GetType().FullName == "OptionalValues.DataAnnotations.SpecifiedAttribute");
effectiveProperty = new DataProperty(property.Name, property.MemberType, isRequired, isNullable, property.IsReadOnly, property.IsWriteOnly, property.MemberInfo);
}

dataContract = DataContract.ForObject(
dataContract.UnderlyingType,
effectiveProperties,
dataContract.ObjectExtensionDataType,
dataContract.ObjectTypeNameProperty,
dataContract.ObjectTypeNameProperty,
dataContract.JsonConverter);
effectiveProperties.Add(effectiveProperty);
}

dataContract = DataContract.ForObject(
dataContract.UnderlyingType,
effectiveProperties,
dataContract.ObjectExtensionDataType,
dataContract.ObjectTypeNameProperty,
dataContract.ObjectTypeNameProperty,
dataContract.JsonConverter);

return dataContract;
}

private static bool GetNullabilityFromRuntimeInformationFlags(MemberInfo memberInfo)
private static bool GetOptionalValueIsNullable(ICustomAttributeProvider? memberInfo)
{
NullabilityInfo? nullabilityInfo = GetNullabilityInfo(memberInfo);
NullabilityInfo? nullabilityInfo = GetOptionalValueNullabilityInfo(memberInfo);
if (nullabilityInfo == null)
{
return false;
}

if (OptionalValue.IsOptionalValueType(nullabilityInfo.Type))
return nullabilityInfo.ReadState == NullabilityState.Nullable;
}

private static NullabilityInfo? GetOptionalValueNullabilityInfo(ICustomAttributeProvider? memberInfo)
{
if (memberInfo == null)
{
return null;
}

NullabilityInfo? nullabilityInfo = GetNullabilityInfo(memberInfo);
if (nullabilityInfo == null
|| !OptionalValue.IsOptionalValueType(nullabilityInfo.Type))
{
return nullabilityInfo.GenericTypeArguments[0].ReadState == NullabilityState.Nullable;
return null;
}

return nullabilityInfo.ReadState == NullabilityState.Nullable;
return nullabilityInfo.GenericTypeArguments[0];
}

private static NullabilityInfo? GetNullabilityInfo(MemberInfo memberInfo)
private static NullabilityInfo? GetNullabilityInfo(ICustomAttributeProvider memberInfo)
{
var nullabilityInfoContext = new NullabilityInfoContext();

return memberInfo switch
{
PropertyInfo propertyInfo => nullabilityInfoContext.Create(propertyInfo),
FieldInfo fieldInfo => nullabilityInfoContext.Create(fieldInfo),
ParameterInfo parameterInfo => nullabilityInfoContext.Create(parameterInfo),
_ => null,
};
}
Expand Down
48 changes: 45 additions & 3 deletions test/OptionalValues.Swashbuckle.V10.Tests/SchemaGeneratorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,46 @@ public void Should_Generate_The_Same_Schema_With_OptionalValues()
schemaDefault.Properties?.Count.ShouldBe(4);
}

[Fact(Skip = "Skip this test until Swashbuckle does not override nullability for ValueTypes, (that is, OptionalValue<T>)")]
public void Should_Generate_The_Same_Schema_For_Nullable_Reference_Types_With_OptionalValues()
{
var schemaRepositoryForOptionalValues = new SchemaRepository();
var schemaRepositoryForDefault = new SchemaRepository();

IOpenApiSchema schemaOptionalValuesAsRef = SchemaGeneratorOptionalValues.GenerateSchema(typeof(ExamplesOptionalValues.NullableReferences), schemaRepositoryForOptionalValues);
schemaOptionalValuesAsRef.ShouldNotBeNull();

IOpenApiSchema schemaDefaultAsRef = SchemaGeneratorDefault.GenerateSchema(typeof(ExamplesPlain.NullableReferences), schemaRepositoryForDefault);
schemaDefaultAsRef.ShouldNotBeNull();

var refId1 = ((OpenApiSchemaReference)schemaOptionalValuesAsRef).Reference.Id;
var refId2 = ((OpenApiSchemaReference)schemaDefaultAsRef).Reference.Id;

IOpenApiSchema schemaOptionalValues = schemaRepositoryForOptionalValues.Schemas[refId1!];
IOpenApiSchema schemaDefault = schemaRepositoryForDefault.Schemas[refId2!];

var schemaOptionalValuesJson = SerializeSchema(schemaOptionalValues);
var schemaDefaultJson = SerializeSchema(schemaDefault);

schemaOptionalValuesJson.ShouldBe(schemaDefaultJson);
schemaOptionalValues.Properties?.Count.ShouldBe(2);
schemaDefault.Properties?.Count.ShouldBe(2);
}

[Fact]
public void OptionalValue_Support_Should_Not_Change_Behavior()
public void OptionalValue_Support_Should_Not_Change_Default_Behavior()
{
var schemaRepositoryForOptionalValues = new SchemaRepository();
var schemaRepositoryForDefault = new SchemaRepository();

IOpenApiSchema schemaOptionalValuesAsRef = SchemaGeneratorOptionalValues.GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForOptionalValues);
// Generate the schema for the ExamplesPlain.Primitives type with both SchemaGenerators.
// They should generate the same schema, because the OptionalValue support should not change the behavior for types that are not OptionalValue<T>
IOpenApiSchema schemaOptionalValuesAsRef = SchemaGeneratorOptionalValues
.GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForOptionalValues);
schemaOptionalValuesAsRef.ShouldNotBeNull();

IOpenApiSchema schemaDefaultAsRef = SchemaGeneratorDefault.GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForDefault);
IOpenApiSchema schemaDefaultAsRef = SchemaGeneratorDefault
.GenerateSchema(typeof(ExamplesPlain.Primitives), schemaRepositoryForDefault);
schemaDefaultAsRef.ShouldNotBeNull();

var refId1 = ((OpenApiSchemaReference)schemaOptionalValuesAsRef).Reference.Id;
Expand Down Expand Up @@ -92,6 +122,12 @@ public class Primitives
public OptionalValue<bool> BoolValue { get; set; }
public OptionalValue<Guid> GuidValue { get; set; }
}

public class NullableReferences
{
public OptionalValue<string> StringValue { get; set; }
public OptionalValue<string?> NullableStringValue { get; set; }
}
}

private static class ExamplesPlain
Expand All @@ -103,5 +139,11 @@ public class Primitives
public bool BoolValue { get; set; }
public Guid GuidValue { get; set; }
}

public class NullableReferences
{
public string StringValue { get; set; } = null!;
public string? NullableStringValue { get; set; }
}
}
}
Loading