Skip to content

Add Async Query Support #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Sep 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afc095b
First pass at basic async querying support
Turnerj Nov 4, 2019
3a62ce0
Use Microsoft.Bcl.AsyncInterfaces and keep ns20 target
slang25 Nov 6, 2019
f50bc66
Updated async tests
Turnerj Sep 21, 2020
ec1c554
Removed unused class
Turnerj Sep 21, 2020
3d2b1ec
Easier access to AsAsyncEnumerable
Turnerj Sep 21, 2020
5058b34
Beginning rewrite of result transformer logic
Turnerj Sep 21, 2020
411334a
Various updates on the path to async queries
Turnerj Sep 25, 2020
4c0b1c2
Add Count transform and pass serializer type
Turnerj Sep 25, 2020
2afa8b9
Fixed tests for SingleAsync for more than one value returned
Turnerj Sep 25, 2020
a8b5722
Count is a SingleOrDefault value
Turnerj Sep 25, 2020
ceac3b9
Add ToArrayAsync and ToListAsync
Turnerj Sep 25, 2020
5854063
Added implementation note
Turnerj Sep 25, 2020
467cfb3
CountAsync, MaxAsync and MinAsync + Tests
Turnerj Sep 25, 2020
6f2369a
Added tests for ToArrayAsync and ToListAsync
Turnerj Sep 25, 2020
24740db
Async builds off of query provider rather than queryable
Turnerj Sep 26, 2020
6884cf9
Add tests for AnyAsync
Turnerj Sep 26, 2020
e71e06e
Cleaned up QueryableAsyncExtensionsTests ahead of sum tests
Turnerj Sep 26, 2020
1b7911b
Add the various SumAsync extensions
Turnerj Sep 26, 2020
3fb48b3
Add decimal serialization processor
Turnerj Sep 26, 2020
fbbbafc
Changed default mapping processor logic to create new instances
Turnerj Sep 26, 2020
1997f81
Downgrading MiniProfiler extension back to .NET Standard 2.0
Turnerj Sep 26, 2020
be7d120
Added missing target frameworks
Turnerj Sep 26, 2020
eb00819
Try different version of OpenCover
Turnerj Sep 26, 2020
23ae603
Enable pre-release versions for OpenCover
Turnerj Sep 26, 2020
aacef9b
Switching to Coverlet for coverage
Turnerj Sep 26, 2020
389222e
Fixing issues with report generation path
Turnerj Sep 26, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ skip_branch_with_pr: true
services:
- mongodb
install:
- choco install opencover.portable codecov
- choco install reportgenerator.portable codecov
- ps: .\build\dotnet-install.ps1 -Version 3.0.100


Expand Down
31 changes: 8 additions & 23 deletions CodeCoverage.runsettings
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- File name extension must be .runsettings -->
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="Code Coverage" uri="datacollector://Microsoft/CodeCoverage/2.0" assemblyQualifiedName="Microsoft.VisualStudio.Coverage.DynamicCoverageDataCollector, Microsoft.VisualStudio.TraceCollector, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<Configuration>
<CodeCoverage>
<ModulePaths>
<Include>
<ModulePath>.*\.dll$</ModulePath>
</Include>
<Exclude>
<ModulePath>MongoFramework.Tests.dll</ModulePath>
</Exclude>
</ModulePaths>

<!-- We recommend you do not change the following values: -->
<UseVerifiableInstrumentation>True</UseVerifiableInstrumentation>
<AllowLowIntegrityProcesses>True</AllowLowIntegrityProcesses>
<CollectFromChildProcesses>True</CollectFromChildProcesses>
<CollectAspDotNet>False</CollectAspDotNet>

</CodeCoverage>
</Configuration>
</DataCollector>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>cobertura</Format>
<Exclude>[MongoFramework.Tests]*</Exclude>
<Include>[MongoFramework.*]*</Include>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
6 changes: 5 additions & 1 deletion MongoFramework.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D007577A-7BD
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CBF5A6D-EC65-4289-B2C9-875BDB654FC4}"
ProjectSection(SolutionItems) = preProject
tests\Directory.build.props = tests\Directory.build.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{BA117612-DD9B-4296-8F56-5D790AB07D72}"
ProjectSection(SolutionItems) = preProject
Expand All @@ -25,11 +28,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{BA1176
build.appveyor.ps1 = build.appveyor.ps1
build.ps1 = build.ps1
buildconfig.json = buildconfig.json
CodeCoverage.runsettings = CodeCoverage.runsettings
License.txt = License.txt
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongoFramework.Benchmarks", "tests\MongoFramework.Benchmarks\MongoFramework.Benchmarks.csproj", "{0177C18B-96AB-45E1-B9FB-1D734B2B7504}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoFramework.Benchmarks", "tests\MongoFramework.Benchmarks\MongoFramework.Benchmarks.csproj", "{0177C18B-96AB-45E1-B9FB-1D734B2B7504}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
15 changes: 13 additions & 2 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ Write-Host " CreatePackages: $CreatePackages"
Write-Host " BuildVersion: $BuildVersion"
Write-Host "Configuration:" -ForegroundColor Cyan
Write-Host " TestProject: $($config.TestProject)"
Write-Host " TestCoverageFilter: $($config.TestCoverageFilter)"
Write-Host "Environment:" -ForegroundColor Cyan
Write-Host " .NET Version:" (dotnet --version)
Write-Host " Artifact Path: $packageOutputFolder"
Expand All @@ -52,17 +51,29 @@ if ($RunTests) {
}
else {
Write-Host "Running tests with coverage..." -ForegroundColor "Magenta"
OpenCover.Console.exe -register:user -target:"%LocalAppData%\Microsoft\dotnet\dotnet.exe" -targetargs:"test $($config.TestProject) /p:DebugType=Full" -filter:"$($config.TestCoverageFilter)" -output:"$packageOutputFolder\coverage.xml" -oldstyle
dotnet test $config.TestProject --logger trx --results-directory $packageOutputFolder\coverage --collect "XPlat Code Coverage" --settings CodeCoverage.runsettings

if ($LastExitCode -ne 0 -Or -Not $?) {
Write-Host "Failure performing tests with coverage, aborting!" -Foreground "Red"
Exit 1
}
else {
Write-Host "Tests passed!" -ForegroundColor "Green"

Write-Host "Finalising coverage report..." -ForegroundColor "Magenta"
reportgenerator -reports:$packageOutputFolder/coverage/*/coverage.cobertura.xml -targetdir:$packageOutputFolder -reporttypes:Cobertura
if ($LastExitCode -ne 0) {
Write-Host "Failure finalising coverage report, aborting!" -Foreground "Red"
Exit 1
}
Rename-Item -Path $packageOutputFolder/Cobertura.xml -NewName $packageOutputFolder/coverage.xml
Write-Host "Coverage report finalised!" -ForegroundColor "Green"

Write-Host "Saving code coverage..." -ForegroundColor "Magenta"
codecov -f "$packageOutputFolder\coverage.xml"
if ($LastExitCode -ne 0 -Or -Not $?) {
Write-Host "Failure saving code coverage!" -Foreground "Red"
Exit 1
}
else {
Write-Host "Coverage saved!" -ForegroundColor "Green"
Expand Down
3 changes: 1 addition & 2 deletions buildconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{
"TestProject": "tests/MongoFramework.Tests/MongoFramework.Tests.csproj",
"TestCoverageFilter": "+[MongoFramework]* -[MongoFramework.Tests]*"
"TestProject": "tests/MongoFramework.Tests/MongoFramework.Tests.csproj"
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using MongoDB.Driver.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace MongoFramework.Infrastructure.Linq
{
public interface IMongoFrameworkQueryProvider : IQueryProvider
{
IMongoDbConnection Connection { get; }
Expression GetBaseExpression();
object ExecuteAsync(Expression expression, CancellationToken cancellationToken = default);
string ToQuery(Expression expression);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace MongoFramework.Infrastructure.Linq
{
Expand Down
91 changes: 91 additions & 0 deletions src/MongoFramework/Infrastructure/Linq/MethodInfoCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace MongoFramework.Infrastructure.Linq
{
internal static class MethodInfoCache
{
private static MethodInfo GetMethodInfo<TResult>(Func<IAsyncEnumerable<object>, CancellationToken, ValueTask<TResult>> methodDelegate) => methodDelegate.Method.GetGenericMethodDefinition();
private static MethodInfo GetMethodInfo<TResult>(Func<IEnumerable<object>, TResult> methodDelegate) => methodDelegate.Method.GetGenericMethodDefinition();
private static MethodInfo GetMethodInfo<TResult>(Func<IQueryable<object>, TResult> methodDelegate) => methodDelegate.Method.GetGenericMethodDefinition();
private static MethodInfo GetMethodInfo_WithParameter<TParam>(Func<IQueryable<object>, TParam, object> methodDelegate) => methodDelegate.Method.GetGenericMethodDefinition();
private static MethodInfo GetMethodInfo_Passthrough_NonGeneric<TSource>(Func<IQueryable<TSource>, TSource> methodDelegate) => methodDelegate.Method;
private static MethodInfo GetMethodInfo_WithParameter<TParam, TResult>(Func<IQueryable<object>, TParam, TResult> methodDelegate) => methodDelegate.Method.GetGenericMethodDefinition();

public static class Queryable
{
public static readonly MethodInfo First_1 = GetMethodInfo(System.Linq.Queryable.First);
public static readonly MethodInfo First_2 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>>(System.Linq.Queryable.First);
public static readonly MethodInfo FirstOrDefault_1 = GetMethodInfo(System.Linq.Queryable.FirstOrDefault);
public static readonly MethodInfo FirstOrDefault_2 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>>(System.Linq.Queryable.FirstOrDefault);

public static readonly MethodInfo Single_1 = GetMethodInfo(System.Linq.Queryable.Single);
public static readonly MethodInfo Single_2 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>>(System.Linq.Queryable.Single);
public static readonly MethodInfo SingleOrDefault_1 = GetMethodInfo(System.Linq.Queryable.SingleOrDefault);
public static readonly MethodInfo SingleOrDefault_2 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>>(System.Linq.Queryable.SingleOrDefault);

public static readonly MethodInfo Count_1 = GetMethodInfo(System.Linq.Queryable.Count);
public static readonly MethodInfo Count_2 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>, int>(System.Linq.Queryable.Count);

public static readonly MethodInfo Any_1 = GetMethodInfo(System.Linq.Queryable.Any);
public static readonly MethodInfo Any_2 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>, bool>(System.Linq.Queryable.Any);

public static readonly MethodInfo All_1 = GetMethodInfo_WithParameter<Expression<Func<object, bool>>, bool>(System.Linq.Queryable.All);

public static readonly MethodInfo Max_1 = GetMethodInfo(System.Linq.Queryable.Max);
public static readonly MethodInfo Max_2 = GetMethodInfo_WithParameter<Expression<Func<object, object>>, object>(System.Linq.Queryable.Max);

public static readonly MethodInfo Min_1 = GetMethodInfo(System.Linq.Queryable.Min);
public static readonly MethodInfo Min_2 = GetMethodInfo_WithParameter<Expression<Func<object, object>>, object>(System.Linq.Queryable.Min);

public static readonly MethodInfo Sum_Int32_1 = GetMethodInfo_Passthrough_NonGeneric<int>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableInt32_1 = GetMethodInfo_Passthrough_NonGeneric<int?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Decimal_1 = GetMethodInfo_Passthrough_NonGeneric<decimal>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableDecimal_1 = GetMethodInfo_Passthrough_NonGeneric<decimal?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Double_1 = GetMethodInfo_Passthrough_NonGeneric<double>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableDouble_1 = GetMethodInfo_Passthrough_NonGeneric<double?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Long_1 = GetMethodInfo_Passthrough_NonGeneric<long>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableLong_1 = GetMethodInfo_Passthrough_NonGeneric<long?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Float_1 = GetMethodInfo_Passthrough_NonGeneric<float>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableFloat_1 = GetMethodInfo_Passthrough_NonGeneric<float?>(System.Linq.Queryable.Sum);

public static readonly MethodInfo Sum_Int32_2 = GetMethodInfo_WithParameter<Expression<Func<object, int>>, int>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableInt32_2 = GetMethodInfo_WithParameter<Expression<Func<object, int?>>, int?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Decimal_2 = GetMethodInfo_WithParameter<Expression<Func<object, decimal>>, decimal>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableDecimal_2 = GetMethodInfo_WithParameter<Expression<Func<object, decimal?>>, decimal?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Double_2 = GetMethodInfo_WithParameter<Expression<Func<object, double>>, double>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableDouble_2 = GetMethodInfo_WithParameter<Expression<Func<object, double?>>, double?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Long_2 = GetMethodInfo_WithParameter<Expression<Func<object, long>>, long>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableLong_2 = GetMethodInfo_WithParameter<Expression<Func<object, long?>>, long?>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_Float_2 = GetMethodInfo_WithParameter<Expression<Func<object, float>>, float>(System.Linq.Queryable.Sum);
public static readonly MethodInfo Sum_NullableFloat_2 = GetMethodInfo_WithParameter<Expression<Func<object, float?>>, float?>(System.Linq.Queryable.Sum);
}

public static class Enumerable
{
public static readonly MethodInfo First_1 = GetMethodInfo(System.Linq.Enumerable.First);
public static readonly MethodInfo FirstOrDefault_1 = GetMethodInfo(System.Linq.Enumerable.FirstOrDefault);

public static readonly MethodInfo Single_1 = GetMethodInfo(System.Linq.Enumerable.Single);
public static readonly MethodInfo SingleOrDefault_1 = GetMethodInfo(System.Linq.Enumerable.SingleOrDefault);

public static readonly MethodInfo Any_1 = GetMethodInfo(System.Linq.Enumerable.Any);
}

public static class AsyncEnumerable
{
public static readonly MethodInfo First_1 = GetMethodInfo(System.Linq.AsyncEnumerable.FirstAsync);
public static readonly MethodInfo FirstOrDefault_1 = GetMethodInfo(System.Linq.AsyncEnumerable.FirstOrDefaultAsync);

public static readonly MethodInfo Single_1 = GetMethodInfo(System.Linq.AsyncEnumerable.SingleAsync);
public static readonly MethodInfo SingleOrDefault_1 = GetMethodInfo(System.Linq.AsyncEnumerable.SingleOrDefaultAsync);

public static readonly MethodInfo Any_1 = GetMethodInfo(System.Linq.AsyncEnumerable.AnyAsync);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace MongoFramework.Infrastructure.Linq
{
Expand Down Expand Up @@ -82,12 +85,39 @@ public TResult Execute<TResult>(Expression expression)
return (TResult)Execute(expression);
}

public object ExecuteAsync(Expression expression, CancellationToken cancellationToken = default)
{
var model = GetExecutionModel(expression, true);
var outputType = model.Serializer.ValueType;

//aka. ExecuteModelAsync<outputType>(model, cancellationToken)

Expression executor = Expression.Call(
Expression.Constant(this),
nameof(ExecuteModelAsync),
new[] { outputType },
Expression.Constant(model, typeof(AggregateExecutionModel)),
Expression.Constant(cancellationToken));

if (model.ResultTransformer != null)
{
executor = Expression.Invoke(
model.ResultTransformer,
Expression.Convert(executor, model.ResultTransformer.Parameters[0].Type),
Expression.Constant(cancellationToken)
);
}

var lambda = Expression.Lambda(executor);
return lambda.Compile().DynamicInvoke(null);
}

private IMongoCollection<TEntity> GetCollection()
{
return Connection.GetDatabase().GetCollection<TEntity>(EntityDefinition.CollectionName);
}

private AggregateExecutionModel GetExecutionModel(Expression expression)
private AggregateExecutionModel GetExecutionModel(Expression expression, bool isAsync = false)
{
//Use the official driver to do the heavy lifting on the query translation
var underlyingProvider = GetCollection().AsQueryable().Provider;
Expand Down Expand Up @@ -124,9 +154,10 @@ private AggregateExecutionModel GetExecutionModel(Expression expression)
var resultTransformer = translatedQueryType.GetProperty("ResultTransformer").GetValue(translatedQuery); //Type: Mixed (implements IResultTransformer (internal))
if (resultTransformer != null)
{
var resultTransformerType = resultTransformer.GetType();
var lambda = resultTransformerType.GetMethod("CreateAggregator").Invoke(resultTransformer, new[] { serializer.ValueType }); //Type: LambdaExpression
result.ResultTransformer = lambda as LambdaExpression;
result.ResultTransformer = ResultTransformers.Transform(expression, serializer.ValueType, isAsync) as LambdaExpression;

//Note: In the future this can change from the initial reflection to a `TryTransform` function where it checks the expression itself
// The reason we are doing this method first is to weed out the bugs and any core missing functionality.
}

return result;
Expand Down Expand Up @@ -172,6 +203,48 @@ private IEnumerable<TResult> ExecuteModel<TResult>(AggregateExecutionModel model
}
}

private async IAsyncEnumerable<TResult> ExecuteModelAsync<TResult>(AggregateExecutionModel model, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var serializer = model.Serializer as IBsonSerializer<TResult>;
var pipeline = PipelineDefinition<TEntity, TResult>.Create(model.Stages, serializer);

using (var diagnostics = DiagnosticRunner.Start<TEntity>(Connection, model))
{
IAsyncCursor<TResult> underlyingCursor;

try
{
underlyingCursor = await GetCollection().AggregateAsync(pipeline, cancellationToken: cancellationToken);
}
catch (Exception exception)
{
diagnostics.Error(exception);
throw;
}

var hasFirstResult = false;
while (await underlyingCursor.MoveNextAsync(cancellationToken))
{
if (!hasFirstResult)
{
hasFirstResult = true;
diagnostics.FirstReadResult<TResult>();
}

var resultBatch = underlyingCursor.Current;
foreach (var item in resultBatch)
{
if (item is TEntity entityItem && (model.ResultTransformer == null || model.ResultTransformer.ReturnType == typeof(TEntity)))
{
EntityProcessors.ProcessEntity(entityItem, Connection);
}

yield return item;
}
}
}
}

public string ToQuery(Expression expression)
{
var model = GetExecutionModel(expression);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using MongoDB.Driver.Linq;
using MongoFramework.Infrastructure.Diagnostics;
using MongoFramework.Infrastructure.Mapping;
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
Expand Down
Loading