Мне нужно сделать что-то вроде следующего, но мне нужно сделать это, не добавляя атрибут или иным образом не загрязняя класс модели. Идеальное решение будет работать через JsonSerializerSettings
, не нарушая другие пользовательские сериализации. Кстати, нижеприведенный вопрос возник из этого вопроса: Пользовательское преобразование определенных объектов в JSON.NET
public class Person
{
public string FirstName { get; set; }
[JsonConverter(typeof(AllCapsConverter))]
public string LastName { get; set; }
// more properties here in the real example, some of which nest to properties that use their own JsonConverters.
}
JsonConverter
для этого игрушечного примера (содержание на самом деле не имеет значения; важно то, что я использую его для свойства):
public class AllCapsConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(string);
public override bool CanRead => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var str = value as string;
var upper = str.ToUpperInvariant();
JToken j = JToken.FromObject(upper);
j.WriteTo(writer);
}
}
Проходящий юнит-тест:
public class PersonSerializationTest
{
[Fact]
public void SerializePerson_LastNameCaps()
{
var person = new Person
{
FirstName = "George",
LastName = "Washington"
};
var serialized = JsonConvert.SerializeObject(person);
var expected = @"{""FirstName"":""George"",""LastName"":""WASHINGTON""}";
Assert.Equal(expected, serialized);
}
}
Вы можете применять конвертеры к определенным свойствам с помощью пользовательского IContractResolver
наследуемого от DefaultContractResolver
.
Сначала возьмите ConfigurableContractResolver
из этого ответа в разделе Как добавить метаданные, чтобы описать, какие свойства являются датами в JSON.Net:
public class ConfigurableContractResolver : DefaultContractResolver
{
// This contract resolver taken from the answer to
// https://stackoverflow.com/questions/46047308/how-to-add-metadata-to-describe-which-properties-are-dates-in-json-net
// /questions/6886001/how-to-add-metadata-to-describe-which-properties-are-dates-in-jsonnet/13693525#13693525
readonly object contractCreatedPadlock = new object();
event EventHandler<ContractCreatedEventArgs> contractCreated;
int contractCount = 0;
void OnContractCreated(JsonContract contract, Type objectType)
{
EventHandler<ContractCreatedEventArgs> created;
lock (contractCreatedPadlock)
{
contractCount++;
created = contractCreated;
}
if (created != null)
{
created(this, new ContractCreatedEventArgs(contract, objectType));
}
}
public event EventHandler<ContractCreatedEventArgs> ContractCreated
{
add
{
lock (contractCreatedPadlock)
{
if (contractCount > 0)
{
throw new InvalidOperationException("ContractCreated events cannot be added after the first contract is generated.");
}
contractCreated += value;
}
}
remove
{
lock (contractCreatedPadlock)
{
if (contractCount > 0)
{
throw new InvalidOperationException("ContractCreated events cannot be removed after the first contract is generated.");
}
contractCreated -= value;
}
}
}
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
OnContractCreated(contract, objectType);
return contract;
}
}
public class ContractCreatedEventArgs : EventArgs
{
public JsonContract Contract { get; private set; }
public Type ObjectType { get; private set; }
public ContractCreatedEventArgs(JsonContract contract, Type objectType)
{
this.Contract = contract;
this.ObjectType = objectType;
}
}
public static class ConfigurableContractResolverExtensions
{
public static ConfigurableContractResolver Configure(this ConfigurableContractResolver resolver, EventHandler<ContractCreatedEventArgs> handler)
{
if (resolver == null || handler == null)
throw new ArgumentNullException();
resolver.ContractCreated += handler;
return resolver;
}
}
Затем создайте метод для настройки JsonObjectContract
for Person
следующим образом:
public static class JsonContractExtensions
{
public static void ConfigurePerson(this JsonContract contract)
{
if (!typeof(Person).IsAssignableFrom(contract.UnderlyingType))
return;
var objectContract = contract as JsonObjectContract;
if (objectContract == null)
return;
var property = objectContract.Properties.Where(p => p.UnderlyingName == nameof(Person.LastName)).Single();
property.Converter = new AllCapsConverter();
}
}
И наконец сериализовать следующим образом:
// Cache the contract resolver statically for best performance.
var resolver = new ConfigurableContractResolver()
.Configure((s, e) => { e.Contract.ConfigurePerson(); });
var settigs = new JsonSerializerSettings
{
ContractResolver = resolver,
};
var person = new Person
{
FirstName = "George",
LastName = "Washington"
};
var serialized = JsonConvert.SerializeObject(person, settigs);
Заметки:
Вместо создания ConfigurableContractResolver
было бы возможно создать подкласс DefaultContractResolver
, переопределить DefaultContractResolver.CreateProperty
и жестко закодировать необходимую логику для Person.LastName
. Однако создание настраиваемого распознавателя, позволяющего объединять настройки во время выполнения, представляется более полезным и многократно используемым.
В AllCapsConverter.WriteJson()
было бы проще использовать writer.WriteValue(string)
для записи строки в верхнем регистре:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var upper = ((string)value).ToUpperInvariant();
writer.WriteValue(upper);
}
Вы можете кэшировать распознаватель контрактов для лучшей производительности.
Образец скрипки здесь.
Вы можете программно применить JsonConverter
к одному или нескольким свойствам в классе модели, не используя атрибуты через пользовательский ContractResolver
. Вот простой пример, который применяет ваш AllCapsConverter
к свойству LastName
в вашем классе Person
. (Если вы ищете более надежное решение, посмотрите на ответ @dbc. Я хотел показать самый простой пример, который мог бы сработать.)
public class CustomResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty prop = base.CreateProperty(member, memberSerialization);
if (prop.DeclaringType == typeof(Person) && prop.UnderlyingName == "LastName")
{
prop.Converter = new AllCapsConverter();
}
return prop;
}
}
Вот обновленный тест и модель Person
которая показывает, как использовать распознаватель:
public class PersonSerializationTest
{
[Fact]
public void SerializePerson_LastNameCaps()
{
var person = new Person
{
FirstName = "George",
LastName = "Washington"
};
var settings = new JsonSerializerSettings
{
ContractResolver = new CustomResolver()
};
var serialized = JsonConvert.SerializeObject(person, settings);
var expected = @"{""FirstName"":""George"",""LastName"":""WASHINGTON""}";
Assert.Equal(expected, serialized);
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
Рабочая демонстрация: https://dotnetfiddle.net/o4e3WP