diff --git a/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs b/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs index f922a20..591b267 100644 --- a/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs +++ b/PoweredSoft.DynamicLinq.Dal/Configurations/Configurations.cs @@ -68,4 +68,22 @@ namespace PoweredSoft.DynamicLinq.Dal.Configurations HasRequired(t => t.Post).WithMany(t => t.Comments).HasForeignKey(t => t.PostId).WillCascadeOnDelete(false); } } + + public class CommentLikeConfiguration : EntityTypeConfiguration + { + public CommentLikeConfiguration() : this("dbo") + { + } + + public CommentLikeConfiguration(string schema) + { + ToTable("CommentLike", schema); + HasKey(t => t.Id); + Property(t => t.Id).HasColumnName("Id").HasColumnType("bigint").IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); + Property(t => t.CommentId).HasColumnName("CommentId").HasColumnType("bigint").IsRequired(); + Property(t => t.CreateTime).HasColumnName("CreateTime").HasColumnType("datetimeoffset").IsRequired(); + + HasRequired(t => t.Comment).WithMany(t => t.CommentLikes).HasForeignKey(t => t.CommentId).WillCascadeOnDelete(false); + } + } } diff --git a/PoweredSoft.DynamicLinq.Dal/Pocos/Comment.cs b/PoweredSoft.DynamicLinq.Dal/Pocos/Comment.cs index df79636..2d674e1 100644 --- a/PoweredSoft.DynamicLinq.Dal/Pocos/Comment.cs +++ b/PoweredSoft.DynamicLinq.Dal/Pocos/Comment.cs @@ -14,5 +14,6 @@ namespace PoweredSoft.DynamicLinq.Dal.Pocos public string Email { get; set; } public string CommentText { get; set; } public Post Post { get; set; } + public ICollection CommentLikes { get; set; } } } diff --git a/PoweredSoft.DynamicLinq.Dal/Pocos/CommentLike.cs b/PoweredSoft.DynamicLinq.Dal/Pocos/CommentLike.cs new file mode 100644 index 0000000..28d8253 --- /dev/null +++ b/PoweredSoft.DynamicLinq.Dal/Pocos/CommentLike.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 CommentLike + { + public long Id { get; set; } + public long CommentId { get; set; } + public DateTimeOffset CreateTime { get; set; } + + public virtual Comment Comment { get; set; } + } +} diff --git a/PoweredSoft.DynamicLinq.Dal/PoweredSoft.DynamicLinq.Dal.csproj b/PoweredSoft.DynamicLinq.Dal/PoweredSoft.DynamicLinq.Dal.csproj index 2dab09c..213bfe5 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.Test/SelectTests.cs b/PoweredSoft.DynamicLinq.Test/SelectTests.cs index edc82e1..ff9861e 100644 --- a/PoweredSoft.DynamicLinq.Test/SelectTests.cs +++ b/PoweredSoft.DynamicLinq.Test/SelectTests.cs @@ -80,8 +80,20 @@ namespace PoweredSoft.DynamicLinq.Test [TestMethod] public void SelectNullChecking() { - // TODO: null checking in select operations :D - throw new Exception("TODO"); + var query = TestData.Authors.AsQueryable(); + + query.Select(t => new + { + Comments = t.Posts == null ? null : t.Posts.SelectMany(t2 => t2.Comments).ToList() + }); + + var querySelect = query.Select(t => + { + t.NullChecking(true); + t.PathToList("Posts.Comments", selectCollectionHandling: SelectCollectionHandling.SelectMany); + }); + + var list = querySelect.ToDynamicClassList(); } } } diff --git a/PoweredSoft.DynamicLinq/Fluent/Select/SelectBuilder.cs b/PoweredSoft.DynamicLinq/Fluent/Select/SelectBuilder.cs index f1d7654..fc18dfe 100644 --- a/PoweredSoft.DynamicLinq/Fluent/Select/SelectBuilder.cs +++ b/PoweredSoft.DynamicLinq/Fluent/Select/SelectBuilder.cs @@ -20,13 +20,20 @@ namespace PoweredSoft.DynamicLinq.Fluent public Type DestinationType { get; set; } public bool Empty => Parts?.Count == 0; public IQueryable Query { get; protected set; } + public bool IsNullCheckingEnabled { get; protected set; } public SelectBuilder(IQueryable query) { Query = query; } - protected void throwIfUsedOrEmpty(string propertyName) + public SelectBuilder NullChecking(bool check = true) + { + IsNullCheckingEnabled = check; + return this; + } + + protected void ThrowIfUsedOrEmpty(string propertyName) { if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException($"{propertyName} cannot end up be empty."); @@ -40,7 +47,7 @@ namespace PoweredSoft.DynamicLinq.Fluent if (propertyName == null) propertyName = path.Split('.').LastOrDefault(); - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { @@ -57,7 +64,7 @@ namespace PoweredSoft.DynamicLinq.Fluent if (propertyName == null) propertyName = path.Split('.').LastOrDefault(); - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { @@ -71,7 +78,7 @@ namespace PoweredSoft.DynamicLinq.Fluent public SelectBuilder Count(string propertyName) { - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { PropertyName = propertyName, @@ -82,7 +89,7 @@ namespace PoweredSoft.DynamicLinq.Fluent public SelectBuilder LongCount(string propertyName) { - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { PropertyName = propertyName, @@ -96,7 +103,7 @@ namespace PoweredSoft.DynamicLinq.Fluent if (propertyName == null) propertyName = path.Split('.').LastOrDefault(); - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { @@ -112,7 +119,7 @@ namespace PoweredSoft.DynamicLinq.Fluent if (propertyName == null) propertyName = path.Split('.').LastOrDefault(); - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { @@ -125,7 +132,7 @@ namespace PoweredSoft.DynamicLinq.Fluent public SelectBuilder ToList(string propertyName) { - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { PropertyName = propertyName, @@ -139,7 +146,7 @@ namespace PoweredSoft.DynamicLinq.Fluent if (propertyName == null) propertyName = path.Split('.').LastOrDefault(); - throwIfUsedOrEmpty(propertyName); + ThrowIfUsedOrEmpty(propertyName); Parts.Add(new SelectPart { @@ -158,7 +165,7 @@ namespace PoweredSoft.DynamicLinq.Fluent throw new Exception("No select specified, please specify at least one select path"); var partsTuple = Parts.Select(t => (selectType: t.SelectType, propertyName: t.PropertyName, path: t.Path, selectCollectionHandling: t.SelectCollectionHandling)).ToList(); - return QueryableHelpers.Select(Query, partsTuple, DestinationType); + return QueryableHelpers.Select(Query, partsTuple, DestinationType, nullChecking: IsNullCheckingEnabled); } } } diff --git a/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs b/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs index 5389c30..352d639 100644 --- a/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs +++ b/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs @@ -118,7 +118,7 @@ namespace PoweredSoft.DynamicLinq.Helpers return result; } - public static IQueryable Select(IQueryable query, List<(SelectTypes selectType, string propertyName, string path, SelectCollectionHandling selectCollectionHandling)> parts, Type destinationType = null) + public static IQueryable Select(IQueryable query, List<(SelectTypes selectType, string propertyName, string path, SelectCollectionHandling selectCollectionHandling)> parts, Type destinationType = null, bool nullChecking = false) { // create parameter. var queryType = query.ElementType; @@ -129,7 +129,7 @@ namespace PoweredSoft.DynamicLinq.Helpers var partExpressions = new List<(Expression expression, string propertyName)>(); parts.ForEach(part => { - var partBodyExpression = CreateSelectExpression(query, parameter, part.selectType, part.path, part.selectCollectionHandling); + var partBodyExpression = CreateSelectExpression(query, parameter, part.selectType, part.path, part.selectCollectionHandling, nullChecking); fields.Add((partBodyExpression.Type, part.propertyName)); partExpressions.Add((partBodyExpression, part.propertyName)); }); @@ -146,7 +146,7 @@ namespace PoweredSoft.DynamicLinq.Helpers return result; } - private static Expression CreateSelectExpressionForGrouping(IQueryable query, ParameterExpression parameter, SelectTypes selectType, string path, SelectCollectionHandling selectCollectionHandling) + private static Expression CreateSelectExpressionForGrouping(IQueryable query, ParameterExpression parameter, SelectTypes selectType, string path, SelectCollectionHandling selectCollectionHandling, bool nullChecking) { if (selectType == SelectTypes.Key) { @@ -193,23 +193,23 @@ namespace PoweredSoft.DynamicLinq.Helpers throw new NotSupportedException($"unkown select type {selectType}"); } - private static Expression CreateSelectExpression(IQueryable query, ParameterExpression parameter, SelectTypes selectType, string path, SelectCollectionHandling selectCollectionHandling) + private static Expression CreateSelectExpression(IQueryable query, ParameterExpression parameter, SelectTypes selectType, string path, SelectCollectionHandling selectCollectionHandling, bool nullChecking) { if (!IsGrouping(query)) - return CreateSelectExpressionRegular(query, parameter, selectType, path, selectCollectionHandling); + return CreateSelectExpressionRegular(query, parameter, selectType, path, selectCollectionHandling, nullChecking); - return CreateSelectExpressionForGrouping(query, parameter, selectType, path, selectCollectionHandling); + return CreateSelectExpressionForGrouping(query, parameter, selectType, path, selectCollectionHandling, nullChecking); } - private static Expression CreateSelectExpressionRegular(IQueryable query, ParameterExpression parameter, SelectTypes selectType, string path, SelectCollectionHandling selectCollectionHandling) + private static Expression CreateSelectExpressionRegular(IQueryable query, ParameterExpression parameter, SelectTypes selectType, string path, SelectCollectionHandling selectCollectionHandling, bool nullChecking) { if (selectType == SelectTypes.Path) { - return ResolvePathForExpression(parameter, path, selectCollectionHandling); + return ResolvePathForExpression(parameter, path, selectCollectionHandling, nullChecking); } else if (selectType == SelectTypes.PathToList) { - var expr = ResolvePathForExpression(parameter, path, selectCollectionHandling); + var expr = ResolvePathForExpression(parameter, path, selectCollectionHandling, nullChecking); var notGroupedType = expr.Type.GenericTypeArguments.FirstOrDefault(); if (notGroupedType == null) throw new Exception($"Path must be a Enumerable but its a {expr.Type}"); @@ -241,7 +241,7 @@ namespace PoweredSoft.DynamicLinq.Helpers return result; } - internal static Expression InternalResolvePathExpression(ParameterExpression param, List parts, SelectCollectionHandling selectCollectionHandling) + internal static Expression InternalResolvePathExpression(int step, ParameterExpression param, List parts, SelectCollectionHandling selectCollectionHandling, bool nullChecking) { var isLast = parts.Count == 1; var currentPart = parts.First(); @@ -251,21 +251,41 @@ namespace PoweredSoft.DynamicLinq.Helpers if (isLast) return memberExpression; + Expression ret = null; + if (!IsEnumerable(memberExpression)) - return InternalResolvePathExpression(param, parts.Skip(1).ToList(), selectCollectionHandling); + { + // TODO: null checking here too. + // should be easier then collection :=| - // enumerable. - var listGenericArgumentType = memberExpression.Type.GetGenericArguments().First(); + ret = InternalResolvePathExpression(step + 1, param, parts.Skip(1).ToList(), selectCollectionHandling, nullChecking); + } + else + { + // enumerable. + var listGenericArgumentType = memberExpression.Type.GetGenericArguments().First(); - // sub param. - var innerParam = Expression.Parameter(listGenericArgumentType); - var innerExpression = InternalResolvePathExpression(innerParam, parts.Skip(1).ToList(), selectCollectionHandling); - var lambda = Expression.Lambda(innerExpression, innerParam); + // sub param. + var innerParam = Expression.Parameter(listGenericArgumentType); + var innerExpression = InternalResolvePathExpression(step + 1, innerParam, parts.Skip(1).ToList(), selectCollectionHandling, nullChecking); + var lambda = Expression.Lambda(innerExpression, innerParam); - if (selectCollectionHandling == SelectCollectionHandling.Select) - return Expression.Call(typeof(Enumerable), "Select", new Type[] { listGenericArgumentType, innerExpression.Type }, memberExpression, lambda); + if (selectCollectionHandling == SelectCollectionHandling.Select) + ret = Expression.Call(typeof(Enumerable), "Select", new Type[] { listGenericArgumentType, innerExpression.Type }, memberExpression, lambda); + else + ret = Expression.Call(typeof(Enumerable), "SelectMany", new Type[] { listGenericArgumentType, innerExpression.Type.GenericTypeArguments.First() }, memberExpression, lambda); + } - return Expression.Call(typeof(Enumerable), "SelectMany", new Type[] { listGenericArgumentType, innerExpression.Type.GenericTypeArguments.First() }, memberExpression, lambda); + if (nullChecking) + { + ret = Expression.Condition( + Expression.Equal(memberExpression, Expression.Constant(null)), + Expression.Constant(null, ret.Type), + ret + ); + } + + return ret; } /// @@ -274,10 +294,10 @@ namespace PoweredSoft.DynamicLinq.Helpers /// Expression.Parameter(typeOfClassOrInterface) /// the path you wish to resolve example Contact.Profile.FirstName /// - public static Expression ResolvePathForExpression(ParameterExpression param, string path, SelectCollectionHandling selectCollectionHandling = SelectCollectionHandling.Select) + public static Expression ResolvePathForExpression(ParameterExpression param, string path, SelectCollectionHandling selectCollectionHandling = SelectCollectionHandling.Select, bool nullChecking = false) { var parts = path.Split('.').ToList(); - return InternalResolvePathExpression(param, parts, selectCollectionHandling); + return InternalResolvePathExpression(1, param, parts, selectCollectionHandling, nullChecking); } public static ConstantExpression GetConstantSameAsLeftOperator(Expression member, object value)