diff --git a/Directory.Build.props b/Directory.Build.props
index caaa013e..44c947a2 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -4,7 +4,7 @@
- 1.0.9.1
+ 1.0.9.2
@@ -16,4 +16,4 @@
-
\ No newline at end of file
+
diff --git a/GitHubReleaseNotes.txt b/GitHubReleaseNotes.txt
index ccdfc7c2..7d4d7f2e 100644
--- a/GitHubReleaseNotes.txt
+++ b/GitHubReleaseNotes.txt
@@ -1,3 +1,3 @@
https://github.com/StefH/GitHubReleaseNotes
-GitHubReleaseNotes.exe . --output CHANGELOG.md --skip-empty-releases --language en --version 1.0.9.1
\ No newline at end of file
+GitHubReleaseNotes.exe . --output CHANGELOG.md --skip-empty-releases --language en --version 1.0.9.2
diff --git a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs
index bc4dd98a..d7b07419 100644
--- a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs
+++ b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs
@@ -1,11 +1,11 @@
-using System;
+using ConsoleAppEF2.Database;
+using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Linq.Dynamic.Core.CustomTypeProviders;
-using ConsoleAppEF2.Database;
-using Microsoft.EntityFrameworkCore;
-using Newtonsoft.Json;
namespace ConsoleAppEF2
{
@@ -83,6 +83,18 @@ static void Main(string[] args)
context.Cars.Add(new Car { Brand = "Alfa", Color = "Black", Vin = "a%bc", Year = "1979", DateLastModified = dateLastModified.AddDays(3) });
context.SaveChanges();
+ var carSingleOrDefault = context.Cars.SingleOrDefault(config, "Brand = \"Ford\"");
+ Console.WriteLine("carSingleOrDefault {0}", JsonConvert.SerializeObject(carSingleOrDefault, Formatting.Indented));
+
+ try
+ {
+ context.Cars.SingleOrDefault(config, "Brand = \"Alfa\"");
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine("Excepted : " + e);
+ }
+
var carDateLastModified = context.Cars.Where(config, "DateLastModified > \"2018-01-16\"");
Console.WriteLine("carDateLastModified {0}", JsonConvert.SerializeObject(carDateLastModified, Formatting.Indented));
diff --git a/src/Microsoft.EntityFrameworkCore.DynamicLinq/EFDynamicQueryableExtensions.cs b/src/Microsoft.EntityFrameworkCore.DynamicLinq/EFDynamicQueryableExtensions.cs
index 917a5b79..38c8f8ac 100644
--- a/src/Microsoft.EntityFrameworkCore.DynamicLinq/EFDynamicQueryableExtensions.cs
+++ b/src/Microsoft.EntityFrameworkCore.DynamicLinq/EFDynamicQueryableExtensions.cs
@@ -576,6 +576,91 @@ public static Task LastOrDefaultAsync([NotNull] this IQueryable source,
}
#endregion LastOrDefault
+ #region SingleOrDefaultAsync
+ private static readonly MethodInfo _singleOrDefault = GetMethod(nameof(Queryable.SingleOrDefault));
+
+ ///
+ /// Asynchronously returns the only element of a sequence that satisfies a specified condition or a default value if no such element exists.
+ /// This method throws an exception if more than one element satisfies the condition.
+ ///
+ ///
+ /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure
+ /// that any asynchronous operations have completed before calling another method on this context.
+ ///
+ ///
+ /// An to return the single element of.
+ ///
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains the single element of the input sequence that satisfies the condition in predicate.
+ ///
+ [PublicAPI]
+ public static Task SingleOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Check.NotNull(source, nameof(source));
+ Check.NotNull(cancellationToken, nameof(cancellationToken));
+
+ return ExecuteAsync(_singleOrDefault, source, cancellationToken);
+ }
+
+ private static readonly MethodInfo _singleOrDefaultPredicate = GetMethod(nameof(Queryable.SingleOrDefault), 1);
+
+ ///
+ /// Asynchronously returns the only element of a sequence that satisfies a specified condition or a default value if no such element exists.
+ /// This method throws an exception if more than one element satisfies the condition.
+ ///
+ ///
+ /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure
+ /// that any asynchronous operations have completed before calling another method on this context.
+ ///
+ ///
+ /// An to return the single element of.
+ ///
+ /// A function to test each element for a condition.
+ /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains the single element of the input sequence that satisfies the condition in , or default if no such element is found.
+ ///
+ [PublicAPI]
+ public static Task SingleOrDefaultAsync([NotNull] this IQueryable source, [NotNull] string predicate, [CanBeNull] params object[] args)
+ {
+ return SingleOrDefaultAsync(source, default(CancellationToken), predicate, args);
+ }
+
+ ///
+ /// Asynchronously returns the only element of a sequence that satisfies a specified condition or a default value if no such element exists.
+ /// This method throws an exception if more than one element satisfies the condition.
+ ///
+ ///
+ /// Multiple active operations on the same context instance are not supported. Use 'await' to ensure
+ /// that any asynchronous operations have completed before calling another method on this context.
+ ///
+ ///
+ /// An to return the single element of.
+ ///
+ /// A function to test each element for a condition.
+ /// An object array that contains zero or more objects to insert into the predicate as parameters. Similar to the way String.Format formats strings.
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains the single element of the input sequence that satisfies the condition in , or default if no such element is found.
+ ///
+ [PublicAPI]
+ public static Task SingleOrDefaultAsync([NotNull] this IQueryable source, CancellationToken cancellationToken, [NotNull] string predicate, [CanBeNull] params object[] args)
+ {
+ Check.NotNull(source, nameof(source));
+ Check.NotNull(predicate, nameof(predicate));
+ Check.NotNull(cancellationToken, nameof(cancellationToken));
+
+ LambdaExpression lambda = DynamicExpressionParser.ParseLambda(false, source.ElementType, null, predicate, args);
+
+ return ExecuteAsync(_singleOrDefaultPredicate, source, Expression.Quote(lambda), cancellationToken);
+ }
+ #endregion SingleOrDefault
+
#region Private Helpers
// Copied from https://github.com/aspnet/EntityFramework/blob/9186d0b78a3176587eeb0f557c331f635760fe92/src/Microsoft.EntityFrameworkCore/EntityFrameworkQueryableExtensions.cs
//private static Task ExecuteAsync(MethodInfo operatorMethodInfo, IQueryable source, CancellationToken cancellationToken = default(CancellationToken))
@@ -651,4 +736,4 @@ private static MethodInfo GetMethod(string name, int parameterCount = 0, Func (mi.GetParameters().Length == parameterCount + 1) && ((predicate == null) || predicate(mi)));
#endregion Private Helpers
}
-}
\ No newline at end of file
+}
diff --git a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.SingleOrDefaultAsync.cs b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.SingleOrDefaultAsync.cs
new file mode 100644
index 00000000..0ee84918
--- /dev/null
+++ b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.SingleOrDefaultAsync.cs
@@ -0,0 +1,61 @@
+#if EFCORE
+using Microsoft.EntityFrameworkCore.DynamicLinq;
+#else
+using EntityFramework.DynamicLinq;
+#endif
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Linq.Dynamic.Core.Tests
+{
+ public partial class EntitiesTests
+ {
+ [Fact]
+ public async Task Entities_SingleOrDefaultAsync()
+ {
+ //Arrange
+ PopulateTestData(1, 0);
+
+ var expectedQueryable1 = _context.Blogs.Where(b => b.BlogId > 0);
+ var expected1 = await expectedQueryable1.SingleOrDefaultAsync();
+
+ var expectedQueryable2 = _context.Blogs.Where(b => b.BlogId > 9999);
+ var expected2 = await expectedQueryable2.SingleOrDefaultAsync();
+
+ //Act
+ IQueryable queryable1 = _context.Blogs.Where("BlogId > 0");
+ var result1 = await queryable1.SingleOrDefaultAsync();
+
+ IQueryable queryable2 = _context.Blogs.Where("BlogId > 9999");
+ var result2 = await queryable2.SingleOrDefaultAsync();
+
+ //Assert
+ Assert.Equal(expected1, result1);
+ Assert.Null(expected2);
+ Assert.Null(result2);
+ }
+
+ [Fact]
+ public async Task Entities_SingleOrDefaultAsync_Predicate()
+ {
+ //Arrange
+ PopulateTestData(1, 0);
+#if EFCORE
+ var expected1 = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(_context.Blogs, b => b.BlogId > 0);
+ var expected2 = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.SingleOrDefaultAsync(_context.Blogs, b => b.BlogId > 9999);
+#else
+ var expected1 = await System.Data.Entity.QueryableExtensions.SingleOrDefaultAsync(_context.Blogs, b => b.BlogId > 0);
+ var expected2 = await System.Data.Entity.QueryableExtensions.SingleOrDefaultAsync(_context.Blogs, b => b.BlogId > 9999);
+#endif
+
+ //Act
+ var result1 = await _context.Blogs.AsQueryable().SingleOrDefaultAsync("it.BlogId > 0");
+ var result2 = await _context.Blogs.AsQueryable().SingleOrDefaultAsync("it.BlogId > 9999");
+
+ //Assert
+ Assert.Equal(expected1, result1);
+ Assert.Null(expected2);
+ Assert.Null(result2);
+ }
+ }
+}