为什么 autorest 用 Swagger 中的对象替换我的自定义结构?
Why autorest is replacing my custom struct by an object in Swagger?
我创建了一个自定义 readonly
struct
来定义一个不可变的值类型,我称之为 TenantId
:
[DebuggerDisplay("ID={m_internalId.ToString()}")]
[JsonConverter(typeof(TenantIdJsonConverter))]
public readonly struct TenantId : IEquatable<TenantId>
{
private readonly Guid m_internalId;
public static TenantId New => new(Guid.NewGuid());
private TenantId(Guid id)
{
m_internalId = id;
}
public TenantId(TenantId otherTenantId)
{
m_internalId = otherTenantId.m_internalId;
}
...
}
我还定义了一个名为 PurchaseContract
的合同,它是 HTTP 响应的一部分:
[JsonObject(MemberSerialization.OptIn)]
public sealed class PurchaseContract
{
[JsonProperty(PropertyName = "tenantId")]
public TenantId TenantId { get; }
[JsonProperty(PropertyName = "total")]
public double Total { get; }
}
最后,我设置了一个 HTTP 触发函数,它将 return PurchaseContract
的一个实例。目前,它已在 ProducesResponseTypeAttribute
:
中进行了描述
[ApiExplorerSettings(GroupName = "Purchases")]
[ProducesResponseType(typeof(PurchaseContract), (int) HttpStatusCode.OK)]
[FunctionName("v1-get-purchase")]
public Task<IActionResult> RunAsync
(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "v1/purchases")]
HttpRequest httpRequest,
[SwaggerIgnore]
ClaimsPrincipal claimsPrincipal
)
{
// Stuff to do.
return Task.FromResult((IActionResult)new OkResult());
}
在我的 Startup
class 中,我是这样设置 swagger 的:
private static void ConfigureSwashBuckle(IFunctionsHostBuilder functionsHostBuilder)
{
functionsHostBuilder.AddSwashBuckle(Assembly.GetExecutingAssembly(), options =>
{
options.SpecVersion = OpenApiSpecVersion.OpenApi3_0;
options.AddCodeParameter = true;
options.PrependOperationWithRoutePrefix = true;
options.XmlPath = "FunctionApp.xml";
options.Documents = new []
{
new SwaggerDocument
{
Title = "My API,
Version = "v1",
Name = "v1",
Description = "Description of my API",
}
};
});
}
在swagger UI页面中,我看到它看起来不错:
问题
使用 Autorest 创建 C# 客户端时出现意外结果。不知何故, TenantId
结构被删除并被 object
代替:
这是为什么?我应该怎么做才能自动生成 TenantId
,就像客户端中的 PurchaseContract
一样?
详情
这里是版本信息。
- 函数应用程序 V3 运行 在 netcore3.1 上;
- OpenApi 3.0;
- Autorest 核心 3.0.6274,autorest.csharp' (~2.3.79->2.3.91) 和 autorest.modeler' (2.3.55->2.3.55);
- NuGet 包 AzureExtensions.Swashbuckle;
我开始调查 Swashbuckle.AspNetCore.SwaggerGen to find out how my readonly
struct
was interpreted. It all happens in the class JsonSerializerDataContractResolver 的源代码,在方法 GetDataContractForType
中确定所提供类型的 DataContract
:
public DataContract GetDataContractForType(Type type)
{
if (type.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement)))
{
...
}
if (PrimitiveTypesAndFormats.ContainsKey(type))
{
...
}
if (type.IsEnum)
{
...
}
if (IsSupportedDictionary(type, out Type keyType, out Type valueType))
{
...
}
if (IsSupportedCollection(type, out Type itemType))
{
...
}
return DataContract.ForObject(
underlyingType: type,
properties: GetDataPropertiesFor(type, out Type extensionDataType),
extensionDataType: extensionDataType,
jsonConverter: JsonConverterFunc);
}
我的习惯 struct
TenantId
不符合这些条件中的任何一个,因此,它退回到被视为 object
(最后一个陈述)。
然后我继续查看现有的 tests,看看 class 是如何使用的,看看我是否可以更改任何内容。令人惊讶的是,我发现了一个名为 GenerateSchema_SupportsOption_CustomTypeMappings
的测试(第 356 行),它显示了一种提供自定义映射的方法(请参阅该方法的第一条语句):
[Theory]
[InlineData(typeof(ComplexType), typeof(ComplexType), "string")]
[InlineData(typeof(GenericType<int, string>), typeof(GenericType<int, string>), "string")]
[InlineData(typeof(GenericType<,>), typeof(GenericType<int, int>), "string")]
public void GenerateSchema_SupportsOption_CustomTypeMappings(
Type mappingType,
Type type,
string expectedSchemaType)
{
var subject = Subject(configureGenerator: c => c.CustomTypeMappings.Add(mappingType, () => new OpenApiSchema { Type = "string" }));
var schema = subject.GenerateSchema(type, new SchemaRepository());
Assert.Equal(expectedSchemaType, schema.Type);
Assert.Empty(schema.Properties);
}
就我而言,我希望将我的 TenantId
映射到 string
。为此,我在 Function App 启动时编辑了 SwashBuckle 的配置:
private static void ConfigureSwashBuckle(IFunctionsHostBuilder functionsHostBuilder)
{
functionsHostBuilder.AddSwashBuckle(Assembly.GetExecutingAssembly(), options =>
{
...
options.ConfigureSwaggerGen = (swaggerGenOptions) => swaggerGenOptions.MapType<TenantId>(() => new OpenApiSchema {Type = "string"});
});
}
在这里,TenantId
现在在 Swagger 中被视为 string
。
我创建了一个自定义 readonly
struct
来定义一个不可变的值类型,我称之为 TenantId
:
[DebuggerDisplay("ID={m_internalId.ToString()}")]
[JsonConverter(typeof(TenantIdJsonConverter))]
public readonly struct TenantId : IEquatable<TenantId>
{
private readonly Guid m_internalId;
public static TenantId New => new(Guid.NewGuid());
private TenantId(Guid id)
{
m_internalId = id;
}
public TenantId(TenantId otherTenantId)
{
m_internalId = otherTenantId.m_internalId;
}
...
}
我还定义了一个名为 PurchaseContract
的合同,它是 HTTP 响应的一部分:
[JsonObject(MemberSerialization.OptIn)]
public sealed class PurchaseContract
{
[JsonProperty(PropertyName = "tenantId")]
public TenantId TenantId { get; }
[JsonProperty(PropertyName = "total")]
public double Total { get; }
}
最后,我设置了一个 HTTP 触发函数,它将 return PurchaseContract
的一个实例。目前,它已在 ProducesResponseTypeAttribute
:
[ApiExplorerSettings(GroupName = "Purchases")]
[ProducesResponseType(typeof(PurchaseContract), (int) HttpStatusCode.OK)]
[FunctionName("v1-get-purchase")]
public Task<IActionResult> RunAsync
(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "v1/purchases")]
HttpRequest httpRequest,
[SwaggerIgnore]
ClaimsPrincipal claimsPrincipal
)
{
// Stuff to do.
return Task.FromResult((IActionResult)new OkResult());
}
在我的 Startup
class 中,我是这样设置 swagger 的:
private static void ConfigureSwashBuckle(IFunctionsHostBuilder functionsHostBuilder)
{
functionsHostBuilder.AddSwashBuckle(Assembly.GetExecutingAssembly(), options =>
{
options.SpecVersion = OpenApiSpecVersion.OpenApi3_0;
options.AddCodeParameter = true;
options.PrependOperationWithRoutePrefix = true;
options.XmlPath = "FunctionApp.xml";
options.Documents = new []
{
new SwaggerDocument
{
Title = "My API,
Version = "v1",
Name = "v1",
Description = "Description of my API",
}
};
});
}
在swagger UI页面中,我看到它看起来不错:
问题
使用 Autorest 创建 C# 客户端时出现意外结果。不知何故, TenantId
结构被删除并被 object
代替:
这是为什么?我应该怎么做才能自动生成 TenantId
,就像客户端中的 PurchaseContract
一样?
详情
这里是版本信息。
- 函数应用程序 V3 运行 在 netcore3.1 上;
- OpenApi 3.0;
- Autorest 核心 3.0.6274,autorest.csharp' (~2.3.79->2.3.91) 和 autorest.modeler' (2.3.55->2.3.55);
- NuGet 包 AzureExtensions.Swashbuckle;
我开始调查 Swashbuckle.AspNetCore.SwaggerGen to find out how my readonly
struct
was interpreted. It all happens in the class JsonSerializerDataContractResolver 的源代码,在方法 GetDataContractForType
中确定所提供类型的 DataContract
:
public DataContract GetDataContractForType(Type type)
{
if (type.IsOneOf(typeof(object), typeof(JsonDocument), typeof(JsonElement)))
{
...
}
if (PrimitiveTypesAndFormats.ContainsKey(type))
{
...
}
if (type.IsEnum)
{
...
}
if (IsSupportedDictionary(type, out Type keyType, out Type valueType))
{
...
}
if (IsSupportedCollection(type, out Type itemType))
{
...
}
return DataContract.ForObject(
underlyingType: type,
properties: GetDataPropertiesFor(type, out Type extensionDataType),
extensionDataType: extensionDataType,
jsonConverter: JsonConverterFunc);
}
我的习惯 struct
TenantId
不符合这些条件中的任何一个,因此,它退回到被视为 object
(最后一个陈述)。
然后我继续查看现有的 tests,看看 class 是如何使用的,看看我是否可以更改任何内容。令人惊讶的是,我发现了一个名为 GenerateSchema_SupportsOption_CustomTypeMappings
的测试(第 356 行),它显示了一种提供自定义映射的方法(请参阅该方法的第一条语句):
[Theory]
[InlineData(typeof(ComplexType), typeof(ComplexType), "string")]
[InlineData(typeof(GenericType<int, string>), typeof(GenericType<int, string>), "string")]
[InlineData(typeof(GenericType<,>), typeof(GenericType<int, int>), "string")]
public void GenerateSchema_SupportsOption_CustomTypeMappings(
Type mappingType,
Type type,
string expectedSchemaType)
{
var subject = Subject(configureGenerator: c => c.CustomTypeMappings.Add(mappingType, () => new OpenApiSchema { Type = "string" }));
var schema = subject.GenerateSchema(type, new SchemaRepository());
Assert.Equal(expectedSchemaType, schema.Type);
Assert.Empty(schema.Properties);
}
就我而言,我希望将我的 TenantId
映射到 string
。为此,我在 Function App 启动时编辑了 SwashBuckle 的配置:
private static void ConfigureSwashBuckle(IFunctionsHostBuilder functionsHostBuilder)
{
functionsHostBuilder.AddSwashBuckle(Assembly.GetExecutingAssembly(), options =>
{
...
options.ConfigureSwaggerGen = (swaggerGenOptions) => swaggerGenOptions.MapType<TenantId>(() => new OpenApiSchema {Type = "string"});
});
}
在这里,TenantId
现在在 Swagger 中被视为 string
。