diff --git a/PoweredSoft.DynamicLinq.ConsoleApp/BetterProto2.cs b/PoweredSoft.DynamicLinq.ConsoleApp/BetterProto2.cs index 0ae2ab3..999cf90 100644 --- a/PoweredSoft.DynamicLinq.ConsoleApp/BetterProto2.cs +++ b/PoweredSoft.DynamicLinq.ConsoleApp/BetterProto2.cs @@ -1,5 +1,6 @@ using PoweredSoft.DynamicLinq.Dal.Pocos; using PoweredSoft.DynamicLinq.Helpers; +using PoweredSoft.DynamicLinq.Parser; using System; using System.Collections.Generic; using System.Linq; @@ -44,7 +45,12 @@ namespace PoweredSoft.DynamicLinq.ConsoleApp public static void Case2() { // the expression parser. - var ep = new ExpressionParser(typeof(Post), "Author.Posts.Author.Posts.Author.FirstName"); + var ep = new ExpressionParser(typeof(Author), "Posts.Author.Posts.Author.Website.Url"); + + new List().AsQueryable().Select(t => new + { + A = t.Posts.Select(t2 => t2.Author.Posts.Select(t3 => t3.Author.Website.Url)) + }); new List().AsQueryable().Select(t => new { @@ -58,7 +64,7 @@ namespace PoweredSoft.DynamicLinq.ConsoleApp // the builder. var per = new PathExpressionResolver(ep); - per.NullChecking = SelectNullHandling.New; + per.NullChecking = SelectNullHandling.Handle; per.Resolve(); // the result expression. @@ -66,20 +72,14 @@ namespace PoweredSoft.DynamicLinq.ConsoleApp } } - public class ExpressionParserPiece - { - public Type Type { get; set; } - public bool IsGenericEnumerable { get; set; } - public Type EnumerableType { get; set; } - public ExpressionParserPiece Parent { get; set; } - public string Name { get; internal set; } - } + public class PathExpressionResolver { public SelectNullHandling NullChecking { get; set; } = SelectNullHandling.LeaveAsIs; public SelectCollectionHandling CollectionHandling { get; set; } = SelectCollectionHandling.LeaveAsIs; public ExpressionParser Parser { get; protected set; } + public Expression Result { get; protected set; } public PathExpressionResolver(ExpressionParser parser) @@ -94,105 +94,86 @@ namespace PoweredSoft.DynamicLinq.ConsoleApp // parse the expression. Parser.Parse(); - // reverse foreach - var last = Parser.Pieces.Last(); + // group the piece by common parameters + var groups = Parser.GroupBySharedParameters(); - // reverse recursive. - Result = RecursiveSelect(last); + // compiled lambdas. + var groupLambdas = groups.Select(group => group.CompileGroup(NullChecking)).ToList(); } - protected Expression RecursiveSelect(ExpressionParserPiece piece) + protected Expression RecursiveSelect(List pieces) + { + // get the last. + var piece = pieces.Last(); + + var firstEnumerableParent = ExpressionParser.GetFirstEnumerableParent(piece); + var chainTillEnumerable = pieces.SkipWhile(t => t != firstEnumerableParent).Skip(1).ToList(); + if (chainTillEnumerable.Count == 0) + { + if (piece.Parent == null) + { + // no parent. + var mostLeftExpression = Expression.PropertyOrField(Parser.Parameter, piece.Name); + return mostLeftExpression; + } + else + { + // we have a parent its probably a enumerable. + var collectionParentParameter = Expression.Parameter(piece.Parent.EnumerableType); + var memberOf = Expression.PropertyOrField(collectionParentParameter, piece.Name); + } + } + else + { + // we have a simple chain to resolve. + var chainTillEnumerableParameter = ResolveParameter(chainTillEnumerable); + var chainTillEnumerableExpression = BuildSimpleChainExpression(chainTillEnumerable, chainTillEnumerableParameter); + var chainTillEnumerableLambda = Expression.Lambda(chainTillEnumerableExpression, chainTillEnumerableParameter); + + // get parent to glue with. + var nextList = pieces.Take(pieces.IndexOf(firstEnumerableParent)+1).ToList(); + var parent = RecursiveSelect(nextList); + + // glue. + var gluedResult = Expression.Call(typeof(Enumerable), + "Select", + new Type[] { firstEnumerableParent.EnumerableType, chainTillEnumerableExpression.Type }, + parent, chainTillEnumerableLambda); + + return gluedResult; + } + + throw null; + + } + + private ExpressionParserPiece GetFirstEnumerableParent(ExpressionParserPiece piece) { if (piece.Parent == null) - { - var parameter = Parser.Parameter; - var me = Expression.PropertyOrField(Parser.Parameter, piece.Name); - return me; - } - else if (piece.Parent.IsGenericEnumerable) - { - var indexOfParentPiece = Parser.Pieces.IndexOf(piece); - var currentSimpleChain = Parser.Pieces.Skip(indexOfParentPiece).TakeWhile(t => !t.IsGenericEnumerable).ToList(); - var parameter = ResolveParameter(currentSimpleChain); - var currentSimpleChainExpression = BuildSimpleChainExpression(currentSimpleChain, parameter); - var currentSimpleChainExpressionLambda = Expression.Lambda(currentSimpleChainExpression, parameter); + return null; - // the left before the parent. - var left = RecursiveSelect(piece.Parent); + if (piece.Parent.IsGenericEnumerable) + return piece.Parent; - // the parent. - var parentExpression = Expression.PropertyOrField(left, piece.Parent.Name) as Expression; - - if (NullChecking != SelectNullHandling.LeaveAsIs) - { - var nullCheckParameter = ResolveParameter(currentSimpleChain); - var nullCheckConditionExpression = BuildNullCheckConditionExpression(currentSimpleChain, nullCheckParameter); - var whereExpression = Expression.Call(typeof(Enumerable), "Where", new[] { piece.Parent.EnumerableType }, parentExpression, nullCheckConditionExpression); - parentExpression = whereExpression as Expression; - } - - // select. - var gluedTogetherExpression = Expression.Call(typeof(Enumerable), - "Select", - new Type[] { piece.Parent.EnumerableType, currentSimpleChainExpression.Type }, - parentExpression, currentSimpleChainExpressionLambda); - - // add current to parent? - return gluedTogetherExpression; - } - - // skip. - return RecursiveSelect(piece.Parent); + return GetFirstEnumerableParent(piece.Parent); } - /* - private void ResolveNullCheckingRightType() + private Expression NullCheckTernary(Expression left, Expression right, ParameterExpression parameter) { - if (NullChecking == SelectNullHandling.LeaveAsIs) - return; - - // last piece. var lastPiece = Parser.Pieces.Last(); - var anyCollections = Parser.Pieces.Any(t => t.IsGenericEnumerable); + var lastPieceType = !lastPiece.IsGenericEnumerable ? lastPiece.Type : lastPiece.EnumerableType; - Type subjectType = null; - if (anyCollections) - subjectType = typeof(List<>).MakeGenericType(lastPiece.EnumerableType); - else - subjectType = lastPiece.Type; + var escapeType = typeof(List<>).MakeGenericType(lastPieceType); + var typeMatch = typeof(IEnumerable<>).MakeGenericType(lastPieceType); + var ifTrueExpression = Expression.New(escapeType); + var testExpression = Expression.Equal(left, Expression.Constant(null)); - if (NullChecking == SelectNullHandling.Default) - NullCheckValueExpression = Expression.Default(subjectType); - else - NullCheckValueExpression = Expression.New(subjectType); - }*/ - - private void HandleCollection(ExpressionParserPiece piece, int index) - { - // simple parent. - - // the current simple chain. - var currentSimpleChain = Parser.Pieces.Skip(index - 1).Where(t => !t.IsGenericEnumerable).ToList(); - - // the parameter being used to select. - var parameter = ResolveParameter(currentSimpleChain); - - // this is the simple chain to select. - var currentSimpleChainExpression = BuildSimpleChainExpression(currentSimpleChain, parameter); - - // add where :) - if (NullChecking != SelectNullHandling.LeaveAsIs) - { - var nullCheckParameter = ResolveParameter(currentSimpleChain); - var nullCheckWhereForChain = BuildNullCheckConditionExpression(currentSimpleChain, nullCheckParameter); - } - else - { - Result = currentSimpleChainExpression; - } + var condition = Expression.Condition(testExpression, ifTrueExpression, right, typeMatch); + var lambda = Expression.Lambda(condition, parameter); + return lambda; } - private Expression BuildNullCheckConditionExpression(List currentSimpleChain, ParameterExpression parameter) + private Expression BuildNullCheckConditionExpressionForWhere(List currentSimpleChain, ParameterExpression parameter) { var path = string.Join(".", currentSimpleChain.Select(t => t.Name).Take(currentSimpleChain.Count - 1)); var where = QueryableHelpers.CreateConditionExpression(parameter.Type, path, ConditionOperators.NotEqual, null, QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, @@ -212,10 +193,10 @@ namespace PoweredSoft.DynamicLinq.ConsoleApp throw new NotSupportedException(); } - private Expression BuildSimpleChainExpression(List currentSimpleChain, ParameterExpression parameter) + private Expression BuildSimpleChainExpression(List chain, ParameterExpression parameter) { var ret = parameter as Expression; - currentSimpleChain.ForEach(p => + chain.ForEach(p => { var me = Expression.PropertyOrField(ret, p.Name); ret = me; @@ -223,92 +204,5 @@ namespace PoweredSoft.DynamicLinq.ConsoleApp return ret; } - - /* - public void HandleNoPreviousResultCollection(ExpressionParserPiece piece, int index) - { - MethodCallExpression result = null; - - // create the lambda. - var lambda = ResolveLambda(piece); - - if (CollectionHandling == SelectCollectionHandling.LeaveAsIs || !piece.IsGenericEnumerable) - result = Expression.Call(typeof(Enumerable), - "Select", - new Type[] { piece.Parent.EnumerableType, piece.MemberExpression.Type }, - piece.Parent.MemberExpression, lambda); - else - result = Expression.Call(typeof(Enumerable), - "SelectMany", - new Type[] { piece.Parent.EnumerableType, piece.EnumerableType }, - piece.Parent.MemberExpression, lambda); - - Result = result; - } - - public void HandlePreviousResultCollection(ExpressionParserPiece piece, int index) - { - var pieceParameter = ResolvePieceParameter(piece); - - MethodCallExpression result = null; - var lambda = Expression.Lambda(Result, ResolvePieceParameter(piece)); - if (CollectionHandling == SelectCollectionHandling.LeaveAsIs) - result = Expression.Call(typeof(Enumerable), "Select", - new Type[] { piece.Parent.EnumerableType, Result.Type }, - piece.Parent.MemberExpression, lambda); - else - result = Expression.Call(typeof(Enumerable), - "SelectMany", - new Type[] { piece.Parent.EnumerableType, Result.Type.GenericTypeArguments.First() }, - piece.Parent.MemberExpression, lambda); - - Result = result; - }*/ - } - - public class ExpressionParser - { - public ParameterExpression Parameter { get; protected set; } - public string Path { get; set; } - public List Pieces { get; set; } = new List(); - - public ExpressionParser(Type type, string path) : this(Expression.Parameter(type), path) - { - - } - - public ExpressionParser(ParameterExpression parameter, string path) - { - Parameter = parameter; - Path = path; - } - - public void Parse() - { - Pieces.Clear(); - - var pathPieces = Path.Split('.').ToList(); - var param = Parameter; - ExpressionParserPiece parent = null; - - pathPieces.ForEach(pp => - { - var memberExpression = Expression.PropertyOrField(param, pp); - var current = new ExpressionParserPiece - { - Type = memberExpression.Type, - IsGenericEnumerable = QueryableHelpers.IsGenericEnumerable(memberExpression), - EnumerableType = memberExpression.Type.GenericTypeArguments.FirstOrDefault(), - Parent = parent, - Name = pp - }; - - Pieces.Add(current); - - // for next iteration. - param = Expression.Parameter(current.IsGenericEnumerable ? current.EnumerableType : current.Type); - parent = current; - }); - } } } diff --git a/PoweredSoft.DynamicLinq.Dal/BlogContext.cs b/PoweredSoft.DynamicLinq.Dal/BlogContext.cs index 3328646..6090b12 100644 --- a/PoweredSoft.DynamicLinq.Dal/BlogContext.cs +++ b/PoweredSoft.DynamicLinq.Dal/BlogContext.cs @@ -36,6 +36,7 @@ namespace PoweredSoft.DynamicLinq.Dal modelBuilder.Configurations.Add(new AuthorConfiguration()); modelBuilder.Configurations.Add(new CommentConfiguration()); modelBuilder.Configurations.Add(new PostConfiguration()); + modelBuilder.Configurations.Add(new WebsiteConfiguration()); } } } diff --git a/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs b/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs index 591b267..6158ab6 100644 --- a/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs +++ b/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs @@ -23,6 +23,8 @@ namespace PoweredSoft.DynamicLinq.Dal.Configurations Property(t => t.Id).HasColumnName("Id").HasColumnType("bigint").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); Property(t => t.FirstName).HasColumnType("nvarchar").HasMaxLength(50).IsRequired(); Property(t => t.LastName).HasColumnType("nvarchar").HasMaxLength(50).IsRequired(); + + HasOptional(t => t.Website).WithMany(t => t.Authors).HasForeignKey(t => t.WebsiteId).WillCascadeOnDelete(false); } } @@ -86,4 +88,21 @@ namespace PoweredSoft.DynamicLinq.Dal.Configurations HasRequired(t => t.Comment).WithMany(t => t.CommentLikes).HasForeignKey(t => t.CommentId).WillCascadeOnDelete(false); } } + + public class WebsiteConfiguration : EntityTypeConfiguration + { + public WebsiteConfiguration() : this("dbo") + { + + } + + public WebsiteConfiguration(string schema) + { + ToTable("Website", schema); + HasKey(t => t.Id); + Property(t => t.Id).HasColumnName("Id").HasColumnType("bigint").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); + Property(t => t.Title).HasColumnName("Title").HasColumnType("nvarchar").HasMaxLength(100).IsRequired(); + Property(t => t.Url).HasColumnName("Url").HasColumnType("nvarchar").HasMaxLength(255).IsRequired(); + } + } } diff --git a/PoweredSoft.DynamicLinq.Dal/Pocos/Author.cs b/PoweredSoft.DynamicLinq.Dal/Pocos/Author.cs index 1ae9114..8b77e6f 100644 --- a/PoweredSoft.DynamicLinq.Dal/Pocos/Author.cs +++ b/PoweredSoft.DynamicLinq.Dal/Pocos/Author.cs @@ -11,7 +11,9 @@ namespace PoweredSoft.DynamicLinq.Dal.Pocos public long Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } + public long? WebsiteId { get; set; } + public virtual Website Website { get; set; } public ICollection Posts { get; set; } } } diff --git a/PoweredSoft.DynamicLinq.Dal/Pocos/Website.cs b/PoweredSoft.DynamicLinq.Dal/Pocos/Website.cs new file mode 100644 index 0000000..452d0f1 --- /dev/null +++ b/PoweredSoft.DynamicLinq.Dal/Pocos/Website.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PoweredSoft.DynamicLinq.Dal.Pocos +{ + public class Website + { + public long Id { get; set; } + public string Url { get; set; } + public string Title { get; set; } + + public ICollection Authors { get; set; } + } +} diff --git a/PoweredSoft.DynamicLinq.Dal/PoweredSoft.DynamicLinq.Dal.csproj b/PoweredSoft.DynamicLinq.Dal/PoweredSoft.DynamicLinq.Dal.csproj index 213bfe5..042f01e 100644 --- a/PoweredSoft.DynamicLinq.Dal/PoweredSoft.DynamicLinq.Dal.csproj +++ b/PoweredSoft.DynamicLinq.Dal/PoweredSoft.DynamicLinq.Dal.csproj @@ -50,6 +50,7 @@ + diff --git a/PoweredSoft.DynamicLinq/Constants.cs b/PoweredSoft.DynamicLinq/Constants.cs index 6187aa0..d8da956 100644 --- a/PoweredSoft.DynamicLinq/Constants.cs +++ b/PoweredSoft.DynamicLinq/Constants.cs @@ -62,8 +62,7 @@ namespace PoweredSoft.DynamicLinq public enum SelectNullHandling { LeaveAsIs, - Default, - New + Handle } internal static class Constants diff --git a/PoweredSoft.DynamicLinq/Parser/ExpressionParser.cs b/PoweredSoft.DynamicLinq/Parser/ExpressionParser.cs new file mode 100644 index 0000000..9629b4f --- /dev/null +++ b/PoweredSoft.DynamicLinq/Parser/ExpressionParser.cs @@ -0,0 +1,88 @@ +using PoweredSoft.DynamicLinq.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace PoweredSoft.DynamicLinq.Parser +{ + public class ExpressionParser + { + public ParameterExpression Parameter { get; protected set; } + public string Path { get; set; } + public List Pieces { get; set; } = new List(); + + public ExpressionParser(Type type, string path) : this(Expression.Parameter(type), path) + { + + } + + public ExpressionParser(ParameterExpression parameter, string path) + { + Parameter = parameter; + Path = path; + } + + public static ExpressionParserPiece GetFirstEnumerableParent(ExpressionParserPiece piece) + { + if (piece.Parent == null) + return null; + + if (piece.Parent.IsGenericEnumerable) + return piece.Parent; + + return GetFirstEnumerableParent(piece.Parent); + } + + public void Parse() + { + Pieces.Clear(); + + var pathPieces = Path.Split('.').ToList(); + var param = Parameter; + ExpressionParserPiece parent = null; + + pathPieces.ForEach(pp => + { + var memberExpression = Expression.PropertyOrField(param, pp); + var current = new ExpressionParserPiece + { + Type = memberExpression.Type, + IsGenericEnumerable = QueryableHelpers.IsGenericEnumerable(memberExpression), + EnumerableType = memberExpression.Type.GenericTypeArguments.FirstOrDefault(), + Parent = parent, + Name = pp + }; + + Pieces.Add(current); + + // for next iteration. + param = Expression.Parameter(current.IsGenericEnumerable ? current.EnumerableType : current.Type); + parent = current; + }); + } + + private ExpressionParserPieceGroup CreateAndAddGroup(List groups, ParameterExpression parameter) + { + var group = new ExpressionParserPieceGroup(); + group.ParameterExpression = parameter; + groups.Add(group); + return group; + } + + public List GroupBySharedParameters() + { + var groups = new List(); + + var group = CreateAndAddGroup(groups, Parameter); + Pieces.ForEach(piece => + { + group.Pieces.Add(piece); + if (piece.IsGenericEnumerable) + group = CreateAndAddGroup(groups, Expression.Parameter(piece.EnumerableType)); + }); + + return groups; + } + } +} diff --git a/PoweredSoft.DynamicLinq/Parser/ExpressionParserPiece.cs b/PoweredSoft.DynamicLinq/Parser/ExpressionParserPiece.cs new file mode 100644 index 0000000..61037b7 --- /dev/null +++ b/PoweredSoft.DynamicLinq/Parser/ExpressionParserPiece.cs @@ -0,0 +1,18 @@ +using System; + +namespace PoweredSoft.DynamicLinq.Parser +{ + public class ExpressionParserPiece + { + public Type Type { get; set; } + public bool IsGenericEnumerable { get; set; } + public Type EnumerableType { get; set; } + public ExpressionParserPiece Parent { get; set; } + public string Name { get; internal set; } + +#if DEBUG + public string DebugPath => $"{Type?.Name} {Name} -> {Parent?.DebugPath ?? "ROOT PARAMETER"}"; + public override string ToString() => $"{Type?.Name} {Name}"; +#endif + } +} diff --git a/PoweredSoft.DynamicLinq/Parser/ExpressionParserPieceGroup.cs b/PoweredSoft.DynamicLinq/Parser/ExpressionParserPieceGroup.cs new file mode 100644 index 0000000..73f349e --- /dev/null +++ b/PoweredSoft.DynamicLinq/Parser/ExpressionParserPieceGroup.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; + +namespace PoweredSoft.DynamicLinq.Parser +{ + public class ExpressionParserPieceGroup + { + public List Pieces { get; set; } = new List(); + public ParameterExpression ParameterExpression { get; set; } + +#if DEBUG + public override string ToString() => $"{ParameterExpression?.ToString()} is {ParameterExpression?.Type} | {(Pieces == null ? "" : string.Join(" -> ", Pieces.Select(t2 => t2.ToString())))}"; + + public object CompileSimpleExpress(SelectNullHandling nullHandling) + { + throw new NotImplementedException(); + } +#endif + } +} diff --git a/PoweredSoft.DynamicLinq/Parser/ParserExtensions.cs b/PoweredSoft.DynamicLinq/Parser/ParserExtensions.cs new file mode 100644 index 0000000..8c377f9 --- /dev/null +++ b/PoweredSoft.DynamicLinq/Parser/ParserExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; + +namespace PoweredSoft.DynamicLinq.Parser +{ + public static class ParserExtensions + { + public static ExpressionParserPiece FirstEnumerableParent(this ExpressionParserPiece piece) + { + var result = ExpressionParser.GetFirstEnumerableParent(piece); + return result; + } + + public static LambdaExpression CompileGroup(this ExpressionParserPieceGroup group, SelectNullHandling NullHandling) + { + var expr = group.ParameterExpression as Expression; + group.Pieces.ForEach(piece => + { + expr = Expression.PropertyOrField(expr, piece.Name); + }); + + var ret = Expression.Lambda(expr, group.ParameterExpression); + return ret; + } + } +}