From 23a0336b7ee87543aeb043b1a775fff0a0d4f7ed Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 18:42:35 +0100 Subject: [PATCH] Add typed join specification builder syntax --- AmalgamationTool/DynamORM.Amalgamation.cs | 77 +++++++++++++++++++ .../Select/TypedFluentBuilderTests.cs | 4 +- .../IDynamicTypedSelectQueryBuilder.cs | 6 ++ .../DynamicTypedSelectQueryBuilder.cs | 14 ++++ DynamORM/Builders/TypedJoinBuilder.cs | 76 ++++++++++++++++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 DynamORM/Builders/TypedJoinBuilder.cs diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 049ade1..ccef0e0 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -7190,6 +7190,12 @@ namespace DynamORM /// Builder instance. IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner); + /// Add typed join using join-spec builder syntax (As(), join kind and On()). + /// Joined mapped entity type. + /// Join specification builder callback. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Join(Func, TypedJoinBuilder> specification); + /// Add INNER JOIN using typed ON predicate. IDynamicTypedSelectQueryBuilder InnerJoin(Expression> on, string alias = null); @@ -7402,6 +7408,64 @@ namespace DynamORM return source; } } + /// Typed join specification builder used by typed fluent select queries. + /// Left side mapped type. + /// Right side mapped type. + public class TypedJoinBuilder + { + internal TypedJoinBuilder() + { + JoinType = DynamicJoinType.Inner; + } + /// Gets join alias. + public string Alias { get; private set; } + + /// Gets join type. + public DynamicJoinType JoinType { get; private set; } + + /// Gets ON predicate. + public Expression> OnPredicate { get; private set; } + + /// Sets join alias. + public TypedJoinBuilder As(string alias) + { + Alias = alias; + return this; + } + /// Sets INNER JOIN. + public TypedJoinBuilder Inner() + { + JoinType = DynamicJoinType.Inner; + return this; + } + /// Sets LEFT JOIN. + public TypedJoinBuilder Left() + { + JoinType = DynamicJoinType.Left; + return this; + } + /// Sets RIGHT JOIN. + public TypedJoinBuilder Right() + { + JoinType = DynamicJoinType.Right; + return this; + } + /// Sets FULL JOIN. + public TypedJoinBuilder Full() + { + JoinType = DynamicJoinType.Full; + return this; + } + /// Sets ON predicate. + public TypedJoinBuilder On(Expression> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + OnPredicate = predicate; + return this; + } + } /// Compatibility and convenience extensions for typed joins. public static class TypedJoinExtensions { @@ -10742,6 +10806,19 @@ namespace DynamORM base.Join(x => joinExpr); return this; } + public IDynamicTypedSelectQueryBuilder Join(Func, TypedJoinBuilder> specification) + { + if (specification == null) + throw new ArgumentNullException("specification"); + + 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"); + + return Join(spec.OnPredicate, spec.Alias, spec.JoinType); + } public IDynamicTypedSelectQueryBuilder InnerJoin(Expression> on, string alias = null) { return Join(on, alias, DynamicJoinType.Inner); diff --git a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs index acbe033..c8c16b8 100644 --- a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs +++ b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs @@ -100,7 +100,7 @@ namespace DynamORM.Tests.Select public void TestTypedJoin() { var cmd = Database.From("u") - .Join((l, r) => l.Id == r.Id, "x") + .Join(j => j.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\")", @@ -111,7 +111,7 @@ namespace DynamORM.Tests.Select public void TestTypedLeftJoin() { var cmd = Database.From("u") - .LeftJoin((l, r) => l.Id == r.Id, "x") + .Join(j => j.Left().As("x").On((l, r) => l.Id == r.Id)) .Select(u => u.Id); Assert.AreEqual("SELECT u.\"id_user\" FROM \"sample_users\" AS u LEFT JOIN \"sample_users\" AS x ON (u.\"id_user\" = x.\"id_user\")", diff --git a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs index 0672ebe..bf4f420 100644 --- a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs +++ b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs @@ -43,6 +43,12 @@ namespace DynamORM.Builders /// Builder instance. IDynamicTypedSelectQueryBuilder Join(Expression> on, string alias = null, DynamicJoinType joinType = DynamicJoinType.Inner); + /// Add typed join using join-spec builder syntax (As(), join kind and On()). + /// Joined mapped entity type. + /// Join specification builder callback. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Join(Func, TypedJoinBuilder> specification); + /// Add INNER JOIN using typed ON predicate. IDynamicTypedSelectQueryBuilder InnerJoin(Expression> on, string alias = null); diff --git a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs index 35cca6e..1e8053a 100644 --- a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs @@ -104,6 +104,20 @@ namespace DynamORM.Builders.Implementation return this; } + public IDynamicTypedSelectQueryBuilder Join(Func, TypedJoinBuilder> specification) + { + if (specification == null) + throw new ArgumentNullException("specification"); + + 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"); + + return Join(spec.OnPredicate, spec.Alias, spec.JoinType); + } + public IDynamicTypedSelectQueryBuilder InnerJoin(Expression> on, string alias = null) { return Join(on, alias, DynamicJoinType.Inner); diff --git a/DynamORM/Builders/TypedJoinBuilder.cs b/DynamORM/Builders/TypedJoinBuilder.cs new file mode 100644 index 0000000..ebc5bb3 --- /dev/null +++ b/DynamORM/Builders/TypedJoinBuilder.cs @@ -0,0 +1,76 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System; +using System.Linq.Expressions; + +namespace DynamORM.Builders +{ + /// Typed join specification builder used by typed fluent select queries. + /// Left side mapped type. + /// Right side mapped type. + public class TypedJoinBuilder + { + internal TypedJoinBuilder() + { + JoinType = DynamicJoinType.Inner; + } + + /// Gets join alias. + public string Alias { get; private set; } + + /// Gets join type. + public DynamicJoinType JoinType { get; private set; } + + /// Gets ON predicate. + public Expression> OnPredicate { get; private set; } + + /// Sets join alias. + public TypedJoinBuilder As(string alias) + { + Alias = alias; + return this; + } + + /// Sets INNER JOIN. + public TypedJoinBuilder Inner() + { + JoinType = DynamicJoinType.Inner; + return this; + } + + /// Sets LEFT JOIN. + public TypedJoinBuilder Left() + { + JoinType = DynamicJoinType.Left; + return this; + } + + /// Sets RIGHT JOIN. + public TypedJoinBuilder Right() + { + JoinType = DynamicJoinType.Right; + return this; + } + + /// Sets FULL JOIN. + public TypedJoinBuilder Full() + { + JoinType = DynamicJoinType.Full; + return this; + } + + /// Sets ON predicate. + public TypedJoinBuilder On(Expression> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + OnPredicate = predicate; + return this; + } + } +}