尽管开放 API 规范 ASP.NET 将每个属性视为强制属性

发布于 2025-01-18 11:12:28 字数 9464 浏览 2 评论 0原文

我通常直接在 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
    }
}

OpenAPI 模型的 JSON 版本:

"Profile" : {
  "example" : {
    "createdAt" : "2022-03-29T16:59:22.9033559Z",
    "emailAddress" : "[email protected]",
    "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" ]
}

我应该能够仅使用两个必填字段来发布此有效负载,但我在所有其他字段上收到验证错误:

{
    "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 = "[email protected]"
            },
            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();

唯一的自定义是控制器名称解决。

非常感谢任何帮助,最诚挚的问候

I normally generate the server code directly in https://swagger.io/ online service

Going on I decided to add more automation so I decided to use the CLI tool with aspnetcore generator: https://openapi-generator.tech/docs/generators/aspnetcore

This is the important part of the PowerShell script that triggers the generator:

"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 part with model schema:

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

Generated 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 verison of OpenAPI model:

"Profile" : {
  "example" : {
    "createdAt" : "2022-03-29T16:59:22.9033559Z",
    "emailAddress" : "[email protected]",
    "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" ]
}

I should be able to post this payload with only the two required fields, bit i get a validation error on about all other fields:

{
    "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"
}

The app is configured in this way:

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 = "[email protected]"
            },
            License = new OpenApiLicense
            {
                Name = "NoLicense",
                Url = new Uri("http://localhost")
            },
            Version = "2.0.0",
        });
        c.CustomSchemaIds(type => type.FriendlyId(true));
        c.IncludeXmlComments(
quot;{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();

The only customization is the controllers name resolution.

Any help really appreciated, best regards

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

土豪 2025-01-25 11:12:28

As

以.net 6开始,新项目包括
&lt; nullable&gt; enable&lt;/nullable&gt; element在项目文件中。一旦
功能已打开,现有的参考变量声明变为
不可用的参考类型

在.NET 6中,必须需要不可解开的属性,否则模型状态将无效。

为了达到您的要求,您可以从项目文件中删除&lt; nullable&gt;/nullable&gt;

另一种方法是您可以添加允许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..
}

As this document said:

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.

In .NET 6 the non-nullable property must be required, otherwise the ModelState will be invalid.

To achieve your requirement, you can remove <Nullable>enable</Nullable> from your project file.

Another way is that you can add ? to allow 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..
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文