Строка конфигурации с Null DefaultValue

У меня есть следующий ConfigurationProperty как часть элемента:

[ConfigurationProperty("example", IsRequired = false, DefaultValue = null)]
public string Example { 
    get { return (string)base["example"]; }
    set { base["example"] = value; }
}

Если я устанавливаю его следующим образом, он берет строку "Hello" и работает правильно:

<myElement example="Hello"/>

Если этого нет, у меня возникают проблемы:

<myElement/>

Вместо того, чтобы принимать значение по умолчанию null, как указано выше, оно принимает значение String.Empty. Почему это, и как я могу заставить его взять значение по умолчанию null?

Обновление

Это определенно потому, что base["example"] возвращает String.Empty, где base является ConfigurationElement (здесь указан указатель: https://msdn.microsoft.com/en-us/library/c8693ks1(v=vs.110).aspx), но Я все еще не уверен, почему он не принимает значение null.

Обновление

Даже DefaultValue = default(string) устанавливает строку в String.Empty.

Обновление

Even base.Properties.Contains("example") возвращает true, если свойство не существует в конфигурации.

Ответ 1

Судя по Reference Source для класса ConfigurationProperty, это, возможно, не ошибка, а особенность.

Вот соответствующий внутренний метод InitDefaultValueFromTypeInfo (с некоторыми незначительными изменениями форматирования):

private void InitDefaultValueFromTypeInfo(ConfigurationPropertyAttribute attribProperty,
                                          DefaultValueAttribute attribStdDefault) {
     object defaultValue = attribProperty.DefaultValue;

     // If there is no default value there - try the other attribute ( the clr standard one )
     if ((defaultValue == null || defaultValue == ConfigurationElement.s_nullPropertyValue) &&
         (attribStdDefault != null)) {
         defaultValue = attribStdDefault.Value;
     }

     // If there was a default value in the prop attribute - check if we need to convert it from string
     if ((defaultValue != null) && (defaultValue is string) && (_type != typeof(string))) {
         // Use the converter to parse this property default value
         try {
             defaultValue = Converter.ConvertFromInvariantString((string)defaultValue);
         }
         catch (Exception ex) {
             throw new ConfigurationErrorsException(SR.GetString(SR.Default_value_conversion_error_from_string, _name, ex.Message));
         }
     }

     if (defaultValue == null || defaultValue == ConfigurationElement.s_nullPropertyValue) {
         if (_type == typeof(string)) {
             defaultValue = String.Empty;
         }
         else if (_type.IsValueType) {
             defaultValue = TypeUtil.CreateInstanceWithReflectionPermission(_type);
         }
     }

     SetDefaultValue(defaultValue);
 }

Интересен последний блок if: если ваше свойство имеет тип string, а значение по умолчанию - null, значение по умолчанию изменяется на string.Empty.

Первый if блок намекает на возможное объяснение этого своеобразного поведения. Атрибут [ConfigurationProperty] атрибут DefaultValue не является обязательным. Если DefaultValue не задан программистом, то по умолчанию он сам null. В первом блоке if используется значение по умолчанию null, чтобы проверить, был ли указан параметр DefaultValue. Если нет, он возвращается к извлечению значения по умолчанию из атрибута [DefaultValue], если таковой присутствует.

Все это означает: Указание DefaultValue = null имеет тот же эффект, что и не указывая его вообще, и в этом случае подсистема конфигурации выбирает "нормальное" значение по умолчанию для строк: пустая строка.

Обход проблемы:

Вот несколько хакерский способ обхода: не объявляйте свое свойство конфигурации как string, а как тонкий тип оболочки вокруг строки; затем объявите подходящий конвертер типов:

[ConfigurationProperty("name", IsRequired = false)]
[TypeConverter(typeof(IncognitoStringConverter))]  // note: additional attribute!
public IncognitoString Name                        // note: different property type
{
    get
    {
        return (IncognitoString)base["name"];
    }
    set
    {
        base["name"] = value;
    }
}

Вот реализации для IncognitoString и IncognitoStringConverter:

public struct IncognitoString
{
    private IncognitoString(string value)
    {
        this.value = value;
    }

    private readonly string value;

    public static implicit operator IncognitoString(string value)
    {
        return new IncognitoString(value);
    }

    public static implicit operator string(IncognitoString incognitoString)
    {
        return incognitoString.value;
    }

    … // perhaps override ToString, GetHashCode, and Equals as well.
}

public sealed class IncognitoStringConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType == typeof(string);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        return (IncognitoString)(string)value;
    }
}

Поскольку IncognitoString неявно конвертируется в string, вы можете присвоить значение свойства любой строковой переменной. Я знаю, он взломан и очень сложный, чтобы получить свойства с нулевым значением. Возможно, просто живите с пустой строкой.

Ответ 2

Другим обходным решением является вызов такого типа:

[ConfigurationProperty("Prompt")]
public string Prompt
{
    get { return this.GetNullableStringValue("Prompt"); }
}

private string GetNullableStringValue(string propertyName)
{
    return (string)this[new ConfigurationProperty(propertyName, typeof(string), null)];
}

Вызов GetNullableString, как это, обходит атрибут свойства конфигурации и останавливает его по умолчанию на значение DefaultValue равным null. Вы можете поместить метод в базовый класс, чтобы сделать его немного более аккуратным.

Вам просто нужно помнить, что вы вызываете его, если хотите изменить значение по умолчанию.

Вы также можете отправить вызов this.ElementInformation.Properties[propertyName], если хотите снять какую-либо другую вещь, которую вы можете определить по атрибуту, - просто не используйте это для заполнения DefaultValue

Ответ 3

Вместо проверки значения свойства для null вы можете легко проверить, было ли задано свойство в файле конфигурации или значение по умолчанию. Это можно сделать, глядя на ValueOrigin в ConfigurationElement ElementInformation.

// if not the default value...    
if (MyConfigurationElement.ElementInformation.Properties["example"].ValueOrigin!=
        PropertyValueOrigin.Default)
{
    ...
}

См. также документацию о значениях PropertyValueOrigin Enumeration.

Ответ 4

Тип ConfigurationElement имеет свойство ElementInformation, которое, в свою очередь, имеет свойство IsPresent.

Итак, вместо того, чтобы возвращать null ConfigurationElement, вы можете проверить свойство IsPresent, чтобы увидеть, связан ли связанный объект ConfigurationElement с конфигурационным файлом. " 1

Например:

if (Configuration.Example.ElementInformation.IsPresent)
{
    ...
}

Ответ 5

Я решил использовать более читаемый и многоразовый метод расширения ToNullIfEmpty(). Я оставил DefaultValue на месте в случае, если когда-либо произойдет изменение неинтуитивного поведения, которое преобразует нулевые строки в String.Empty.

[ConfigurationProperty("dataCenterRegion", DefaultValue = null)]
public string DataCenterRegion
{
    get { return ((string)this["dataCenterRegion"]).ToNullIfEmpty(); }
    set { this["dataCenterRegion"] = value; }
}

public static partial class ExtensionMethods
{        
    /// <summary>
    /// Return null if the string is empty or is already null.
    /// Otherwise, return the original string.
    /// </summary>
    public static string ToNullIfEmpty(this string str)
    {
        return String.IsNullOrEmpty(str) ? null : str;
    }

    /// <summary>
    /// Return null if the string is white space, empty or is already null.
    /// Otherwise, return the original string.
    /// </summary>
    public static string ToNullIfWhiteSpaceOrEmpty(this string str)
    {
        return String.IsNullOrWhiteSpace(str) ? null : str;
    }
}