在 WebApi 中自动绑定来自 snake case JSON 的 pascal case c# 模型

Automatically bind pascal case c# model from snake case JSON in WebApi

我正在尝试从 snake_cased JSON 中的 WebApi v2(完整框架,而非点网核心)绑定我的 PascalCased c# 模型。

这是我的 api:

public class MyApi : ApiController
{
    [HttpPost]
    public IHttpActionResult DoSomething([FromBody]InputObjectDTO inputObject)
    {
        database.InsertData(inputObject.FullName, inputObject.TotalPrice)
        return Ok();
    }
}

这是我的输入对象:

public class InputObjectDTO
{
    public string FullName { get; set; }
    public int TotalPrice { get; set; }
    ...
}

我遇到的问题是 JSON 看起来像这样:

{
    "full_name": "John Smith",
    "total_price": "20.00"
}

我知道我可以使用 JsonProperty 属性:

public class InputObjectDTO
{
    [JsonProperty(PropertyName = "full_name")]
    public string FullName { get; set; }

    [JsonProperty(PropertyName = "total_price")]
    public int TotalPrice { get; set; }
}

但是我的 InputObjectDTO 是 巨大的,而且还有很多其他人也喜欢它。它有数百个属性都是蛇形的,如果不必为每个 属性 指定 JsonProperty 属性就好了。我可以让它工作 "automatically" 吗?也许使用自定义模型活页夹或自定义 json 转换器?

您可以添加自定义 json 转换器代码,如下所示。这应该允许您指定 属性 映射。

public class ApiErrorConverter : JsonConverter
{
private readonly Dictionary<string, string>     _propertyMappings = new Dictionary<string, string>
{
    {"name", "error"},
    {"code", "errorCode"},
    {"description", "message"}
};

public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    throw new NotImplementedException();
}

public override bool CanConvert(Type objectType)
{
    return objectType.GetTypeInfo().IsClass;
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    object instance = Activator.CreateInstance(objectType);
    var props = objectType.GetTypeInfo().DeclaredProperties.ToList();

    JObject jo = JObject.Load(reader);
    foreach (JProperty jp in jo.Properties())
    {
        if (!_propertyMappings.TryGetValue(jp.Name, out var name))
            name = jp.Name;

        PropertyInfo prop = props.FirstOrDefault(pi =>
            pi.CanWrite && pi.GetCustomAttribute<JsonPropertyAttribute>().PropertyName == name);

        prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
    }

    return instance;
    }
}

然后在您的 class 上指定此属性。

这应该有效。

这篇博客解释了使用控制台应用程序的方法。 https://www.jerriepelser.com/blog/deserialize-different-json-object-same-class/

好吧,您应该可以使用自定义 JsonConverter 来读取您的数据。使用 中提供的反序列化,您可以创建一个 DefaultContractResolver,它会在 class 具有上面指定的 SnakeCasedAttribute 时创建自定义反序列化。

ContractResolver 如下所示

public class SnakeCaseContractResolver : DefaultContractResolver {
  public new static readonly SnakeCaseContractResolver Instance = new SnakeCaseContractResolver();

  protected override JsonContract CreateContract(Type objectType) {
    JsonContract contract = base.CreateContract(objectType);

    if (objectType?.GetCustomAttributes(true).OfType<SnakeCasedAttribute>().Any() == true) {
      contract.Converter = new SnakeCaseConverter();
    }

    return contract;
  }
}

SnakeCaseConverter会是这样的吗?

public class SnakeCaseConverter : JsonConverter {
  public override bool CanConvert(Type objectType) => objectType.GetCustomAttributes(true).OfType<SnakeCasedAttribute>().Any() == true;
  private static string ConvertFromSnakeCase(string snakeCased) {
    return string.Join("", snakeCased.Split('_').Select(part => part.Substring(0, 1).ToUpper() + part.Substring(1)));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
    var target = Activator.CreateInstance( objectType );
    var jobject = JObject.Load(reader);

    foreach (var property in jobject.Properties()) {
      var propName = ConvertFromSnakeCase(property.Name);
      var prop = objectType.GetProperty(propName);
      if (prop == null || !prop.CanWrite) {
        continue;
      }
      prop.SetValue(target, property.Value.ToObject(prop.PropertyType, serializer));
    }
    return target;
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
    throw new NotImplementedException();
  }
}

然后您可以使用此属性(这只是一个占位符)

注释您的 dto class
[SnakeCased]
public class InputObjectDTO {
  public string FullName { get; set; }
  public int TotalPrice { get; set; }
}

作为参考,这是使用的属性

[AttributeUsage(AttributeTargets.Class)]
public class SnakeCasedAttribute : Attribute {
  public SnakeCasedAttribute() {
    // intended blank
  }
}

还有一件事要注意,在您当前的形式中,JSON 转换器会抛出一个错误(“20.00”不是一个整数),但我猜您可以从这里处理它分开你自己:)

要获得完整参考,您可以在 this dotnetfiddle

中查看工作版本

无需重新发明轮子。 Json.Net 通过设置已经有一个 SnakeCaseNamingStrategy class to do exactly what you want. You just need to set it as the NamingStrategy on the DefaultContractResolver

将此行添加到 WebApiConfig 中的 Register 方法 class:

config.Formatters.JsonFormatter.SerializerSettings.ContractResolver =
    new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() };

这里有一个演示(控制台应用程序)来证明这个概念:https://dotnetfiddle.net/v5siz7


如果您想将蛇形外壳应用于某些 class 而不是其他,您可以通过应用指定命名策略的 [JsonObject] 属性来实现,如下所示:

[JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public class InputObjectDTO
{
    public string FullName { get; set; }
    public decimal TotalPrice { get; set; }
}

通过属性设置的命名策略优先于通过解析器设置的命名策略,因此您可以在解析器中设置默认策略,然后在需要时使用属性覆盖它。 (Json.Net 包含三种命名策略:SnakeCaseNamingStrategy, CamelCaseNamingStrategy and DefaultNamingStrategy。)


现在,如果您想要 反序列化 使用一种命名策略,而 序列化 使用不同的策略 class (es),那么以上两种解决方案都不适合您,因为命名策略将在 Web 中双向应用 API。因此,在那种情况下,您将需要一些自定义的东西,例如@icepickle 的 中显示的内容来控制何时应用每个。