diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs
index a59403d..528d5f2 100644
--- a/AmalgamationTool/DynamORM.Amalgamation.cs
+++ b/AmalgamationTool/DynamORM.Amalgamation.cs
@@ -7371,6 +7371,204 @@ namespace DynamORM
return source;
}
}
+ /// Typed join helpers for typed select builder.
+ public static class TypedJoinExtensions
+ {
+ /// Add typed join on mapped members. Supports simple equality join expression only.
+ public static IDynamicTypedSelectQueryBuilder JoinTyped(
+ this IDynamicTypedSelectQueryBuilder builder,
+ string alias,
+ Expression> on,
+ string joinType = "INNER JOIN")
+ {
+ if (builder == null) throw new ArgumentNullException("builder");
+ if (on == null) throw new ArgumentNullException("on");
+
+ var 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 = string.IsNullOrEmpty(rightMapper.Table.NullOr(t => t.Name))
+ ? typeof(TRight).Name
+ : rightMapper.Table.Name;
+
+ string rightOwner = rightMapper.Table.NullOr(t => t.Owner);
+ string rightAlias = string.IsNullOrEmpty(alias) ? "t" + (builder.Tables.Count + 1).ToString() : alias;
+
+ var be = on.Body as BinaryExpression;
+ if (be == null || be.NodeType != ExpressionType.Equal)
+ throw new NotSupportedException("JoinTyped currently supports only equality join expressions.");
+
+ string leftPrefix = builder.Tables.FirstOrDefault().NullOr(t => string.IsNullOrEmpty(t.Alias) ? t.Name : t.Alias, null);
+ if (string.IsNullOrEmpty(leftPrefix))
+ throw new InvalidOperationException("JoinTyped requires source table to be present.");
+
+ string leftExpr = ResolveMappedSide(be.Left, typeof(TLeft), leftPrefix, builder.Database);
+ string rightExpr = ResolveMappedSide(be.Right, typeof(TRight), rightAlias, builder.Database);
+
+ string ownerPrefix = string.IsNullOrEmpty(rightOwner) ? string.Empty : builder.Database.DecorateName(rightOwner) + ".";
+ string rightTableExpr = ownerPrefix + builder.Database.DecorateName(rightTable);
+ string joinExpr = string.Format("{0} {1} AS {2} ON ({3} = {4})", joinType, rightTableExpr, rightAlias, leftExpr, rightExpr);
+
+ builder.Join(x => joinExpr);
+ return builder;
+ }
+ private static string ResolveMappedSide(Expression expression, Type modelType, string prefix, DynamicDatabase db)
+ {
+ expression = UnwrapConvert(expression);
+ var member = expression as MemberExpression;
+ if (member == null)
+ throw new NotSupportedException("Join side must be mapped member access.");
+
+ var mapper = DynamicMapperCache.GetMapper(modelType);
+ string col = 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}", prefix, db.DecorateName(col));
+ }
+ private static Expression UnwrapConvert(Expression expression)
+ {
+ while (expression is UnaryExpression &&
+ (((UnaryExpression)expression).NodeType == ExpressionType.Convert ||
+ ((UnaryExpression)expression).NodeType == ExpressionType.ConvertChecked))
+ expression = ((UnaryExpression)expression).Operand;
+
+ return expression;
+ }
+ }
+ /// Typed helper extensions for update/insert/delete fluent APIs.
+ public static class TypedModifyExtensions
+ {
+ /// Add typed where predicate (AND-composed comparisons) for update builder.
+ public static IDynamicUpdateQueryBuilder WhereTyped(this IDynamicUpdateQueryBuilder builder, Expression> predicate)
+ {
+ AddTypedWhere(builder, predicate == null ? null : predicate.Body);
+ return builder;
+ }
+ /// Add typed where predicate (AND-composed comparisons) for delete builder.
+ public static IDynamicDeleteQueryBuilder WhereTyped(this IDynamicDeleteQueryBuilder builder, Expression> predicate)
+ {
+ AddTypedWhere(builder, predicate == null ? null : predicate.Body);
+ return builder;
+ }
+ /// Add typed value assignment for update builder.
+ public static IDynamicUpdateQueryBuilder SetTyped(this IDynamicUpdateQueryBuilder builder, Expression> selector, object value)
+ {
+ string col = GetColumnName(typeof(T), selector == null ? null : selector.Body);
+ return builder.Values(col, value);
+ }
+ /// Add typed value assignment for insert builder.
+ public static IDynamicInsertQueryBuilder InsertTyped(this IDynamicInsertQueryBuilder builder, Expression> selector, object value)
+ {
+ string col = GetColumnName(typeof(T), selector == null ? null : selector.Body);
+ return builder.Insert(col, value);
+ }
+ /// Insert mapped object with compile-time type information.
+ public static IDynamicInsertQueryBuilder InsertTyped(this IDynamicInsertQueryBuilder builder, T value) where T : class
+ {
+ return builder.Insert(value);
+ }
+ /// Update mapped object with compile-time type information.
+ public static IDynamicUpdateQueryBuilder UpdateTyped(this IDynamicUpdateQueryBuilder builder, T value) where T : class
+ {
+ return builder.Update(value);
+ }
+ private static void AddTypedWhere(dynamic builder, Expression expression)
+ {
+ if (expression == null)
+ throw new ArgumentNullException("predicate");
+
+ expression = UnwrapConvert(expression);
+ BinaryExpression be = expression as BinaryExpression;
+
+ if (be != null && (be.NodeType == ExpressionType.AndAlso || be.NodeType == ExpressionType.And))
+ {
+ AddTypedWhere(builder, be.Left);
+ AddTypedWhere(builder, be.Right);
+ return;
+ }
+ if (be != null)
+ {
+ string col = GetColumnName(GetRootParameterType(be.Left), be.Left);
+ object val = EvaluateExpression(be.Right);
+
+ switch (be.NodeType)
+ {
+ case ExpressionType.Equal:
+ builder.Where(col, DynamicColumn.CompareOperator.Eq, val);
+ return;
+ case ExpressionType.NotEqual:
+ builder.Where(col, DynamicColumn.CompareOperator.Not, val);
+ return;
+ case ExpressionType.GreaterThan:
+ builder.Where(col, DynamicColumn.CompareOperator.Gt, val);
+ return;
+ case ExpressionType.GreaterThanOrEqual:
+ builder.Where(col, DynamicColumn.CompareOperator.Gte, val);
+ return;
+ case ExpressionType.LessThan:
+ builder.Where(col, DynamicColumn.CompareOperator.Lt, val);
+ return;
+ case ExpressionType.LessThanOrEqual:
+ builder.Where(col, DynamicColumn.CompareOperator.Lte, val);
+ return;
+ }
+ }
+ throw new NotSupportedException(string.Format("Typed where expression is currently limited to AND-composed binary comparisons: {0}", expression));
+ }
+ private static Type GetRootParameterType(Expression expression)
+ {
+ expression = UnwrapConvert(expression);
+ MemberExpression m = expression as MemberExpression;
+ if (m != null && m.Expression is ParameterExpression)
+ return ((ParameterExpression)m.Expression).Type;
+
+ throw new NotSupportedException(string.Format("Unsupported typed selector: {0}", expression));
+ }
+ private static string GetColumnName(Type modelType, Expression expression)
+ {
+ if (modelType == null)
+ modelType = GetRootParameterType(expression);
+
+ expression = UnwrapConvert(expression);
+ MemberExpression member = expression as MemberExpression;
+ if (member == null)
+ throw new NotSupportedException(string.Format("Selector must target a mapped property: {0}", expression));
+
+ DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType);
+ if (mapper == null)
+ throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", modelType.FullName));
+
+ string col = 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 col;
+ }
+ private static Expression UnwrapConvert(Expression expression)
+ {
+ while (expression is UnaryExpression &&
+ (((UnaryExpression)expression).NodeType == ExpressionType.Convert ||
+ ((UnaryExpression)expression).NodeType == ExpressionType.ConvertChecked))
+ expression = ((UnaryExpression)expression).Operand;
+
+ return expression;
+ }
+ private static object EvaluateExpression(Expression expression)
+ {
+ expression = UnwrapConvert(expression);
+ var objectMember = Expression.Convert(expression, typeof(object));
+ var getter = Expression.Lambda>(objectMember).Compile();
+ return getter();
+ }
+ }
namespace Extensions
{
internal static class DynamicHavingQueryExtensions
diff --git a/DynamORM.Tests/Modify/TypedModifyExtensionsTests.cs b/DynamORM.Tests/Modify/TypedModifyExtensionsTests.cs
new file mode 100644
index 0000000..a5390fa
--- /dev/null
+++ b/DynamORM.Tests/Modify/TypedModifyExtensionsTests.cs
@@ -0,0 +1,75 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System.Linq;
+using DynamORM.Builders;
+using DynamORM.Tests.Helpers;
+using NUnit.Framework;
+
+namespace DynamORM.Tests.Modify
+{
+ [TestFixture]
+ public class TypedModifyExtensionsTests : TestsBase
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ CreateTestDatabase();
+ CreateDynamicDatabase(
+ DynamicDatabaseOptions.SingleConnection |
+ DynamicDatabaseOptions.SingleTransaction |
+ DynamicDatabaseOptions.SupportLimitOffset |
+ DynamicDatabaseOptions.SupportSchema);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ DestroyDynamicDatabase();
+ DestroyTestDatabase();
+ }
+
+ [Test]
+ public void TestTypedUpdateSetAndWhere()
+ {
+ var cmd = Database.Update()
+ .SetTyped(u => u.Code, "777")
+ .WhereTyped(u => u.Id == 1 && u.Code == "1");
+
+ Assert.AreEqual(
+ string.Format("UPDATE \"sample_users\" SET \"code\" = [${0}] WHERE (\"id\" = [${1}]) AND (\"code\" = [${2}])",
+ cmd.Parameters.Keys.ElementAt(0),
+ cmd.Parameters.Keys.ElementAt(1),
+ cmd.Parameters.Keys.ElementAt(2)),
+ cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedDeleteWhere()
+ {
+ var cmd = Database.Delete()
+ .WhereTyped(u => u.Id == 2);
+
+ Assert.AreEqual(
+ string.Format("DELETE FROM \"sample_users\" WHERE (\"id\" = [${0}])", cmd.Parameters.Keys.First()),
+ cmd.CommandText());
+ }
+
+ [Test]
+ public void TestTypedInsertColumns()
+ {
+ var cmd = Database.Insert()
+ .InsertTyped(u => u.Code, "900")
+ .InsertTyped(u => u.First, "Typed");
+
+ Assert.AreEqual(
+ string.Format("INSERT INTO \"sample_users\" (\"code\", \"first\") VALUES ([${0}], [${1}])",
+ cmd.Parameters.Keys.ElementAt(0),
+ cmd.Parameters.Keys.ElementAt(1)),
+ cmd.CommandText());
+ }
+ }
+}
diff --git a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs
index 0d83954..6528378 100644
--- a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs
+++ b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs
@@ -95,5 +95,16 @@ namespace DynamORM.Tests.Select
Assert.AreEqual("SELECT u.\"user_code\" FROM \"sample_users\" AS u GROUP BY u.\"user_code\" HAVING (u.\"user_code\" IS NOT NULL) ORDER BY u.\"user_code\" ASC",
cmd.CommandText());
}
+
+ [Test]
+ public void TestTypedJoin()
+ {
+ var cmd = Database.From("u")
+ .JoinTyped("x", (l, r) => l.Id == r.Id)
+ .SelectTyped(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\")",
+ cmd.CommandText());
+ }
}
}
diff --git a/DynamORM/Builders/TypedJoinExtensions.cs b/DynamORM/Builders/TypedJoinExtensions.cs
new file mode 100644
index 0000000..6ce4e27
--- /dev/null
+++ b/DynamORM/Builders/TypedJoinExtensions.cs
@@ -0,0 +1,86 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using DynamORM.Helpers;
+using DynamORM.Mapper;
+
+namespace DynamORM.Builders
+{
+ /// Typed join helpers for typed select builder.
+ public static class TypedJoinExtensions
+ {
+ /// Add typed join on mapped members. Supports simple equality join expression only.
+ public static IDynamicTypedSelectQueryBuilder JoinTyped(
+ this IDynamicTypedSelectQueryBuilder builder,
+ string alias,
+ Expression> on,
+ string joinType = "INNER JOIN")
+ {
+ if (builder == null) throw new ArgumentNullException("builder");
+ if (on == null) throw new ArgumentNullException("on");
+
+ var 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 = string.IsNullOrEmpty(rightMapper.Table.NullOr(t => t.Name))
+ ? typeof(TRight).Name
+ : rightMapper.Table.Name;
+
+ string rightOwner = rightMapper.Table.NullOr(t => t.Owner);
+ string rightAlias = string.IsNullOrEmpty(alias) ? "t" + (builder.Tables.Count + 1).ToString() : alias;
+
+ var be = on.Body as BinaryExpression;
+ if (be == null || be.NodeType != ExpressionType.Equal)
+ throw new NotSupportedException("JoinTyped currently supports only equality join expressions.");
+
+ string leftPrefix = builder.Tables.FirstOrDefault().NullOr(t => string.IsNullOrEmpty(t.Alias) ? t.Name : t.Alias, null);
+ if (string.IsNullOrEmpty(leftPrefix))
+ throw new InvalidOperationException("JoinTyped requires source table to be present.");
+
+ string leftExpr = ResolveMappedSide(be.Left, typeof(TLeft), leftPrefix, builder.Database);
+ string rightExpr = ResolveMappedSide(be.Right, typeof(TRight), rightAlias, builder.Database);
+
+ string ownerPrefix = string.IsNullOrEmpty(rightOwner) ? string.Empty : builder.Database.DecorateName(rightOwner) + ".";
+ string rightTableExpr = ownerPrefix + builder.Database.DecorateName(rightTable);
+ string joinExpr = string.Format("{0} {1} AS {2} ON ({3} = {4})", joinType, rightTableExpr, rightAlias, leftExpr, rightExpr);
+
+ builder.Join(x => joinExpr);
+ return builder;
+ }
+
+ private static string ResolveMappedSide(Expression expression, Type modelType, string prefix, DynamicDatabase db)
+ {
+ expression = UnwrapConvert(expression);
+ var member = expression as MemberExpression;
+ if (member == null)
+ throw new NotSupportedException("Join side must be mapped member access.");
+
+ var mapper = DynamicMapperCache.GetMapper(modelType);
+ string col = 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}", prefix, db.DecorateName(col));
+ }
+
+ private static Expression UnwrapConvert(Expression expression)
+ {
+ while (expression is UnaryExpression &&
+ (((UnaryExpression)expression).NodeType == ExpressionType.Convert ||
+ ((UnaryExpression)expression).NodeType == ExpressionType.ConvertChecked))
+ expression = ((UnaryExpression)expression).Operand;
+
+ return expression;
+ }
+ }
+}
diff --git a/DynamORM/Builders/TypedModifyExtensions.cs b/DynamORM/Builders/TypedModifyExtensions.cs
new file mode 100644
index 0000000..f1687c9
--- /dev/null
+++ b/DynamORM/Builders/TypedModifyExtensions.cs
@@ -0,0 +1,155 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using DynamORM.Mapper;
+
+namespace DynamORM.Builders
+{
+ /// Typed helper extensions for update/insert/delete fluent APIs.
+ public static class TypedModifyExtensions
+ {
+ /// Add typed where predicate (AND-composed comparisons) for update builder.
+ public static IDynamicUpdateQueryBuilder WhereTyped(this IDynamicUpdateQueryBuilder builder, Expression> predicate)
+ {
+ AddTypedWhere(builder, predicate == null ? null : predicate.Body);
+ return builder;
+ }
+
+ /// Add typed where predicate (AND-composed comparisons) for delete builder.
+ public static IDynamicDeleteQueryBuilder WhereTyped(this IDynamicDeleteQueryBuilder builder, Expression> predicate)
+ {
+ AddTypedWhere(builder, predicate == null ? null : predicate.Body);
+ return builder;
+ }
+
+ /// Add typed value assignment for update builder.
+ public static IDynamicUpdateQueryBuilder SetTyped(this IDynamicUpdateQueryBuilder builder, Expression> selector, object value)
+ {
+ string col = GetColumnName(typeof(T), selector == null ? null : selector.Body);
+ return builder.Values(col, value);
+ }
+
+ /// Add typed value assignment for insert builder.
+ public static IDynamicInsertQueryBuilder InsertTyped(this IDynamicInsertQueryBuilder builder, Expression> selector, object value)
+ {
+ string col = GetColumnName(typeof(T), selector == null ? null : selector.Body);
+ return builder.Insert(col, value);
+ }
+
+ /// Insert mapped object with compile-time type information.
+ public static IDynamicInsertQueryBuilder InsertTyped(this IDynamicInsertQueryBuilder builder, T value) where T : class
+ {
+ return builder.Insert(value);
+ }
+
+ /// Update mapped object with compile-time type information.
+ public static IDynamicUpdateQueryBuilder UpdateTyped(this IDynamicUpdateQueryBuilder builder, T value) where T : class
+ {
+ return builder.Update(value);
+ }
+
+ private static void AddTypedWhere(dynamic builder, Expression expression)
+ {
+ if (expression == null)
+ throw new ArgumentNullException("predicate");
+
+ expression = UnwrapConvert(expression);
+ BinaryExpression be = expression as BinaryExpression;
+
+ if (be != null && (be.NodeType == ExpressionType.AndAlso || be.NodeType == ExpressionType.And))
+ {
+ AddTypedWhere(builder, be.Left);
+ AddTypedWhere(builder, be.Right);
+ return;
+ }
+
+ if (be != null)
+ {
+ string col = GetColumnName(GetRootParameterType(be.Left), be.Left);
+ object val = EvaluateExpression(be.Right);
+
+ switch (be.NodeType)
+ {
+ case ExpressionType.Equal:
+ builder.Where(col, DynamicColumn.CompareOperator.Eq, val);
+ return;
+ case ExpressionType.NotEqual:
+ builder.Where(col, DynamicColumn.CompareOperator.Not, val);
+ return;
+ case ExpressionType.GreaterThan:
+ builder.Where(col, DynamicColumn.CompareOperator.Gt, val);
+ return;
+ case ExpressionType.GreaterThanOrEqual:
+ builder.Where(col, DynamicColumn.CompareOperator.Gte, val);
+ return;
+ case ExpressionType.LessThan:
+ builder.Where(col, DynamicColumn.CompareOperator.Lt, val);
+ return;
+ case ExpressionType.LessThanOrEqual:
+ builder.Where(col, DynamicColumn.CompareOperator.Lte, val);
+ return;
+ }
+ }
+
+ throw new NotSupportedException(string.Format("Typed where expression is currently limited to AND-composed binary comparisons: {0}", expression));
+ }
+
+ private static Type GetRootParameterType(Expression expression)
+ {
+ expression = UnwrapConvert(expression);
+ MemberExpression m = expression as MemberExpression;
+ if (m != null && m.Expression is ParameterExpression)
+ return ((ParameterExpression)m.Expression).Type;
+
+ throw new NotSupportedException(string.Format("Unsupported typed selector: {0}", expression));
+ }
+
+ private static string GetColumnName(Type modelType, Expression expression)
+ {
+ if (modelType == null)
+ modelType = GetRootParameterType(expression);
+
+ expression = UnwrapConvert(expression);
+ MemberExpression member = expression as MemberExpression;
+ if (member == null)
+ throw new NotSupportedException(string.Format("Selector must target a mapped property: {0}", expression));
+
+ DynamicTypeMap mapper = DynamicMapperCache.GetMapper(modelType);
+ if (mapper == null)
+ throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", modelType.FullName));
+
+ string col = 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 col;
+ }
+
+ private static Expression UnwrapConvert(Expression expression)
+ {
+ while (expression is UnaryExpression &&
+ (((UnaryExpression)expression).NodeType == ExpressionType.Convert ||
+ ((UnaryExpression)expression).NodeType == ExpressionType.ConvertChecked))
+ expression = ((UnaryExpression)expression).Operand;
+
+ return expression;
+ }
+
+ private static object EvaluateExpression(Expression expression)
+ {
+ expression = UnwrapConvert(expression);
+ var objectMember = Expression.Convert(expression, typeof(object));
+ var getter = Expression.Lambda>(objectMember).Compile();
+ return getter();
+ }
+ }
+}