Skip to content

Commit 04ecfde

Browse files
committed
Add an option to register a custom pre-transformer for a Linq query
1 parent da4b6c9 commit 04ecfde

10 files changed

+264
-16
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Collections.Generic;
13+
using System.Linq;
14+
using System.Linq.Expressions;
15+
using System.Reflection;
16+
using NHibernate.Linq;
17+
using NHibernate.Linq.Visitors;
18+
using NHibernate.Util;
19+
using NUnit.Framework;
20+
using Remotion.Linq.Parsing.ExpressionVisitors.Transformation;
21+
22+
namespace NHibernate.Test.Linq
23+
{
24+
using System.Threading.Tasks;
25+
[TestFixture]
26+
public class CustomPreTransformInitializerTestsAsync : LinqTestCase
27+
{
28+
protected override void Configure(Cfg.Configuration configuration)
29+
{
30+
configuration.Properties[Cfg.Environment.PreTransformerInitializer] = typeof(PreTransformerInitializer).AssemblyQualifiedName;
31+
}
32+
33+
[Test]
34+
public async Task RewriteLikeAsync()
35+
{
36+
// This example shows how to use the pre-transformer initializer to rewrite the
37+
// query so that StartsWith, EndsWith and Contains methods will generate the same sql.
38+
var queryPlanCache = GetQueryPlanCache();
39+
queryPlanCache.Clear();
40+
await (db.Customers.Where(o => o.ContactName.StartsWith("A")).ToListAsync());
41+
await (db.Customers.Where(o => o.ContactName.EndsWith("A")).ToListAsync());
42+
await (db.Customers.Where(o => o.ContactName.Contains("A")).ToListAsync());
43+
44+
Assert.That(queryPlanCache.Count, Is.EqualTo(1));
45+
}
46+
47+
[Serializable]
48+
public class PreTransformerInitializer : IExpressionTransformerInitializer
49+
{
50+
public void Initialize(ExpressionTransformerRegistry expressionTransformerRegistry)
51+
{
52+
expressionTransformerRegistry.Register(new LikeTransformer());
53+
}
54+
}
55+
56+
private class LikeTransformer : IExpressionTransformer<MethodCallExpression>
57+
{
58+
private static readonly MethodInfo Like = ReflectHelper.GetMethodDefinition(() => SqlMethods.Like(null, null));
59+
private static readonly MethodInfo EndsWith = ReflectHelper.GetMethodDefinition<string>(x => x.EndsWith(null));
60+
private static readonly MethodInfo StartsWith = ReflectHelper.GetMethodDefinition<string>(x => x.StartsWith(null));
61+
private static readonly MethodInfo Contains = ReflectHelper.GetMethodDefinition<string>(x => x.Contains(null));
62+
private static readonly Dictionary<MethodInfo, Func<object, string>> ValueTransformers =
63+
new Dictionary<MethodInfo, Func<object, string>>
64+
{
65+
{StartsWith, s => $"{s}%"},
66+
{EndsWith, s => $"%{s}"},
67+
{Contains, s => $"%{s}%"},
68+
};
69+
70+
public Expression Transform(MethodCallExpression expression)
71+
{
72+
if (ValueTransformers.TryGetValue(expression.Method, out var valueTransformer) &&
73+
expression.Arguments[0] is ConstantExpression constantExpression)
74+
{
75+
return Expression.Call(
76+
Like,
77+
expression.Object,
78+
Expression.Constant(valueTransformer(constantExpression.Value))
79+
);
80+
}
81+
82+
return expression;
83+
}
84+
85+
public ExpressionType[] SupportedExpressionTypes { get; } = {ExpressionType.Call};
86+
}
87+
}
88+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using NHibernate.Linq;
7+
using NHibernate.Linq.Visitors;
8+
using NHibernate.Util;
9+
using NUnit.Framework;
10+
using Remotion.Linq.Parsing.ExpressionVisitors.Transformation;
11+
12+
namespace NHibernate.Test.Linq
13+
{
14+
[TestFixture]
15+
public class CustomPreTransformInitializerTests : LinqTestCase
16+
{
17+
protected override void Configure(Cfg.Configuration configuration)
18+
{
19+
configuration.Properties[Cfg.Environment.PreTransformerInitializer] = typeof(PreTransformerInitializer).AssemblyQualifiedName;
20+
}
21+
22+
[Test]
23+
public void RewriteLike()
24+
{
25+
// This example shows how to use the pre-transformer initializer to rewrite the
26+
// query so that StartsWith, EndsWith and Contains methods will generate the same sql.
27+
var queryPlanCache = GetQueryPlanCache();
28+
queryPlanCache.Clear();
29+
db.Customers.Where(o => o.ContactName.StartsWith("A")).ToList();
30+
db.Customers.Where(o => o.ContactName.EndsWith("A")).ToList();
31+
db.Customers.Where(o => o.ContactName.Contains("A")).ToList();
32+
33+
Assert.That(queryPlanCache.Count, Is.EqualTo(1));
34+
}
35+
36+
[Serializable]
37+
public class PreTransformerInitializer : IExpressionTransformerInitializer
38+
{
39+
public void Initialize(ExpressionTransformerRegistry expressionTransformerRegistry)
40+
{
41+
expressionTransformerRegistry.Register(new LikeTransformer());
42+
}
43+
}
44+
45+
private class LikeTransformer : IExpressionTransformer<MethodCallExpression>
46+
{
47+
private static readonly MethodInfo Like = ReflectHelper.GetMethodDefinition(() => SqlMethods.Like(null, null));
48+
private static readonly MethodInfo EndsWith = ReflectHelper.GetMethodDefinition<string>(x => x.EndsWith(null));
49+
private static readonly MethodInfo StartsWith = ReflectHelper.GetMethodDefinition<string>(x => x.StartsWith(null));
50+
private static readonly MethodInfo Contains = ReflectHelper.GetMethodDefinition<string>(x => x.Contains(null));
51+
private static readonly Dictionary<MethodInfo, Func<object, string>> ValueTransformers =
52+
new Dictionary<MethodInfo, Func<object, string>>
53+
{
54+
{StartsWith, s => $"{s}%"},
55+
{EndsWith, s => $"%{s}"},
56+
{Contains, s => $"%{s}%"},
57+
};
58+
59+
public Expression Transform(MethodCallExpression expression)
60+
{
61+
if (ValueTransformers.TryGetValue(expression.Method, out var valueTransformer) &&
62+
expression.Arguments[0] is ConstantExpression constantExpression)
63+
{
64+
return Expression.Call(
65+
Like,
66+
expression.Object,
67+
Expression.Constant(valueTransformer(constantExpression.Value))
68+
);
69+
}
70+
71+
return expression;
72+
}
73+
74+
public ExpressionType[] SupportedExpressionTypes { get; } = {ExpressionType.Call};
75+
}
76+
}
77+
}

src/NHibernate.Test/TestCase.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ public abstract class TestCase
2929
private SchemaExport _schemaExport;
3030

3131
private static readonly ILog log = LogManager.GetLogger(typeof(TestCase));
32+
private static readonly FieldInfo PlanCacheField;
33+
34+
static TestCase()
35+
{
36+
PlanCacheField = typeof(QueryPlanCache)
37+
.GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
38+
?? throw new InvalidOperationException(
39+
"planCache field does not exist in QueryPlanCache.");
40+
}
3241

3342
protected Dialect.Dialect Dialect
3443
{
@@ -488,14 +497,14 @@ protected void AssumeFunctionSupported(string functionName)
488497
$"{dialect} doesn't support {functionName} standard function.");
489498
}
490499

491-
protected void ClearQueryPlanCache()
500+
protected SoftLimitMRUCache GetQueryPlanCache()
492501
{
493-
var planCacheField = typeof(QueryPlanCache)
494-
.GetField("planCache", BindingFlags.NonPublic | BindingFlags.Instance)
495-
?? throw new InvalidOperationException("planCache field does not exist in QueryPlanCache.");
502+
return (SoftLimitMRUCache) PlanCacheField.GetValue(Sfi.QueryPlanCache);
503+
}
496504

497-
var planCache = (SoftLimitMRUCache) planCacheField.GetValue(Sfi.QueryPlanCache);
498-
planCache.Clear();
505+
protected void ClearQueryPlanCache()
506+
{
507+
GetQueryPlanCache().Clear();
499508
}
500509

501510
protected Substitute<Dialect.Dialect> SubstituteDialect()

src/NHibernate/Cfg/Environment.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using NHibernate.Cfg.ConfigurationSchema;
77
using NHibernate.Engine;
88
using NHibernate.Linq;
9+
using NHibernate.Linq.Visitors;
910
using NHibernate.Util;
1011

1112
namespace NHibernate.Cfg
@@ -276,6 +277,11 @@ public static string Version
276277

277278
public const string QueryModelRewriterFactory = "query.query_model_rewriter_factory";
278279

280+
/// <summary>
281+
/// The class name of the LINQ query pre-transformer initializer, implementing <see cref="IExpressionTransformerInitializer"/>.
282+
/// </summary>
283+
public const string PreTransformerInitializer = "query.pre_transformer_initializer";
284+
279285
/// <summary>
280286
/// Set the default length used in casting when the target type is length bound and
281287
/// does not specify it. <c>4000</c> by default, automatically trimmed down according to dialect type registration.

src/NHibernate/Cfg/Settings.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using NHibernate.Linq.Functions;
1111
using NHibernate.Linq.Visitors;
1212
using NHibernate.Transaction;
13+
using Remotion.Linq.Parsing.Structure;
1314

1415
namespace NHibernate.Cfg
1516
{
@@ -182,7 +183,14 @@ public Settings()
182183
public bool LinqToHqlFallbackOnPreEvaluation { get; internal set; }
183184

184185
public IQueryModelRewriterFactory QueryModelRewriterFactory { get; internal set; }
185-
186+
187+
/// <summary>
188+
/// The pre-transformer initializer used to register custom expression transformers.
189+
/// </summary>
190+
public IExpressionTransformerInitializer PreTransformerInitializer { get; internal set; }
191+
192+
internal IExpressionTreeProcessor LinqPreTransformer { get; set; }
193+
186194
#endregion
187195

188196
internal string GetFullCacheRegionName(string name)

src/NHibernate/Cfg/SettingsFactory.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@
1010
using NHibernate.Exceptions;
1111
using NHibernate.Hql;
1212
using NHibernate.Linq;
13+
using NHibernate.Linq.ExpressionTransformers;
1314
using NHibernate.Linq.Functions;
1415
using NHibernate.Linq.Visitors;
1516
using NHibernate.Transaction;
1617
using NHibernate.Util;
18+
using Remotion.Linq.Parsing.ExpressionVisitors.Transformation;
19+
using Remotion.Linq.Parsing.Structure;
20+
using Remotion.Linq.Parsing.Structure.ExpressionTreeProcessors;
1721

1822
namespace NHibernate.Cfg
1923
{
@@ -308,7 +312,9 @@ public Settings BuildSettings(IDictionary<string, string> properties)
308312
// Not ported - JdbcBatchVersionedData
309313

310314
settings.QueryModelRewriterFactory = CreateQueryModelRewriterFactory(properties);
311-
315+
settings.PreTransformerInitializer = CreatePreTransformerInitializer(properties);
316+
settings.LinqPreTransformer = CreateLinqPreTransformer(settings.PreTransformerInitializer);
317+
312318
// NHibernate-specific:
313319
settings.IsolationLevel = isolation;
314320

@@ -442,5 +448,36 @@ private static IQueryModelRewriterFactory CreateQueryModelRewriterFactory(IDicti
442448
throw new HibernateException("could not instantiate IQueryModelRewriterFactory: " + className, cnfe);
443449
}
444450
}
451+
452+
private static IExpressionTransformerInitializer CreatePreTransformerInitializer(IDictionary<string, string> properties)
453+
{
454+
var className = PropertiesHelper.GetString(Environment.PreTransformerInitializer, properties, null);
455+
if (className == null)
456+
return null;
457+
458+
log.Info("Pre-transformer initializer: {0}", className);
459+
460+
try
461+
{
462+
return
463+
(IExpressionTransformerInitializer)
464+
Environment.ObjectsFactory.CreateInstance(ReflectHelper.ClassForName(className));
465+
}
466+
catch (Exception e)
467+
{
468+
throw new HibernateException("could not instantiate IExpressionTransformerInitializer: " + className, e);
469+
}
470+
}
471+
472+
private static IExpressionTreeProcessor CreateLinqPreTransformer(IExpressionTransformerInitializer expressionTransformerInitializer)
473+
{
474+
var preTransformerRegistry = new ExpressionTransformerRegistry();
475+
// NH-3247: must remove .Net compiler char to int conversion before
476+
// parameterization occurs.
477+
preTransformerRegistry.Register(new RemoveCharToIntConversion());
478+
expressionTransformerInitializer?.Initialize(preTransformerRegistry);
479+
480+
return new TransformingExpressionTreeProcessor(preTransformerRegistry);
481+
}
445482
}
446483
}

src/NHibernate/Linq/NhRelinqQueryParser.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,9 @@ namespace NHibernate.Linq
2121
public static class NhRelinqQueryParser
2222
{
2323
private static readonly QueryParser QueryParser;
24-
private static readonly IExpressionTreeProcessor PreProcessor;
2524

2625
static NhRelinqQueryParser()
2726
{
28-
var preTransformerRegistry = new ExpressionTransformerRegistry();
29-
// NH-3247: must remove .Net compiler char to int conversion before
30-
// parameterization occurs.
31-
preTransformerRegistry.Register(new RemoveCharToIntConversion());
32-
PreProcessor = new TransformingExpressionTreeProcessor(preTransformerRegistry);
33-
3427
var transformerRegistry = ExpressionTransformerRegistry.CreateDefault();
3528
transformerRegistry.Register(new RemoveRedundantCast());
3629
transformerRegistry.Register(new SimplifyCompareTransformer());
@@ -78,7 +71,7 @@ public static PreTransformationResult PreTransform(Expression expression, PreTra
7871
.EvaluateIndependentSubtrees(expression, parameters);
7972

8073
return new PreTransformationResult(
81-
PreProcessor.Process(partiallyEvaluatedExpression),
74+
parameters.PreTransformer.Process(partiallyEvaluatedExpression),
8275
parameters.SessionFactory,
8376
parameters.QueryVariables);
8477
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Remotion.Linq.Parsing.ExpressionVisitors.Transformation;
2+
3+
namespace NHibernate.Linq.Visitors
4+
{
5+
/// <summary>
6+
/// Provides a way to register custom transformers for expressions.
7+
/// </summary>
8+
public interface IExpressionTransformerInitializer
9+
{
10+
/// <summary>
11+
/// Initialize expression transformer registry by registering additional transformers.
12+
/// </summary>
13+
/// <param name="expressionTransformerRegistry">The expression transformer registry.</param>
14+
void Initialize(ExpressionTransformerRegistry expressionTransformerRegistry);
15+
}
16+
}

src/NHibernate/Linq/Visitors/PreTransformationParameters.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Linq.Expressions;
33
using NHibernate.Engine;
44
using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation;
5+
using Remotion.Linq.Parsing.Structure;
56

67
namespace NHibernate.Linq.Visitors
78
{
@@ -19,6 +20,7 @@ public PreTransformationParameters(QueryMode queryMode, ISessionFactoryImplement
1920
{
2021
QueryMode = queryMode;
2122
SessionFactory = sessionFactory;
23+
PreTransformer = sessionFactory.Settings.LinqPreTransformer;
2224
// Skip detecting variables for DML queries as HQL does not support reusing parameters for them.
2325
MinimizeParameters = QueryMode == QueryMode.Select;
2426
}
@@ -33,6 +35,11 @@ public PreTransformationParameters(QueryMode queryMode, ISessionFactoryImplement
3335
/// </summary>
3436
public ISessionFactoryImplementor SessionFactory { get; }
3537

38+
/// <summary>
39+
/// The transformer that will be used to pre-transform the query expression.
40+
/// </summary>
41+
internal IExpressionTreeProcessor PreTransformer { get; }
42+
3643
/// <summary>
3744
/// Whether to minimize the number of parameters for variables.
3845
/// </summary>

src/NHibernate/nhibernate-configuration.xsd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@
151151
<xs:enumeration value="order_inserts" />
152152
<xs:enumeration value="order_updates" />
153153
<xs:enumeration value="query.query_model_rewriter_factory" />
154+
<xs:enumeration value="query.pre_transformer_initializer">
155+
<xs:annotation>
156+
<xs:documentation>
157+
The pre-transformer initializer used to register custom expression transformers.
158+
</xs:documentation>
159+
</xs:annotation>
160+
</xs:enumeration>
154161
<xs:enumeration value="linqtohql.generatorsregistry" />
155162
<xs:enumeration value="linqtohql.legacy_preevaluation">
156163
<xs:annotation>

0 commit comments

Comments
 (0)