尽管 Open API 规范 ASP.NET 认为每个 属性 都是强制性的
Despite Open API specification ASP.NET sees every property as mandatory
我一般直接在https://swagger.io/在线服务中生成服务器代码
继续,我决定添加更多自动化功能,因此我决定将 CLI 工具与 aspnetcore
生成器一起使用:https://openapi-generator.tech/docs/generators/aspnetcore
这是触发生成器的 PowerShell 脚本的重要部分:
"openapi-generator-cli generate -g aspnetcore -i $ymlPath -o $outPath " + `
"--additional-properties=`"packageName=$projectName,nullableReferenceTypes=true,operationResultTask=true," +
"operationModifier=$opModifier,generateBody=$generateBody,aspnetCoreVersion=5.0`"" | iex
具有模型架构的 YML 部分:
Profile:
required: [ walletAddress, publicName ]
properties:
walletAddress:
type: string
example: '0x008F7c856B71190C6E44A51a30A4ec32F68545e0'
type:
type: string
example: 'itemtype'
id:
type: string
example: 'pre_LfeolJ7DINWPTmQzArvT'
createdAt:
type: string
format: date-time
example: '2022-03-29T16:59:22.9033559Z'
publicName:
type: string
publicSlugUrl:
type: string
format: url
emailAddress:
type: string
format: email
phoneNumber:
type: string
location:
type: string
about:
type: string
生成的 C#:
namespace XxxxXxxxx.WebApi.Models
{
[DataContract]
public partial class Profile : IEquatable<Profile>
{
[Required]
[DataMember(Name="walletAddress", EmitDefaultValue=false)]
public string WalletAddress { get; set; }
[DataMember(Name="type", EmitDefaultValue=false)]
public string Type { get; set; }
[DataMember(Name="id", EmitDefaultValue=false)]
public string Id { get; set; }
[DataMember(Name="createdAt", EmitDefaultValue=false)]
public DateTime? CreatedAt { get; set; }
[Required]
[DataMember(Name="publicName", EmitDefaultValue=false)]
public string PublicName { get; set; }
[DataMember(Name="publicSlugUrl", EmitDefaultValue=false)]
public string PublicSlugUrl { get; set; }
[DataMember(Name="emailAddress", EmitDefaultValue=false)]
public string EmailAddress { get; set; }
[DataMember(Name="phoneNumber", EmitDefaultValue=false)]
public string PhoneNumber { get; set; }
[DataMember(Name="location", EmitDefaultValue=false)]
public string Location { get; set; }
[DataMember(Name="about", EmitDefaultValue=false)]
public string About { get; set; }
public override string ToString() { //...
}
public string ToJson()
{
return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented);
}
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((Profile)obj);
}
public bool Equals(Profile other) { //...
}
public override int GetHashCode( { //...
}
#region Operators
// ...
#endregion Operators
}
}
JSON OpenAPI 模型版本:
"Profile" : {
"example" : {
"createdAt" : "2022-03-29T16:59:22.9033559Z",
"emailAddress" : "onxy_party@gmx.de",
"phoneNumber" : "+6563297537",
"publicName" : "Onxy DAO",
"about" : "about",
"publicSlugUrl" : "https://chain.party/onxy_dao",
"location" : "Germany",
"id" : "pre_LfeolJ7DINWPTmQzArvT",
"walletAddress" : "0x008F7c856B71190C6E44A51a30A4ec32F68545e0",
"type" : "itemtype"
},
"properties" : {
"walletAddress" : {
"example" : "0x008F7c856B71190C6E44A51a30A4ec32F68545e0",
"type" : "string"
},
"type" : {
"example" : "itemtype",
"type" : "string"
},
"id" : {
"example" : "pre_LfeolJ7DINWPTmQzArvT",
"type" : "string"
},
"createdAt" : {
"example" : "2022-03-29T16:59:22.9033559Z",
"format" : "date-time",
"type" : "string"
},
"publicName" : {
"type" : "string"
},
"publicSlugUrl" : {
"format" : "url",
"type" : "string"
},
"emailAddress" : {
"format" : "email",
"type" : "string"
},
"phoneNumber" : {
"type" : "string"
},
"location" : {
"type" : "string"
},
"about" : {
"type" : "string"
}
},
"required" : [ "publicName", "walletAddress" ]
}
我应该能够 post 这个负载只有两个必填字段,但我在所有其他字段上收到验证错误:
{
"errors": {
"id": [
"The Id field is required."
],
"about": [
"The About field is required."
],
"location": [
"The Location field is required."
],
"publicName": [
"The PublicName field is required."
],
"phoneNumber": [
"The PhoneNumber field is required."
],
"emailAddress": [
"The EmailAddress field is required."
],
"publicSlugUrl": [
"The PublicSlugUrl field is required."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-42995fb014cad1fbb999645cb61e4cf9-ade43e222f9917ae-00"
}
应用程序是这样配置的:
using System.Reflection;
using ChainParty.WebApi.Filters;
using ChainParty.WebApi.Formatters;
using ChainParty.WebApi.OpenApi;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services
.AddMvc(opts => opts.InputFormatters.Insert(0, new InputFormatterStream()))
.ConfigureApplicationPartManager(apm => {
var originals = apm.FeatureProviders.OfType<ControllerFeatureProvider>().ToList();
foreach (var original in originals)
apm.FeatureProviders.Remove(original);
apm.FeatureProviders.Add(new DefaultControllerFeatureProvider());
})
.AddNewtonsoftJson(opts => {
opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
opts.SerializerSettings.Converters.Add(new StringEnumConverter
{
NamingStrategy = new CamelCaseNamingStrategy()
});
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services
.AddSwaggerGen(c => {
c.SwaggerDoc("2.0.0", new OpenApiInfo
{
Title = "ChainParty",
Description = "ChainParty (ASP.NET Core 3.1)",
TermsOfService = new Uri("https://github.com/openapitools/openapi-generator"),
Contact = new OpenApiContact
{
Name = "OpenAPI-Generator Contributors",
Url = new Uri("https://github.com/openapitools/openapi-generator"),
Email = "chainparty@dev4side.com"
},
License = new OpenApiLicense
{
Name = "NoLicense",
Url = new Uri("http://localhost")
},
Version = "2.0.0",
});
c.CustomSchemaIds(type => type.FriendlyId(true));
c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{Assembly.GetEntryAssembly().GetName().Name}.xml");
// Include DataAnnotation attributes on Controller Action parameters as OpenAPI validation rules (e.g required, pattern, ..)
// Use [ValidateModelState] on Actions to actually validate it in C# as well!
c.OperationFilter<GeneratePathParamsValidationFilter>();
});
builder.Services
.AddSwaggerGenNewtonsoftSupport();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger(c => {
c.RouteTemplate = "openapi/{documentName}/openapi.json";
})
.UseSwaggerUI(c => {
// set route prefix to openapi, e.g. http://localhost:8080/openapi/index.html
c.RoutePrefix = "openapi";
//TODO: Either use the SwaggerGen generated OpenAPI contract (generated from C# classes)
c.SwaggerEndpoint("/openapi/2.0.0/openapi.json", "ChainParty");
//TODO: Or alternatively use the original OpenAPI contract that's included in the static files
// c.SwaggerEndpoint("/openapi-original.json", "ChainParty Original");
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseRouting();
app.MapControllers();
app.Run();
唯一的自定义是控制器名称解析。
非常感谢任何帮助,最好的问候
正如this document所说:
Beginning with .NET 6, new projects include the
<Nullable>enable</Nullable>
element in the project file. Once the
feature is turned on, existing reference variable declarations become
non-nullable reference types.
在 .NET 6 中,non-nullable 属性 必须是必需的,否则 ModelState 将无效。
为达到您的要求,您可以从项目文件中删除 <Nullable>enable</Nullable>
。
另一种方法是您可以添加 ?
以允许为空:
[DataContract]
public partial class Profile : IEquatable<Profile>
{
[Required]
[DataMember(Name="walletAddress", EmitDefaultValue=false)]
public string WalletAddress { get; set; }
[DataMember(Name="type", EmitDefaultValue=false)]
public string Type { get; set; }
[DataMember(Name="id", EmitDefaultValue=false)]
public string? Id { get; set; }
[DataMember(Name="createdAt", EmitDefaultValue=false)]
public DateTime? CreatedAt { get; set; }
[Required]
[DataMember(Name="publicName", EmitDefaultValue=false)]
public string? PublicName { get; set; }
//other proerpties..
}
我一般直接在https://swagger.io/在线服务中生成服务器代码
继续,我决定添加更多自动化功能,因此我决定将 CLI 工具与 aspnetcore
生成器一起使用:https://openapi-generator.tech/docs/generators/aspnetcore
这是触发生成器的 PowerShell 脚本的重要部分:
"openapi-generator-cli generate -g aspnetcore -i $ymlPath -o $outPath " + `
"--additional-properties=`"packageName=$projectName,nullableReferenceTypes=true,operationResultTask=true," +
"operationModifier=$opModifier,generateBody=$generateBody,aspnetCoreVersion=5.0`"" | iex
具有模型架构的 YML 部分:
Profile:
required: [ walletAddress, publicName ]
properties:
walletAddress:
type: string
example: '0x008F7c856B71190C6E44A51a30A4ec32F68545e0'
type:
type: string
example: 'itemtype'
id:
type: string
example: 'pre_LfeolJ7DINWPTmQzArvT'
createdAt:
type: string
format: date-time
example: '2022-03-29T16:59:22.9033559Z'
publicName:
type: string
publicSlugUrl:
type: string
format: url
emailAddress:
type: string
format: email
phoneNumber:
type: string
location:
type: string
about:
type: string
生成的 C#:
namespace XxxxXxxxx.WebApi.Models
{
[DataContract]
public partial class Profile : IEquatable<Profile>
{
[Required]
[DataMember(Name="walletAddress", EmitDefaultValue=false)]
public string WalletAddress { get; set; }
[DataMember(Name="type", EmitDefaultValue=false)]
public string Type { get; set; }
[DataMember(Name="id", EmitDefaultValue=false)]
public string Id { get; set; }
[DataMember(Name="createdAt", EmitDefaultValue=false)]
public DateTime? CreatedAt { get; set; }
[Required]
[DataMember(Name="publicName", EmitDefaultValue=false)]
public string PublicName { get; set; }
[DataMember(Name="publicSlugUrl", EmitDefaultValue=false)]
public string PublicSlugUrl { get; set; }
[DataMember(Name="emailAddress", EmitDefaultValue=false)]
public string EmailAddress { get; set; }
[DataMember(Name="phoneNumber", EmitDefaultValue=false)]
public string PhoneNumber { get; set; }
[DataMember(Name="location", EmitDefaultValue=false)]
public string Location { get; set; }
[DataMember(Name="about", EmitDefaultValue=false)]
public string About { get; set; }
public override string ToString() { //...
}
public string ToJson()
{
return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented);
}
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((Profile)obj);
}
public bool Equals(Profile other) { //...
}
public override int GetHashCode( { //...
}
#region Operators
// ...
#endregion Operators
}
}
JSON OpenAPI 模型版本:
"Profile" : {
"example" : {
"createdAt" : "2022-03-29T16:59:22.9033559Z",
"emailAddress" : "onxy_party@gmx.de",
"phoneNumber" : "+6563297537",
"publicName" : "Onxy DAO",
"about" : "about",
"publicSlugUrl" : "https://chain.party/onxy_dao",
"location" : "Germany",
"id" : "pre_LfeolJ7DINWPTmQzArvT",
"walletAddress" : "0x008F7c856B71190C6E44A51a30A4ec32F68545e0",
"type" : "itemtype"
},
"properties" : {
"walletAddress" : {
"example" : "0x008F7c856B71190C6E44A51a30A4ec32F68545e0",
"type" : "string"
},
"type" : {
"example" : "itemtype",
"type" : "string"
},
"id" : {
"example" : "pre_LfeolJ7DINWPTmQzArvT",
"type" : "string"
},
"createdAt" : {
"example" : "2022-03-29T16:59:22.9033559Z",
"format" : "date-time",
"type" : "string"
},
"publicName" : {
"type" : "string"
},
"publicSlugUrl" : {
"format" : "url",
"type" : "string"
},
"emailAddress" : {
"format" : "email",
"type" : "string"
},
"phoneNumber" : {
"type" : "string"
},
"location" : {
"type" : "string"
},
"about" : {
"type" : "string"
}
},
"required" : [ "publicName", "walletAddress" ]
}
我应该能够 post 这个负载只有两个必填字段,但我在所有其他字段上收到验证错误:
{
"errors": {
"id": [
"The Id field is required."
],
"about": [
"The About field is required."
],
"location": [
"The Location field is required."
],
"publicName": [
"The PublicName field is required."
],
"phoneNumber": [
"The PhoneNumber field is required."
],
"emailAddress": [
"The EmailAddress field is required."
],
"publicSlugUrl": [
"The PublicSlugUrl field is required."
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-42995fb014cad1fbb999645cb61e4cf9-ade43e222f9917ae-00"
}
应用程序是这样配置的:
using System.Reflection;
using ChainParty.WebApi.Filters;
using ChainParty.WebApi.Formatters;
using ChainParty.WebApi.OpenApi;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services
.AddMvc(opts => opts.InputFormatters.Insert(0, new InputFormatterStream()))
.ConfigureApplicationPartManager(apm => {
var originals = apm.FeatureProviders.OfType<ControllerFeatureProvider>().ToList();
foreach (var original in originals)
apm.FeatureProviders.Remove(original);
apm.FeatureProviders.Add(new DefaultControllerFeatureProvider());
})
.AddNewtonsoftJson(opts => {
opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
opts.SerializerSettings.Converters.Add(new StringEnumConverter
{
NamingStrategy = new CamelCaseNamingStrategy()
});
});
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services
.AddSwaggerGen(c => {
c.SwaggerDoc("2.0.0", new OpenApiInfo
{
Title = "ChainParty",
Description = "ChainParty (ASP.NET Core 3.1)",
TermsOfService = new Uri("https://github.com/openapitools/openapi-generator"),
Contact = new OpenApiContact
{
Name = "OpenAPI-Generator Contributors",
Url = new Uri("https://github.com/openapitools/openapi-generator"),
Email = "chainparty@dev4side.com"
},
License = new OpenApiLicense
{
Name = "NoLicense",
Url = new Uri("http://localhost")
},
Version = "2.0.0",
});
c.CustomSchemaIds(type => type.FriendlyId(true));
c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{Assembly.GetEntryAssembly().GetName().Name}.xml");
// Include DataAnnotation attributes on Controller Action parameters as OpenAPI validation rules (e.g required, pattern, ..)
// Use [ValidateModelState] on Actions to actually validate it in C# as well!
c.OperationFilter<GeneratePathParamsValidationFilter>();
});
builder.Services
.AddSwaggerGenNewtonsoftSupport();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger(c => {
c.RouteTemplate = "openapi/{documentName}/openapi.json";
})
.UseSwaggerUI(c => {
// set route prefix to openapi, e.g. http://localhost:8080/openapi/index.html
c.RoutePrefix = "openapi";
//TODO: Either use the SwaggerGen generated OpenAPI contract (generated from C# classes)
c.SwaggerEndpoint("/openapi/2.0.0/openapi.json", "ChainParty");
//TODO: Or alternatively use the original OpenAPI contract that's included in the static files
// c.SwaggerEndpoint("/openapi-original.json", "ChainParty Original");
});
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseRouting();
app.MapControllers();
app.Run();
唯一的自定义是控制器名称解析。
非常感谢任何帮助,最好的问候
正如this document所说:
Beginning with .NET 6, new projects include the
<Nullable>enable</Nullable>
element in the project file. Once the feature is turned on, existing reference variable declarations become non-nullable reference types.
在 .NET 6 中,non-nullable 属性 必须是必需的,否则 ModelState 将无效。
为达到您的要求,您可以从项目文件中删除 <Nullable>enable</Nullable>
。
另一种方法是您可以添加 ?
以允许为空:
[DataContract]
public partial class Profile : IEquatable<Profile>
{
[Required]
[DataMember(Name="walletAddress", EmitDefaultValue=false)]
public string WalletAddress { get; set; }
[DataMember(Name="type", EmitDefaultValue=false)]
public string Type { get; set; }
[DataMember(Name="id", EmitDefaultValue=false)]
public string? Id { get; set; }
[DataMember(Name="createdAt", EmitDefaultValue=false)]
public DateTime? CreatedAt { get; set; }
[Required]
[DataMember(Name="publicName", EmitDefaultValue=false)]
public string? PublicName { get; set; }
//other proerpties..
}