Как создать объект, который имеет все свойства другого, только Nullable (когда это применимо)? - PullRequest
0 голосов
/ 04 июля 2019

Я создаю проект, который будет управлять конфигурациями приложений. Он будет очень общим, многократно используемым в разных приложениях (с разными моделями конфигурации в каждом) и очень гибким, включая возможность создавать / сохранять / хранить / читать / объединять частичные конфигурации из нескольких источников.

Не вдаваясь в подробности, вот пример того, что мне нужно сделать.
У меня есть класс, как показано ниже:

public class TestConfigModel
{
    public int SomeIntValue { get; set; }
    public string SomeStringValue { get; set; }
    public TestConfigSubsection Subsection { get; set; }
}

public class TestConfigSubsection
{
    public System.DayOfWeek SomeSubsectionEnumValue { get; set; }
    public Guid SomeSubsectionGuidValue { get; set; }
}

Мне нужно динамически сгенерировать версию этой модели, которая имеет все свойства, допускающие значение NULL (если они уже не принимают значение NULL):

public class TestConfigModelNullable
{
    public int? SomeIntValue { get; set; }
    public string SomeStringValue { get; set; } // already takes a null
    public TestConfigSubsection Subsection { get; set; } // already takes a null
}

public class TestConfigSubsectionNullable
{
    public System.DayOfWeek? SomeSubsectionEnumValue { get; set; }
    public Guid? SomeSubsectionGuidValue { get; set; }
}

Пример использования:

У меня есть конфигурация по умолчанию (полная), например:

var aConfigInstance = new TestConfigModel()
{
    SomeIntValue = 3,
    SomeStringValue = "hey",
    Subsection = new TestConfigSubsection()
    {
        SomeSubsectionEnumValue = DayOfWeek.Thursday,
        SomeSubsectionGuidValue = Guid.Parse("{2C475019-5AAC-43C6-AC87-21947A40E3B7}")
    }
};

Теперь мне нужно иметь возможность создавать, сериализовать, хранить, а затем десериализовать и работать с моделью частичной конфигурации, как показано ниже:

var aPartialConfigInstance = new TestConfigModelNullable()
{
    SomeIntValue = 4,
    Subsection = new TestConfigSubsection()
    {
        SomeSubsectionEnumValue = DayOfWeek.Monday
    }
};

... со всеми недостающими свойствами null. Если я попытаюсь сделать то же самое с исходным классом, все остальные необнуляемые поля получат значения по умолчанию, и это будет плохо (как мне узнать, предназначено ли значение int 0 или нет? Может быть, это имеет смысл для потребителя приложение).

Тем не менее, я новичок в размышлениях в целом и не уверен, как подойти к этому. Ваша помощь будет высоко ценится:)

Напомним, что мы не знаем модель заранее.

Ответы [ 2 ]

0 голосов
/ 12 июля 2019
// Just for convenience
public Type CreateNullableTypeFrom<T>()
{
  return CreateNullableTypeFrom(typeof(T));
}

public Type CreateNullableTypeFrom(Type typeToConvert)
{
  // Get the AssemblyName where the type is defined
  AssemblyName assembly = typeToConvert.Assembly.GetName();
  AssemblyBuilder dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(assembly, AssemblyBuilderAccess.Run);
  ModuleBuilder dynamicModule = dynamicAssembly.DefineDynamicModule(assembly.Name);
  TypeBuilder typeBuilder = dynamicModule.DefineType(typeToConvert.Name + "Nullable");

  // Loop through the properties
  foreach(PropertyInfo property in typeToConvert.GetProperties())
  {
    // If property is value type, it can't be null
    if(property.PropertyType.IsValueType)
    {
      // Create a nullable type for the property
      typeBuilder.DefineProperty(property.Name, property.Attributes, typeof(Nullable<>).MakeGenericType(property.PropertyType), Type.EmptyTypes);
    }
    // The property can be null
    else
    {
      // Create a similar property
      typeBuilder.DefineProperty(property.Name, property.Attributes, property.PropertyType, Type.EmptyTypes);
    }
  }

  // Finally, create the type
  Type convertedType = typeBuilder.CreateType();
  Console.WriteLine(convertedType.Name);
  // Note: to access the properties of the converted type through reflection,
  //       use GetRuntimeProperties method, not GetProperties, since GetProperties
  //       will return an empty array because the type was created an runtime

  return convertedType;
}
0 голосов
/ 04 июля 2019

Мне довелось реализовать несколько подобных механизмов в нескольких разных вариантах.Реализация «автомагистрального» механизма подразумевает немало тяжелой работы.

Здесь я бы не предлагал создавать отдельные обнуляемые версии моделей.Вместо этого я бы предпочел сделать все свойства модели 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
        }
    }
}

...