Skip to content

Commit

Permalink
Add EF extension for lambda injection (#24)
Browse files Browse the repository at this point in the history
Add EF extension for lambda injection
  • Loading branch information
axelheer authored Apr 17, 2021
1 parent be8c81c commit b37b52b
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 2 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ Usage of specific flavors is encouraged for EF6 / EFCore (otherwise async querie

PM> Install-Package NeinLinq.Interactive

***New:*** with Version `5.1.0` the package *NeinLinq.EntityFrameworkCore* introduced an explicit `DbContext` extension for enabling *Lambda injection* globally:

```csharp
services.AddDbContext<MyContext>(options =>
options.UseSqlOrTheLike("...").WithLambdaInjection());
```

Lambda injection
----------------

Expand Down
2 changes: 1 addition & 1 deletion build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Nullable>Enable</Nullable>
<LangVersion>9.0</LangVersion>
<VersionPrefix>5.0.1</VersionPrefix>
<VersionPrefix>5.1.0</VersionPrefix>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
Expand Down
8 changes: 8 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage:
status:
patch:
default:
target: 80%
project:
default:
target: 90%
2 changes: 1 addition & 1 deletion pack.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ To support different LINQ implementations, the following flavours are available.
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/axelheer/nein-linq</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReleaseNotes>Improved handling of dynamic lambda factories.</PackageReleaseNotes>
<PackageReleaseNotes>Add EF Core extension for global lambda injection.</PackageReleaseNotes>
<PackageTags>LINQ;EF;IX</PackageTags>
</PropertyGroup>

Expand Down
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();
}
}
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)
{
}
}
}
}
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));
}
}
}
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));
}
}
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));
}
}
}
9 changes: 9 additions & 0 deletions test/NeinLinq.Fakes/EntityExtension/Dummy.cs
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; }
}
}
18 changes: 18 additions & 0 deletions test/NeinLinq.Fakes/EntityExtension/DummyExtensions.cs
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";
}
}
16 changes: 16 additions & 0 deletions test/NeinLinq.Tests/EntityExtension/Context.cs
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>();
}
}
}
71 changes: 71 additions & 0 deletions test/NeinLinq.Tests/EntityExtension/ExtensionTest.cs
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());
}
}
}

0 comments on commit b37b52b

Please sign in to comment.