如何在 Web API 请求中的 FromBody ViewModel 中使用带有 EnumMember 属性的枚举?

cit*_*nas 7 c# asp.net-mvc asp.net-web-api asp.net-core .net-core-3.0

我正在尝试使用视图模型和枚举在 ASP.NET Core Web API 项目中实现 HttpPost 方法[FromBody]。过去,将视图模型与[FromBody]属性绑定效果很好。

在我的特定场景中,我想提供一个 JSON 端点,在其中将给定值转换为具有不同名称的 C# 枚举。这个例子应该解释我想要实现的目标:

    公共枚举 WeatherEnum
    {
        [EnumMember(值 = "好")]
        好的,

        [EnumMember(值 = "坏")]
        坏的
    }

在内部,我想使用WeatherEnum.Goodand WeatherEnum.Bad,并且我的端点的使用者希望使用小写值。因此,我尝试将 JSON 正文中传递的值映射到我的 Enum 表示形式。

我读过有关EnumMember属性 和 的内容StringEnumConverter。我已经从新的 ASP.NET Core Web API 3.0 模板创建了一个最小示例(您需要添加这些 NuGet 包Microsoft.Extensions.DependencyInjectionMicrosoft.AspNetCore.Mvc.NewtonsoftJsonNewtonsoft.Json

配置服务

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
    }).SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
    .AddNewtonsoftJson(json =>
    {
        json.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
        json.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
    });
    services.AddControllers();
}
Run Code Online (Sandbox Code Playgroud)

天气预测控制器

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Runtime.Serialization;

namespace WebAPITestEnum.Controllers
{
    [ApiController]
    [Produces("application/json")]
    [Consumes("application/json")]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        [HttpPost]
        [Route("method")]
        public ActionResult<QueryResponseClass> TestMethod([FromBody] QueryRequestClass request)
        {
            // do something with the request ...

            return new QueryResponseClass()
            {
                Foo = "bar"
            };
        }
    }

    public class QueryRequestClass
    {
        public WeatherEnum Weather { get; set; }
    }

    public class QueryResponseClass
    {
        public string Foo { get; set; }
    }


    [JsonConverter(typeof(StringEnumConverter))]
    public enum WeatherEnum
    {
        [EnumMember(Value = "good")]
        Good,

        [EnumMember(Value = "bad")]
        Bad
    }
}
Run Code Online (Sandbox Code Playgroud)

我的端点是从 Postman 调用的,正文如下

{
  "Weather": "good"
}
Run Code Online (Sandbox Code Playgroud)

这会导致此错误:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|245d862e-4ab01d3956be5f60.",
    "errors": {
        "$.Weather": [
            "The JSON value could not be converted to WebAPITestEnum.Controllers.WeatherEnum. Path: $.Weather | LineNumber: 1 | BytePositionInLine: 18."
        ]
    }
}
Run Code Online (Sandbox Code Playgroud)

感觉好像我只在某个地方漏掉了一行。可以在视图模型中使用带有FromBody属性的枚举吗?

cit*_*nas 6

我发布的问题中的代码确实有效。[Required]在我的最小示例中,我忘记在枚举上设置属性。但是,然后我遇到了如果未设置该值该方法应如何反应的问题。它正确地(?)假设了枚举的默认值,这不是我想要的。

我四处搜索并找到了这个解决方案/sf/answers/3794471621/枚举可为空,这并不理想,但至少我进行了验证,并且如果该值丢失,我会收到一条错误消息

更新/警告:您可以使用上面引用的解决方案,但是!代码似乎可以编译,但会抛出问题中的错误消息。我进一步将我自己的项目与测试项目进行比较,发现我还需要两个包含 2 个 NuGet 包才能使一切正常工作:

  • Microsoft.AspNetCore.Mvc.NewtonsoftJson
  • Newtonsoft.Json

似乎 Microsoft.AspNetCore.Mvc.NewtonsoftJson 覆盖了默认行为?如果有人能对此有所启发,我将非常感激。

更新 2:我还更新了引用的解决方案,以根据 EnumMemberAttribute 解析枚举值:

[JsonConverter(typeof(CustomStringToEnumConverter<WeatherEnum>))]
public enum WeatherEnum
{
    [EnumMember(Value = "123good")]
    Good,

    [EnumMember(Value = "bad")]
    Bad
}

public class CustomStringToEnumConverter<T> : StringEnumConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (string.IsNullOrEmpty(reader.Value?.ToString()))
        {
            return null;
        }
        try
        {
            return EnumExtensions.GetValueFromEnumMember<T>(reader.Value.ToString());
        }
        catch (Exception ex)
        {
            return null;
        }
    }
}

public static class EnumExtensions
{
    public static T GetValueFromEnumMember<T>(string value)
    {
        var type = typeof(T);
        if (!type.IsEnum) throw new InvalidOperationException();
        foreach (var field in type.GetFields())
        {
            var attribute = Attribute.GetCustomAttribute(field,
                typeof(EnumMemberAttribute)) as EnumMemberAttribute;
            if (attribute != null)
            {
                if (attribute.Value == value)
                    return (T)field.GetValue(null);
            }
            else
            {
                if (field.Name == value)
                    return (T)field.GetValue(null);
            }
        }
        throw new ArgumentException($"unknow value: {value}");
    }
}
Run Code Online (Sandbox Code Playgroud)