Преобразование ненулевого выражения в нулевое без жесткого ввода/приведения в запросах linq

Я регулярно ловлю себя на том, что пишу такие вещи, как

int? min = someQueryable
    .Where(x => someCondition)
    .Select(x => (int?)x.someNonNullableIntProperty)
    .Min();

Это позволяет мне получить null вместо «System.ArgumentNullException: значение не может быть null», когда ничто не соответствует условию. (И я не хочу получать 0 вместо null.)

Я бы хотел избежать актерского состава. Есть ли какой-то другой способ, который я пропустил?

Сложность заключается в том, что он должен быть достаточно широко используемым, чтобы его изначально понимали (или игнорировали) поставщики linq. (linq-to-entities и linq-to -nhibernate для моих конкретных нужд.)

В противном случае мне пришлось бы добавить некоторые пользовательские расширения AsNullable к провайдеру linq-to-nh (что требует некоторой работы), и я не знаю, позволяет ли EF (6) это сделать.

Почему я хочу избежать приведения? Приведение может остаться незамеченным во время рефакторинга, а затем вызвать ошибки. И большая часть приведения, которое я вижу в коде, происходит из-за небрежности разработчиков, не пытающихся запомнить, например, что они могут иметь ненулевое значение из .Value для типов, допускающих значение NULL. (Существует также тернарный случай, такой как someCondition ? someIntProperty : (int?)null, но оператор "Элвис" ?., вероятно, позволит избежать большинства из них; и мы также можем написать default(int?), хотя это немного длинно. ?. можно было бы даже использовать для моего примера запроса, но это не было бы общим решением.)

Попытка new Nullable<int>(x.someNonNullableIntProperty) как предлагается здесь завершается ошибкой с NotSupportedException (с NH, не тестировалась с EF). Во всяком случае, мне бы это тоже не подошло. В случае более позднего изменения типа свойства это также может остаться незамеченным из-за неявного преобразования. (И попытка new Nullable(x.someNonNullableIntProperty) не компилируется, вывод аргумента универсального типа плохо работает с конструкторами.)

Попытка x.someNonNullableIntProperty as int? (которая по-прежнему является приведением, но в этом случае менее терпима к несоответствию типов, см. здесь) завершается неудачей с ArgumentException (NH тестировался снова, < sub>Выражение типа «System.Int32» не может использоваться в качестве возвращаемого типа «System.Nullable`1[System.Int32]» (переведено)).


person Frédéric    schedule 23.03.2016    source источник
comment
Если это простой синтаксис метода верхнего уровня Select, как в примере, я думаю, что-то можно сделать. Но вообще так делать нельзя.   -  person Ivan Stoev    schedule 23.03.2016


Ответы (2)


Я попробовал это один раз, но для IEnumerable и пришел вверх с

    public static T? max<T>(IEnumerable<T> values) where T: struct, IComparable<T>
    {
        T? result = null;
        foreach (var v in values)
            if (!result.HasValue || (v.CompareTo(result.Value) > 0))
                result = v;
        return result;
    }

Чтобы справиться с IQueryable, вам потребуется расширить библиотеку доступа к данным. В случае NHibernate механизм называется HqlGenerator. Ссылки см. в этом ответе.

person devio    schedule 23.03.2016
comment
Вместо того, чтобы переопределять min, max и все остальные, я бы определил некоторый AsNullable и расширил с ним linq-to-nh, как я уже сделал это здесь для другого тема. Но я думаю, что у нас действительно должен быть какой-то встроенный в .Net способ получить значение NULL из любого выражения struct, не допускающего значение NULL, без необходимости явно указывать его базовый тип. Похоже, что это недостающая функция. - person Frédéric; 24.03.2016
comment
Выполнено. Но не принимая ни то, ни другое, это не касается случая EF. - person Frédéric; 25.03.2016

Для Entity Framework я отказался. Это выглядит как слишком много усилий для меня. (См. здесь.)

Для NHibernate я сделал расширение AsNullable, о котором думал.

Это следует той же логике, что и другие мои расширения здесь и здесь.

Сначала определите расширение AsNullable:

public static class NullableExtensions
{
    public static T? AsNullable<T>(this T value) where T : struct
    {
        // Allow runtime use.
        // Not useful for linq-to-nhibernate, could be:
        // throw NotSupportedException();
        return value;
    }
}

Затем реализует свой HQL перевод (первоначально на основе NHibernate сравните реализацию, затем довольно упрощенную, см. правки):

public class AsNullableGenerator : BaseHqlGeneratorForMethod
{
    public AsNullableGenerator()
    {
        SupportedMethods = new[]
        {
            ReflectionHelper.GetMethodDefinition(() => NullableExtensions.AsNullable(0))
        };
    }

    public override HqlTreeNode BuildHql(MethodInfo method,
        Expression targetObject,
        ReadOnlyCollection<Expression> arguments,
        HqlTreeBuilder treeBuilder,
        IHqlExpressionVisitor visitor)
    {
        // Just have to transmit the argument "as is", HQL does not need a specific call
        // for null conversion.
        return visitor.Visit(arguments[0]).AsExpression();
    }
}

Расширьте реестр linq2NH по умолчанию с помощью вашего генератора:

public class ExtendedLinqToHqlGeneratorsRegistry :
    DefaultLinqToHqlGeneratorsRegistry
{
    public ExtendedLinqToHqlGeneratorsRegistry()
        : base()
    {
        this.Merge(new AsNullableGenerator());
    }
}

Теперь настройте NH для использования вашего нового реестра. В файле hibernate.cfg.xml добавьте следующий узел property под узлом session-factory:

<property name="linqtohql.generatorsregistry">YourNameSpace.ExtendedLinqToHqlGeneratorsRegistry, YourAssemblyName</property>

Или используя код:

using NHibernate.Cfg;
// ...

var cfg = new Configuration();
cfg.LinqToHqlGeneratorsRegistry<ExtendedLinqToHqlGeneratorsRegistry>();
// And build the session factory using this configuration.

Теперь мы можем переписать запрос.

int? min = someQueryable
    .Where(x => someCondition)
    .Select(x => x.someNonNullableIntProperty.AsNullable())
    .Min();
person Frédéric    schedule 24.03.2016