Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="IvAt.CommonFramework" Version="2.0.3" />
<PackageVersion Include="IvAt.CommonFramework.DependencyInjection" Version="2.0.3" />
<PackageVersion Include="IvAt.CommonFramework" Version="2.0.4" />
<PackageVersion Include="IvAt.CommonFramework.DependencyInjection" Version="2.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
using Microsoft.EntityFrameworkCore;
using GenericQueryable.DependencyInjection;

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace GenericQueryable.EntityFramework;

public static class DbContextOptionsBuilderExtensions
{
public static DbContextOptionsBuilder UseGenericQueryable(this DbContextOptionsBuilder optionsBuilder)
public static DbContextOptionsBuilder UseGenericQueryable(this DbContextOptionsBuilder optionsBuilder, Action<IGenericQueryableSetup>? setupAction = null)
{
var extension = optionsBuilder.Options.FindExtension<GenericQueryableOptionsExtension>()
?? new GenericQueryableOptionsExtension();
?? new GenericQueryableOptionsExtension(setupAction);

((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);

Expand Down
8 changes: 5 additions & 3 deletions src/GenericQueryable.EntityFramework/EfFetchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@

namespace GenericQueryable.EntityFramework;

public class EfFetchService : IFetchService
public class EfFetchService(IEnumerable<IFetchRuleExpander> expanders) : IFetchService
{
public virtual IQueryable<TSource> ApplyFetch<TSource>(IQueryable<TSource> source, FetchRule<TSource> fetchRule)
where TSource : class
{
return fetchRule switch
{
var expandedFetchRule = expanders.Aggregate(fetchRule, (state, expander) => expander.TryExpand(state) ?? state);

return expandedFetchRule switch
{
UntypedFetchRule<TSource> untypedFetchRule => source.Include(untypedFetchRule.Path),

Expand Down Expand Up @@ -43,7 +45,7 @@
private IQueryable<TSource> ApplyFetch<TSource>(IQueryable<TSource> source, LambdaExpression prop, LambdaExpression? prevProp)
where TSource : class
{
return this.GetFetchMethod<TSource>(prop, prevProp).Invoke<IQueryable<TSource>>(this, source, prop);

Check warning on line 48 in src/GenericQueryable.EntityFramework/EfFetchService.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'LambdaExpression' cannot be used for parameter 'args' of type 'object?[]' in 'IQueryable<TSource> extension(MethodInfo).Invoke<IQueryable<TSource>>(object? source, object? arg1, params object?[] args)' due to differences in the nullability of reference types.

Check warning on line 48 in src/GenericQueryable.EntityFramework/EfFetchService.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'LambdaExpression' cannot be used for parameter 'args' of type 'object?[]' in 'IQueryable<TSource> extension(MethodInfo).Invoke<IQueryable<TSource>>(object? source, object? arg1, params object?[] args)' due to differences in the nullability of reference types.

Check warning on line 48 in src/GenericQueryable.EntityFramework/EfFetchService.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'LambdaExpression' cannot be used for parameter 'args' of type 'object?[]' in 'IQueryable<TSource> extension(MethodInfo).Invoke<IQueryable<TSource>>(object? source, object? arg1, params object?[] args)' due to differences in the nullability of reference types.

Check warning on line 48 in src/GenericQueryable.EntityFramework/EfFetchService.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'LambdaExpression' cannot be used for parameter 'args' of type 'object?[]' in 'IQueryable<TSource> extension(MethodInfo).Invoke<IQueryable<TSource>>(object? source, object? arg1, params object?[] args)' due to differences in the nullability of reference types.
}

private MethodInfo GetFetchMethod<TSource>(LambdaExpression prop, LambdaExpression? prevProp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ namespace GenericQueryable.EntityFramework;

public class GenericQueryableOptionsExtension : IDbContextOptionsExtension
{
public GenericQueryableOptionsExtension()
{
this.Info = new ExtensionInfo(this);
}
private readonly Action<IGenericQueryableSetup>? setupAction;

public GenericQueryableOptionsExtension(Action<IGenericQueryableSetup>? setupAction)
{
this.setupAction = setupAction;
this.Info = new ExtensionInfo(this);
}

public DbContextOptionsExtensionInfo Info { get; }

public void ApplyServices(IServiceCollection services)
{
services.AddGenericQueryable(v => v.SetFetchService<EfFetchService>().SetTargetMethodExtractor<EfTargetMethodExtractor>());
services.AddGenericQueryable(v =>
{
v.SetFetchService<EfFetchService>().SetTargetMethodExtractor<EfTargetMethodExtractor>();

setupAction?.Invoke(v);
});

services.ReplaceScoped<IAsyncQueryProvider, VisitedEfQueryProvider>();
}
Expand Down
9 changes: 9 additions & 0 deletions src/GenericQueryable.IntegrationTests/AppFetchRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using GenericQueryable.Fetching;
using GenericQueryable.IntegrationTests.Domain;

namespace GenericQueryable.IntegrationTests;

public static class AppFetchRule
{
public static FetchRule<TestObject> TestFetchRule { get; } = new FetchRuleHeader<TestObject>(nameof(TestFetchRule));
}
84 changes: 43 additions & 41 deletions src/GenericQueryable.IntegrationTests/MainTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CommonFramework.DependencyInjection;

using GenericQueryable.EntityFramework;
using GenericQueryable.Fetching;
using GenericQueryable.IntegrationTests.Domain;

using Microsoft.EntityFrameworkCore;
Expand All @@ -10,61 +11,62 @@ namespace GenericQueryable.IntegrationTests;

public class MainTests
{
private readonly CancellationToken ct = TestContext.Current.CancellationToken;

[Fact]
public async Task DefaultGenericQueryable_InvokeToListAsync_MethodInvoked()
{
// Arrange
var sp = new ServiceCollection()
.AddDbContext<TestDbContext>(optionsBuilder => optionsBuilder
.UseSqlite("Data Source=test.db")
.UseGenericQueryable(),
contextLifetime: ServiceLifetime.Singleton,
optionsLifetime: ServiceLifetime.Singleton)
private readonly CancellationToken ct = TestContext.Current.CancellationToken;

[Fact]
public async Task DefaultGenericQueryable_InvokeToListAsync_MethodInvoked()
{
// Arrange
var sp = new ServiceCollection()
.AddDbContext<TestDbContext>(optionsBuilder => optionsBuilder
.UseSqlite("Data Source=test.db")
.UseGenericQueryable(b =>
b.AddFetchRule(AppFetchRule.TestFetchRule, FetchRule<TestObject>.Create(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))),
contextLifetime: ServiceLifetime.Singleton,
optionsLifetime: ServiceLifetime.Singleton)
.AddValidator<DuplicateServiceUsageValidator>()
.Validate()
.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });
.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });

var dbContext = sp.GetRequiredService<TestDbContext>();
var dbContext = sp.GetRequiredService<TestDbContext>();

await dbContext.Database.EnsureDeletedAsync(ct);
await dbContext.Database.EnsureCreatedAsync(ct);
await dbContext.Database.EnsureDeletedAsync(ct);
await dbContext.Database.EnsureCreatedAsync(ct);

var testSet = dbContext.Set<TestObject>();
var testSet = dbContext.Set<TestObject>();

var fetchObj = new FetchObject();
var fetchObj = new FetchObject();

await dbContext.Set<FetchObject>().AddAsync(fetchObj, ct);
await dbContext.Set<FetchObject>().AddAsync(fetchObj, ct);

var testObj = new TestObject { Id = Guid.NewGuid() };
var testObj = new TestObject { Id = Guid.NewGuid() };

await testSet.AddAsync(testObj, ct);
await testSet.AddAsync(testObj, ct);

await dbContext.SaveChangesAsync(ct);
await dbContext.SaveChangesAsync(ct);

// Act
var result0 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToArrayAsync(cancellationToken: ct);
// Act
var result0 = await testSet
.WithFetch(AppFetchRule.TestFetchRule)
.GenericToArrayAsync(cancellationToken: ct);

var result1 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToListAsync(cancellationToken: ct);
var result1 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToListAsync(cancellationToken: ct);

var result2 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToHashSetAsync(cancellationToken: ct);
var result2 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToHashSetAsync(cancellationToken: ct);

var result3 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToDictionaryAsync(v => v.Id, cancellationToken: ct);
var result3 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToDictionaryAsync(v => v.Id, cancellationToken: ct);

var result4 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToDictionaryAsync(v => v.Id, v => v, cancellationToken: ct);
var result4 = await testSet
.WithFetch(r => r.Fetch(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))
.GenericToDictionaryAsync(v => v.Id, v => v, cancellationToken: ct);

//Assert
result0.Should().Contain(testObj);
}
//Assert
result0.Should().Contain(testObj);
}
}
67 changes: 48 additions & 19 deletions src/GenericQueryable/DependencyInjection/GenericQueryableSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,61 @@ namespace GenericQueryable.DependencyInjection;

public class GenericQueryableSetup : IGenericQueryableSetup
{
private Type targetMethodExtractorType = typeof(SyncTargetMethodExtractor);
private Type targetMethodExtractorType = typeof(SyncTargetMethodExtractor);

private Type fetchServiceType = typeof(IgnoreFetchService);
private Type fetchServiceType = typeof(IgnoreFetchService);

public void Initialize(IServiceCollection services)
{
services.AddSingleton<IGenericQueryableExecutor, GenericQueryableExecutor>();
services.AddSingleton<IMethodRedirector, MethodRedirector>();
private readonly List<Type> fetchRuleExpanderTypeList = [typeof(FetchRuleHeaderExpander)];

services.AddSingleton(typeof(IFetchService), this.fetchServiceType);
services.AddSingleton(typeof(ITargetMethodExtractor), this.targetMethodExtractorType);
}
private readonly List<FetchRuleHeaderInfo> fetchRuleHeaderInfoList = new();

public IGenericQueryableSetup SetFetchService<TFetchService>()
public void Initialize(IServiceCollection services)
{
services.AddSingleton<IGenericQueryableExecutor, GenericQueryableExecutor>();
services.AddSingleton<IMethodRedirector, MethodRedirector>();

services.AddSingleton(typeof(IFetchService), this.fetchServiceType);
services.AddSingleton(typeof(ITargetMethodExtractor), this.targetMethodExtractorType);

foreach (var fetchRuleExpanderType in this.fetchRuleExpanderTypeList)
{
services.AddSingleton(typeof(IFetchRuleExpander), fetchRuleExpanderType);
}

foreach (var fetchRuleHeaderInfo in this.fetchRuleHeaderInfoList)
{
services.AddSingleton(fetchRuleHeaderInfo);
}
}

public IGenericQueryableSetup SetFetchService<TFetchService>()
where TFetchService : IFetchService
{
this.fetchServiceType = typeof(TFetchService);
{
this.fetchServiceType = typeof(TFetchService);

return this;
}
return this;
}

public IGenericQueryableSetup SetTargetMethodExtractor<TTargetMethodExtractor>()
public IGenericQueryableSetup AddFetchRule<TSource>(FetchRule<TSource> header, FetchRule<TSource> implementation)
{
this.fetchRuleHeaderInfoList.Add(new FetchRuleHeaderInfo<TSource>(header, implementation));

return this;
}

public IGenericQueryableSetup SetTargetMethodExtractor<TTargetMethodExtractor>()
where TTargetMethodExtractor : ITargetMethodExtractor
{
this.targetMethodExtractorType = typeof(TTargetMethodExtractor);
{
this.targetMethodExtractorType = typeof(TTargetMethodExtractor);

return this;
}

public IGenericQueryableSetup AddFetchRuleExpander<TFetchRuleExpander>()
where TFetchRuleExpander : IFetchRuleExpander
{
this.fetchRuleExpanderTypeList.Add(typeof(TFetchRuleExpander));

return this;
}
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public interface IGenericQueryableSetup
IGenericQueryableSetup SetFetchService<TFetchService>()
where TFetchService : IFetchService;

IGenericQueryableSetup SetTargetMethodExtractor<TTargetMethodExtractor>()
IGenericQueryableSetup AddFetchRuleExpander<TFetchRuleExpander>()
where TFetchRuleExpander : IFetchRuleExpander;

IGenericQueryableSetup AddFetchRule<TSource>(FetchRule<TSource> header, FetchRule<TSource> implementation);

IGenericQueryableSetup SetTargetMethodExtractor<TTargetMethodExtractor>()
where TTargetMethodExtractor : ITargetMethodExtractor;
}
3 changes: 3 additions & 0 deletions src/GenericQueryable/Fetching/FetchRuleHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace GenericQueryable.Fetching;

public record FetchRuleHeader<TSource>(string Name) : FetchRule<TSource>;
22 changes: 22 additions & 0 deletions src/GenericQueryable/Fetching/FetchRuleHeaderExpander.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Concurrent;

using CommonFramework;

namespace GenericQueryable.Fetching;

public class FetchRuleHeaderExpander(IEnumerable<FetchRuleHeaderInfo> fetchRuleHeaderInfoList) : IFetchRuleExpander
{
private readonly IReadOnlyDictionary<Type, IReadOnlyList<FetchRuleHeaderInfo>> headersDict =
fetchRuleHeaderInfoList.GroupBy(v => v.SourceType).ToDictionary(g => g.Key, IReadOnlyList<FetchRuleHeaderInfo> (g) => g.ToList());

private readonly ConcurrentDictionary<Type, object> cache = new();

public FetchRule<TSource>? TryExpand<TSource>(FetchRule<TSource> fetchRule)
{
return cache.GetOrAdd(typeof(TSource),
_ => headersDict[typeof(TSource)].Cast<FetchRuleHeaderInfo<TSource>>()
.ToDictionary(info => info.Header, info => info.Implementation))
.Pipe(innerCache => (IReadOnlyDictionary<FetchRule<TSource>, FetchRule<TSource>>)innerCache)
.Pipe(innerCache => innerCache.GetValueOrDefault(fetchRule));
}
}
11 changes: 11 additions & 0 deletions src/GenericQueryable/Fetching/FetchRuleHeaderInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace GenericQueryable.Fetching;

public abstract record FetchRuleHeaderInfo
{
public abstract Type SourceType { get; }
}

public record FetchRuleHeaderInfo<TSource>(FetchRule<TSource> Header, FetchRule<TSource> Implementation) : FetchRuleHeaderInfo
{
public override Type SourceType { get; } = typeof(TSource);
}
6 changes: 6 additions & 0 deletions src/GenericQueryable/Fetching/IFetchRuleExpander.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace GenericQueryable.Fetching;

public interface IFetchRuleExpander
{
FetchRule<TSource>? TryExpand<TSource>(FetchRule<TSource> fetchRule);
}
2 changes: 1 addition & 1 deletion src/__SolutionItems/CommonAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[assembly: AssemblyProduct("GenericQueryable")]
[assembly: AssemblyCompany("IvAt")]

[assembly: AssemblyVersion("2.0.3.0")]
[assembly: AssemblyVersion("2.0.4.0")]
[assembly: AssemblyInformationalVersion("changes at build")]

#if DEBUG
Expand Down
Loading