Bläddra i källkod

- support ToListAsync
- support ToExecuteString
- support FirstOrDefault

Lith 4 månader sedan
förälder
incheckning
1a18211644
25 ändrade filer med 376 tillägg och 225 borttagningar
  1. 3 0
      doc/ReleaseNotes.md
  2. 1 17
      src/Vitorm.MongoDB/DbSet.Query.cs
  3. 0 1
      src/Vitorm.MongoDB/EntityReader/ModelReader.cs
  4. 0 1
      src/Vitorm.MongoDB/EntityReader/ValueReader.cs
  5. 5 9
      src/Vitorm.MongoDB/QueryExecutor/Async/ToListAsync.cs
  6. 51 0
      src/Vitorm.MongoDB/QueryExecutor/QueryExecutorArgument.ReverseOrder.cs
  7. 3 10
      src/Vitorm.MongoDB/QueryExecutor/QueryExecutorArgument.cs
  8. 1 3
      src/Vitorm.MongoDB/QueryExecutor/Sync/Count.cs
  9. 10 137
      src/Vitorm.MongoDB/QueryExecutor/Sync/FirstOrDefault.cs
  10. 2 8
      src/Vitorm.MongoDB/QueryExecutor/Sync/ToExecuteString.cs
  11. 5 5
      src/Vitorm.MongoDB/QueryExecutor/Sync/ToList.cs
  12. 3 6
      src/Vitorm.MongoDB/QueryExecutor/TranslateService.cs
  13. 83 0
      src/Vitorm.MongoDB/SearchExecutor/GroupExecutor.FirstOrDefault.cs
  14. 0 8
      src/Vitorm.MongoDB/SearchExecutor/GroupExecutor.ToList.cs
  15. 0 2
      src/Vitorm.MongoDB/SearchExecutor/GroupExecutor.cs
  16. 2 0
      src/Vitorm.MongoDB/SearchExecutor/ISearchExecutor.cs
  17. 72 0
      src/Vitorm.MongoDB/SearchExecutor/PlainDistinctExecutor.FirstOrDefault.cs
  18. 1 1
      src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.Count.cs
  19. 69 0
      src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.FirstOrDefault.cs
  20. 1 1
      src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.ToExecuteString.cs
  21. 3 6
      src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.ToList.cs
  22. 1 2
      src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.cs
  23. 4 4
      test/Vitorm.MongoDB.Data.MsTest/CommonTest/Common_Test.cs
  24. 4 4
      test/Vitorm.MongoDB.MsTest/CommonTest/Query_Method_Test.cs
  25. 52 0
      test/Vitorm.MongoDB.MsTest/CustomTest/Query_Method_Test.cs

+ 3 - 0
doc/ReleaseNotes.md

@@ -7,4 +7,7 @@
 - support group query
 - support PlainDistinctSearch
 - support Count
+- support ToListAsync
+- support ToExecuteString
+- support FirstOrDefault
 

+ 1 - 17
src/Vitorm.MongoDB/DbSet.Query.cs

@@ -19,21 +19,6 @@ namespace Vitorm.MongoDB
             return QueryableBuilder.Build<Entity>(QueryExecutor, DbContext.dbConfig.dbGroupName);
         }
 
-        protected object QueryExecutor(Expression expression, Type expressionResultType)
-        {
-            object result = null;
-            Action dispose = null;
-            try
-            {
-                return result = ExecuteQuery(expression, expressionResultType, dispose);
-            }
-            catch
-            {
-                dispose?.Invoke();
-                throw;
-            }
-        }
-
 
         #region QueryExecutor
 
@@ -114,7 +99,7 @@ namespace Vitorm.MongoDB
             return false;
         }
 
-        protected virtual object ExecuteQuery(Expression expression, Type expressionResultType, Action dispose)
+        protected virtual object QueryExecutor(Expression expression, Type expressionResultType)
         {
             // #1 convert to ExpressionNode 
             ExpressionNode_Lambda node = DbContext.convertService.ConvertToData_LambdaNode(expression, autoReduce: true, isArgument: QueryIsFromSameDb);
@@ -133,7 +118,6 @@ namespace Vitorm.MongoDB
                 dbContext = DbContext,
                 expression = expression,
                 expressionResultType = expressionResultType,
-                dispose = dispose,
             };
 
 

+ 0 - 1
src/Vitorm.MongoDB/EntityReader/ModelReader.cs

@@ -6,7 +6,6 @@ using Vitorm.Entity.PropertyType;
 
 namespace Vitorm.MongoDB.EntityReader
 {
-
     class ModelReader : IArgReader
     {
         public string argName { get; set; }

+ 0 - 1
src/Vitorm.MongoDB/EntityReader/ValueReader.cs

@@ -4,7 +4,6 @@ using MongoDB.Bson;
 
 namespace Vitorm.MongoDB.EntityReader
 {
-
     class ValueReader : IArgReader
     {
         public string argName { get; set; }

+ 5 - 9
src/Vitorm.MongoDB/QueryExecutor/Async/ToListAsync.cs

@@ -18,27 +18,23 @@ namespace Vitorm.MongoDB.QueryExecutor
 
         public string methodName => nameof(Queryable_Extensions.ToListAsync);
 
-        public object ExecuteQuery(QueryExecutorArgument execArg)
+        public object ExecuteQuery(QueryExecutorArgument arg)
         {
-            // #1
-            CombinedStream combinedStream = execArg.combinedStream;
-            var dbContext = execArg.dbContext;
-
 
             IQueryable query = null;
-            if (combinedStream.source is SourceStream sourceStream)
+            if (arg.combinedStream.source is SourceStream sourceStream)
             {
                 query = sourceStream.GetSource() as IQueryable;
             }
-            else if (combinedStream.source is CombinedStream baseStream)
+            else if (arg.combinedStream.source is CombinedStream baseStream)
             {
                 query = (baseStream.source as SourceStream)?.GetSource() as IQueryable;
             }
 
             var entityType = query.ElementType;
-            var resultEntityType = execArg.expression.Type.GetGenericArguments().First().GetGenericArguments().First();
+            var resultEntityType = arg.expression.Type.GetGenericArguments().First().GetGenericArguments().First();
 
-            return Execute_MethodInfo(entityType, resultEntityType).Invoke(null, new object[] { execArg });
+            return Execute_MethodInfo(entityType, resultEntityType).Invoke(null, new object[] { arg });
         }
 
 

+ 51 - 0
src/Vitorm.MongoDB/QueryExecutor/QueryExecutorArgument.ReverseOrder.cs

@@ -0,0 +1,51 @@
+using System.Linq;
+
+using Vit.Linq.ExpressionNodes.ComponentModel;
+
+using Vitorm.StreamQuery;
+
+namespace Vitorm.MongoDB.QueryExecutor
+{
+    public partial class QueryExecutorArgument
+    {
+        public virtual void ReverseOrder()
+        {
+            DbContext dbContext = this.dbContext;
+            CombinedStream stream = this.combinedStream;
+
+            stream.orders ??= new();
+            var orders = stream.orders;
+            // make sure orders exist
+            if (!orders.Any())
+            {
+                AddOrder(stream.source);
+                //stream.joins?.ForEach(right => AddOrder(right.right));
+
+                #region AddOrder
+                void AddOrder(IStream source)
+                {
+                    if (source is SourceStream sourceStream)
+                    {
+                        var entityType = sourceStream.GetEntityType();
+                        var entityDescriptor = dbContext.GetEntityDescriptor(entityType);
+                        if (entityDescriptor != null)
+                        {
+                            var parentMember = ExpressionNode.Member(objectValue: null, memberName: null);
+                            parentMember.Member_SetType(entityType);
+
+                            var member = ExpressionNode.Member(objectValue: parentMember, memberName: entityDescriptor.key.propertyName);
+                            member.Member_SetType(entityDescriptor.key.type);
+
+                            orders.Add(new ExpressionNodeOrderField { member = member, asc = true });
+                        }
+                    }
+                }
+                #endregion
+            }
+
+            // reverse order
+            orders?.ForEach(order => order.asc = !order.asc);
+        }
+    }
+
+}

+ 3 - 10
src/Vitorm.MongoDB/QueryExecutor/QueryExecutorArgument.cs

@@ -9,7 +9,7 @@ using Vitorm.StreamQuery;
 
 namespace Vitorm.MongoDB.QueryExecutor
 {
-    public class QueryExecutorArgument : IDisposable
+    public partial class QueryExecutorArgument
     {
         public CombinedStream combinedStream;
         public DbContext dbContext;
@@ -17,12 +17,7 @@ namespace Vitorm.MongoDB.QueryExecutor
         public Expression expression;
         public Type expressionResultType;
 
-        public Action dispose;
 
-        public void Dispose()
-        {
-            dispose?.Invoke();
-        }
         public virtual string GetFieldPath(ExpressionNode member) => GetFieldPath(this.dbContext, member, out Type propertyType);
 
         public static string GetFieldPath(DbContext dbContext, ExpressionNode member, out Type propertyType)
@@ -36,9 +31,8 @@ namespace Vitorm.MongoDB.QueryExecutor
                             // nested field
                             var parentPath = GetFieldPath(dbContext, member.objectValue, out Type parentPropertyType);
 
-                            var memberType = member.objectValue.Member_GetType();
                             // bool?.Value
-                            if (member.memberName == nameof(Nullable<bool>.Value) && TypeUtil.IsNullable(memberType))
+                            if (member.memberName == nameof(Nullable<bool>.Value) && TypeUtil.IsNullable(parentPropertyType))
                             {
                                 propertyType = parentPropertyType;
                                 return parentPath;
@@ -140,9 +134,8 @@ namespace Vitorm.MongoDB.QueryExecutor
                             // nested field
                             var parentPath = GetFieldPath(dbContext, member.objectValue, out IPropertyType parentPropertyType);
 
-                            var memberType = member.objectValue.Member_GetType();
                             // bool?.Value
-                            if (member.memberName == nameof(Nullable<bool>.Value) && TypeUtil.IsNullable(memberType))
+                            if (member.memberName == nameof(Nullable<bool>.Value) && TypeUtil.IsNullable(parentPropertyType.type))
                             {
                                 propertyType = parentPropertyType;
                                 return parentPath;

+ 1 - 3
src/Vitorm.MongoDB/QueryExecutor/Sync/Count.cs

@@ -16,10 +16,8 @@ namespace Vitorm.MongoDB.QueryExecutor
 
         public object ExecuteQuery(QueryExecutorArgument arg)
         {
-            using var _ = arg;
-
             CombinedStream combinedStream = arg.combinedStream;
-            var dbContext = arg.dbContext;
+
             IQueryable query = null;
             if (combinedStream.source is SourceStream sourceStream)
             {

+ 10 - 137
src/Vitorm.MongoDB/QueryExecutor/Sync/FirstOrDefault.cs

@@ -1,12 +1,7 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
 
-using MongoDB.Bson;
-using MongoDB.Driver;
-
-using Vitorm.Entity;
 using Vitorm.StreamQuery;
 
 namespace Vitorm.MongoDB.QueryExecutor
@@ -17,28 +12,22 @@ namespace Vitorm.MongoDB.QueryExecutor
 
         public string methodName => nameof(Queryable.FirstOrDefault);
 
-        public object ExecuteQuery(QueryExecutorArgument execArg)
+        public object ExecuteQuery(QueryExecutorArgument arg)
         {
-            using var _ = execArg;
-
-            CombinedStream combinedStream = execArg.combinedStream;
-            var dbContext = execArg.dbContext;
-            var translateService = dbContext.translateService;
-
             IQueryable query = null;
-            if (combinedStream.source is SourceStream sourceStream)
+            if (arg.combinedStream.source is SourceStream sourceStream)
             {
                 query = sourceStream.GetSource() as IQueryable;
             }
-            else if (combinedStream.source is CombinedStream baseStream)
+            else if (arg.combinedStream.source is CombinedStream baseStream)
             {
                 query = (baseStream.source as SourceStream)?.GetSource() as IQueryable;
             }
 
             var entityType = query?.ElementType;
-            var resultEntityType = execArg.expression.Type;
+            var resultEntityType = arg.expression.Type;
 
-            return Execute_MethodInfo(entityType, resultEntityType).Invoke(null, new object[] { execArg });
+            return Execute_MethodInfo(entityType, resultEntityType).Invoke(null, new object[] { arg });
         }
 
 
@@ -48,131 +37,15 @@ namespace Vitorm.MongoDB.QueryExecutor
             .MakeGenericMethod(entityType, resultEntityType);
 
 
-        public static ResultEntity Execute<Entity, ResultEntity>(QueryExecutorArgument execArg)
-        {
-            // #1
-            CombinedStream combinedStream = execArg.combinedStream;
-            var dbContext = execArg.dbContext;
-            var translateService = dbContext.translateService;
-
-            var entityType = typeof(Entity);
-            var entityDescriptor = dbContext.GetEntityDescriptor(entityType);
-
-
-            // #2 filter
-            var filter = translateService.TranslateFilter(execArg, combinedStream);
-
-
-            #region #3 sortDoc
-            List<(string fieldPath, bool asc)> orders = combinedStream.orders?.Select(item =>
-            {
-                var fieldPath = translateService.GetFieldPath(execArg, item.member);
-                return (fieldPath, item.asc);
-            }).ToList() ?? new();
-
-            BsonDocument sortDoc = null;
-            if (combinedStream.method?.Contains("Last") == true)
-            {
-                if (combinedStream.skip.HasValue)
-                {
-                    combinedStream.skip = combinedStream.skip.Value + (combinedStream.take ?? 0) - 1;
-                }
-                else
-                {
-                    //ReverseOrder
-
-                    // make sure orders exist
-                    if (!orders.Any())
-                    {
-                        orders.Add((entityDescriptor.key.columnName, true));
-                    }
-                    // reverse order
-                    orders = orders.Select(order => (order.fieldPath, !order.asc)).ToList();
-                }
-            }
-            if (orders?.Any() == true)
-            {
-                sortDoc = new BsonDocument();
-                orders.ForEach(item =>
-                {
-                    sortDoc.Add(item.fieldPath, BsonValue.Create(item.asc ? 1 : -1));
-                });
-            }
-            #endregion
-
-            // #4 Event_OnExecuting
-            dbContext.Event_OnExecuting(new Lazy<ExecuteEventArgument>(() => new ExecuteEventArgument(
-                dbContext: dbContext,
-                executeString: filter.ToJson(),
-                extraParam: new()
-                {
-                    ["entityDescriptor"] = entityDescriptor,
-                    ["Method"] = combinedStream.method,
-                    ["sortDoc"] = sortDoc,
-                    ["combinedStream"] = combinedStream,
-                }))
-            );
-
-
 
-            // #5 execute query
-            var database = dbContext.dbConfig.GetDatabase();
-            var collection = database.GetCollection<BsonDocument>(entityDescriptor.tableName);
-            var fluent = collection.Find(filter);
-
-
-            if (sortDoc != null) fluent = fluent.Sort(sortDoc);
-            if (combinedStream.skip > 0) fluent = fluent.Skip(combinedStream.skip);
-            //if (combinedStream.take > 0) 
-            fluent = fluent.Limit(combinedStream.take ?? 1);
-
-
-            // #6 read entity
-            BsonDocument document;
-
-            // result 
-            var method = combinedStream.method;
-            if (method.EndsWith("Async")) method = method.Substring(0, method.Length - "Async".Length);
-            switch (method)
-            {
-                case nameof(Queryable.FirstOrDefault):
-                case nameof(Queryable.LastOrDefault):
-                    {
-                        document = fluent.FirstOrDefault();
-                        break;
-                    }
-                case nameof(Queryable.First):
-                case nameof(Queryable.Last):
-                    {
-                        document = fluent.First();
-                        break;
-                    }
-
-                default: throw new NotSupportedException("not supported query type: " + combinedStream.method);
-            }
-
-            // Convert
-            if (combinedStream.select?.resultSelector != null)
-            {
-                // Select
-                var lambdaExp = combinedStream.select.resultSelector.Lambda_GetLambdaExpression();
-
-                var delSelect = (Func<Entity, ResultEntity>)lambdaExp.Compile();
-
-                var entity = ReadEntity<Entity>(dbContext, entityDescriptor, document);
-                return delSelect(entity);
-            }
-            else
-            {
-                return ReadEntity<ResultEntity>(dbContext, entityDescriptor, document);
-            }
+        public static ResultEntity Execute<Entity, ResultEntity>(QueryExecutorArgument arg)
+        {
+            var executor = arg.dbContext.GetSearchExecutor(arg);
+            if (executor == null) throw new NotImplementedException();
+            return executor.FirstOrDefault<Entity, ResultEntity>(arg);
         }
 
 
-        static Entity ReadEntity<Entity>(DbContext dbContext, IEntityDescriptor entityDescriptor, BsonDocument document)
-        {
-            return dbContext.Deserialize<Entity>(document, entityDescriptor);
-        }
 
     }
 }

+ 2 - 8
src/Vitorm.MongoDB/QueryExecutor/Sync/ToExecuteString.cs

@@ -13,18 +13,12 @@ namespace Vitorm.MongoDB.QueryExecutor
 
         public object ExecuteQuery(QueryExecutorArgument arg)
         {
-            using var _ = arg;
-
-            CombinedStream combinedStream = arg.combinedStream;
-            var dbContext = arg.dbContext;
-            var translateService = dbContext.translateService;
-
             IQueryable query = null;
-            if (combinedStream.source is SourceStream sourceStream)
+            if (arg.combinedStream.source is SourceStream sourceStream)
             {
                 query = sourceStream.GetSource() as IQueryable;
             }
-            else if (combinedStream.source is CombinedStream baseStream)
+            else if (arg.combinedStream.source is CombinedStream baseStream)
             {
                 query = (baseStream.source as SourceStream)?.GetSource() as IQueryable;
             }

+ 5 - 5
src/Vitorm.MongoDB/QueryExecutor/Sync/ToList.cs

@@ -17,22 +17,22 @@ namespace Vitorm.MongoDB.QueryExecutor
 
 
 
-        public object ExecuteQuery(QueryExecutorArgument execArg)
+        public object ExecuteQuery(QueryExecutorArgument arg)
         {
             IQueryable query = null;
-            if (execArg.combinedStream.source is SourceStream sourceStream)
+            if (arg.combinedStream.source is SourceStream sourceStream)
             {
                 query = sourceStream.GetSource() as IQueryable;
             }
-            else if (execArg.combinedStream.source is CombinedStream baseStream)
+            else if (arg.combinedStream.source is CombinedStream baseStream)
             {
                 query = (baseStream.source as SourceStream)?.GetSource() as IQueryable;
             }
 
             var entityType = query?.ElementType;
-            var resultEntityType = execArg.expression.Type.GetGenericArguments().First();
+            var resultEntityType = arg.expression.Type.GetGenericArguments().First();
 
-            return Execute_MethodInfo(entityType, resultEntityType).Invoke(null, new object[] { execArg });
+            return Execute_MethodInfo(entityType, resultEntityType).Invoke(null, new object[] { arg });
         }
 
 

+ 3 - 6
src/Vitorm.MongoDB/QueryExecutor/TranslateService.cs

@@ -8,17 +8,14 @@ using MongoDB.Bson;
 
 using Vit.Linq.ExpressionNodes.ComponentModel;
 
-using Vitorm.StreamQuery;
-
 namespace Vitorm.MongoDB.QueryExecutor
 {
     public class TranslateService
     {
-        public virtual BsonDocument TranslateFilter(QueryExecutorArgument arg, CombinedStream combinedStream)
+        public virtual BsonDocument TranslateFilter(QueryExecutorArgument arg)
         {
-            if (combinedStream?.where == null) return new();
-
-            return TranslateFilter(arg, combinedStream.where);
+            if (arg?.combinedStream?.where == null) return new();
+            return TranslateFilter(arg, arg.combinedStream.where);
         }
 
 

+ 83 - 0
src/Vitorm.MongoDB/SearchExecutor/GroupExecutor.FirstOrDefault.cs

@@ -0,0 +1,83 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+using MongoDB.Bson;
+
+using MongoDB.Driver;
+
+using Vitorm.MongoDB.QueryExecutor;
+
+namespace Vitorm.MongoDB.SearchExecutor
+{
+    public partial class GroupExecutor
+    {
+        public ResultEntity FirstOrDefault<Entity, ResultEntity>(QueryExecutorArgument arg)
+        {
+            Type keyType;
+            var resultSelector = arg.combinedStream.select.resultSelector;
+            if (resultSelector == null)
+            {
+                keyType = typeof(ResultEntity);
+            }
+            else
+            {
+                var groupType = resultSelector.Lambda_GetParamTypes()[0];
+                keyType = groupType.GetGenericArguments()[0];
+            }
+
+            return (ResultEntity)FirstOrDefault_MethodInfo(typeof(Entity), typeof(ResultEntity), keyType).Invoke(null, new[] { arg });
+        }
+
+
+        private static MethodInfo FirstOrDefault_MethodInfo_;
+        static MethodInfo FirstOrDefault_MethodInfo(Type entityType, Type resultEntityType, Type keyType) =>
+            (FirstOrDefault_MethodInfo_ ??= new Func<QueryExecutorArgument, string>(FirstOrDefault<string, string, string>).Method.GetGenericMethodDefinition())
+            .MakeGenericMethod(entityType, resultEntityType, keyType);
+
+
+        static ResultEntity FirstOrDefault<Entity, ResultEntity, Key>(QueryExecutorArgument arg)
+        {
+            var combinedStream = arg.combinedStream;
+            var dbContext = arg.dbContext;
+            var entityDescriptor = dbContext.GetEntityDescriptor(typeof(Entity));
+
+            if (combinedStream.method.Contains("Last"))
+            {
+                if (combinedStream.skip.HasValue)
+                {
+                    combinedStream.skip = combinedStream.skip.Value + (combinedStream.take ?? 0) - 1;
+                }
+                else
+                {
+                    arg.ReverseOrder();
+                }
+            }
+            if (combinedStream.take != 0)
+                combinedStream.take = 1;
+
+
+            using var cursor = Execute<Entity, ResultEntity>(arg, entityDescriptor);
+
+            var nullable = combinedStream.method.Contains("OrDefault");
+            if (combinedStream.take == 0)
+            {
+                return nullable ? default : throw new InvalidOperationException("Sequence contains no elements");
+            }
+
+            BsonDocument document = nullable ? cursor?.FirstOrDefault() : cursor.First();
+            if (document == null) return default;
+
+            var firstGroup = new Grouping<Key, Entity>(dbContext, entityDescriptor, document);
+
+            var lambdaExpression = combinedStream.select.resultSelector.Lambda_GetLambdaExpression();
+            var delSelect = (Func<IGrouping<Key, Entity>, ResultEntity>)lambdaExpression.Compile();
+
+            return delSelect(firstGroup);
+        }
+
+
+
+
+    }
+}

+ 0 - 8
src/Vitorm.MongoDB/SearchExecutor/GroupExecutor.ToList.cs

@@ -92,13 +92,5 @@ namespace Vitorm.MongoDB.SearchExecutor
             }
         }
 
-
-
-
-
-
-
-
-
     }
 }

+ 0 - 2
src/Vitorm.MongoDB/SearchExecutor/GroupExecutor.cs

@@ -32,8 +32,6 @@ namespace Vitorm.MongoDB.SearchExecutor
             return true;
         }
 
-
-
         public static BsonDocument[] GetPipeline(QueryExecutorArgument arg)
         {
             CombinedStream combinedStream = arg.combinedStream;

+ 2 - 0
src/Vitorm.MongoDB/SearchExecutor/ISearchExecutor.cs

@@ -14,5 +14,7 @@ namespace Vitorm.MongoDB.SearchExecutor
         Task<List<ResultEntity>> ToListAsync<Entity, ResultEntity>(QueryExecutorArgument arg);
         int Count(QueryExecutorArgument arg, Type entityType);
         string ToExecuteString(QueryExecutorArgument arg, Type entityType);
+
+        ResultEntity FirstOrDefault<Entity, ResultEntity>(QueryExecutorArgument arg);
     }
 }

+ 72 - 0
src/Vitorm.MongoDB/SearchExecutor/PlainDistinctExecutor.FirstOrDefault.cs

@@ -0,0 +1,72 @@
+using System;
+
+using MongoDB.Bson;
+using MongoDB.Driver;
+
+using Vitorm.MongoDB.QueryExecutor;
+using Vitorm.StreamQuery;
+
+namespace Vitorm.MongoDB.SearchExecutor
+{
+    public partial class PlainDistinctExecutor
+    {
+        public ResultEntity FirstOrDefault<Entity, ResultEntity>(QueryExecutorArgument arg)
+        {
+            CombinedStream combinedStream = arg.combinedStream;
+            var dbContext = arg.dbContext;
+            var entityDescriptor = dbContext.GetEntityDescriptor(typeof(Entity));
+
+            if (combinedStream.method.Contains("Last"))
+            {
+                if (combinedStream.skip.HasValue)
+                {
+                    combinedStream.skip = combinedStream.skip.Value + (combinedStream.take ?? 0) - 1;
+                }
+                else
+                {
+                    arg.ReverseOrder();
+                }
+            }
+            if (combinedStream.take != 0)
+                combinedStream.take = 1;
+
+
+            var entityReader = new EntityReader.EntityReader();
+            entityReader.Init(dbContext, typeof(Entity), combinedStream.select.fields);
+
+
+            var pipeline = GetPipeline(arg, entityReader);
+
+            // Event_OnExecuting
+            dbContext.Event_OnExecuting(new Lazy<ExecuteEventArgument>(() => new ExecuteEventArgument(
+                dbContext: dbContext,
+                executeString: pipeline.ToJson(),
+                extraParam: new()
+                {
+                    ["entityDescriptor"] = entityDescriptor,
+                    ["Method"] = arg.combinedStream.method,
+                    ["combinedStream"] = combinedStream,
+                }))
+            );
+
+            var nullable = combinedStream.method.Contains("OrDefault");
+            if (combinedStream.take == 0)
+            {
+                return nullable ? default : throw new InvalidOperationException("Sequence contains no elements");
+            }
+
+
+            var collection = dbContext.dbConfig.GetDatabase().GetCollection<BsonDocument>(entityDescriptor.tableName);
+            using var cursor = dbContext.session == null ? collection.Aggregate<BsonDocument>(pipeline) : collection.Aggregate<BsonDocument>(dbContext.session, pipeline);
+
+
+            // read entity
+            BsonDocument document = nullable ? cursor?.FirstOrDefault() : cursor.First();
+            if (document == null) return default;
+
+            var group = document?["_id"].AsBsonDocument;
+            return (ResultEntity)entityReader.ReadEntity(group);
+        }
+
+    }
+}

+ 1 - 1
src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.Count.cs

@@ -17,7 +17,7 @@ namespace Vitorm.MongoDB.SearchExecutor
             var translateService = dbContext.translateService;
 
             // #2 filter
-            var filter = translateService.TranslateFilter(arg, combinedStream);
+            var filter = translateService.TranslateFilter(arg);
 
 
             // Event_OnExecuting

+ 69 - 0
src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.FirstOrDefault.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Linq;
+
+using MongoDB.Bson;
+using MongoDB.Driver;
+
+using Vitorm.MongoDB.QueryExecutor;
+using Vitorm.StreamQuery;
+
+namespace Vitorm.MongoDB.SearchExecutor
+{
+    public partial class PlainExecutor
+    {
+        public ResultEntity FirstOrDefault<Entity, ResultEntity>(QueryExecutorArgument arg)
+        {
+            CombinedStream combinedStream = arg.combinedStream;
+            var dbContext = arg.dbContext;
+            var entityDescriptor = dbContext.GetEntityDescriptor(typeof(Entity));
+
+
+            if (combinedStream.method.Contains("Last"))
+            {
+                if (combinedStream.skip.HasValue)
+                {
+                    combinedStream.skip = combinedStream.skip.Value + (combinedStream.take ?? 0) - 1;
+                }
+                else
+                {
+                    arg.ReverseOrder();
+                }
+            }
+            if (combinedStream.take != 0)
+                combinedStream.take = 1;
+
+
+            var fluent = ExecuteQuery<Entity, ResultEntity>(arg, entityDescriptor);
+
+            var nullable = combinedStream.method.Contains("OrDefault");
+            if (combinedStream.take == 0)
+            {
+                return nullable ? default : throw new InvalidOperationException("Sequence contains no elements");
+            }
+
+
+            // read entity
+            BsonDocument document = nullable ? fluent?.FirstOrDefault() : fluent.First();
+
+            if (combinedStream.select?.resultSelector != null)
+            {
+                // Select
+                var lambdaExp = combinedStream.select.resultSelector.Lambda_GetLambdaExpression();
+                var delSelect = (Func<Entity, ResultEntity>)lambdaExp.Compile();
+
+                var entity = dbContext.Deserialize<Entity>(document, entityDescriptor);
+                return entity == null ? default : delSelect(entity);
+            }
+            else
+            {
+                var entity = dbContext.Deserialize<ResultEntity>(document, entityDescriptor);
+                return entity;
+            }
+
+        }
+
+
+
+
+    }
+}

+ 1 - 1
src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.ToExecuteString.cs

@@ -19,7 +19,7 @@ namespace Vitorm.MongoDB.SearchExecutor
 
 
             // #2 filter
-            var filter = translateService.TranslateFilter(arg, combinedStream);
+            var filter = translateService.TranslateFilter(arg);
 
 
             // #3 sortDoc

+ 3 - 6
src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.ToList.cs

@@ -12,9 +12,8 @@ using Vitorm.StreamQuery;
 
 namespace Vitorm.MongoDB.SearchExecutor
 {
-    public partial class PlainExecutor : ISearchExecutor
+    public partial class PlainExecutor
     {
-
         public List<ResultEntity> ToList<Entity, ResultEntity>(QueryExecutorArgument arg)
         {
             CombinedStream combinedStream = arg.combinedStream;
@@ -31,13 +30,11 @@ namespace Vitorm.MongoDB.SearchExecutor
             {
                 // Select
                 var lambdaExp = combinedStream.select.resultSelector.Lambda_GetLambdaExpression();
-
                 var delSelect = (Func<Entity, ResultEntity>)lambdaExp.Compile();
-                Type resultEntityType = typeof(ResultEntity);
 
-                var entities = ReadList<Entity>(dbContext, entityDescriptor, cursor);
+                var entities = ReadList<Entity>(dbContext, entityDescriptor, cursor).Select(delSelect);
 
-                result = entities.Select(delSelect).ToList();
+                result = entities.ToList();
             }
             else
             {

+ 1 - 2
src/Vitorm.MongoDB/SearchExecutor/PlainExecutor.cs

@@ -38,9 +38,8 @@ namespace Vitorm.MongoDB.SearchExecutor
             var translateService = dbContext.translateService;
 
 
-
             // #2 filter
-            var filter = translateService.TranslateFilter(arg, combinedStream);
+            var filter = translateService.TranslateFilter(arg);
 
 
             // #3 sortDoc

+ 4 - 4
test/Vitorm.MongoDB.Data.MsTest/CommonTest/Common_Test.cs

@@ -21,9 +21,9 @@ namespace Vitorm.MsTest.CommonTest
             Test_Get();
             Test_Query();
             //Test_QueryJoin();
-            //Test_ToExecuteString();
+            Test_ToExecuteString();
             //Test_ExecuteUpdate();
-            Test_ExecuteDelete();
+            //Test_ExecuteDelete();
             Test_Create();
             Test_Update();
             Test_Delete();
@@ -37,8 +37,8 @@ namespace Vitorm.MsTest.CommonTest
             await Test_GetAsync();
             await Test_QueryAsync();
             //await Test_QueryJoinAsync();
-            await Test_ExecuteUpdateAsync();
-            await Test_ExecuteDeleteAsync();
+            //await Test_ExecuteUpdateAsync();
+            //await Test_ExecuteDeleteAsync();
             await Test_UpdateAsync();
             await Test_DeleteAsync();
         }

+ 4 - 4
test/Vitorm.MongoDB.MsTest/CommonTest/Query_Method_Test.cs

@@ -320,10 +320,10 @@ namespace Vitorm.MsTest.CommonTest
             using var dbContext = DataSource.CreateDbContext();
             var userQuery = dbContext.Query<User>();
 
-            {
-                var user = userQuery.OrderBy(m => m.id).Last();
-                Assert.AreEqual(6, user?.id);
-            }
+            //{
+            //    var user = userQuery.OrderBy(m => m.id).Last();
+            //    Assert.AreEqual(6, user?.id);
+            //}
 
             {
                 var user = userQuery.Last(user => user.id == 3);

+ 52 - 0
test/Vitorm.MongoDB.MsTest/CustomTest/Query_Method_Test.cs

@@ -123,5 +123,57 @@ namespace Vitorm.MsTest.CustomTest
 
 
 
+        [TestMethod]
+        public void FirstOrDefault()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // PlainQuery
+            {
+                var item = userQuery.Where(m => m.fatherId != null).OrderBy(m => m.fatherId).Select(m => new { fatherId = m.fatherId }).FirstOrDefault();
+                Assert.AreEqual(4, item.fatherId);
+            }
+
+            // Group
+            {
+                var item = userQuery.Where(m => m.fatherId != null).GroupBy(m => m.fatherId).OrderBy(g => g.Key).Select(g => new { fatherId = g.Key }).FirstOrDefault();
+                Assert.AreEqual(4, item.fatherId);
+            }
+
+            // PlainDistinctSearch
+            {
+                var item = userQuery.Where(m => m.fatherId != null).Select(m => new { fatherId = m.fatherId }).OrderBy(m => m.fatherId).Distinct().FirstOrDefault();
+                Assert.AreEqual(4, item.fatherId);
+            }
+        }
+
+
+        [TestMethod]
+        public void LastOrDefault()
+        {
+            using var dbContext = DataSource.CreateDbContext();
+            var userQuery = dbContext.Query<User>();
+
+            // PlainQuery
+            {
+                var item = userQuery.Where(m => m.fatherId != null).OrderBy(m => m.fatherId).Select(m => new { fatherId = m.fatherId }).LastOrDefault();
+                Assert.AreEqual(5, item.fatherId);
+            }
+
+            // Group
+            {
+                var item = userQuery.Where(m => m.fatherId != null).GroupBy(m => m.fatherId).OrderBy(g => g.Key).Select(g => new { fatherId = g.Key }).LastOrDefault();
+                Assert.AreEqual(5, item.fatherId);
+            }
+
+            // PlainDistinctSearch
+            {
+                var item = userQuery.Where(m => m.fatherId != null).Select(m => new { fatherId = m.fatherId }).OrderBy(m => m.fatherId).Distinct().LastOrDefault();
+                Assert.AreEqual(5, item.fatherId);
+            }
+        }
+
+
     }
 }