-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add EF extension for lambda injection (#24)
Add EF extension for lambda injection
- Loading branch information
Showing
13 changed files
with
338 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
coverage: | ||
status: | ||
patch: | ||
default: | ||
target: 80% | ||
project: | ||
default: | ||
target: 90% |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
src/NeinLinq.EntityFrameworkCore/InjectableDbContextOptionsBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
using System; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Infrastructure; | ||
|
||
#pragma warning disable CA1508 | ||
|
||
namespace NeinLinq | ||
{ | ||
/// <summary> | ||
/// Replaces method calls with lambda expressions. | ||
/// </summary> | ||
public static class InjectableDbContextOptionsBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Replaces method calls with lambda expressions. | ||
/// </summary> | ||
/// <param name="optionsBuilder">The builder being used to configure the context.</param> | ||
/// <param name="greenlist">A list of types to inject, whether marked as injectable or not.</param> | ||
/// <returns>The options builder so that further configuration can be chained.</returns> | ||
public static DbContextOptionsBuilder WithLambdaInjection(this DbContextOptionsBuilder optionsBuilder, params Type[] greenlist) | ||
{ | ||
if (optionsBuilder is null) | ||
throw new ArgumentNullException(nameof(optionsBuilder)); | ||
if (greenlist is null) | ||
throw new ArgumentNullException(nameof(greenlist)); | ||
|
||
var extension = GetOrCreateExtension(optionsBuilder).WithParams(greenlist); | ||
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); | ||
|
||
return optionsBuilder; | ||
} | ||
|
||
private static InjectableDbContextOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) | ||
=> optionsBuilder.Options.FindExtension<InjectableDbContextOptionsExtension>() | ||
?? new InjectableDbContextOptionsExtension(); | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
src/NeinLinq.EntityFrameworkCore/InjectableDbContextOptionsExtension.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.EntityFrameworkCore.Infrastructure; | ||
using Microsoft.EntityFrameworkCore.Query; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
#pragma warning disable CA1307 | ||
#pragma warning disable CA1508 | ||
#pragma warning disable CA1822 | ||
|
||
namespace NeinLinq | ||
{ | ||
internal class InjectableDbContextOptionsExtension : IDbContextOptionsExtension | ||
{ | ||
private readonly Type[] greenlist; | ||
|
||
public InjectableDbContextOptionsExtension(params Type[] greenlist) | ||
{ | ||
this.greenlist = greenlist ?? throw new ArgumentNullException(nameof(greenlist)); | ||
} | ||
|
||
public DbContextOptionsExtensionInfo Info | ||
=> new ExtensionInfo(this); | ||
|
||
public void ApplyServices(IServiceCollection services) | ||
{ | ||
if (services is null) | ||
throw new ArgumentNullException(nameof(services)); | ||
|
||
for (var index = services.Count - 1; index >= 0; index--) | ||
{ | ||
var descriptor = services[index]; | ||
if (descriptor.ServiceType != typeof(IQueryTranslationPreprocessorFactory)) | ||
continue; | ||
if (descriptor.ImplementationType is null) | ||
continue; | ||
|
||
// Add Injectable factory for actual implementation | ||
services[index] = new ServiceDescriptor( | ||
descriptor.ServiceType, | ||
typeof(InjectableQueryTranslationPreprocessorFactory<>) | ||
.MakeGenericType(descriptor.ImplementationType), | ||
descriptor.Lifetime | ||
); | ||
|
||
// Add actual implementation as it is | ||
services.Add( | ||
new ServiceDescriptor( | ||
descriptor.ImplementationType, | ||
descriptor.ImplementationType, | ||
descriptor.Lifetime | ||
) | ||
); | ||
} | ||
|
||
_ = services.AddSingleton(new InjectableQueryTranslationPreprocessorOptions(greenlist)); | ||
} | ||
|
||
public InjectableDbContextOptionsExtension WithParams(Type[] greenlist) | ||
{ | ||
if (greenlist is null) | ||
throw new ArgumentNullException(nameof(greenlist)); | ||
|
||
return new InjectableDbContextOptionsExtension(greenlist); | ||
} | ||
|
||
public void Validate(IDbContextOptions options) | ||
{ | ||
} | ||
|
||
private class ExtensionInfo : DbContextOptionsExtensionInfo | ||
{ | ||
public ExtensionInfo(IDbContextOptionsExtension extension) | ||
: base(extension) | ||
{ | ||
} | ||
|
||
public override bool IsDatabaseProvider | ||
=> false; | ||
|
||
public override string LogFragment | ||
=> "LambdaInjection"; | ||
|
||
public override long GetServiceProviderHashCode() | ||
=> "LambdaInjection".GetHashCode(); | ||
|
||
public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) | ||
{ | ||
} | ||
} | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
src/NeinLinq.EntityFrameworkCore/InjectableQueryTranslationPreprocessor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq.Expressions; | ||
using Microsoft.EntityFrameworkCore.Query; | ||
|
||
#pragma warning disable CA1812 | ||
|
||
namespace NeinLinq | ||
{ | ||
[ExcludeFromCodeCoverage] | ||
internal class InjectableQueryTranslationPreprocessor : QueryTranslationPreprocessor | ||
{ | ||
private readonly InjectableQueryTranslationPreprocessorOptions options; | ||
private readonly QueryTranslationPreprocessor innerPreprocessor; | ||
|
||
public InjectableQueryTranslationPreprocessor(InjectableQueryTranslationPreprocessorOptions options, | ||
QueryTranslationPreprocessorDependencies dependencies, | ||
QueryCompilationContext queryCompilationContext, | ||
QueryTranslationPreprocessor innerPreprocessor) | ||
: base(dependencies, queryCompilationContext) | ||
{ | ||
this.options = options ?? throw new ArgumentNullException(nameof(options)); | ||
this.innerPreprocessor = innerPreprocessor ?? throw new ArgumentNullException(nameof(innerPreprocessor)); | ||
} | ||
|
||
public override Expression Process(Expression query) | ||
{ | ||
var rewriter = new InjectableQueryRewriter(options.Greenlist); | ||
|
||
return innerPreprocessor.Process(rewriter.Visit(query)); | ||
} | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/NeinLinq.EntityFrameworkCore/InjectableQueryTranslationPreprocessorFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using Microsoft.EntityFrameworkCore.Query; | ||
|
||
#pragma warning disable CA1812 | ||
|
||
namespace NeinLinq | ||
{ | ||
[ExcludeFromCodeCoverage] | ||
internal class InjectableQueryTranslationPreprocessorFactory<TInnerFactory> : IQueryTranslationPreprocessorFactory | ||
where TInnerFactory : class, IQueryTranslationPreprocessorFactory | ||
{ | ||
private readonly InjectableQueryTranslationPreprocessorOptions options; | ||
private readonly QueryTranslationPreprocessorDependencies dependencies; | ||
private readonly TInnerFactory innerFactory; | ||
|
||
public InjectableQueryTranslationPreprocessorFactory(InjectableQueryTranslationPreprocessorOptions options, | ||
QueryTranslationPreprocessorDependencies dependencies, | ||
TInnerFactory innerFactory) | ||
{ | ||
this.options = options ?? throw new ArgumentNullException(nameof(options)); | ||
this.dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies)); | ||
this.innerFactory = innerFactory ?? throw new ArgumentNullException(nameof(innerFactory)); | ||
} | ||
|
||
public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) | ||
=> new InjectableQueryTranslationPreprocessor(options, dependencies, queryCompilationContext, innerFactory.Create(queryCompilationContext)); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/NeinLinq.EntityFrameworkCore/InjectableQueryTranslationPreprocessorOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace NeinLinq | ||
{ | ||
[ExcludeFromCodeCoverage] | ||
internal class InjectableQueryTranslationPreprocessorOptions | ||
{ | ||
public Type[] Greenlist { get; } | ||
|
||
public InjectableQueryTranslationPreprocessorOptions(Type[] greenlist) | ||
{ | ||
Greenlist = greenlist ?? throw new ArgumentNullException(nameof(greenlist)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace NeinLinq.Fakes.EntityExtension | ||
{ | ||
public class Dummy | ||
{ | ||
public int Id { get; set; } | ||
|
||
public string? Name { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using System; | ||
using System.Linq.Expressions; | ||
|
||
#pragma warning disable CA1801 | ||
#pragma warning disable CA1822 | ||
|
||
namespace NeinLinq.Fakes.EntityExtension | ||
{ | ||
public static class DummyExtensions | ||
{ | ||
[InjectLambda] | ||
public static bool IsNarf(this Dummy dummy) | ||
=> throw new InvalidOperationException(); | ||
|
||
public static Expression<Func<Dummy, bool>> IsNarf() | ||
=> d => d.Name == "Narf"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using Microsoft.EntityFrameworkCore; | ||
using NeinLinq.Fakes.EntityExtension; | ||
|
||
namespace NeinLinq.Tests.EntityExtension | ||
{ | ||
public class Context : DbContext | ||
{ | ||
public DbSet<Dummy> Dummies { get; } | ||
|
||
public Context(DbContextOptions<Context> options) | ||
: base(options) | ||
{ | ||
Dummies = Set<Dummy>(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
using System; | ||
using System.Linq; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using NeinLinq.Fakes.EntityExtension; | ||
using Xunit; | ||
|
||
namespace NeinLinq.Tests.EntityExtension | ||
{ | ||
public class ExtensionTest | ||
{ | ||
[Fact] | ||
public void ShouldValidateArguments() | ||
{ | ||
_ = Assert.Throws<ArgumentNullException>(() => InjectableDbContextOptionsBuilderExtensions.WithLambdaInjection(null!, Array.Empty<Type>())); | ||
_ = Assert.Throws<ArgumentNullException>(() => InjectableDbContextOptionsBuilderExtensions.WithLambdaInjection(new DbContextOptionsBuilder(), null!)); | ||
} | ||
|
||
[Fact] | ||
public void ShouldInject() | ||
{ | ||
var services = new ServiceCollection(); | ||
|
||
_ = services.AddDbContext<Context>(options => | ||
options.UseSqlite("Data Source=NeinLinq.EntityExtension.db").WithLambdaInjection()); | ||
|
||
using var serviceProvider = services.BuildServiceProvider(); | ||
|
||
var context = serviceProvider.GetRequiredService<Context>(); | ||
|
||
_ = context.Database.EnsureDeleted(); | ||
_ = context.Database.EnsureCreated(); | ||
|
||
context.Dummies.AddRange( | ||
new Dummy { Name = "Heinz" }, | ||
new Dummy { Name = "Narf" }, | ||
new Dummy { Name = "Wat" } | ||
); | ||
|
||
_ = context.SaveChanges(); | ||
|
||
var query = from d in context.Dummies.AsQueryable() | ||
where d.IsNarf() | ||
select d.Id; | ||
|
||
Assert.Equal(1, query.Count()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldNotInject() | ||
{ | ||
var services = new ServiceCollection(); | ||
|
||
_ = services.AddDbContext<Context>(options => | ||
options.UseSqlite("Data Source=NeinLinq.EntityExtension.db")); | ||
|
||
using var serviceProvider = services.BuildServiceProvider(); | ||
|
||
var context = serviceProvider.GetRequiredService<Context>(); | ||
|
||
_ = context.Database.EnsureDeleted(); | ||
_ = context.Database.EnsureCreated(); | ||
|
||
var query = from d in context.Dummies.AsQueryable() | ||
where d.IsNarf() | ||
select d.Id; | ||
|
||
_ = Assert.Throws<InvalidOperationException>(() => query.Count()); | ||
} | ||
} | ||
} |