diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index e9ebcd5..a59403d 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -1957,23 +1957,26 @@ namespace DynamORM /// Table alias. /// use no lock. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder From(string alias = null, bool noLock = false) + public virtual IDynamicTypedSelectQueryBuilder From(string alias = null, bool noLock = false) { // TODO: Make it more readable and maitainable + DynamicTypedSelectQueryBuilder builder = new DynamicTypedSelectQueryBuilder(this); + if (noLock) { if (string.IsNullOrEmpty(alias)) - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T)).NoLock()); + builder.From(x => x(typeof(T)).NoLock()); else - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T)).As(alias).NoLock()); + builder.From(x => x(typeof(T)).As(alias).NoLock()); } else { if (string.IsNullOrEmpty(alias)) - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T))); + builder.From(x => x(typeof(T))); else - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T)).As(alias)); + builder.From(x => x(typeof(T)).As(alias)); } + return builder; } /// Adds to the FROM clause using . /// Type which can be represented in database. @@ -7161,6 +7164,41 @@ namespace DynamORM #endregion Top/Limit/Offset/Distinct } + /// Typed select query builder for mapped entities. + /// Mapped entity type. + public interface IDynamicTypedSelectQueryBuilder : IDynamicSelectQueryBuilder + { + /// Add typed where predicate using mapped properties. + /// Predicate to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Where(Expression> predicate); + + /// Add typed having predicate using mapped properties. + /// Predicate to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Having(Expression> predicate); + + /// Add typed selected columns using mapped properties. + /// Projection type. + /// Selector to parse. + /// Additional selectors to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Select(Expression> selector, params Expression>[] selectors); + + /// Add typed group by columns using mapped properties. + /// Projection type. + /// Selector to parse. + /// Additional selectors to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder GroupBy(Expression> selector, params Expression>[] selectors); + + /// Add typed order by columns using mapped properties. Supports Asc()/Desc(). + /// Projection type. + /// Selector to parse. + /// Additional selectors to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors); + } /// Dynamic update query builder interface. /// This interface it publicly available. Implementation should be hidden. public interface IDynamicUpdateQueryBuilder : IDynamicQueryBuilder @@ -7290,6 +7328,49 @@ namespace DynamORM /// Gets table schema. Dictionary Schema { get; } } + /// Marker extensions for typed fluent builder expressions. + public static class TypedFluentExtensions + { + /// Typed select helper that avoids overload resolution with dynamic methods. + public static IDynamicTypedSelectQueryBuilder SelectTyped( + this IDynamicTypedSelectQueryBuilder builder, + System.Linq.Expressions.Expression> selector, + params System.Linq.Expressions.Expression>[] selectors) + { + return builder.Select(selector, selectors); + } + /// Typed group by helper that avoids overload resolution with dynamic methods. + public static IDynamicTypedSelectQueryBuilder GroupByTyped( + this IDynamicTypedSelectQueryBuilder builder, + System.Linq.Expressions.Expression> selector, + params System.Linq.Expressions.Expression>[] selectors) + { + return builder.GroupBy(selector, selectors); + } + /// Typed order by helper that avoids overload resolution with dynamic methods. + public static IDynamicTypedSelectQueryBuilder OrderByTyped( + this IDynamicTypedSelectQueryBuilder builder, + System.Linq.Expressions.Expression> selector, + params System.Linq.Expressions.Expression>[] selectors) + { + return builder.OrderBy(selector, selectors); + } + /// Marks select projection alias in typed expressions. + public static T As(this T source, string alias) + { + return source; + } + /// Marks ascending order in typed order expressions. + public static T Asc(this T source) + { + return source; + } + /// Marks descending order in typed order expressions. + public static T Desc(this T source) + { + return source; + } + } namespace Extensions { internal static class DynamicHavingQueryExtensions @@ -10304,6 +10385,437 @@ namespace DynamORM } #endregion IExtendedDisposable } + /// Typed wrapper over with property-to-column translation. + /// Mapped entity type. + internal class DynamicTypedSelectQueryBuilder : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder + { + private readonly DynamicTypeMap _mapper; + + public DynamicTypedSelectQueryBuilder(DynamicDatabase db) + : base(db) + { + _mapper = DynamicMapperCache.GetMapper() + ?? throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", typeof(T).FullName)); + } + public IDynamicTypedSelectQueryBuilder Where(Expression> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = ParseTypedCondition(predicate.Body); + + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + public new IDynamicTypedSelectQueryBuilder Join(params Func[] func) + { + base.Join(func); + return this; + } + public new IDynamicTypedSelectQueryBuilder Where(DynamicColumn column) + { + base.Where(column); + return this; + } + public new IDynamicTypedSelectQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value) + { + base.Where(column, op, value); + return this; + } + public new IDynamicTypedSelectQueryBuilder Where(string column, object value) + { + base.Where(column, value); + return this; + } + public new IDynamicTypedSelectQueryBuilder Where(object conditions, bool schema = false) + { + base.Where(conditions, schema); + return this; + } + public IDynamicTypedSelectQueryBuilder Select(Expression> selector, params Expression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddSelectSelector(selector); + + if (selectors != null) + foreach (var s in selectors) + { + if (s == null) + throw new ArgumentNullException("selectors", "Array of selectors cannot contain null."); + + AddSelectSelector(s); + } + return this; + } + public IDynamicTypedSelectQueryBuilder GroupBy(Expression> selector, params Expression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddGroupBySelector(selector); + + if (selectors != null) + foreach (var s in selectors) + { + if (s == null) + throw new ArgumentNullException("selectors", "Array of selectors cannot contain null."); + + AddGroupBySelector(s); + } + return this; + } + public IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddOrderBySelector(selector); + + if (selectors != null) + foreach (var s in selectors) + { + if (s == null) + throw new ArgumentNullException("selectors", "Array of selectors cannot contain null."); + + AddOrderBySelector(s); + } + return this; + } + public new IDynamicTypedSelectQueryBuilder Top(int? top) + { + base.Top(top); + return this; + } + public new IDynamicTypedSelectQueryBuilder Limit(int? limit) + { + base.Limit(limit); + return this; + } + public new IDynamicTypedSelectQueryBuilder Offset(int? offset) + { + base.Offset(offset); + return this; + } + public new IDynamicTypedSelectQueryBuilder Distinct(bool distinct = true) + { + base.Distinct(distinct); + return this; + } + public IDynamicTypedSelectQueryBuilder Having(Expression> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = ParseTypedCondition(predicate.Body); + + if (string.IsNullOrEmpty(HavingCondition)) + HavingCondition = condition; + else + HavingCondition = string.Format("{0} AND {1}", HavingCondition, condition); + + return this; + } + public new IDynamicTypedSelectQueryBuilder Having(DynamicColumn column) + { + base.Having(column); + return this; + } + public new IDynamicTypedSelectQueryBuilder Having(string column, DynamicColumn.CompareOperator op, object value) + { + base.Having(column, op, value); + return this; + } + public new IDynamicTypedSelectQueryBuilder Having(string column, object value) + { + base.Having(column, value); + return this; + } + public new IDynamicTypedSelectQueryBuilder Having(object conditions, bool schema = false) + { + base.Having(conditions, schema); + return this; + } + private void AddSelectSelector(Expression> selector) + { + var body = UnwrapConvert(selector.Body); + + if (body is NewExpression ne) + { + foreach (var argument in ne.Arguments) + { + var parsed = ParseTypedSelectExpression(argument); + ((IDynamicSelectQueryBuilder)this).Select(x => parsed); + } + } + else + { + var parsed = ParseTypedSelectExpression(body); + ((IDynamicSelectQueryBuilder)this).Select(x => parsed); + } + } + private void AddGroupBySelector(Expression> selector) + { + var body = UnwrapConvert(selector.Body); + + if (body is NewExpression ne) + { + foreach (var argument in ne.Arguments) + { + var parsed = ParseTypedMemberAccess(argument); + ((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed); + } + } + else + { + var parsed = ParseTypedMemberAccess(body); + ((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed); + } + } + private void AddOrderBySelector(Expression> selector) + { + var body = UnwrapConvert(selector.Body); + bool ascending = true; + + if (body is MethodCallExpression call && IsAscOrDesc(call)) + { + ascending = call.Method.Name.ToUpper() != "DESC"; + body = UnwrapConvert(call.Object ?? call.Arguments.FirstOrDefault()); + } + string main = ParseTypedMemberAccess(body); + string parsed = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); + ((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed); + } + private string ParseTypedCondition(Expression expression) + { + expression = UnwrapConvert(expression); + + if (expression is BinaryExpression binary) + { + switch (binary.NodeType) + { + case ExpressionType.AndAlso: + case ExpressionType.And: + return string.Format("({0} AND {1})", ParseTypedCondition(binary.Left), ParseTypedCondition(binary.Right)); + case ExpressionType.OrElse: + case ExpressionType.Or: + return string.Format("({0} OR {1})", ParseTypedCondition(binary.Left), ParseTypedCondition(binary.Right)); + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + { + DynamicSchemaColumn? columnSchema = null; + string left = ParseTypedValue(binary.Left, ref columnSchema); + string right = ParseTypedValue(binary.Right, ref columnSchema); + 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})", ParseTypedCondition(unary.Operand)); + + if (expression is MethodCallExpression call && IsEnumerableContains(call)) + { + DynamicSchemaColumn? col = null; + return ParseTypedContains(call, ref col); + } + throw new NotSupportedException(string.Format("Typed fluent where expression is not supported: {0}", expression)); + } + private string ParseTypedValue(Expression expression, ref DynamicSchemaColumn? columnSchema) + { + expression = UnwrapConvert(expression); + + if (IsMemberFromTypedParameter(expression)) + { + string col = ParseTypedMemberAccess(expression); + columnSchema = GetColumnFromSchema(col); + return col; + } + if (expression is MethodCallExpression call && IsEnumerableContains(call)) + return ParseTypedContains(call, ref columnSchema); + + object value = EvaluateExpression(expression); + return ParseConstant(value, Parameters, columnSchema); + } + private string ParseTypedContains(MethodCallExpression call, ref DynamicSchemaColumn? columnSchema) + { + // Supports: list.Contains(x.Property) and Enumerable.Contains(list, x.Property) + Expression collection; + Expression candidate; + + if (call.Object != null) + { + collection = call.Object; + candidate = call.Arguments[0]; + } + else + { + collection = call.Arguments[0]; + candidate = call.Arguments[1]; + } + candidate = UnwrapConvert(candidate); + if (!IsMemberFromTypedParameter(candidate)) + throw new NotSupportedException(string.Format("Typed Contains() must target a mapped member: {0}", call)); + + string left = ParseTypedMemberAccess(candidate); + columnSchema = GetColumnFromSchema(left); + + var values = EvaluateExpression(collection) as IEnumerable; + if (values == null && collection is MethodCallExpression implicitCall && + string.Equals(implicitCall.Method.Name, "op_Implicit", StringComparison.Ordinal) && + implicitCall.Arguments.Count > 0) + { + values = EvaluateExpression(implicitCall.Arguments[0]) as IEnumerable; + } + if (values == null) + throw new NotSupportedException(string.Format("Typed Contains() source is not enumerable: {0}", call)); + + var inList = new List(); + foreach (var item in values.Cast()) + inList.Add(ParseConstant(item, Parameters, columnSchema)); + + if (!inList.Any()) + return "(1 = 0)"; + + return string.Format("({0} IN({1}))", left, string.Join(", ", inList)); + } + private string ParseTypedMemberAccess(Expression expression) + { + expression = UnwrapConvert(expression); + if (!(expression is MemberExpression member) || !IsMemberFromTypedParameter(member)) + throw new NotSupportedException(string.Format("Typed fluent member access is not supported: {0}", expression)); + + string mappedColumn = null; + var property = member.Member as PropertyInfo; + if (property != null) + { + var attrs = property.GetCustomAttributes(typeof(ColumnAttribute), true); + var 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() + ?? _mapper.ColumnsMap + .Where(x => string.Equals(x.Value.Name, member.Member.Name, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Key) + .FirstOrDefault() + ?? member.Member.Name; + } + string tablePrefix = GetRootAliasOrTableName(); + + string qualified = string.IsNullOrEmpty(tablePrefix) + ? mappedColumn + : string.Format("{0}.{1}", tablePrefix, mappedColumn); + + return FixObjectName(qualified); + } + private string ParseTypedSelectExpression(Expression expression) + { + expression = UnwrapConvert(expression); + + if (expression is MethodCallExpression call && IsAsCall(call)) + { + string left = ParseTypedMemberAccess(call.Object ?? call.Arguments.FirstOrDefault()); + var alias = EvaluateExpression(call.Arguments.Last()) == null ? null : EvaluateExpression(call.Arguments.Last()).ToString(); + alias = alias.Validated("Alias"); + + return string.Format("{0} AS {1}", left, Database.DecorateName(alias)); + } + return ParseTypedMemberAccess(expression); + } + private string GetRootAliasOrTableName() + { + var mappedTable = _mapper.Table == null || string.IsNullOrEmpty(_mapper.Table.Name) + ? _mapper.Type.Name + : _mapper.Table.Name; + + var table = Tables.FirstOrDefault(t => t.Name == mappedTable || t.Name == Database.StripName(mappedTable)); + if (table == null) + table = Tables.FirstOrDefault(); + + if (table == null) + return null; + + return string.IsNullOrEmpty(table.Alias) ? table.Name : table.Alias; + } + private static bool IsMemberFromTypedParameter(Expression expression) + { + var member = expression as MemberExpression; + if (member == null) + return false; + + var parameter = member.Expression as ParameterExpression; + return parameter != null && parameter.Type == typeof(T); + } + private static Expression UnwrapConvert(Expression expression) + { + while (expression is UnaryExpression unary && + (unary.NodeType == ExpressionType.Convert || unary.NodeType == ExpressionType.ConvertChecked)) + expression = unary.Operand; + + return expression; + } + private static bool IsNullConstant(Expression expression) + { + expression = UnwrapConvert(expression); + return expression is ConstantExpression constant && constant.Value == null; + } + private static string GetBinaryOperator(ExpressionType type, bool rightIsNull) + { + switch (type) + { + case ExpressionType.Equal: return rightIsNull ? "IS" : "="; + case ExpressionType.NotEqual: return rightIsNull ? "IS NOT" : "<>"; + case ExpressionType.GreaterThan: return ">"; + case ExpressionType.GreaterThanOrEqual: return ">="; + case ExpressionType.LessThan: return "<"; + case ExpressionType.LessThanOrEqual: return "<="; + default: throw new NotSupportedException(string.Format("Expression operation is not supported: {0}", type)); + } + } + private static bool IsEnumerableContains(MethodCallExpression call) + { + if (!string.Equals(call.Method.Name, "Contains", StringComparison.Ordinal)) + return false; + + if (call.Object != null && call.Arguments.Count == 1) + return true; + + return call.Object == null && call.Arguments.Count == 2; + } + private static bool IsAsCall(MethodCallExpression call) + { + return string.Equals(call.Method.Name, "As", StringComparison.Ordinal) + && (call.Arguments.Count == 1 || call.Arguments.Count == 2); + } + private static bool IsAscOrDesc(MethodCallExpression call) + { + string name = call.Method.Name.ToUpper(); + return (name == "ASC" || name == "DESC") + && (call.Arguments.Count == 0 || call.Arguments.Count == 1); + } + private static object EvaluateExpression(Expression expression) + { + var objectMember = Expression.Convert(expression, typeof(object)); + var getter = Expression.Lambda>(objectMember).Compile(); + return getter(); + } + } /// Update query builder. internal class DynamicUpdateQueryBuilder : DynamicModifyBuilder, IDynamicUpdateQueryBuilder, DynamicQueryBuilder.IQueryWithWhere { diff --git a/DynamORM.Tests/Helpers/TypedFluentUser.cs b/DynamORM.Tests/Helpers/TypedFluentUser.cs new file mode 100644 index 0000000..52cd8da --- /dev/null +++ b/DynamORM.Tests/Helpers/TypedFluentUser.cs @@ -0,0 +1,42 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. +*/ + +using DynamORM.Mapper; + +namespace DynamORM.Tests.Helpers +{ + [Table(Name = "sample_users")] + public class TypedFluentUser + { + [Column("id_user", true)] + public long Id { get; set; } + + [Column("user_code")] + public string Code { get; set; } + } +} diff --git a/DynamORM.Tests/Select/TypedFluentBuilderTests.cs b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs new file mode 100644 index 0000000..0d83954 --- /dev/null +++ b/DynamORM.Tests/Select/TypedFluentBuilderTests.cs @@ -0,0 +1,99 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. +*/ + +using System.Linq; +using DynamORM.Builders; +using DynamORM.Tests.Helpers; +using NUnit.Framework; + +namespace DynamORM.Tests.Select +{ + [TestFixture] + public class TypedFluentBuilderTests : TestsBase + { + [SetUp] + public void SetUp() + { + CreateTestDatabase(); + CreateDynamicDatabase( + DynamicDatabaseOptions.SingleConnection | + DynamicDatabaseOptions.SingleTransaction | + DynamicDatabaseOptions.SupportLimitOffset); + } + + [TearDown] + public void TearDown() + { + DestroyDynamicDatabase(); + DestroyTestDatabase(); + } + + [Test] + public void TestTypedWhereAndSelectUsesPropertyMap() + { + var cmd = Database.From("u") + .Where(u => u.Id == 1) + .SelectTyped(u => u.Code.As("CodeAlias")) + .OrderByTyped(u => u.Code.Desc()); + + Assert.AreEqual( + string.Format("SELECT u.\"user_code\" AS \"CodeAlias\" FROM \"sample_users\" AS u WHERE (u.\"id_user\" = [${0}]) ORDER BY u.\"user_code\" DESC", cmd.Parameters.Keys.First()), + cmd.CommandText()); + } + + [Test] + public void TestTypedWhereSupportsContains() + { + var ids = new[] { 1L, 2L, 3L }.ToList(); + + var cmd = Database.From("u") + .Where(u => ids.Contains(u.Id)) + .SelectTyped(u => u.Id); + + Assert.AreEqual( + string.Format("SELECT u.\"id_user\" FROM \"sample_users\" AS u WHERE (u.\"id_user\" IN([${0}], [${1}], [${2}]))", + cmd.Parameters.Keys.ElementAt(0), + cmd.Parameters.Keys.ElementAt(1), + cmd.Parameters.Keys.ElementAt(2)), + cmd.CommandText()); + } + + [Test] + public void TestTypedGroupByHavingOrderBy() + { + var cmd = Database.From("u") + .SelectTyped(u => u.Code) + .GroupByTyped(u => u.Code) + .Having(u => u.Code != null) + .OrderByTyped(u => u.Code.Asc()); + + 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()); + } + } +} diff --git a/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs new file mode 100644 index 0000000..2a7be96 --- /dev/null +++ b/DynamORM/Builders/IDynamicTypedSelectQueryBuilder.cs @@ -0,0 +1,69 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. +*/ + +using System; +using System.Linq.Expressions; + +namespace DynamORM.Builders +{ + /// Typed select query builder for mapped entities. + /// Mapped entity type. + public interface IDynamicTypedSelectQueryBuilder : IDynamicSelectQueryBuilder + { + /// Add typed where predicate using mapped properties. + /// Predicate to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Where(Expression> predicate); + + /// Add typed having predicate using mapped properties. + /// Predicate to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Having(Expression> predicate); + + /// Add typed selected columns using mapped properties. + /// Projection type. + /// Selector to parse. + /// Additional selectors to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder Select(Expression> selector, params Expression>[] selectors); + + /// Add typed group by columns using mapped properties. + /// Projection type. + /// Selector to parse. + /// Additional selectors to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder GroupBy(Expression> selector, params Expression>[] selectors); + + /// Add typed order by columns using mapped properties. Supports Asc()/Desc(). + /// Projection type. + /// Selector to parse. + /// Additional selectors to parse. + /// Builder instance. + IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors); + } +} diff --git a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs index 5bc86d3..387f54b 100644 --- a/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs +++ b/DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs @@ -42,7 +42,7 @@ using DynamORM.Mapper; namespace DynamORM.Builders.Implementation { /// Implementation of dynamic select query builder. - internal class DynamicSelectQueryBuilder : DynamicQueryBuilder, IDynamicSelectQueryBuilder, DynamicQueryBuilder.IQueryWithWhere, DynamicQueryBuilder.IQueryWithHaving + internal class DynamicSelectQueryBuilder : DynamicQueryBuilder, IDynamicSelectQueryBuilder, DynamicQueryBuilder.IQueryWithWhere, DynamicQueryBuilder.IQueryWithHaving { private int? _limit = null; private int? _offset = null; @@ -1452,4 +1452,4 @@ namespace DynamORM.Builders.Implementation #endregion IExtendedDisposable } -} \ No newline at end of file +} diff --git a/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs new file mode 100644 index 0000000..b615a93 --- /dev/null +++ b/DynamORM/Builders/Implementation/DynamicTypedSelectQueryBuilder.cs @@ -0,0 +1,517 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using DynamORM.Helpers; +using DynamORM.Mapper; + +namespace DynamORM.Builders.Implementation +{ + /// Typed wrapper over with property-to-column translation. + /// Mapped entity type. + internal class DynamicTypedSelectQueryBuilder : DynamicSelectQueryBuilder, IDynamicTypedSelectQueryBuilder + { + private readonly DynamicTypeMap _mapper; + + public DynamicTypedSelectQueryBuilder(DynamicDatabase db) + : base(db) + { + _mapper = DynamicMapperCache.GetMapper() + ?? throw new InvalidOperationException(string.Format("Cant assign unmapable type as a table ({0}).", typeof(T).FullName)); + } + + public IDynamicTypedSelectQueryBuilder Where(Expression> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = ParseTypedCondition(predicate.Body); + + if (string.IsNullOrEmpty(WhereCondition)) + WhereCondition = condition; + else + WhereCondition = string.Format("{0} AND {1}", WhereCondition, condition); + + return this; + } + + public new IDynamicTypedSelectQueryBuilder Join(params Func[] func) + { + base.Join(func); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Where(DynamicColumn column) + { + base.Where(column); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Where(string column, DynamicColumn.CompareOperator op, object value) + { + base.Where(column, op, value); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Where(string column, object value) + { + base.Where(column, value); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Where(object conditions, bool schema = false) + { + base.Where(conditions, schema); + return this; + } + + public IDynamicTypedSelectQueryBuilder Select(Expression> selector, params Expression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddSelectSelector(selector); + + if (selectors != null) + foreach (var s in selectors) + { + if (s == null) + throw new ArgumentNullException("selectors", "Array of selectors cannot contain null."); + + AddSelectSelector(s); + } + + return this; + } + + public IDynamicTypedSelectQueryBuilder GroupBy(Expression> selector, params Expression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddGroupBySelector(selector); + + if (selectors != null) + foreach (var s in selectors) + { + if (s == null) + throw new ArgumentNullException("selectors", "Array of selectors cannot contain null."); + + AddGroupBySelector(s); + } + + return this; + } + + public IDynamicTypedSelectQueryBuilder OrderBy(Expression> selector, params Expression>[] selectors) + { + if (selector == null) + throw new ArgumentNullException("selector"); + + AddOrderBySelector(selector); + + if (selectors != null) + foreach (var s in selectors) + { + if (s == null) + throw new ArgumentNullException("selectors", "Array of selectors cannot contain null."); + + AddOrderBySelector(s); + } + + return this; + } + + public new IDynamicTypedSelectQueryBuilder Top(int? top) + { + base.Top(top); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Limit(int? limit) + { + base.Limit(limit); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Offset(int? offset) + { + base.Offset(offset); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Distinct(bool distinct = true) + { + base.Distinct(distinct); + return this; + } + + public IDynamicTypedSelectQueryBuilder Having(Expression> predicate) + { + if (predicate == null) + throw new ArgumentNullException("predicate"); + + string condition = ParseTypedCondition(predicate.Body); + + if (string.IsNullOrEmpty(HavingCondition)) + HavingCondition = condition; + else + HavingCondition = string.Format("{0} AND {1}", HavingCondition, condition); + + return this; + } + + public new IDynamicTypedSelectQueryBuilder Having(DynamicColumn column) + { + base.Having(column); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Having(string column, DynamicColumn.CompareOperator op, object value) + { + base.Having(column, op, value); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Having(string column, object value) + { + base.Having(column, value); + return this; + } + + public new IDynamicTypedSelectQueryBuilder Having(object conditions, bool schema = false) + { + base.Having(conditions, schema); + return this; + } + + private void AddSelectSelector(Expression> selector) + { + var body = UnwrapConvert(selector.Body); + + if (body is NewExpression ne) + { + foreach (var argument in ne.Arguments) + { + var parsed = ParseTypedSelectExpression(argument); + ((IDynamicSelectQueryBuilder)this).Select(x => parsed); + } + } + else + { + var parsed = ParseTypedSelectExpression(body); + ((IDynamicSelectQueryBuilder)this).Select(x => parsed); + } + } + + private void AddGroupBySelector(Expression> selector) + { + var body = UnwrapConvert(selector.Body); + + if (body is NewExpression ne) + { + foreach (var argument in ne.Arguments) + { + var parsed = ParseTypedMemberAccess(argument); + ((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed); + } + } + else + { + var parsed = ParseTypedMemberAccess(body); + ((IDynamicSelectQueryBuilder)this).GroupBy(x => parsed); + } + } + + private void AddOrderBySelector(Expression> selector) + { + var body = UnwrapConvert(selector.Body); + bool ascending = true; + + if (body is MethodCallExpression call && IsAscOrDesc(call)) + { + ascending = call.Method.Name.ToUpper() != "DESC"; + body = UnwrapConvert(call.Object ?? call.Arguments.FirstOrDefault()); + } + + string main = ParseTypedMemberAccess(body); + string parsed = string.Format("{0} {1}", main, ascending ? "ASC" : "DESC"); + ((IDynamicSelectQueryBuilder)this).OrderBy(x => parsed); + } + + private string ParseTypedCondition(Expression expression) + { + expression = UnwrapConvert(expression); + + if (expression is BinaryExpression binary) + { + switch (binary.NodeType) + { + case ExpressionType.AndAlso: + case ExpressionType.And: + return string.Format("({0} AND {1})", ParseTypedCondition(binary.Left), ParseTypedCondition(binary.Right)); + case ExpressionType.OrElse: + case ExpressionType.Or: + return string.Format("({0} OR {1})", ParseTypedCondition(binary.Left), ParseTypedCondition(binary.Right)); + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + { + DynamicSchemaColumn? columnSchema = null; + string left = ParseTypedValue(binary.Left, ref columnSchema); + string right = ParseTypedValue(binary.Right, ref columnSchema); + 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})", ParseTypedCondition(unary.Operand)); + + if (expression is MethodCallExpression call && IsEnumerableContains(call)) + { + DynamicSchemaColumn? col = null; + return ParseTypedContains(call, ref col); + } + + throw new NotSupportedException(string.Format("Typed fluent where expression is not supported: {0}", expression)); + } + + private string ParseTypedValue(Expression expression, ref DynamicSchemaColumn? columnSchema) + { + expression = UnwrapConvert(expression); + + if (IsMemberFromTypedParameter(expression)) + { + string col = ParseTypedMemberAccess(expression); + columnSchema = GetColumnFromSchema(col); + return col; + } + + if (expression is MethodCallExpression call && IsEnumerableContains(call)) + return ParseTypedContains(call, ref columnSchema); + + object value = EvaluateExpression(expression); + return ParseConstant(value, Parameters, columnSchema); + } + + private string ParseTypedContains(MethodCallExpression call, ref DynamicSchemaColumn? columnSchema) + { + // Supports: list.Contains(x.Property) and Enumerable.Contains(list, x.Property) + Expression collection; + Expression candidate; + + if (call.Object != null) + { + collection = call.Object; + candidate = call.Arguments[0]; + } + else + { + collection = call.Arguments[0]; + candidate = call.Arguments[1]; + } + + candidate = UnwrapConvert(candidate); + if (!IsMemberFromTypedParameter(candidate)) + throw new NotSupportedException(string.Format("Typed Contains() must target a mapped member: {0}", call)); + + string left = ParseTypedMemberAccess(candidate); + columnSchema = GetColumnFromSchema(left); + + var values = EvaluateExpression(collection) as IEnumerable; + if (values == null && collection is MethodCallExpression implicitCall && + string.Equals(implicitCall.Method.Name, "op_Implicit", StringComparison.Ordinal) && + implicitCall.Arguments.Count > 0) + { + values = EvaluateExpression(implicitCall.Arguments[0]) as IEnumerable; + } + + if (values == null) + throw new NotSupportedException(string.Format("Typed Contains() source is not enumerable: {0}", call)); + + var inList = new List(); + foreach (var item in values.Cast()) + inList.Add(ParseConstant(item, Parameters, columnSchema)); + + if (!inList.Any()) + return "(1 = 0)"; + + return string.Format("({0} IN({1}))", left, string.Join(", ", inList)); + } + + private string ParseTypedMemberAccess(Expression expression) + { + expression = UnwrapConvert(expression); + if (!(expression is MemberExpression member) || !IsMemberFromTypedParameter(member)) + throw new NotSupportedException(string.Format("Typed fluent member access is not supported: {0}", expression)); + + string mappedColumn = null; + var property = member.Member as PropertyInfo; + if (property != null) + { + var attrs = property.GetCustomAttributes(typeof(ColumnAttribute), true); + var 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() + ?? _mapper.ColumnsMap + .Where(x => string.Equals(x.Value.Name, member.Member.Name, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Key) + .FirstOrDefault() + ?? member.Member.Name; + } + string tablePrefix = GetRootAliasOrTableName(); + + string qualified = string.IsNullOrEmpty(tablePrefix) + ? mappedColumn + : string.Format("{0}.{1}", tablePrefix, mappedColumn); + + return FixObjectName(qualified); + } + + private string ParseTypedSelectExpression(Expression expression) + { + expression = UnwrapConvert(expression); + + if (expression is MethodCallExpression call && IsAsCall(call)) + { + string left = ParseTypedMemberAccess(call.Object ?? call.Arguments.FirstOrDefault()); + var alias = EvaluateExpression(call.Arguments.Last()) == null ? null : EvaluateExpression(call.Arguments.Last()).ToString(); + alias = alias.Validated("Alias"); + + return string.Format("{0} AS {1}", left, Database.DecorateName(alias)); + } + + return ParseTypedMemberAccess(expression); + } + + private string GetRootAliasOrTableName() + { + var mappedTable = _mapper.Table == null || string.IsNullOrEmpty(_mapper.Table.Name) + ? _mapper.Type.Name + : _mapper.Table.Name; + + var table = Tables.FirstOrDefault(t => t.Name == mappedTable || t.Name == Database.StripName(mappedTable)); + if (table == null) + table = Tables.FirstOrDefault(); + + if (table == null) + return null; + + return string.IsNullOrEmpty(table.Alias) ? table.Name : table.Alias; + } + + private static bool IsMemberFromTypedParameter(Expression expression) + { + var member = expression as MemberExpression; + if (member == null) + return false; + + var parameter = member.Expression as ParameterExpression; + return parameter != null && parameter.Type == typeof(T); + } + + private static Expression UnwrapConvert(Expression expression) + { + while (expression is UnaryExpression unary && + (unary.NodeType == ExpressionType.Convert || unary.NodeType == ExpressionType.ConvertChecked)) + expression = unary.Operand; + + return expression; + } + + private static bool IsNullConstant(Expression expression) + { + expression = UnwrapConvert(expression); + return expression is ConstantExpression constant && constant.Value == null; + } + + private static string GetBinaryOperator(ExpressionType type, bool rightIsNull) + { + switch (type) + { + case ExpressionType.Equal: return rightIsNull ? "IS" : "="; + case ExpressionType.NotEqual: return rightIsNull ? "IS NOT" : "<>"; + case ExpressionType.GreaterThan: return ">"; + case ExpressionType.GreaterThanOrEqual: return ">="; + case ExpressionType.LessThan: return "<"; + case ExpressionType.LessThanOrEqual: return "<="; + default: throw new NotSupportedException(string.Format("Expression operation is not supported: {0}", type)); + } + } + + private static bool IsEnumerableContains(MethodCallExpression call) + { + if (!string.Equals(call.Method.Name, "Contains", StringComparison.Ordinal)) + return false; + + if (call.Object != null && call.Arguments.Count == 1) + return true; + + return call.Object == null && call.Arguments.Count == 2; + } + + private static bool IsAsCall(MethodCallExpression call) + { + return string.Equals(call.Method.Name, "As", StringComparison.Ordinal) + && (call.Arguments.Count == 1 || call.Arguments.Count == 2); + } + + private static bool IsAscOrDesc(MethodCallExpression call) + { + string name = call.Method.Name.ToUpper(); + return (name == "ASC" || name == "DESC") + && (call.Arguments.Count == 0 || call.Arguments.Count == 1); + } + + private static object EvaluateExpression(Expression expression) + { + var objectMember = Expression.Convert(expression, typeof(object)); + var getter = Expression.Lambda>(objectMember).Compile(); + return getter(); + } + } +} diff --git a/DynamORM/Builders/TypedFluentExtensions.cs b/DynamORM/Builders/TypedFluentExtensions.cs new file mode 100644 index 0000000..a79f331 --- /dev/null +++ b/DynamORM/Builders/TypedFluentExtensions.cs @@ -0,0 +1,79 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. +*/ + +namespace DynamORM.Builders +{ + /// Marker extensions for typed fluent builder expressions. + public static class TypedFluentExtensions + { + /// Typed select helper that avoids overload resolution with dynamic methods. + public static IDynamicTypedSelectQueryBuilder SelectTyped( + this IDynamicTypedSelectQueryBuilder builder, + System.Linq.Expressions.Expression> selector, + params System.Linq.Expressions.Expression>[] selectors) + { + return builder.Select(selector, selectors); + } + + /// Typed group by helper that avoids overload resolution with dynamic methods. + public static IDynamicTypedSelectQueryBuilder GroupByTyped( + this IDynamicTypedSelectQueryBuilder builder, + System.Linq.Expressions.Expression> selector, + params System.Linq.Expressions.Expression>[] selectors) + { + return builder.GroupBy(selector, selectors); + } + + /// Typed order by helper that avoids overload resolution with dynamic methods. + public static IDynamicTypedSelectQueryBuilder OrderByTyped( + this IDynamicTypedSelectQueryBuilder builder, + System.Linq.Expressions.Expression> selector, + params System.Linq.Expressions.Expression>[] selectors) + { + return builder.OrderBy(selector, selectors); + } + + /// Marks select projection alias in typed expressions. + public static T As(this T source, string alias) + { + return source; + } + + /// Marks ascending order in typed order expressions. + public static T Asc(this T source) + { + return source; + } + + /// Marks descending order in typed order expressions. + public static T Desc(this T source) + { + return source; + } + } +} diff --git a/DynamORM/DynamicDatabase.cs b/DynamORM/DynamicDatabase.cs index f627e4e..39695e1 100644 --- a/DynamORM/DynamicDatabase.cs +++ b/DynamORM/DynamicDatabase.cs @@ -453,24 +453,28 @@ namespace DynamORM /// Table alias. /// use no lock. /// This instance to permit chaining. - public virtual IDynamicSelectQueryBuilder From(string alias = null, bool noLock = false) - { - // TODO: Make it more readable and maitainable - if (noLock) - { - if (string.IsNullOrEmpty(alias)) - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T)).NoLock()); - else - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T)).As(alias).NoLock()); - } - else - { - if (string.IsNullOrEmpty(alias)) - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T))); - else - return new DynamicSelectQueryBuilder(this).From(x => x(typeof(T)).As(alias)); - } - } + public virtual IDynamicTypedSelectQueryBuilder From(string alias = null, bool noLock = false) + { + // TODO: Make it more readable and maitainable + DynamicTypedSelectQueryBuilder builder = new DynamicTypedSelectQueryBuilder(this); + + if (noLock) + { + if (string.IsNullOrEmpty(alias)) + builder.From(x => x(typeof(T)).NoLock()); + else + builder.From(x => x(typeof(T)).As(alias).NoLock()); + } + else + { + if (string.IsNullOrEmpty(alias)) + builder.From(x => x(typeof(T))); + else + builder.From(x => x(typeof(T)).As(alias)); + } + + return builder; + } /// Adds to the FROM clause using . /// Type which can be represented in database.