Skip to content
Merged
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
4 changes: 4 additions & 0 deletions eng/MSBuild/LegacySupport.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\StringSyntaxAttribute\*.cs" LinkBase="LegacySupport\StringSyntaxAttribute" />
</ItemGroup>

<ItemGroup Condition="'$(InjectJsonSchemaExporterOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' or '$(TargetFramework)' == 'net8.0')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\Shared\JsonSchemaExporter\**\*.cs" LinkBase="Shared\EmptyCollections" />
</ItemGroup>

<ItemGroup Condition="'$(InjectGetOrAddOnLegacy)' == 'true' AND ('$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0')">
<Compile Include="$(MSBuildThisFileDirectory)\..\..\src\LegacySupport\GetOrAdd\*.cs" LinkBase="LegacySupport\GetOrAdd" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions eng/packages/TestOnly.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<PackageVersion Include="Verify.Xunit" Version="20.4.0" />
<PackageVersion Include="Xunit.Combinatorial" Version="1.6.24" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.4.2" />
<PackageVersion Include="JsonSchema.Net" Version="7.2.3" />
<PackageVersion Include="JsonSchema.Net.Generation" Version="4.3.0.2" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</PropertyGroup>

<PropertyGroup>
<InjectJsonSchemaExporterOnLegacy>true</InjectJsonSchemaExporterOnLegacy>
<InjectSharedEmptyCollections>true</InjectSharedEmptyCollections>
<InjectStringHashOnLegacy>true</InjectStringHashOnLegacy>
<InjectStringSyntaxAttributeOnLegacy>true</InjectStringSyntaxAttributeOnLegacy>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
#if !NET9_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
Expand All @@ -16,6 +19,7 @@
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
#pragma warning disable S107 // Methods should not have too many parameters
#pragma warning disable S1075 // URIs should not be hardcoded
#pragma warning disable SA1118 // Parameter should not span multiple lines

using FunctionParameterKey = (
System.Type? Type,
Expand Down Expand Up @@ -174,6 +178,11 @@ private static JsonElement GetJsonSchemaCached(JsonSerializerOptions options, Fu
#endif
}

#if !NET9_0_OR_GREATER
Comment thread
eiriktsarpalis marked this conversation as resolved.
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access",
Justification = "Pre STJ-9 schema extraction can fail with a runtime exception if certain reflection metadata have been trimmed. " +
"The exception message will guide users to turn off 'IlcTrimMetadata' which resolves all issues.")]
#endif
private static JsonElement GetJsonSchemaCore(JsonSerializerOptions options, FunctionParameterKey key)
{
_ = Throw.IfNull(options);
Expand Down Expand Up @@ -236,16 +245,9 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
const string DefaultPropertyName = "default";
const string RefPropertyName = "$ref";

// Find the first DescriptionAttribute, starting first from the property, then the parameter, and finally the type itself.
Type descAttrType = typeof(DescriptionAttribute);
var descriptionAttribute =
GetAttrs(descAttrType, ctx.PropertyInfo?.AttributeProvider)?.FirstOrDefault() ??
GetAttrs(descAttrType, ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider)?.FirstOrDefault() ??
GetAttrs(descAttrType, ctx.TypeInfo.Type)?.FirstOrDefault();

if (descriptionAttribute is DescriptionAttribute attr)
if (ctx.ResolveAttribute<DescriptionAttribute>() is { } attr)
{
ConvertSchemaToObject(ref schema).Insert(0, DescriptionPropertyName, (JsonNode)attr.Description);
ConvertSchemaToObject(ref schema).InsertAtStart(DescriptionPropertyName, (JsonNode)attr.Description);
}

if (schema is JsonObject objSchema)
Expand All @@ -268,7 +270,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
// Include the type keyword in enum types
if (key.IncludeTypeInEnumSchemas && ctx.TypeInfo.Type.IsEnum && objSchema.ContainsKey(EnumPropertyName) && !objSchema.ContainsKey(TypePropertyName))
{
objSchema.Insert(0, TypePropertyName, "string");
objSchema.InsertAtStart(TypePropertyName, "string");
}

// Disallow additional properties in object schemas
Expand Down Expand Up @@ -303,7 +305,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
if (index < 0)
{
// If there's no description property, insert it at the beginning of the doc.
obj.Insert(0, DescriptionPropertyName, (JsonNode)key.Description!);
obj.InsertAtStart(DescriptionPropertyName, (JsonNode)key.Description!);
}
else
{
Expand All @@ -321,15 +323,12 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
if (key.IncludeSchemaUri)
{
// The $schema property must be the first keyword in the object
ConvertSchemaToObject(ref schema).Insert(0, SchemaPropertyName, (JsonNode)SchemaKeywordUri);
ConvertSchemaToObject(ref schema).InsertAtStart(SchemaPropertyName, (JsonNode)SchemaKeywordUri);
}
}

return schema;

static object[]? GetAttrs(Type attrType, ICustomAttributeProvider? provider) =>
provider?.GetCustomAttributes(attrType, inherit: false);

static JsonObject ConvertSchemaToObject(ref JsonNode schema)
{
JsonObject obj;
Expand Down Expand Up @@ -368,6 +367,62 @@ private static bool TypeIsArrayContainingInteger(JsonObject schema)
return false;
}

private static void InsertAtStart(this JsonObject jsonObject, string key, JsonNode value)
{
#if NET9_0_OR_GREATER
jsonObject.Insert(0, key, value);
#else
jsonObject.Remove(key);
var copiedEntries = jsonObject.ToArray();
jsonObject.Clear();

jsonObject.Add(key, value);
foreach (var entry in copiedEntries)
{
jsonObject[entry.Key] = entry.Value;
}
#endif
}

#if !NET9_0_OR_GREATER
private static int IndexOf(this JsonObject jsonObject, string key)
{
int i = 0;
foreach (var entry in jsonObject)
{
if (string.Equals(entry.Key, key, StringComparison.Ordinal))
{
return i;
}

i++;
}

return -1;
}
#endif

private static TAttribute? ResolveAttribute<TAttribute>(this JsonSchemaExporterContext ctx)
where TAttribute : Attribute
{
// Resolve attributes from locations in the following order:
// 1. Property-level attributes
// 2. Parameter-level attributes and
// 3. Type-level attributes.
return
#if NET9_0_OR_GREATER
GetAttrs(ctx.PropertyInfo?.AttributeProvider) ??
GetAttrs(ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider) ??
#else
GetAttrs(ctx.PropertyAttributeProvider) ??
GetAttrs(ctx.ParameterInfo) ??
#endif
GetAttrs(ctx.TypeInfo.Type);

static TAttribute? GetAttrs(ICustomAttributeProvider? provider) =>
(TAttribute?)provider?.GetCustomAttributes(typeof(TAttribute), inherit: false).FirstOrDefault();
}

private static JsonElement ParseJsonElement(ReadOnlySpan<byte> utf8Json)
{
Utf8JsonReader reader = new(utf8Json);
Expand Down
Loading