路由中的枚举序列化

时间:2018-11-13 00:29:02

标签: c# asp.net-core json.net asp.net-core-2.1

我们有一个Web API,它对所有属性(包括枚举)使用了蛇形序列化,为了启用它,我们在启动时就使用了它:

services
    .AddMvcCore()
    .AddJsonOptions(opt =>
    {
        opt.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Local;
        opt.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
        opt.SerializerSettings.ContractResolver = new DefaultContractResolver
        {
            NamingStrategy = new SnakeCaseNamingStrategy { ProcessDictionaryKeys = true }
        };
        opt.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
        opt.SerializerSettings.Converters.Add(new StringEnumConverter());
    })
    .AddApiExplorer()
    .AddJsonFormatters(j => j.ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() { ProcessDictionaryKeys = true } })

这在属性上效果很好,但是我们在路由和枚举方面遇到了问题,例如,我们有这个枚举(我们也尝试过使用JsonProperty,但失败的方式相同):

[DataContract(Name = "document_type")]
public enum DocumentType
{
    [EnumMember(Value = "passport")]
    Passport,

    [EnumMember(Value = "proof_of_address")]
    ProofOfAddress,
}

,并且我们尝试按类型搜索文档,因此我们采用以下路线:

/clients/{clientId:guid/documents/{documentType}

,并在控制器中显示:

[HttpGet]
[Route("/clients/{clientId:guid}/documents/{documentType}")]
public async Task<IActionResult> FindClientDocuments([FromRoute] Guid clientId, [FromRoute] DocumentType documentType)

通过此路线,一切正常:

/ clients / 60a00cd4-59e2-4f52-871a-4029370f6dd8 / documents / ProofOfAddress

但不适用于此

clients / 60a00cd4-59e2-4f52-871a-4029370f6dd8 / documents / proof_of_address

在后一种情况下,枚举始终是默认值,或者如果我们添加动作过滤器,则错误为“值'proof_of_address'无效。”

除了尝试使用过滤器自己转换值之外,是否有其他方法可以使这种设想有效?

谢谢

1 个答案:

答案 0 :(得分:1)

我认为这个问题可以分为两部分,

  1. 使用EnumMemberAttribute
  2. 将自定义字符串转换为Enum
  3. 告诉MVC使用此自定义转换来绑定模型输入。

我将回答第二部分,希望第一部分应该相对容易实施/研究。我直接在第一部分中加入了link to a question

告诉MVC使用自定义枚举转换来绑定模型输入。

总是通过模型绑定从客户端获取值,而json序列化主要处理将响应数据格式化为json以便输出。因此,您的解决方案应考虑将要使用的枚举反序列化告知模型绑定程序。 (enum <=>字符串转换的默认实现不考虑属性)。

我为我的枚举尝试了以下自定义模型活页夹,并且效果很好。 FooType是我的枚举:

public enum FooType
{
    [Description("test")]
    TestFoo,
    [Description("another")]
    AnotherFooType
}

我跳过[EnumMember]属性并选择了[Description]属性仅仅是因为我想使用Humanizer's Enum utility,但是您可以实现自己的从{{1 }}属性,只需替换我对EnumMember的调用,即可在this question上找到一个示例。

DeHumanizeTo<FooType>()

然后,在我的控制器中,我告诉MVC使用此绑定器来绑定我的参数,如下所示:

    public class FooTypeBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext) => Task.Run(() => this.BindModel(bindingContext));

        private void BindModel(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (valueProviderResult.Length == 0)
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "No value was provided for enum");
                return;
            }

            var stringValue = valueProviderResult.FirstValue;
            try
            {
                bindingContext.Model = this.FromString(stringValue);
            }
            catch (NoMatchFoundException ex)
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
            }
        }

        private FooType FromString(string input)
        {
            //Here you should implement your custom way of checking the [EnumMember] attribute, and convert input into your enum.
            if (Enum.TryParse(typeof(FooType), input, true, out object value))
            {
                return (FooType)value;
            }
            else return input.DehumanizeTo<FooType>();
        }
    }

我希望这会有所帮助。