Merge pull request #7 from PoweredSoft/feature/execution-options
execution options.
This commit is contained in:
commit
5be4b7e3ac
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace PoweredSoft.DynamicQuery.Core
|
||||||
|
{
|
||||||
|
public interface IQueryExecutionOptionsInterceptor : IQueryInterceptor
|
||||||
|
{
|
||||||
|
IQueryExecutionOptions InterceptQueryExecutionOptions(IQueryable queryable, IQueryExecutionOptions current);
|
||||||
|
}
|
||||||
|
}
|
8
PoweredSoft.DynamicQuery.Core/IQueryExecutionOptions.cs
Normal file
8
PoweredSoft.DynamicQuery.Core/IQueryExecutionOptions.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace PoweredSoft.DynamicQuery.Core
|
||||||
|
{
|
||||||
|
public interface IQueryExecutionOptions
|
||||||
|
{
|
||||||
|
bool GroupByInMemory { get; set; }
|
||||||
|
bool GroupByInMemoryNullCheck { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -15,11 +15,15 @@ namespace PoweredSoft.DynamicQuery.Core
|
|||||||
{
|
{
|
||||||
IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria);
|
IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria);
|
||||||
IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria);
|
IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria);
|
||||||
|
IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options);
|
||||||
|
IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IQueryHandlerAsync : IInterceptableQueryHandler
|
public interface IQueryHandlerAsync : IInterceptableQueryHandler
|
||||||
{
|
{
|
||||||
Task<IQueryExecutionResult<TSource>> ExecuteAsync<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default(CancellationToken));
|
Task<IQueryExecutionResult<TSource>> ExecuteAsync<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default);
|
||||||
Task<IQueryExecutionResult<TRecord>> ExecuteAsync<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default(CancellationToken));
|
Task<IQueryExecutionResult<TRecord>> ExecuteAsync<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default);
|
||||||
|
Task<IQueryExecutionResult<TSource>> ExecuteAsync<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options, CancellationToken cancellationToken = default);
|
||||||
|
Task<IQueryExecutionResult<TRecord>> ExecuteAsync<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
PoweredSoft.DynamicQuery.Core/QueryExecutionOptions.cs
Normal file
8
PoweredSoft.DynamicQuery.Core/QueryExecutionOptions.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace PoweredSoft.DynamicQuery.Core
|
||||||
|
{
|
||||||
|
public class QueryExecutionOptions : IQueryExecutionOptions
|
||||||
|
{
|
||||||
|
public bool GroupByInMemory { get; set; } = false;
|
||||||
|
public bool GroupByInMemoryNullCheck { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using PoweredSoft.DynamicQuery.Core;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using PoweredSoft.DynamicQuery.Test.Mock;
|
using PoweredSoft.DynamicQuery.Test.Mock;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -14,7 +15,7 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
{
|
{
|
||||||
public IAggregate InterceptAggregate(IAggregate aggregate) => new Aggregate
|
public IAggregate InterceptAggregate(IAggregate aggregate) => new Aggregate
|
||||||
{
|
{
|
||||||
Path = "Item.Price",
|
Path = "Price",
|
||||||
Type = AggregateType.Avg
|
Type = AggregateType.Avg
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -24,10 +25,12 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
{
|
{
|
||||||
MockContextFactory.SeedAndTestContextFor("AggregatorInterceptorTests_Simple", TestSeeders.SimpleSeedScenario, ctx =>
|
MockContextFactory.SeedAndTestContextFor("AggregatorInterceptorTests_Simple", TestSeeders.SimpleSeedScenario, ctx =>
|
||||||
{
|
{
|
||||||
var expected = ctx.OrderItems.GroupBy(t => true).Select(t => new
|
var expected = ctx.Items
|
||||||
{
|
.GroupBy(t => true)
|
||||||
PriceAtTheTime = t.Average(t2 => t2.Item.Price)
|
.Select(t => new
|
||||||
}).First();
|
{
|
||||||
|
PriceAtTheTime = t.Average(t2 => t2.Price)
|
||||||
|
}).First();
|
||||||
|
|
||||||
var criteria = new QueryCriteria();
|
var criteria = new QueryCriteria();
|
||||||
criteria.Aggregates.Add(new Aggregate
|
criteria.Aggregates.Add(new Aggregate
|
||||||
@ -37,7 +40,7 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
});
|
});
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
queryHandler.AddInterceptor(new MockAggregateInterceptor());
|
queryHandler.AddInterceptor(new MockAggregateInterceptor());
|
||||||
var result = queryHandler.Execute(ctx.OrderItems, criteria);
|
var result = queryHandler.Execute(ctx.Items, criteria);
|
||||||
Assert.Equal(expected.PriceAtTheTime, result.Aggregates.First().Value);
|
Assert.Equal(expected.PriceAtTheTime, result.Aggregates.First().Value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using PoweredSoft.DynamicQuery.Core;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using PoweredSoft.DynamicQuery.Test.Mock;
|
using PoweredSoft.DynamicQuery.Test.Mock;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -27,10 +28,11 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
ItemQuantityAverage = t.Average(t2 => t2.Quantity),
|
ItemQuantityAverage = t.Average(t2 => t2.Quantity),
|
||||||
ItemQuantitySum = t.Sum(t2 => t2.Quantity),
|
ItemQuantitySum = t.Sum(t2 => t2.Quantity),
|
||||||
AvgOfPrice = t.Average(t2 => t2.PriceAtTheTime),
|
AvgOfPrice = t.Average(t2 => t2.PriceAtTheTime),
|
||||||
|
/* not supported by ef core 3.0
|
||||||
First = t.First(),
|
First = t.First(),
|
||||||
FirstOrDefault = t.FirstOrDefault(),
|
FirstOrDefault = t.FirstOrDefault(),
|
||||||
Last = t.Last(),
|
Last = t.Last(),
|
||||||
LastOrDefault = t.LastOrDefault()
|
LastOrDefault = t.LastOrDefault()*/
|
||||||
})
|
})
|
||||||
.First();
|
.First();
|
||||||
|
|
||||||
@ -45,21 +47,28 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
new Aggregate { Type = AggregateType.Avg, Path = "PriceAtTheTime"},
|
new Aggregate { Type = AggregateType.Avg, Path = "PriceAtTheTime"},
|
||||||
new Aggregate { Type = AggregateType.Min, Path = "Quantity"},
|
new Aggregate { Type = AggregateType.Min, Path = "Quantity"},
|
||||||
new Aggregate { Type = AggregateType.Max, Path = "Quantity" },
|
new Aggregate { Type = AggregateType.Max, Path = "Quantity" },
|
||||||
|
/*not support by ef core 3.0
|
||||||
new Aggregate { Type = AggregateType.First },
|
new Aggregate { Type = AggregateType.First },
|
||||||
new Aggregate { Type = AggregateType.FirstOrDefault },
|
new Aggregate { Type = AggregateType.FirstOrDefault },
|
||||||
new Aggregate { Type = AggregateType.Last },
|
new Aggregate { Type = AggregateType.Last },
|
||||||
new Aggregate { Type = AggregateType.LastOrDefault },
|
new Aggregate { Type = AggregateType.LastOrDefault },
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
var result = queryHandler.Execute(ctx.OrderItems, criteria);
|
var result = queryHandler.Execute(ctx.OrderItems, criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true
|
||||||
|
});
|
||||||
|
|
||||||
var aggCount = result.Aggregates.First(t => t.Type == AggregateType.Count);
|
var aggCount = result.Aggregates.First(t => t.Type == AggregateType.Count);
|
||||||
|
|
||||||
|
/*
|
||||||
var aggFirst = result.Aggregates.First(t => t.Type == AggregateType.First);
|
var aggFirst = result.Aggregates.First(t => t.Type == AggregateType.First);
|
||||||
var aggFirstOrDefault = result.Aggregates.First(t => t.Type == AggregateType.FirstOrDefault);
|
var aggFirstOrDefault = result.Aggregates.First(t => t.Type == AggregateType.FirstOrDefault);
|
||||||
var aggLast = result.Aggregates.First(t => t.Type == AggregateType.Last);
|
var aggLast = result.Aggregates.First(t => t.Type == AggregateType.Last);
|
||||||
var aggLastOrDefault = result.Aggregates.First(t => t.Type == AggregateType.LastOrDefault);
|
var aggLastOrDefault = result.Aggregates.First(t => t.Type == AggregateType.LastOrDefault);*/
|
||||||
|
|
||||||
var aggItemQuantityMin = result.Aggregates.First(t => t.Type == AggregateType.Min && t.Path == "Quantity");
|
var aggItemQuantityMin = result.Aggregates.First(t => t.Type == AggregateType.Min && t.Path == "Quantity");
|
||||||
var aggItemQuantityMax = result.Aggregates.First(t => t.Type == AggregateType.Max && t.Path == "Quantity");
|
var aggItemQuantityMax = result.Aggregates.First(t => t.Type == AggregateType.Max && t.Path == "Quantity");
|
||||||
@ -68,10 +77,11 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
var aggAvgOfPrice = result.Aggregates.First(t => t.Type == AggregateType.Avg && t.Path == "PriceAtTheTime");
|
var aggAvgOfPrice = result.Aggregates.First(t => t.Type == AggregateType.Avg && t.Path == "PriceAtTheTime");
|
||||||
|
|
||||||
Assert.Equal(shouldResult.Count, aggCount.Value);
|
Assert.Equal(shouldResult.Count, aggCount.Value);
|
||||||
|
/*
|
||||||
Assert.Equal(shouldResult.First?.Id, (aggFirst.Value as OrderItem)?.Id);
|
Assert.Equal(shouldResult.First?.Id, (aggFirst.Value as OrderItem)?.Id);
|
||||||
Assert.Equal(shouldResult.FirstOrDefault?.Id, (aggFirstOrDefault.Value as OrderItem)?.Id);
|
Assert.Equal(shouldResult.FirstOrDefault?.Id, (aggFirstOrDefault.Value as OrderItem)?.Id);
|
||||||
Assert.Equal(shouldResult.Last?.Id, (aggLast.Value as OrderItem)?.Id);
|
Assert.Equal(shouldResult.Last?.Id, (aggLast.Value as OrderItem)?.Id);
|
||||||
Assert.Equal(shouldResult.LastOrDefault?.Id, (aggLastOrDefault.Value as OrderItem)?.Id);
|
Assert.Equal(shouldResult.LastOrDefault?.Id, (aggLastOrDefault.Value as OrderItem)?.Id);*/
|
||||||
|
|
||||||
Assert.Equal(shouldResult.ItemQuantityAverage, aggItemQuantityAverage.Value);
|
Assert.Equal(shouldResult.ItemQuantityAverage, aggItemQuantityAverage.Value);
|
||||||
Assert.Equal(shouldResult.ItemQuantitySum, aggItemQuantitySum.Value);
|
Assert.Equal(shouldResult.ItemQuantitySum, aggItemQuantitySum.Value);
|
||||||
@ -113,7 +123,11 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
};
|
};
|
||||||
|
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
var result = queryHandler.Execute(ctx.OrderItems, criteria);
|
var queryable = ctx.OrderItems.Include(t => t.Order);
|
||||||
|
var result = queryHandler.Execute(queryable, criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true
|
||||||
|
});
|
||||||
|
|
||||||
var groupedResult = result as IQueryExecutionGroupResult<OrderItem>;
|
var groupedResult = result as IQueryExecutionGroupResult<OrderItem>;
|
||||||
Assert.NotNull(groupedResult);
|
Assert.NotNull(groupedResult);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using PoweredSoft.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PoweredSoft.Data;
|
||||||
using PoweredSoft.Data.EntityFrameworkCore;
|
using PoweredSoft.Data.EntityFrameworkCore;
|
||||||
using PoweredSoft.DynamicQuery.Core;
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using PoweredSoft.DynamicQuery.Extensions;
|
using PoweredSoft.DynamicQuery.Extensions;
|
||||||
@ -9,6 +10,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using static PoweredSoft.DynamicQuery.Test.GroupInterceptorTests;
|
||||||
|
|
||||||
namespace PoweredSoft.DynamicQuery.Test
|
namespace PoweredSoft.DynamicQuery.Test
|
||||||
{
|
{
|
||||||
@ -65,7 +67,11 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
};
|
};
|
||||||
var asyncService = new AsyncQueryableService(new[] { new AsyncQueryableHandlerService() });
|
var asyncService = new AsyncQueryableService(new[] { new AsyncQueryableHandlerService() });
|
||||||
var queryHandler = new QueryHandlerAsync(asyncService);
|
var queryHandler = new QueryHandlerAsync(asyncService);
|
||||||
var result = await queryHandler.ExecuteAsync(ctx.OrderItems, criteria);
|
var result = await queryHandler.ExecuteAsync(ctx.OrderItems.Include(t => t.Order.Customer), criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true
|
||||||
|
});
|
||||||
|
|
||||||
var groups = result.GroupedResult().Groups;
|
var groups = result.GroupedResult().Groups;
|
||||||
|
|
||||||
// validate group and aggregates of groups.
|
// validate group and aggregates of groups.
|
||||||
@ -165,6 +171,68 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
Assert.Equal(resultShouldMatch, result.Data);
|
Assert.Equal(resultShouldMatch, result.Data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithGroupingInterceptorOptions()
|
||||||
|
{
|
||||||
|
MockContextFactory.SeedAndTestContextFor("AsyncTests_WithGroupingInterceptorOptions", TestSeeders.SimpleSeedScenario, async ctx =>
|
||||||
|
{
|
||||||
|
var shouldResults = ctx.OrderItems
|
||||||
|
.GroupBy(t => t.Order.CustomerId)
|
||||||
|
.Select(t => new
|
||||||
|
{
|
||||||
|
GroupValue = t.Key,
|
||||||
|
Count = t.Count(),
|
||||||
|
ItemQuantityAverage = t.Average(t2 => t2.Quantity),
|
||||||
|
ItemQuantitySum = t.Sum(t2 => t2.Quantity),
|
||||||
|
AvgOfPrice = t.Average(t2 => t2.PriceAtTheTime)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// query handler that is empty should be the same as running to list.
|
||||||
|
var criteria = new QueryCriteria()
|
||||||
|
{
|
||||||
|
Groups = new List<IGroup>
|
||||||
|
{
|
||||||
|
new Group { Path = "Order.CustomerId" }
|
||||||
|
},
|
||||||
|
Aggregates = new List<Core.IAggregate>
|
||||||
|
{
|
||||||
|
new Aggregate { Type = AggregateType.Count },
|
||||||
|
new Aggregate { Type = AggregateType.Avg, Path = "Quantity" },
|
||||||
|
new Aggregate { Type = AggregateType.Sum, Path = "Quantity" },
|
||||||
|
new Aggregate { Type = AggregateType.Avg, Path = "PriceAtTheTime"}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var asyncService = new AsyncQueryableService(new[] { new AsyncQueryableHandlerService() });
|
||||||
|
var queryHandler = new QueryHandlerAsync(asyncService);
|
||||||
|
queryHandler.AddInterceptor(new MockQueryExecutionOptionsInterceptor());
|
||||||
|
var result = await queryHandler.ExecuteAsync(ctx.OrderItems.Include(t => t.Order.Customer), criteria);
|
||||||
|
|
||||||
|
var groups = result.GroupedResult().Groups;
|
||||||
|
|
||||||
|
// validate group and aggregates of groups.
|
||||||
|
Assert.Equal(groups.Count, shouldResults.Count);
|
||||||
|
Assert.All(groups, g =>
|
||||||
|
{
|
||||||
|
var index = groups.IndexOf(g);
|
||||||
|
var shouldResult = shouldResults[index];
|
||||||
|
|
||||||
|
// validate the group value.
|
||||||
|
Assert.Equal(g.GroupValue, shouldResult.GroupValue);
|
||||||
|
|
||||||
|
// validate the group aggregates.
|
||||||
|
var aggCount = g.Aggregates.First(t => t.Type == AggregateType.Count);
|
||||||
|
var aggItemQuantityAverage = g.Aggregates.First(t => t.Type == AggregateType.Avg && t.Path == "Quantity");
|
||||||
|
var aggItemQuantitySum = g.Aggregates.First(t => t.Type == AggregateType.Sum && t.Path == "Quantity");
|
||||||
|
var aggAvgOfPrice = g.Aggregates.First(t => t.Type == AggregateType.Avg && t.Path == "PriceAtTheTime");
|
||||||
|
Assert.Equal(shouldResult.Count, aggCount.Value);
|
||||||
|
Assert.Equal(shouldResult.ItemQuantityAverage, aggItemQuantityAverage.Value);
|
||||||
|
Assert.Equal(shouldResult.ItemQuantitySum, aggItemQuantitySum.Value);
|
||||||
|
Assert.Equal(shouldResult.AvgOfPrice, aggAvgOfPrice.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using PoweredSoft.DynamicQuery.Core;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using PoweredSoft.DynamicQuery.Extensions;
|
using PoweredSoft.DynamicQuery.Extensions;
|
||||||
using PoweredSoft.DynamicQuery.Test.Mock;
|
using PoweredSoft.DynamicQuery.Test.Mock;
|
||||||
using System;
|
using System;
|
||||||
@ -9,7 +10,7 @@ using Xunit;
|
|||||||
|
|
||||||
namespace PoweredSoft.DynamicQuery.Test
|
namespace PoweredSoft.DynamicQuery.Test
|
||||||
{
|
{
|
||||||
public class GroupInterceptorTests
|
public partial class GroupInterceptorTests
|
||||||
{
|
{
|
||||||
private class MockGroupInterceptor : IGroupInterceptor
|
private class MockGroupInterceptor : IGroupInterceptor
|
||||||
{
|
{
|
||||||
@ -37,7 +38,34 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
criteria.Groups.Add(new Group { Path = "CustomerFirstName" });
|
criteria.Groups.Add(new Group { Path = "CustomerFirstName" });
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
queryHandler.AddInterceptor(new MockGroupInterceptor());
|
queryHandler.AddInterceptor(new MockGroupInterceptor());
|
||||||
var result = queryHandler.Execute(ctx.Orders, criteria);
|
var result = queryHandler.Execute(ctx.Orders.Include(t => t.Customer), criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var groupedResult = result.GroupedResult();
|
||||||
|
var actual = groupedResult.Groups.Select(t => t.GroupValue).ToList();
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithInterptorSimple()
|
||||||
|
{
|
||||||
|
MockContextFactory.SeedAndTestContextFor("GroupInterceptorTests_Simple", TestSeeders.SimpleSeedScenario, ctx =>
|
||||||
|
{
|
||||||
|
var expected = ctx.Orders
|
||||||
|
.OrderBy(t => t.Customer.FirstName)
|
||||||
|
.GroupBy(t => t.Customer.FirstName)
|
||||||
|
.Select(t => t.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var criteria = new QueryCriteria();
|
||||||
|
criteria.Groups.Add(new Group { Path = "CustomerFirstName" });
|
||||||
|
var queryHandler = new QueryHandler();
|
||||||
|
queryHandler.AddInterceptor(new MockGroupInterceptor());
|
||||||
|
queryHandler.AddInterceptor(new MockQueryExecutionOptionsInterceptor());
|
||||||
|
var result = queryHandler.Execute(ctx.Orders.Include(t => t.Customer), criteria);
|
||||||
|
|
||||||
var groupedResult = result.GroupedResult();
|
var groupedResult = result.GroupedResult();
|
||||||
var actual = groupedResult.Groups.Select(t => t.GroupValue).ToList();
|
var actual = groupedResult.Groups.Select(t => t.GroupValue).ToList();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using PoweredSoft.DynamicQuery.Core;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
using PoweredSoft.DynamicQuery.Extensions;
|
using PoweredSoft.DynamicQuery.Extensions;
|
||||||
using PoweredSoft.DynamicQuery.Test.Mock;
|
using PoweredSoft.DynamicQuery.Test.Mock;
|
||||||
using System;
|
using System;
|
||||||
@ -18,23 +19,32 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
{
|
{
|
||||||
MockContextFactory.SeedAndTestContextFor("GroupTests_Simple", TestSeeders.SimpleSeedScenario, ctx =>
|
MockContextFactory.SeedAndTestContextFor("GroupTests_Simple", TestSeeders.SimpleSeedScenario, ctx =>
|
||||||
{
|
{
|
||||||
var shouldResult = ctx.Orders.OrderBy(t => t.Customer).GroupBy(t => t.Customer).Select(t => new
|
var shouldResult = ctx.Orders
|
||||||
{
|
.OrderBy(t => t.CustomerId)
|
||||||
Customer = t.Key,
|
.ToList()
|
||||||
Orders = t.ToList()
|
.GroupBy(t => t.CustomerId)
|
||||||
}).ToList();
|
.Select(t => new
|
||||||
|
{
|
||||||
|
CustomerId = t.Key,
|
||||||
|
Orders = t.ToList()
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// query handler that is empty should be the same as running to list.
|
// query handler that is empty should be the same as running to list.
|
||||||
var criteria = new QueryCriteria()
|
var criteria = new QueryCriteria()
|
||||||
{
|
{
|
||||||
Groups = new List<IGroup>
|
Groups = new List<IGroup>
|
||||||
{
|
{
|
||||||
new Group { Path = "Customer" }
|
new Group { Path = "CustomerId" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
var result = queryHandler.Execute(ctx.Orders, criteria);
|
var result = queryHandler.Execute(ctx.Orders, criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true,
|
||||||
|
GroupByInMemoryNullCheck = false
|
||||||
|
});
|
||||||
var groupedResult = result.GroupedResult();
|
var groupedResult = result.GroupedResult();
|
||||||
|
|
||||||
// top level should have same amount of group levels.
|
// top level should have same amount of group levels.
|
||||||
@ -43,7 +53,7 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
{
|
{
|
||||||
var expected = shouldResult[0];
|
var expected = shouldResult[0];
|
||||||
var actual = groupedResult.Groups[0];
|
var actual = groupedResult.Groups[0];
|
||||||
Assert.Equal(expected.Customer.Id, (actual.GroupValue as Customer).Id);
|
Assert.Equal(expected.CustomerId, actual.GroupValue);
|
||||||
|
|
||||||
var expectedOrderIds = expected.Orders.Select(t => t.Id).ToList();
|
var expectedOrderIds = expected.Orders.Select(t => t.Id).ToList();
|
||||||
var actualOrderIds = actual.Data.Cast<Order>().Select(t => t.Id).ToList();
|
var actualOrderIds = actual.Data.Cast<Order>().Select(t => t.Id).ToList();
|
||||||
@ -71,7 +81,10 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
};
|
};
|
||||||
|
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
var result = queryHandler.Execute(ctx.Tickets, criteria);
|
var result = queryHandler.Execute(ctx.Tickets, criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true
|
||||||
|
});
|
||||||
|
|
||||||
var groupedResult = result.GroupedResult();
|
var groupedResult = result.GroupedResult();
|
||||||
|
|
||||||
@ -106,7 +119,11 @@ namespace PoweredSoft.DynamicQuery.Test
|
|||||||
var interceptor = new InterceptorsWithGrouping();
|
var interceptor = new InterceptorsWithGrouping();
|
||||||
var queryHandler = new QueryHandler();
|
var queryHandler = new QueryHandler();
|
||||||
queryHandler.AddInterceptor(interceptor);
|
queryHandler.AddInterceptor(interceptor);
|
||||||
var result = queryHandler.Execute<Ticket, InterceptorWithGroupingFakeModel>(ctx.Tickets, criteria);
|
var result = queryHandler.Execute<Ticket, InterceptorWithGroupingFakeModel>(ctx.Tickets, criteria, new QueryExecutionOptions
|
||||||
|
{
|
||||||
|
GroupByInMemory = true
|
||||||
|
});
|
||||||
|
|
||||||
Assert.Equal(4, interceptor.Count);
|
Assert.Equal(4, interceptor.Count);
|
||||||
Assert.True(interceptor.Test);
|
Assert.True(interceptor.Test);
|
||||||
Assert.True(interceptor.Test2);
|
Assert.True(interceptor.Test2);
|
||||||
|
@ -14,12 +14,9 @@ namespace PoweredSoft.DynamicQuery.Test.Mock
|
|||||||
public static void TestContextFor(string testName, Action<MockContext> action)
|
public static void TestContextFor(string testName, Action<MockContext> action)
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<MockContext>()
|
var options = new DbContextOptionsBuilder<MockContext>()
|
||||||
.ConfigureWarnings(warnings =>
|
|
||||||
warnings.Ignore(RelationalEventId.QueryClientEvaluationWarning)
|
|
||||||
)
|
|
||||||
.UseInMemoryDatabase(databaseName: testName).Options;
|
.UseInMemoryDatabase(databaseName: testName).Options;
|
||||||
|
|
||||||
using (var ctx = new MockContext(options))
|
using var ctx = new MockContext(options);
|
||||||
action(ctx);
|
action(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Query.Internal;
|
||||||
|
using PoweredSoft.DynamicQuery.Core;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace PoweredSoft.DynamicQuery.Test
|
||||||
|
{
|
||||||
|
public partial class GroupInterceptorTests
|
||||||
|
{
|
||||||
|
public class MockQueryExecutionOptionsInterceptor : IQueryExecutionOptionsInterceptor
|
||||||
|
{
|
||||||
|
public IQueryExecutionOptions InterceptQueryExecutionOptions(IQueryable queryable, IQueryExecutionOptions current)
|
||||||
|
{
|
||||||
|
if (queryable.Provider is IAsyncQueryProvider)
|
||||||
|
{
|
||||||
|
current.GroupByInMemory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,12 +8,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bogus" Version="28.3.2" />
|
<PackageReference Include="Bogus" Version="28.3.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
|
||||||
<PackageReference Include="PoweredSoft.Data.EntityFrameworkCore" Version="1.1.3" />
|
<PackageReference Include="PoweredSoft.Data.EntityFrameworkCore" Version="2.0.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
@ -43,14 +43,23 @@ namespace PoweredSoft.DynamicQuery
|
|||||||
ApplySorting<TSource>();
|
ApplySorting<TSource>();
|
||||||
ApplyPaging<TSource>();
|
ApplyPaging<TSource>();
|
||||||
|
|
||||||
// create group & select expression.
|
if (Options.GroupByInMemory)
|
||||||
CurrentQueryable = CurrentQueryable.GroupBy(QueryableUnderlyingType, gb => finalGroups.ForEach((fg, index) => gb.Path(fg.Path, $"Key_{index}")));
|
CurrentQueryable = CurrentQueryable.ToObjectList().Cast<TSource>().AsQueryable();
|
||||||
|
|
||||||
|
CurrentQueryable = CurrentQueryable.GroupBy(QueryableUnderlyingType, gb =>
|
||||||
|
{
|
||||||
|
gb.NullChecking(Options.GroupByInMemory ? Options.GroupByInMemoryNullCheck : false);
|
||||||
|
finalGroups.ForEach((fg, index) => gb.Path(fg.Path, $"Key_{index}"));
|
||||||
|
});
|
||||||
|
|
||||||
CurrentQueryable = CurrentQueryable.Select(sb =>
|
CurrentQueryable = CurrentQueryable.Select(sb =>
|
||||||
{
|
{
|
||||||
|
sb.NullChecking(Options.GroupByInMemory ? Options.GroupByInMemoryNullCheck : false);
|
||||||
finalGroups.ForEach((fg, index) => sb.Key($"Key_{index}", $"Key_{index}"));
|
finalGroups.ForEach((fg, index) => sb.Key($"Key_{index}", $"Key_{index}"));
|
||||||
sb.ToList("Records");
|
sb.ToList("Records");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// loop through the grouped records.
|
// loop through the grouped records.
|
||||||
var groupRecords = CurrentQueryable.ToDynamicClassList();
|
var groupRecords = CurrentQueryable.ToDynamicClassList();
|
||||||
|
|
||||||
@ -118,13 +127,25 @@ namespace PoweredSoft.DynamicQuery
|
|||||||
|
|
||||||
public IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria)
|
public IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria)
|
||||||
{
|
{
|
||||||
Reset(queryable, criteria);
|
Reset(queryable, criteria, new QueryExecutionOptions());
|
||||||
return FinalExecute<TSource, TSource>();
|
return FinalExecute<TSource, TSource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria)
|
public IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria)
|
||||||
{
|
{
|
||||||
Reset(queryable, criteria);
|
Reset(queryable, criteria, new QueryExecutionOptions());
|
||||||
|
return FinalExecute<TSource, TRecord>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryExecutionResult<TSource> Execute<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options)
|
||||||
|
{
|
||||||
|
Reset(queryable, criteria, options);
|
||||||
|
return FinalExecute<TSource, TSource>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryExecutionResult<TRecord> Execute<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options)
|
||||||
|
{
|
||||||
|
Reset(queryable, criteria, options);
|
||||||
return FinalExecute<TSource, TRecord>();
|
return FinalExecute<TSource, TRecord>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ namespace PoweredSoft.DynamicQuery
|
|||||||
AsyncQueryableService = asyncQueryableService;
|
AsyncQueryableService = asyncQueryableService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Task<IQueryExecutionResult<TRecord>> FinalExecuteAsync<TSource, TRecord>(CancellationToken cancellationToken = default(CancellationToken))
|
protected virtual Task<IQueryExecutionResult<TRecord>> FinalExecuteAsync<TSource, TRecord>(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
CommonBeforeExecute<TSource>();
|
CommonBeforeExecute<TSource>();
|
||||||
return HasGrouping ? ExecuteAsyncGrouping<TSource, TRecord>(cancellationToken) : ExecuteAsyncNoGrouping<TSource, TRecord>(cancellationToken);
|
return HasGrouping ? ExecuteAsyncGrouping<TSource, TRecord>(cancellationToken) : ExecuteAsyncNoGrouping<TSource, TRecord>(cancellationToken);
|
||||||
@ -50,16 +50,44 @@ namespace PoweredSoft.DynamicQuery
|
|||||||
ApplySorting<TSource>();
|
ApplySorting<TSource>();
|
||||||
ApplyPaging<TSource>();
|
ApplyPaging<TSource>();
|
||||||
|
|
||||||
// create group & select expression.
|
List<DynamicClass> groupRecords;
|
||||||
CurrentQueryable = CurrentQueryable.GroupBy(QueryableUnderlyingType, gb => finalGroups.ForEach((fg, index) => gb.Path(fg.Path, $"Key_{index}")));
|
|
||||||
CurrentQueryable = CurrentQueryable.Select(sb =>
|
|
||||||
{
|
|
||||||
finalGroups.ForEach((fg, index) => sb.Key($"Key_{index}", $"Key_{index}"));
|
|
||||||
sb.ToList("Records");
|
|
||||||
});
|
|
||||||
|
|
||||||
// loop through the grouped records.
|
if (Options.GroupByInMemory)
|
||||||
var groupRecords = await AsyncQueryableService.ToListAsync(CurrentQueryable.Cast<DynamicClass>(), cancellationToken);
|
{
|
||||||
|
CurrentQueryable = CurrentQueryable.ToObjectList().Cast<TSource>().AsQueryable();
|
||||||
|
|
||||||
|
// create group & select expression.
|
||||||
|
CurrentQueryable = CurrentQueryable.GroupBy(QueryableUnderlyingType, gb =>
|
||||||
|
{
|
||||||
|
gb.NullChecking(Options.GroupByInMemory ? Options.GroupByInMemoryNullCheck : false);
|
||||||
|
finalGroups.ForEach((fg, index) => gb.Path(fg.Path, $"Key_{index}"));
|
||||||
|
});
|
||||||
|
CurrentQueryable = CurrentQueryable.Select(sb =>
|
||||||
|
{
|
||||||
|
sb.NullChecking(Options.GroupByInMemory ? Options.GroupByInMemoryNullCheck : false);
|
||||||
|
finalGroups.ForEach((fg, index) => sb.Key($"Key_{index}", $"Key_{index}"));
|
||||||
|
sb.ToList("Records");
|
||||||
|
});
|
||||||
|
|
||||||
|
// loop through the grouped records.
|
||||||
|
groupRecords = CurrentQueryable.Cast<DynamicClass>().ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// create group & select expression.
|
||||||
|
CurrentQueryable = CurrentQueryable.GroupBy(QueryableUnderlyingType, gb =>
|
||||||
|
{
|
||||||
|
finalGroups.ForEach((fg, index) => gb.Path(fg.Path, $"Key_{index}"));
|
||||||
|
});
|
||||||
|
CurrentQueryable = CurrentQueryable.Select(sb =>
|
||||||
|
{
|
||||||
|
finalGroups.ForEach((fg, index) => sb.Key($"Key_{index}", $"Key_{index}"));
|
||||||
|
sb.ToList("Records");
|
||||||
|
});
|
||||||
|
|
||||||
|
// loop through the grouped records.
|
||||||
|
groupRecords = await AsyncQueryableService.ToListAsync(CurrentQueryable.Cast<DynamicClass>(), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
// now join them into logical collections
|
// now join them into logical collections
|
||||||
var lastLists = new List<(List<TSource> entities, IGroupQueryResult<TRecord> group)>();
|
var lastLists = new List<(List<TSource> entities, IGroupQueryResult<TRecord> group)>();
|
||||||
@ -127,15 +155,27 @@ namespace PoweredSoft.DynamicQuery
|
|||||||
return finalResult;
|
return finalResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IQueryExecutionResult<TSource>> ExecuteAsync<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default(CancellationToken))
|
public Task<IQueryExecutionResult<TSource>> ExecuteAsync<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Reset(queryable, criteria);
|
Reset(queryable, criteria, new QueryExecutionOptions());
|
||||||
return FinalExecuteAsync<TSource, TSource>(cancellationToken);
|
return FinalExecuteAsync<TSource, TSource>(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IQueryExecutionResult<TRecord>> ExecuteAsync<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default(CancellationToken))
|
public Task<IQueryExecutionResult<TRecord>> ExecuteAsync<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Reset(queryable, criteria);
|
Reset(queryable, criteria, new QueryExecutionOptions());
|
||||||
|
return FinalExecuteAsync<TSource, TRecord>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IQueryExecutionResult<TSource>> ExecuteAsync<TSource>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Reset(queryable, criteria, options);
|
||||||
|
return FinalExecuteAsync<TSource, TSource>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IQueryExecutionResult<TRecord>> ExecuteAsync<TSource, TRecord>(IQueryable<TSource> queryable, IQueryCriteria criteria, IQueryExecutionOptions options, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Reset(queryable, criteria, options);
|
||||||
return FinalExecuteAsync<TSource, TRecord>(cancellationToken);
|
return FinalExecuteAsync<TSource, TRecord>(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,24 +20,36 @@ namespace PoweredSoft.DynamicQuery
|
|||||||
protected IQueryCriteria Criteria { get; set; }
|
protected IQueryCriteria Criteria { get; set; }
|
||||||
protected IQueryable QueryableAtStart { get; private set; }
|
protected IQueryable QueryableAtStart { get; private set; }
|
||||||
protected IQueryable CurrentQueryable { get; set; }
|
protected IQueryable CurrentQueryable { get; set; }
|
||||||
|
protected IQueryExecutionOptions Options { get; private set; }
|
||||||
|
|
||||||
protected Type QueryableUnderlyingType => QueryableAtStart.ElementType;
|
protected Type QueryableUnderlyingType => QueryableAtStart.ElementType;
|
||||||
protected bool HasGrouping => Criteria.Groups?.Any() == true;
|
protected bool HasGrouping => Criteria.Groups?.Any() == true;
|
||||||
protected bool HasPaging => Criteria.PageSize.HasValue && Criteria.PageSize > 0;
|
protected bool HasPaging => Criteria.PageSize.HasValue && Criteria.PageSize > 0;
|
||||||
|
|
||||||
protected virtual void Reset(IQueryable queryable, IQueryCriteria criteria)
|
protected virtual void Reset(IQueryable queryable, IQueryCriteria criteria, IQueryExecutionOptions options)
|
||||||
{
|
{
|
||||||
Criteria = criteria ?? throw new ArgumentNullException("criteria");
|
Criteria = criteria ?? throw new ArgumentNullException("criteria");
|
||||||
QueryableAtStart = queryable ?? throw new ArgumentNullException("queryable");
|
QueryableAtStart = queryable ?? throw new ArgumentNullException("queryable");
|
||||||
CurrentQueryable = QueryableAtStart;
|
CurrentQueryable = QueryableAtStart;
|
||||||
|
Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void CommonBeforeExecute<TSource>()
|
protected virtual void CommonBeforeExecute<TSource>()
|
||||||
{
|
{
|
||||||
|
ApplyQueryExecutionOptionIncerceptors();
|
||||||
ApplyIncludeStrategyInterceptors<TSource>();
|
ApplyIncludeStrategyInterceptors<TSource>();
|
||||||
ApplyBeforeFilterInterceptors<TSource>();
|
ApplyBeforeFilterInterceptors<TSource>();
|
||||||
ApplyFilters<TSource>();
|
ApplyFilters<TSource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void ApplyQueryExecutionOptionIncerceptors()
|
||||||
|
{
|
||||||
|
Options = Interceptors
|
||||||
|
.Where(t => t is IQueryExecutionOptionsInterceptor)
|
||||||
|
.Cast<IQueryExecutionOptionsInterceptor>()
|
||||||
|
.Aggregate(Options, (prev, curr) => curr.InterceptQueryExecutionOptions(CurrentQueryable, prev));
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void AddInterceptor(IQueryInterceptor interceptor)
|
public virtual void AddInterceptor(IQueryInterceptor interceptor)
|
||||||
{
|
{
|
||||||
if (interceptor == null) throw new ArgumentNullException("interceptor");
|
if (interceptor == null) throw new ArgumentNullException("interceptor");
|
||||||
|
Loading…
Reference in New Issue
Block a user