Особая проблема не в типе DateTime
, а в методе DateTime.Today
.
Общая проблема заключается в том, что HqlGenerators вызываются слишком поздно в конвейере обработки выражения запроса NHibernate LINQ, так много частейпервоначальной предварительной обработки выражения, такой как частичная оценка, параметризация и т. д.Разницу можно легко увидеть даже с помощью «рабочего» запроса: если вы используете x => x.Sex == "Male"
непосредственно в запросе LINQ, запрос SQL параметризуется, а в переведенном SQL из x => x.IsMale
используется константный литерал.
То, чего вы пытаетесь достичь, - это в основном замена одного выражения другим внутри дерева выражений, для чего и нужны ExpressionVisitor
s.И все, что вам нужно, - это иметь возможность предварительно обработать выражение запроса перед поставщиком запросов.
Как ни странно, ни один из основных поставщиков запросов LINQ (NHibernate, EF6, EF Core) не предоставляет способсделать это.Но об этом позже.Позвольте мне сначала показать метод, необходимый для применения спецификаций (проверка ошибок исключена):
public static class SpecificationExtensions
{
public static Expression ApplySpecifications(this Expression source) =>
new SpecificationsProcessor().Visit(source);
class SpecificationsProcessor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression != null && node.Member is PropertyInfo property)
{
var info = property.GetCustomAttribute<SpecificationAttribute>();
if (info != null)
{
var type = property.DeclaringType;
var specificationMemberInfo = type.GetFields(BindingFlags.Static | BindingFlags.Public)
.Single(x => x.Name == info.FieldName);
var specification = (BaseSpecification)specificationMemberInfo.GetValue(null);
var specificationExpression = (LambdaExpression)specification.ToExpression();
var expression = specificationExpression.Body.ReplaceParameter(
specificationExpression.Parameters.Single(), Visit(node.Expression));
return Visit(expression);
}
}
return base.VisitMember(node);
}
}
}
, который использует следующий помощник:
public static partial class ExpressionExtensions
{
public static Expression ReplaceParameter(this Expression source, ParameterExpression from, Expression to)
=> new ParameterReplacer { From = from, To = to }.Visit(source);
class ParameterReplacer : ExpressionVisitor
{
public ParameterExpression From;
public Expression To;
protected override Expression VisitParameter(ParameterExpression node) => node == From ? To : base.VisitParameter(node);
}
}
Теперь часть сантехники.На самом деле NHibernate позволяет вам заменить поставщика LINQ своим собственным.Теоретически вы должны иметь возможность создать производный класс DefaultQueryProvider
, переопределить метод PrepareQuery
и предварительно обработать переданное выражение перед вызовом базовой реализации.
К сожалению, в реализации метода IQueryProviderWithOptions.WithOptions
есть недостаток реализацииDefaultQueryProvider
класс, который требует некрасивых отражений на основе хаков.Но без этого поставщик запросов будет заменен на стандартный по умолчанию, если в запросе используются некоторые методы расширения WithOptions
, что сводит на нет все наши усилия.
С учетом сказанного ниже приведен код поставщика:
public class CustomQueryProvider : DefaultQueryProvider, IQueryProviderWithOptions
{
// Required constructors
public CustomQueryProvider(ISessionImplementor session) : base(session) { }
public CustomQueryProvider(ISessionImplementor session, object collection) : base(session, collection) { }
// The code we need
protected override NhLinqExpression PrepareQuery(Expression expression, out IQuery query)
=> base.PrepareQuery(expression.ApplySpecifications(), out query);
// Hacks for correctly supporting IQueryProviderWithOptions
IQueryProvider IQueryProviderWithOptions.WithOptions(Action<NhQueryableOptions> setOptions)
{
if (setOptions == null)
throw new ArgumentNullException(nameof(setOptions));
var options = (NhQueryableOptions)_options.GetValue(this);
var newOptions = options != null ? (NhQueryableOptions)CloneOptions.Invoke(options, null) : new NhQueryableOptions();
setOptions(newOptions);
var clone = (CustomQueryProvider)this.MemberwiseClone();
_options.SetValue(clone, newOptions);
return clone;
}
static readonly FieldInfo _options = typeof(DefaultQueryProvider).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance);
static readonly MethodInfo CloneOptions = typeof(NhQueryableOptions).GetMethod("Clone", BindingFlags.NonPublic | BindingFlags.Instance);
}
Классы LinqToHqlGeneratorsRegistry
и SpecificationHqlGenerator
больше не нужны, поэтому удалите их и замените
cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();
на
cfg.LinqQueryProvider<CustomQueryProvider>();
и все будет работатькак и ожидалось.