Expand typed join syntax with no lock, outer and custom join types

This commit is contained in:
root
2026-02-26 19:49:00 +01:00
parent 15e1d9031c
commit 59ce1115ea
10 changed files with 611 additions and 47 deletions

View File

@@ -10,8 +10,12 @@ namespace DynamORM.Builders
public enum DynamicJoinType
{
Inner = 0,
Join,
Left,
Right,
Full
Full,
LeftOuter,
RightOuter,
FullOuter
}
}

View File

@@ -40,8 +40,18 @@ namespace DynamORM.Builders
/// <param name="on">Join ON predicate.</param>
/// <param name="alias">Optional alias for joined table.</param>
/// <param name="joinType">Join type.</param>
/// <param name="noLock">Adds NOLOCK hint to joined source when supported by provider options.</param>
/// <returns>Builder instance.</returns>
IDynamicTypedSelectQueryBuilder<T> Join<TRight>(Expression<Func<T, TRight, bool>> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner);
IDynamicTypedSelectQueryBuilder<T> Join<TRight>(Expression<Func<T, TRight, bool>> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner, bool noLock = false);
/// <summary>Add typed join with custom join type text (for example: CROSS APPLY).</summary>
/// <typeparam name="TRight">Joined mapped entity type.</typeparam>
/// <param name="on">Optional join ON predicate.</param>
/// <param name="alias">Optional alias for joined table.</param>
/// <param name="joinType">Join type text.</param>
/// <param name="noLock">Adds NOLOCK hint to joined source when supported by provider options.</param>
/// <returns>Builder instance.</returns>
IDynamicTypedSelectQueryBuilder<T> Join<TRight>(Expression<Func<T, TRight, bool>> on, string alias, string joinType, bool noLock = false);
/// <summary>Add typed join using join-spec builder syntax (<c>As()</c>, join kind and <c>On()</c>).</summary>
/// <typeparam name="TRight">Joined mapped entity type.</typeparam>

View File

@@ -50,7 +50,7 @@ namespace DynamORM.Builders.Implementation
private string _select;
private string _from;
private string _join;
protected string _join;
private string _groupby;
private string _orderby;

View File

@@ -65,10 +65,15 @@ namespace DynamORM.Builders.Implementation
return this;
}
public IDynamicTypedSelectQueryBuilder<T> Join<TRight>(Expression<Func<T, TRight, bool>> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner)
public IDynamicTypedSelectQueryBuilder<T> Join<TRight>(Expression<Func<T, TRight, bool>> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner, bool noLock = false)
{
if (on == null)
throw new ArgumentNullException("on");
return Join(on, alias, GetJoinKeyword(joinType), noLock);
}
public IDynamicTypedSelectQueryBuilder<T> Join<TRight>(Expression<Func<T, TRight, bool>> on, string alias, string joinType, bool noLock = false)
{
if (string.IsNullOrEmpty(joinType))
throw new ArgumentNullException("joinType");
DynamicTypeMap rightMapper = DynamicMapperCache.GetMapper(typeof(TRight));
if (rightMapper == null)
@@ -80,27 +85,28 @@ namespace DynamORM.Builders.Implementation
string rightOwner = rightMapper.Table == null ? null : rightMapper.Table.Owner;
string rightAlias = string.IsNullOrEmpty(alias) ? "t" + (Tables.Count + 1).ToString() : alias;
BinaryExpression be = on.Body as BinaryExpression;
if (be == null || be.NodeType != ExpressionType.Equal)
throw new NotSupportedException("Typed join expression is currently limited to equality comparisons.");
string leftPrefix = GetRootAliasOrTableName();
if (string.IsNullOrEmpty(leftPrefix))
throw new InvalidOperationException("Join requires source table to be present.");
string leftExpr = ParseTypedJoinMember<T>(be.Left, leftPrefix, _mapper);
string rightExpr = ParseTypedJoinMember<TRight>(be.Right, rightAlias, rightMapper);
string condition = null;
if (on != null)
condition = ParseTypedJoinCondition(on.Body, leftPrefix, rightAlias, _mapper, rightMapper, on.Parameters[0], on.Parameters[1]);
string ownerPrefix = string.IsNullOrEmpty(rightOwner) ? string.Empty : Database.DecorateName(rightOwner) + ".";
string rightTableExpr = ownerPrefix + Database.DecorateName(rightTable);
string joinExpr = string.Format("{0} {1} AS {2} ON ({3} = {4})",
GetJoinKeyword(joinType),
string joinExpr = string.Format("{0} {1} AS {2}",
joinType.Trim(),
rightTableExpr,
rightAlias,
leftExpr,
rightExpr);
rightAlias);
base.Join(x => joinExpr);
if (SupportNoLock && noLock)
joinExpr += " WITH(NOLOCK)";
if (!string.IsNullOrEmpty(condition))
joinExpr += string.Format(" ON {0}", condition);
AppendJoinClause(joinExpr);
return this;
}
@@ -112,10 +118,34 @@ namespace DynamORM.Builders.Implementation
TypedJoinBuilder<T, TRight> spec = specification(new TypedJoinBuilder<T, TRight>());
if (spec == null)
throw new ArgumentException("Join specification cannot resolve to null.", "specification");
if (spec.OnPredicate == null)
throw new ArgumentException("Join specification must define ON predicate.", "specification");
if (spec.OnPredicate != null)
return Join(spec.OnPredicate, spec.Alias, spec.CustomJoinType ?? GetJoinKeyword(spec.JoinType), spec.UseNoLock);
return Join(spec.OnPredicate, spec.Alias, spec.JoinType);
DynamicTypeMap rightMapper = DynamicMapperCache.GetMapper(typeof(TRight));
if (rightMapper == null)
throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", typeof(TRight).FullName));
string rightTable = rightMapper.Table == null || string.IsNullOrEmpty(rightMapper.Table.Name)
? typeof(TRight).Name
: rightMapper.Table.Name;
string rightOwner = rightMapper.Table == null ? null : rightMapper.Table.Owner;
string rightAlias = string.IsNullOrEmpty(spec.Alias) ? "t" + (Tables.Count + 1).ToString() : spec.Alias;
string ownerPrefix = string.IsNullOrEmpty(rightOwner) ? string.Empty : Database.DecorateName(rightOwner) + ".";
string rightTableExpr = ownerPrefix + Database.DecorateName(rightTable);
string joinExpr = string.Format("{0} {1} AS {2}",
(spec.CustomJoinType ?? GetJoinKeyword(spec.JoinType)).Trim(),
rightTableExpr,
rightAlias);
if (SupportNoLock && spec.UseNoLock)
joinExpr += " WITH(NOLOCK)";
if (!string.IsNullOrEmpty(spec.OnRawCondition))
joinExpr += string.Format(" ON {0}", spec.OnRawCondition);
AppendJoinClause(joinExpr);
return this;
}
public new IDynamicTypedSelectQueryBuilder<T> Join(params Func<dynamic, object>[] func)
@@ -511,17 +541,88 @@ namespace DynamORM.Builders.Implementation
{
switch (joinType)
{
case DynamicJoinType.Join:
return "JOIN";
case DynamicJoinType.Left:
return "LEFT JOIN";
case DynamicJoinType.Right:
return "RIGHT JOIN";
case DynamicJoinType.Full:
return "FULL JOIN";
case DynamicJoinType.LeftOuter:
return "LEFT OUTER JOIN";
case DynamicJoinType.RightOuter:
return "RIGHT OUTER JOIN";
case DynamicJoinType.FullOuter:
return "FULL OUTER JOIN";
default:
return "INNER JOIN";
}
}
private void AppendJoinClause(string joinClause)
{
_join = string.IsNullOrEmpty(_join)
? joinClause
: string.Format("{0} {1}", _join, joinClause);
}
private string ParseTypedJoinCondition(Expression expression, string leftPrefix, string rightPrefix, DynamicTypeMap leftMapper, DynamicTypeMap rightMapper, ParameterExpression leftParameter, ParameterExpression rightParameter)
{
expression = UnwrapConvert(expression);
if (expression is BinaryExpression binary)
{
switch (binary.NodeType)
{
case ExpressionType.AndAlso:
case ExpressionType.And:
return string.Format("({0} AND {1})",
ParseTypedJoinCondition(binary.Left, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter),
ParseTypedJoinCondition(binary.Right, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter));
case ExpressionType.OrElse:
case ExpressionType.Or:
return string.Format("({0} OR {1})",
ParseTypedJoinCondition(binary.Left, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter),
ParseTypedJoinCondition(binary.Right, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter));
case ExpressionType.Equal:
case ExpressionType.NotEqual:
case ExpressionType.GreaterThan:
case ExpressionType.GreaterThanOrEqual:
case ExpressionType.LessThan:
case ExpressionType.LessThanOrEqual:
{
string left = ParseTypedJoinValue(binary.Left, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter);
string right = ParseTypedJoinValue(binary.Right, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter);
string op = GetBinaryOperator(binary.NodeType, IsNullConstant(binary.Right));
return string.Format("({0} {1} {2})", left, op, right);
}
}
}
if (expression is UnaryExpression unary && unary.NodeType == ExpressionType.Not)
return string.Format("(NOT {0})", ParseTypedJoinCondition(unary.Operand, leftPrefix, rightPrefix, leftMapper, rightMapper, leftParameter, rightParameter));
throw new NotSupportedException(string.Format("Typed join condition is not supported: {0}", expression));
}
private string ParseTypedJoinValue(Expression expression, string leftPrefix, string rightPrefix, DynamicTypeMap leftMapper, DynamicTypeMap rightMapper, ParameterExpression leftParameter, ParameterExpression rightParameter)
{
expression = UnwrapConvert(expression);
MemberExpression member = expression as MemberExpression;
if (member != null && member.Expression is ParameterExpression parameter)
{
if (parameter == leftParameter)
return ParseTypedJoinMemberByMapper(member, leftPrefix, leftMapper);
if (parameter == rightParameter)
return ParseTypedJoinMemberByMapper(member, rightPrefix, rightMapper);
}
DynamicSchemaColumn? col = null;
object value = EvaluateExpression(expression);
return ParseConstant(value, Parameters, col);
}
private string ParseTypedJoinMember<TModel>(Expression expression, string tablePrefix, DynamicTypeMap mapper)
{
expression = UnwrapConvert(expression);
@@ -550,6 +651,29 @@ namespace DynamORM.Builders.Implementation
return string.Format("{0}.{1}", tablePrefix, Database.DecorateName(mappedColumn));
}
private string ParseTypedJoinMemberByMapper(MemberExpression member, string tablePrefix, DynamicTypeMap mapper)
{
string mappedColumn = null;
PropertyInfo property = member.Member as PropertyInfo;
if (property != null)
{
var attrs = property.GetCustomAttributes(typeof(ColumnAttribute), true);
ColumnAttribute colAttr = attrs == null ? null : attrs.Cast<ColumnAttribute>().FirstOrDefault();
if (colAttr != null && !string.IsNullOrEmpty(colAttr.Name))
mappedColumn = colAttr.Name;
}
if (string.IsNullOrEmpty(mappedColumn))
mappedColumn = mapper.PropertyMap.TryGetValue(member.Member.Name)
?? mapper.PropertyMap
.Where(x => string.Equals(x.Key, member.Member.Name, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Value)
.FirstOrDefault()
?? member.Member.Name;
return string.Format("{0}.{1}", tablePrefix, Database.DecorateName(mappedColumn));
}
private static Expression UnwrapConvert(Expression expression)
{
while (expression is UnaryExpression unary &&

View File

@@ -16,7 +16,7 @@ namespace DynamORM.Builders
{
internal TypedJoinBuilder()
{
JoinType = DynamicJoinType.Inner;
JoinType = DynamicJoinType.Join;
}
/// <summary>Gets join alias.</summary>
@@ -25,9 +25,18 @@ namespace DynamORM.Builders
/// <summary>Gets join type.</summary>
public DynamicJoinType JoinType { get; private set; }
/// <summary>Gets custom join type text.</summary>
public string CustomJoinType { get; private set; }
/// <summary>Gets a value indicating whether joined source should use NOLOCK.</summary>
public bool UseNoLock { get; private set; }
/// <summary>Gets ON predicate.</summary>
public Expression<Func<TLeft, TRight, bool>> OnPredicate { get; private set; }
/// <summary>Gets raw ON condition.</summary>
public string OnRawCondition { get; private set; }
/// <summary>Sets join alias.</summary>
public TypedJoinBuilder<TLeft, TRight> As(string alias)
{
@@ -39,6 +48,15 @@ namespace DynamORM.Builders
public TypedJoinBuilder<TLeft, TRight> Inner()
{
JoinType = DynamicJoinType.Inner;
CustomJoinType = null;
return this;
}
/// <summary>Sets plain JOIN.</summary>
public TypedJoinBuilder<TLeft, TRight> Join()
{
JoinType = DynamicJoinType.Join;
CustomJoinType = null;
return this;
}
@@ -46,6 +64,7 @@ namespace DynamORM.Builders
public TypedJoinBuilder<TLeft, TRight> Left()
{
JoinType = DynamicJoinType.Left;
CustomJoinType = null;
return this;
}
@@ -53,6 +72,7 @@ namespace DynamORM.Builders
public TypedJoinBuilder<TLeft, TRight> Right()
{
JoinType = DynamicJoinType.Right;
CustomJoinType = null;
return this;
}
@@ -60,6 +80,48 @@ namespace DynamORM.Builders
public TypedJoinBuilder<TLeft, TRight> Full()
{
JoinType = DynamicJoinType.Full;
CustomJoinType = null;
return this;
}
/// <summary>Sets LEFT OUTER JOIN.</summary>
public TypedJoinBuilder<TLeft, TRight> LeftOuter()
{
JoinType = DynamicJoinType.LeftOuter;
CustomJoinType = null;
return this;
}
/// <summary>Sets RIGHT OUTER JOIN.</summary>
public TypedJoinBuilder<TLeft, TRight> RightOuter()
{
JoinType = DynamicJoinType.RightOuter;
CustomJoinType = null;
return this;
}
/// <summary>Sets FULL OUTER JOIN.</summary>
public TypedJoinBuilder<TLeft, TRight> FullOuter()
{
JoinType = DynamicJoinType.FullOuter;
CustomJoinType = null;
return this;
}
/// <summary>Sets custom join type text (for example: CROSS APPLY).</summary>
public TypedJoinBuilder<TLeft, TRight> Type(string joinType)
{
if (string.IsNullOrEmpty(joinType))
throw new ArgumentNullException("joinType");
CustomJoinType = joinType.Trim();
return this;
}
/// <summary>Marks joined source with NOLOCK hint.</summary>
public TypedJoinBuilder<TLeft, TRight> NoLock(bool use = true)
{
UseNoLock = use;
return this;
}
@@ -70,6 +132,18 @@ namespace DynamORM.Builders
throw new ArgumentNullException("predicate");
OnPredicate = predicate;
OnRawCondition = null;
return this;
}
/// <summary>Sets raw ON clause (without the ON keyword).</summary>
public TypedJoinBuilder<TLeft, TRight> OnRaw(string condition)
{
if (string.IsNullOrEmpty(condition))
throw new ArgumentNullException("condition");
OnRawCondition = condition.Trim();
OnPredicate = null;
return this;
}
}

View File

@@ -27,12 +27,22 @@ namespace DynamORM.Builders
DynamicJoinType jt = DynamicJoinType.Inner;
string normalized = (joinType ?? string.Empty).Trim().ToUpperInvariant();
if (normalized == "LEFT JOIN" || normalized == "LEFT")
if (normalized == "JOIN")
jt = DynamicJoinType.Join;
else if (normalized == "LEFT OUTER JOIN" || normalized == "LEFT OUTER")
jt = DynamicJoinType.LeftOuter;
else if (normalized == "RIGHT OUTER JOIN" || normalized == "RIGHT OUTER")
jt = DynamicJoinType.RightOuter;
else if (normalized == "FULL OUTER JOIN" || normalized == "FULL OUTER")
jt = DynamicJoinType.FullOuter;
else if (normalized == "LEFT JOIN" || normalized == "LEFT")
jt = DynamicJoinType.Left;
else if (normalized == "RIGHT JOIN" || normalized == "RIGHT")
jt = DynamicJoinType.Right;
else if (normalized == "FULL JOIN" || normalized == "FULL")
jt = DynamicJoinType.Full;
else if (normalized != "INNER JOIN" && normalized != "INNER" && normalized != "JOIN")
return builder.Join(on, alias, joinType);
return builder.Join(on, alias, jt);
}