diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 4fe90ae..b53da50 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,8 +3,8 @@ true - - + + diff --git a/src/GenericQueryable.EntityFramework/DbContextOptionsBuilderExtensions.cs b/src/GenericQueryable.EntityFramework/DbContextOptionsBuilderExtensions.cs index 8685344..c095d7e 100644 --- a/src/GenericQueryable.EntityFramework/DbContextOptionsBuilderExtensions.cs +++ b/src/GenericQueryable.EntityFramework/DbContextOptionsBuilderExtensions.cs @@ -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? setupAction = null) { var extension = optionsBuilder.Options.FindExtension() - ?? new GenericQueryableOptionsExtension(); + ?? new GenericQueryableOptionsExtension(setupAction); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); diff --git a/src/GenericQueryable.EntityFramework/EfFetchService.cs b/src/GenericQueryable.EntityFramework/EfFetchService.cs index 0242584..761bd02 100644 --- a/src/GenericQueryable.EntityFramework/EfFetchService.cs +++ b/src/GenericQueryable.EntityFramework/EfFetchService.cs @@ -10,12 +10,14 @@ namespace GenericQueryable.EntityFramework; -public class EfFetchService : IFetchService +public class EfFetchService(IEnumerable expanders) : IFetchService { public virtual IQueryable ApplyFetch(IQueryable source, FetchRule fetchRule) where TSource : class - { - return fetchRule switch + { + var expandedFetchRule = expanders.Aggregate(fetchRule, (state, expander) => expander.TryExpand(state) ?? state); + + return expandedFetchRule switch { UntypedFetchRule untypedFetchRule => source.Include(untypedFetchRule.Path), diff --git a/src/GenericQueryable.EntityFramework/GenericQueryableOptionsExtension.cs b/src/GenericQueryable.EntityFramework/GenericQueryableOptionsExtension.cs index 0dce99f..139e3ec 100644 --- a/src/GenericQueryable.EntityFramework/GenericQueryableOptionsExtension.cs +++ b/src/GenericQueryable.EntityFramework/GenericQueryableOptionsExtension.cs @@ -10,16 +10,24 @@ namespace GenericQueryable.EntityFramework; public class GenericQueryableOptionsExtension : IDbContextOptionsExtension { - public GenericQueryableOptionsExtension() - { - this.Info = new ExtensionInfo(this); - } + private readonly Action? setupAction; + + public GenericQueryableOptionsExtension(Action? setupAction) + { + this.setupAction = setupAction; + this.Info = new ExtensionInfo(this); + } public DbContextOptionsExtensionInfo Info { get; } public void ApplyServices(IServiceCollection services) { - services.AddGenericQueryable(v => v.SetFetchService().SetTargetMethodExtractor()); + services.AddGenericQueryable(v => + { + v.SetFetchService().SetTargetMethodExtractor(); + + setupAction?.Invoke(v); + }); services.ReplaceScoped(); } diff --git a/src/GenericQueryable.IntegrationTests/AppFetchRule.cs b/src/GenericQueryable.IntegrationTests/AppFetchRule.cs new file mode 100644 index 0000000..cd20b04 --- /dev/null +++ b/src/GenericQueryable.IntegrationTests/AppFetchRule.cs @@ -0,0 +1,9 @@ +using GenericQueryable.Fetching; +using GenericQueryable.IntegrationTests.Domain; + +namespace GenericQueryable.IntegrationTests; + +public static class AppFetchRule +{ + public static FetchRule TestFetchRule { get; } = new FetchRuleHeader(nameof(TestFetchRule)); +} \ No newline at end of file diff --git a/src/GenericQueryable.IntegrationTests/MainTests.cs b/src/GenericQueryable.IntegrationTests/MainTests.cs index add4fc8..c803727 100644 --- a/src/GenericQueryable.IntegrationTests/MainTests.cs +++ b/src/GenericQueryable.IntegrationTests/MainTests.cs @@ -1,6 +1,7 @@ using CommonFramework.DependencyInjection; using GenericQueryable.EntityFramework; +using GenericQueryable.Fetching; using GenericQueryable.IntegrationTests.Domain; using Microsoft.EntityFrameworkCore; @@ -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(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(optionsBuilder => optionsBuilder + .UseSqlite("Data Source=test.db") + .UseGenericQueryable(b => + b.AddFetchRule(AppFetchRule.TestFetchRule, FetchRule.Create(v => v.DeepFetchObjects).ThenFetch(v => v.FetchObject))), + contextLifetime: ServiceLifetime.Singleton, + optionsLifetime: ServiceLifetime.Singleton) .AddValidator() .Validate() - .BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }); + .BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true }); - var dbContext = sp.GetRequiredService(); + var dbContext = sp.GetRequiredService(); - await dbContext.Database.EnsureDeletedAsync(ct); - await dbContext.Database.EnsureCreatedAsync(ct); + await dbContext.Database.EnsureDeletedAsync(ct); + await dbContext.Database.EnsureCreatedAsync(ct); - var testSet = dbContext.Set(); + var testSet = dbContext.Set(); - var fetchObj = new FetchObject(); + var fetchObj = new FetchObject(); - await dbContext.Set().AddAsync(fetchObj, ct); + await dbContext.Set().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); + } } \ No newline at end of file diff --git a/src/GenericQueryable/DependencyInjection/GenericQueryableSetup.cs b/src/GenericQueryable/DependencyInjection/GenericQueryableSetup.cs index d045c62..6b5bf48 100644 --- a/src/GenericQueryable/DependencyInjection/GenericQueryableSetup.cs +++ b/src/GenericQueryable/DependencyInjection/GenericQueryableSetup.cs @@ -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(); - services.AddSingleton(); + private readonly List fetchRuleExpanderTypeList = [typeof(FetchRuleHeaderExpander)]; - services.AddSingleton(typeof(IFetchService), this.fetchServiceType); - services.AddSingleton(typeof(ITargetMethodExtractor), this.targetMethodExtractorType); - } + private readonly List fetchRuleHeaderInfoList = new(); - public IGenericQueryableSetup SetFetchService() + public void Initialize(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + + 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() where TFetchService : IFetchService - { - this.fetchServiceType = typeof(TFetchService); + { + this.fetchServiceType = typeof(TFetchService); - return this; - } + return this; + } - public IGenericQueryableSetup SetTargetMethodExtractor() + public IGenericQueryableSetup AddFetchRule(FetchRule header, FetchRule implementation) + { + this.fetchRuleHeaderInfoList.Add(new FetchRuleHeaderInfo(header, implementation)); + + return this; + } + + public IGenericQueryableSetup SetTargetMethodExtractor() where TTargetMethodExtractor : ITargetMethodExtractor - { - this.targetMethodExtractorType = typeof(TTargetMethodExtractor); + { + this.targetMethodExtractorType = typeof(TTargetMethodExtractor); + + return this; + } + + public IGenericQueryableSetup AddFetchRuleExpander() + where TFetchRuleExpander : IFetchRuleExpander + { + this.fetchRuleExpanderTypeList.Add(typeof(TFetchRuleExpander)); - return this; - } + return this; + } } \ No newline at end of file diff --git a/src/GenericQueryable/DependencyInjection/IGenericQueryableSetup.cs b/src/GenericQueryable/DependencyInjection/IGenericQueryableSetup.cs index a8a4e36..cb834d4 100644 --- a/src/GenericQueryable/DependencyInjection/IGenericQueryableSetup.cs +++ b/src/GenericQueryable/DependencyInjection/IGenericQueryableSetup.cs @@ -8,6 +8,11 @@ public interface IGenericQueryableSetup IGenericQueryableSetup SetFetchService() where TFetchService : IFetchService; - IGenericQueryableSetup SetTargetMethodExtractor() + IGenericQueryableSetup AddFetchRuleExpander() + where TFetchRuleExpander : IFetchRuleExpander; + + IGenericQueryableSetup AddFetchRule(FetchRule header, FetchRule implementation); + + IGenericQueryableSetup SetTargetMethodExtractor() where TTargetMethodExtractor : ITargetMethodExtractor; } \ No newline at end of file diff --git a/src/GenericQueryable/Fetching/FetchRuleHeader.cs b/src/GenericQueryable/Fetching/FetchRuleHeader.cs new file mode 100644 index 0000000..f890f6b --- /dev/null +++ b/src/GenericQueryable/Fetching/FetchRuleHeader.cs @@ -0,0 +1,3 @@ +namespace GenericQueryable.Fetching; + +public record FetchRuleHeader(string Name) : FetchRule; \ No newline at end of file diff --git a/src/GenericQueryable/Fetching/FetchRuleHeaderExpander.cs b/src/GenericQueryable/Fetching/FetchRuleHeaderExpander.cs new file mode 100644 index 0000000..07efc28 --- /dev/null +++ b/src/GenericQueryable/Fetching/FetchRuleHeaderExpander.cs @@ -0,0 +1,22 @@ +using System.Collections.Concurrent; + +using CommonFramework; + +namespace GenericQueryable.Fetching; + +public class FetchRuleHeaderExpander(IEnumerable fetchRuleHeaderInfoList) : IFetchRuleExpander +{ + private readonly IReadOnlyDictionary> headersDict = + fetchRuleHeaderInfoList.GroupBy(v => v.SourceType).ToDictionary(g => g.Key, IReadOnlyList (g) => g.ToList()); + + private readonly ConcurrentDictionary cache = new(); + + public FetchRule? TryExpand(FetchRule fetchRule) + { + return cache.GetOrAdd(typeof(TSource), + _ => headersDict[typeof(TSource)].Cast>() + .ToDictionary(info => info.Header, info => info.Implementation)) + .Pipe(innerCache => (IReadOnlyDictionary, FetchRule>)innerCache) + .Pipe(innerCache => innerCache.GetValueOrDefault(fetchRule)); + } +} \ No newline at end of file diff --git a/src/GenericQueryable/Fetching/FetchRuleHeaderInfo.cs b/src/GenericQueryable/Fetching/FetchRuleHeaderInfo.cs new file mode 100644 index 0000000..e63b805 --- /dev/null +++ b/src/GenericQueryable/Fetching/FetchRuleHeaderInfo.cs @@ -0,0 +1,11 @@ +namespace GenericQueryable.Fetching; + +public abstract record FetchRuleHeaderInfo +{ + public abstract Type SourceType { get; } +} + +public record FetchRuleHeaderInfo(FetchRule Header, FetchRule Implementation) : FetchRuleHeaderInfo +{ + public override Type SourceType { get; } = typeof(TSource); +} \ No newline at end of file diff --git a/src/GenericQueryable/Fetching/IFetchRuleExpander.cs b/src/GenericQueryable/Fetching/IFetchRuleExpander.cs new file mode 100644 index 0000000..db85e27 --- /dev/null +++ b/src/GenericQueryable/Fetching/IFetchRuleExpander.cs @@ -0,0 +1,6 @@ +namespace GenericQueryable.Fetching; + +public interface IFetchRuleExpander +{ + FetchRule? TryExpand(FetchRule fetchRule); +} \ No newline at end of file diff --git a/src/__SolutionItems/CommonAssemblyInfo.cs b/src/__SolutionItems/CommonAssemblyInfo.cs index d4bbf69..baee29e 100644 --- a/src/__SolutionItems/CommonAssemblyInfo.cs +++ b/src/__SolutionItems/CommonAssemblyInfo.cs @@ -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