Автоматически связывать модель паскаля case c # из случая JSON в WebApi - PullRequest
0 голосов
/ 05 февраля 2019

Я пытаюсь связать мою модель PascalCased c # из snake_cased JSON в WebApi v2 (полный фреймворк, а не ядро ​​с точечной сетью).

Вот мой 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 для каждого свойства.Можно ли заставить его работать "автоматически"?Возможно, с пользовательским механизмом связывания моделей или пользовательским конвертером json?

Ответы [ 3 ]

0 голосов
/ 05 февраля 2019

Ну, вы должны быть в состоянии сделать это, используя пользовательский JsonConverter для чтения ваших данных.Используя десериализацию, предоставленную в ответе Manojs , вы можете создать DefaultContractResolver, который будет создавать настраиваемую десериализацию, когда класс имеет SnakeCasedAttribute, указанный выше.

КонтрактныйResolver будет выглядеть следующим образомследующее

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, используя этот атрибут (который является просто заполнителем)

[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» не являетсяint), но я собираюсь догадаться, что отсюда вы можете сами справиться с этой частью:)

И для полной справки вы можете увидеть рабочую версию в this dotnetfiddle

0 голосов
/ 05 февраля 2019

Не нужно изобретать велосипед.Json.Net уже имеет класс SnakeCaseNamingStrategy, чтобы делать именно то, что вы хотите.Вам просто нужно установить его как NamingStrategy на DefaultContractResolver через настройки.

Добавьте эту строку к методу Register в вашем WebApiConfig классе:

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

Вот демонстрационная версия (консольное приложение) для подтверждения концепции: https://dotnetfiddle.net/v5siz7


Если вы хотите применить кожух змеи к некоторым классам, но не к другим, вы можете сделать это, применив атрибут [JsonObject], задающий стратегию именования следующим образом:

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

НаименованиеСтратегия, установленная с помощью атрибута, имеет приоритет над стратегией именования, установленной с помощью резольвера, поэтому вы можете установить стратегию по умолчанию в резолвере и затем использовать атрибуты, чтобы переопределить ее при необходимости.(В Json.Net включены три стратегии именования: SnakeCaseNamingStrategy, CamelCaseNamingStrategy и DefaultNamingStrategy.)


Теперь, если вы хотите десериализовать , используя одну стратегию именования, и сериализовать , используя другую стратегию для того же класса (классов), то ни одно из вышеуказанных решений не будет работать для вас,потому что стратегии именования будут применяться в обоих направлениях в веб-API.Так что в этом случае вам понадобится что-то нестандартное, например, то, что показано в answer @ icepickle, чтобы контролировать применение каждого из них.

0 голосов
/ 05 февраля 2019

Вы можете добавить код конвертера cusrom 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;
    }
}

Затем укажите этот атрибут в вашем классе.

Это должно работать.

Этот блог объясняет подход с использованием консольного приложения.https://www.jerriepelser.com/blog/deserialize-different-json-object-same-class/

...