diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs
index 4f01c18..73602bb 100644
--- a/AmalgamationTool/DynamORM.Amalgamation.cs
+++ b/AmalgamationTool/DynamORM.Amalgamation.cs
@@ -6782,9 +6782,13 @@ namespace DynamORM
public enum DynamicJoinType
{
Inner = 0,
+ Join,
Left,
Right,
- Full
+ Full,
+ LeftOuter,
+ RightOuter,
+ FullOuter
}
/// Dynamic delete query builder interface.
/// This interface it publicly available. Implementation should be hidden.
@@ -7187,8 +7191,18 @@ namespace DynamORM
/// Join ON predicate.
/// Optional alias for joined table.
/// Join type.
+ /// Adds NOLOCK hint to joined source when supported by provider options.
/// Builder instance.
- IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner);
+ IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner, bool noLock = false);
+
+ /// Add typed join with custom join type text (for example: CROSS APPLY).
+ /// Joined mapped entity type.
+ /// Optional join ON predicate.
+ /// Optional alias for joined table.
+ /// Join type text.
+ /// Adds NOLOCK hint to joined source when supported by provider options.
+ /// Builder instance.
+ IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias, string joinType, bool noLock = false);
/// Add typed join using join-spec builder syntax (As(), join kind and On()).
/// Joined mapped entity type.
@@ -7415,7 +7429,7 @@ namespace DynamORM
{
internal TypedJoinBuilder()
{
- JoinType = DynamicJoinType.Inner;
+ JoinType = DynamicJoinType.Join;
}
/// Gets join alias.
public string Alias { get; private set; }
@@ -7423,9 +7437,18 @@ namespace DynamORM
/// Gets join type.
public DynamicJoinType JoinType { get; private set; }
+ /// Gets custom join type text.
+ public string CustomJoinType { get; private set; }
+
+ /// Gets a value indicating whether joined source should use NOLOCK.
+ public bool UseNoLock { get; private set; }
+
/// Gets ON predicate.
public Expression> OnPredicate { get; private set; }
+ /// Gets raw ON condition.
+ public string OnRawCondition { get; private set; }
+
/// Sets join alias.
public TypedJoinBuilder As(string alias)
{
@@ -7436,24 +7459,71 @@ namespace DynamORM
public TypedJoinBuilder Inner()
{
JoinType = DynamicJoinType.Inner;
+ CustomJoinType = null;
+ return this;
+ }
+ /// Sets plain JOIN.
+ public TypedJoinBuilder Join()
+ {
+ JoinType = DynamicJoinType.Join;
+ CustomJoinType = null;
return this;
}
/// Sets LEFT JOIN.
public TypedJoinBuilder Left()
{
JoinType = DynamicJoinType.Left;
+ CustomJoinType = null;
return this;
}
/// Sets RIGHT JOIN.
public TypedJoinBuilder Right()
{
JoinType = DynamicJoinType.Right;
+ CustomJoinType = null;
return this;
}
/// Sets FULL JOIN.
public TypedJoinBuilder Full()
{
JoinType = DynamicJoinType.Full;
+ CustomJoinType = null;
+ return this;
+ }
+ /// Sets LEFT OUTER JOIN.
+ public TypedJoinBuilder LeftOuter()
+ {
+ JoinType = DynamicJoinType.LeftOuter;
+ CustomJoinType = null;
+ return this;
+ }
+ /// Sets RIGHT OUTER JOIN.
+ public TypedJoinBuilder RightOuter()
+ {
+ JoinType = DynamicJoinType.RightOuter;
+ CustomJoinType = null;
+ return this;
+ }
+ /// Sets FULL OUTER JOIN.
+ public TypedJoinBuilder FullOuter()
+ {
+ JoinType = DynamicJoinType.FullOuter;
+ CustomJoinType = null;
+ return this;
+ }
+ /// Sets custom join type text (for example: CROSS APPLY).
+ public TypedJoinBuilder Type(string joinType)
+ {
+ if (string.IsNullOrEmpty(joinType))
+ throw new ArgumentNullException("joinType");
+
+ CustomJoinType = joinType.Trim();
+ return this;
+ }
+ /// Marks joined source with NOLOCK hint.
+ public TypedJoinBuilder NoLock(bool use = true)
+ {
+ UseNoLock = use;
return this;
}
/// Sets ON predicate.
@@ -7463,6 +7533,17 @@ namespace DynamORM
throw new ArgumentNullException("predicate");
OnPredicate = predicate;
+ OnRawCondition = null;
+ return this;
+ }
+ /// Sets raw ON clause (without the ON keyword).
+ public TypedJoinBuilder OnRaw(string condition)
+ {
+ if (string.IsNullOrEmpty(condition))
+ throw new ArgumentNullException("condition");
+
+ OnRawCondition = condition.Trim();
+ OnPredicate = null;
return this;
}
}
@@ -7484,12 +7565,22 @@ namespace DynamORM
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);
}
@@ -9223,7 +9314,7 @@ namespace DynamORM
private string _select;
private string _from;
- private string _join;
+ protected string _join;
private string _groupby;
private string _orderby;
@@ -10639,10 +10730,14 @@ namespace DynamORM
return this;
}
- public IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner)
+ public IDynamicTypedSelectQueryBuilder Join(Expression> 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 Join(Expression> 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)
@@ -10654,27 +10749,28 @@ namespace DynamORM
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(be.Left, leftPrefix, _mapper);
- string rightExpr = ParseTypedJoinMember(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;
}
public IDynamicTypedSelectQueryBuilder Join(Func, TypedJoinBuilder> specification)
@@ -10685,10 +10781,34 @@ namespace DynamORM
TypedJoinBuilder spec = specification(new TypedJoinBuilder());
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 IDynamicTypedSelectQueryBuilder InnerJoin(Expression> on, string alias = null)
{
@@ -11061,16 +11181,82 @@ namespace DynamORM
{
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(Expression expression, string tablePrefix, DynamicTypeMap mapper)
{
expression = UnwrapConvert(expression);
@@ -11097,6 +11283,27 @@ namespace DynamORM
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().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 &&
diff --git a/DynamORM.Tests/Helpers/TypedJoinModels.cs b/DynamORM.Tests/Helpers/TypedJoinModels.cs
new file mode 100644
index 0000000..e29b31a
--- /dev/null
+++ b/DynamORM.Tests/Helpers/TypedJoinModels.cs
@@ -0,0 +1,33 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using DynamORM.Mapper;
+
+namespace DynamORM.Tests.Helpers
+{
+ [Table(Name = "Users", Owner = "dbo")]
+ public class TypedJoinUser
+ {
+ [Column("Id_User", true)]
+ public long IdUser { get; set; }
+
+ [Column("Active")]
+ public int Active { get; set; }
+ }
+
+ [Table(Name = "UserClients", Owner = "dbo")]
+ public class TypedJoinUserClient
+ {
+ [Column("User_Id", true)]
+ public long UserId { get; set; }
+
+ [Column("Users")]
+ public string Users { get; set; }
+
+ [Column("Deleted")]
+ public int Deleted { get; set; }
+ }
+}
diff --git a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs
index 968e120..84ff65a 100644
--- a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs
+++ b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs
@@ -99,7 +99,7 @@ namespace DynamORM.Tests.Select
public void TestTypedJoin()
{
var cmd = Database.FromTyped("u")
- .Join(j => j.As("x").On((l, r) => l.Id == r.Id))
+ .Join(j => j.Inner().As("x").On((l, r) => l.Id == r.Id))
.Select(u => u.Id);
Assert.AreEqual("SELECT u.\"id_user\" FROM \"sample_users\" AS u INNER JOIN \"sample_users\" AS x ON (u.\"id_user\" = x.\"id_user\")",
diff --git a/DynamORM.Tests/Select/TypedFluentJoinSyntaxTests.cs b/DynamORM.Tests/Select/TypedFluentJoinSyntaxTests.cs
new file mode 100644
index 0000000..4c29f68
--- /dev/null
+++ b/DynamORM.Tests/Select/TypedFluentJoinSyntaxTests.cs
@@ -0,0 +1,102 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System.Linq;
+using DynamORM.Tests.Helpers;
+using NUnit.Framework;
+
+namespace DynamORM.Tests.Select
+{
+ [TestFixture]
+ public class TypedFluentJoinSyntaxTests : TestsBase
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ CreateTestDatabase();
+ CreateDynamicDatabase(
+ DynamicDatabaseOptions.SingleConnection |
+ DynamicDatabaseOptions.SingleTransaction |
+ DynamicDatabaseOptions.SupportNoLock);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ DestroyDynamicDatabase();
+ DestroyTestDatabase();
+ }
+
+ [Test]
+ public void TestTypedJoinDefaultJoinKeyword()
+ {
+ var cmd = Database.From("usr")
+ .Join(j => j.As("uc").On((u, c) => u.IdUser == c.UserId));
+
+ Assert.AreEqual("SELECT * FROM \"dbo\".\"Users\" AS usr JOIN \"dbo\".\"UserClients\" AS uc ON (usr.\"Id_User\" = uc.\"User_Id\")", cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedInnerJoinWithAndNull()
+ {
+ var cmd = Database.From("usr")
+ .Join(j => j.Inner().As("uc").On((u, c) => u.IdUser == c.UserId && c.Users != null))
+ .Select(u => u.IdUser);
+
+ Assert.AreEqual("SELECT usr.\"Id_User\" FROM \"dbo\".\"Users\" AS usr INNER JOIN \"dbo\".\"UserClients\" AS uc ON ((usr.\"Id_User\" = uc.\"User_Id\") AND (uc.\"Users\" IS NOT NULL))", cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedJoinWithNoLock()
+ {
+ var cmd = Database.From("usr", noLock: true)
+ .Join(j => j.Inner().As("uc").NoLock().On((u, c) => u.IdUser == c.UserId));
+
+ Assert.AreEqual("SELECT * FROM \"dbo\".\"Users\" AS usr WITH(NOLOCK) INNER JOIN \"dbo\".\"UserClients\" AS uc WITH(NOLOCK) ON (usr.\"Id_User\" = uc.\"User_Id\")", cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedLeftOuterJoin()
+ {
+ var cmd = Database.From("usr")
+ .Join(j => j.LeftOuter().As("uc").On((u, c) => u.IdUser == c.UserId));
+
+ Assert.AreEqual("SELECT * FROM \"dbo\".\"Users\" AS usr LEFT OUTER JOIN \"dbo\".\"UserClients\" AS uc ON (usr.\"Id_User\" = uc.\"User_Id\")", cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedRightOuterJoin()
+ {
+ var cmd = Database.From("usr")
+ .Join(j => j.RightOuter().As("uc").On((u, c) => u.IdUser == c.UserId));
+
+ Assert.AreEqual("SELECT * FROM \"dbo\".\"Users\" AS usr RIGHT OUTER JOIN \"dbo\".\"UserClients\" AS uc ON (usr.\"Id_User\" = uc.\"User_Id\")", cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedCustomJoinTypeCrossApply()
+ {
+ var cmd = Database.From("usr")
+ .Join(j => j.Type("CROSS APPLY").As("uc"));
+
+ Assert.AreEqual("SELECT * FROM \"dbo\".\"Users\" AS usr CROSS APPLY \"dbo\".\"UserClients\" AS uc", cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedJoinAndWhereParameterOrder()
+ {
+ var cmd = Database.From("usr")
+ .Join(j => j.As("uc").On((u, c) => u.IdUser == c.UserId && c.Deleted == 0))
+ .Where(u => u.Active == 1);
+
+ Assert.AreEqual(
+ string.Format("SELECT * FROM \"dbo\".\"Users\" AS usr JOIN \"dbo\".\"UserClients\" AS uc ON ((usr.\"Id_User\" = uc.\"User_Id\") AND (uc.\"Deleted\" = [${0}])) WHERE (usr.\"Active\" = [${1}])",
+ cmd.Parameters.Keys.ElementAt(0),
+ cmd.Parameters.Keys.ElementAt(1)),
+ cmd.CommandText());
+ }
+ }
+}
diff --git a/DynamORM/Builders/DynamicJoinType.cs b/DynamORM/Builders/DynamicJoinType.cs
index a4da19f..b81a498 100644
--- a/DynamORM/Builders/DynamicJoinType.cs
+++ b/DynamORM/Builders/DynamicJoinType.cs
@@ -10,8 +10,12 @@ namespace DynamORM.Builders
public enum DynamicJoinType
{
Inner = 0,
+ Join,
Left,
Right,
- Full
+ Full,
+ LeftOuter,
+ RightOuter,
+ FullOuter
}
}
diff --git a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs
index 237f30f..4a637a5 100644
--- a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs
+++ b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs
@@ -40,8 +40,18 @@ namespace DynamORM.Builders
/// Join ON predicate.
/// Optional alias for joined table.
/// Join type.
+ /// Adds NOLOCK hint to joined source when supported by provider options.
/// Builder instance.
- IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner);
+ IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner, bool noLock = false);
+
+ /// Add typed join with custom join type text (for example: CROSS APPLY).
+ /// Joined mapped entity type.
+ /// Optional join ON predicate.
+ /// Optional alias for joined table.
+ /// Join type text.
+ /// Adds NOLOCK hint to joined source when supported by provider options.
+ /// Builder instance.
+ IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias, string joinType, bool noLock = false);
/// Add typed join using join-spec builder syntax (As(), join kind and On()).
/// Joined mapped entity type.
diff --git a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs
index 387f54b..f2bae87 100644
--- a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs
+++ b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs
@@ -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;
diff --git a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs
index 085b344..af340e0 100644
--- a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs
+++ b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs
@@ -65,10 +65,15 @@ namespace DynamORM.Builders.Implementation
return this;
}
- public IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner)
+ public IDynamicTypedSelectQueryBuilder Join(Expression> 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 Join(Expression> 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(be.Left, leftPrefix, _mapper);
- string rightExpr = ParseTypedJoinMember(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 spec = specification(new TypedJoinBuilder());
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 Join(params Func[] 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(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().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 &&
diff --git a/DynamORM/Builders/TypedJoinBuilder.cs b/DynamORM/Builders/TypedJoinBuilder.cs
index ebc5bb3..ac0440b 100644
--- a/DynamORM/Builders/TypedJoinBuilder.cs
+++ b/DynamORM/Builders/TypedJoinBuilder.cs
@@ -16,7 +16,7 @@ namespace DynamORM.Builders
{
internal TypedJoinBuilder()
{
- JoinType = DynamicJoinType.Inner;
+ JoinType = DynamicJoinType.Join;
}
/// Gets join alias.
@@ -25,9 +25,18 @@ namespace DynamORM.Builders
/// Gets join type.
public DynamicJoinType JoinType { get; private set; }
+ /// Gets custom join type text.
+ public string CustomJoinType { get; private set; }
+
+ /// Gets a value indicating whether joined source should use NOLOCK.
+ public bool UseNoLock { get; private set; }
+
/// Gets ON predicate.
public Expression> OnPredicate { get; private set; }
+ /// Gets raw ON condition.
+ public string OnRawCondition { get; private set; }
+
/// Sets join alias.
public TypedJoinBuilder As(string alias)
{
@@ -39,6 +48,15 @@ namespace DynamORM.Builders
public TypedJoinBuilder Inner()
{
JoinType = DynamicJoinType.Inner;
+ CustomJoinType = null;
+ return this;
+ }
+
+ /// Sets plain JOIN.
+ public TypedJoinBuilder Join()
+ {
+ JoinType = DynamicJoinType.Join;
+ CustomJoinType = null;
return this;
}
@@ -46,6 +64,7 @@ namespace DynamORM.Builders
public TypedJoinBuilder Left()
{
JoinType = DynamicJoinType.Left;
+ CustomJoinType = null;
return this;
}
@@ -53,6 +72,7 @@ namespace DynamORM.Builders
public TypedJoinBuilder Right()
{
JoinType = DynamicJoinType.Right;
+ CustomJoinType = null;
return this;
}
@@ -60,6 +80,48 @@ namespace DynamORM.Builders
public TypedJoinBuilder Full()
{
JoinType = DynamicJoinType.Full;
+ CustomJoinType = null;
+ return this;
+ }
+
+ /// Sets LEFT OUTER JOIN.
+ public TypedJoinBuilder LeftOuter()
+ {
+ JoinType = DynamicJoinType.LeftOuter;
+ CustomJoinType = null;
+ return this;
+ }
+
+ /// Sets RIGHT OUTER JOIN.
+ public TypedJoinBuilder RightOuter()
+ {
+ JoinType = DynamicJoinType.RightOuter;
+ CustomJoinType = null;
+ return this;
+ }
+
+ /// Sets FULL OUTER JOIN.
+ public TypedJoinBuilder FullOuter()
+ {
+ JoinType = DynamicJoinType.FullOuter;
+ CustomJoinType = null;
+ return this;
+ }
+
+ /// Sets custom join type text (for example: CROSS APPLY).
+ public TypedJoinBuilder Type(string joinType)
+ {
+ if (string.IsNullOrEmpty(joinType))
+ throw new ArgumentNullException("joinType");
+
+ CustomJoinType = joinType.Trim();
+ return this;
+ }
+
+ /// Marks joined source with NOLOCK hint.
+ public TypedJoinBuilder 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;
+ }
+
+ /// Sets raw ON clause (without the ON keyword).
+ public TypedJoinBuilder OnRaw(string condition)
+ {
+ if (string.IsNullOrEmpty(condition))
+ throw new ArgumentNullException("condition");
+
+ OnRawCondition = condition.Trim();
+ OnPredicate = null;
return this;
}
}
diff --git a/DynamORM/Builders/TypedJoinExtensions.cs b/DynamORM/Builders/TypedJoinExtensions.cs
index c3b5d07..1c8e7ce 100644
--- a/DynamORM/Builders/TypedJoinExtensions.cs
+++ b/DynamORM/Builders/TypedJoinExtensions.cs
@@ -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);
}