Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EF extension for lambda injection #24

Merged
merged 5 commits into from
Apr 17, 2021
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
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());
}
}
}