Мне довелось реализовать несколько подобных механизмов в нескольких разных вариантах.Реализация «автомагистрального» механизма подразумевает немало тяжелой работы.
Здесь я бы не предлагал создавать отдельные обнуляемые версии моделей.Вместо этого я бы предпочел сделать все свойства модели Optional<T>
, что похоже на Nullable<T>
, но работает и для ссылочных типов.Таким образом, частичные модели будут представлены теми же типами, что и «базовые» модели.
Такой подход избавит от сложности генерации кода (T4, Roslyn, CodeDom или Reflection.Emit - все это требует больших усилий, включая их включение в процесс сборки).
Кроме того, в любом подходе должна быть реализована логика «слияния», которая применяет частичную модель к «базовой».В подходе генерации кода логика слияния может быть сгенерирована как часть обнуляемых моделей.В подходе Optional<T>
он может быть жестко запрограммирован или реализован в общем виде с отражением времени выполнения (не Reflection.Emit).Жестко запрограммированный способ представляется наиболее простым, но для большого количества моделей и свойств может лучше подходить время выполнения Reflection.
Как это будет выглядеть
Модели будут выглядеть так:
public class TestConfigModel
{
public Optional<int> SomeIntValue { get; set; }
public Optional<string> SomeStringValue { get; set; }
public Optional<TestConfigSubsection> Subsection { get; set; }
}
С помощью операторов неявного преобразования Optional<T>
вы сможете инициализировать значения разделов как обычно:
var config = new TestConfigModel {
SomeIntValue = 123,
SomeStringValue = "ABC",
Subsection = new TestConfigSubsection {
SomeSubsectionEnumValue = DayOfWeek.Thursday
}
};
Общая логика слияния может быть реализована путем введенияМетод Apply
для Optional<T>
:
Optional<T> Apply(Optional<T> partial, Func<T, T, Optional<T>> merge = null)
Каждая модель должна будет реализовать свой собственный метод ApplyXxxx()
, который будет передан в параметре merge
, например:
public class TestConfigModel
{
// ...properties
public Optional<TestConfigModel> ApplyModel(TestConfigModel partial)
{
SomeIntValue = SomeIntValue.Apply(partial.SomeIntValue);
SomeStringValue = SomeStringValue.Apply(partial.SomeStringValue);
Subsection = Subsection.Apply(
partial.Subsection,
merge: (left, right) => left.ApplySubsection(right));
return this;
}
}
public class TestConfigSubsection
{
// ...properties
public Optional<TestConfigSubsection> ApplySubsection(TestConfigSubsection partial)
{
SomeSubsectionEnumValue = SomeSubsectionEnumValue.Apply(partial.SomeSubsectionEnumValue);
SomeSubsectionGuidValue = SomeSubsectionGuidValue.Apply(partial.SomeSubsectionGuidValue);
return this;
}
}
Optional<T>
Встроенная реализация Optional<T>
запланирована для C # 8, но может быть легко реализована (в основном аналогично Nullable<T>
).
public interface IOptional
{
bool HasValue { get; }
object Value { get; }
}
public struct Optional<T> : IOptional
{
private readonly bool _hasValue;
private readonly T _value;
public Optional(T value)
{
_value = value;
_hasValue = true;
}
public bool HasValue => _hasValue;
object IOptional.Value => Value;
public T Value
{
get
{
if (!_hasValue)
{
throw new InvalidOperationException("has no value");
}
return _value;
}
}
public T GetValueOrDefault() => _value;
public T GetValueOrDefault(T defaultValue)
{
if (!_hasValue)
{
return defaultValue;
}
return _value;
}
public bool IsNullValue => _hasValue && ReferenceEquals(_value, null);
public override bool Equals(object other)
{
if (other is Optional<T> otherOptional)
{
if (_hasValue != otherOptional.HasValue)
{
return false;
}
if (_hasValue)
{
return CompareValue(otherOptional.Value);
}
return true;
}
return false;
}
bool CompareValue(object otherValue)
{
if (_value == null)
{
return (otherValue == null);
}
return _value.Equals(otherValue);
}
public override int GetHashCode()
{
if (_hasValue || ReferenceEquals(_value, null))
{
return 0;
}
return _value.GetHashCode();
}
public override string ToString()
{
if (!_hasValue || ReferenceEquals(_value, null))
{
return "";
}
return _value.ToString();
}
public Optional<T> Apply(Optional<T> partial, Func<T, T, Optional<T>> merge = null)
{
if (!_hasValue && partial.HasValue)
{
return partial;
}
if (_hasValue && partial.HasValue)
{
if (ReferenceEquals(_value, null))
{
return partial.Value;
}
if (!ReferenceEquals(partial.Value, null))
{
if (merge != null)
{
return merge(_value, partial.Value);
}
throw new InvalidOperationException("both values exist and merge not provided");
}
}
return this;
}
public static implicit operator Optional<T>(T value)
{
return new Optional<T>(value);
}
public static explicit operator T(Optional<T> value)
{
return value.Value;
}
}
Сериализация
Последнее, что осталось, - научить сериализаторы обрабатывать Optional<T>
.Например, Newtonsoft.Json потребует пользовательский JsonConverter
.Ниже не полная реализация, но она демонстрирует подход:
public class OptionalConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// TODO: implement properly
// roughly the approach is like this:
var hasValue = reader.ReadAsBoolean().GetValueOrDefault();
var innerValue = hasValue
? serializer.Deserialize(reader, objectType.GetGenericArguments([0])
: null;
return Activator.CreateInstance(
objectType,
innerValue != null ? new[] {innerValue} : new object[0]);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is IOptional optional)
{
// TODO: implement writing
}
}
}