Last active
August 23, 2024 15:42
-
-
Save blemasle/03a0dde89b9d27536bf9f470d5413fe7 to your computer and use it in GitHub Desktop.
Using expressions parameters to join on a composite key
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
// Workaround to make EF's join happy despite the needs to use a composite key | |
// with expression values declared outside of the join calling method. | |
// Inspired by https://stackoverflow.com/a/56337784 | |
// See https://github.com/dotnet/efcore/issues/25075 | |
using Microsoft.EntityFrameworkCore; | |
using Microsoft.Extensions.DependencyInjection; | |
using System.Linq.Expressions; | |
var services = new ServiceCollection() | |
.AddDbContext<MyDbContext>(opts => | |
{ | |
opts.UseSqlite("Data Source=LocalDatabase.db"); | |
}); | |
var serviceProvider = services.BuildServiceProvider(); | |
var dbContext = serviceProvider.GetRequiredService<MyDbContext>(); | |
await dbContext.Database.EnsureCreatedAsync(); | |
var addresses = await NonWorkingJoinAsync(dbContext, | |
a => LocationType.Business, | |
a => a.Id | |
); | |
async Task<IReadOnlyCollection<Place>> NonWorkingJoinAsync(MyDbContext dbContext, | |
Expression<Func<Address, LocationType>> locationExp, | |
Expression<Func<Address, int>> idExp) | |
{ | |
var exp = AddressKeySelectorExpressionFactory.Create( | |
// Defines the template expression that will be used and modified with both supplied expression. | |
// The anonymous type returned by that expression matches the one expressed in the inner key selector | |
// and it would compile if it does not. | |
address => new { Type = default(LocationType), Id = default(int) }, | |
locationExp, | |
idExp); | |
var places = await dbContext.Addresses | |
.Join( | |
dbContext.Places, | |
exp, | |
p => new { p.Type, p.Id }, | |
(a, p) => p) | |
.ToArrayAsync(); | |
return places; | |
} | |
public static class AddressKeySelectorExpressionFactory | |
{ | |
public static Expression<Func<Address, TOutput>> Create<TOutput>( | |
Expression<Func<Address, TOutput>> templateExpression, | |
Expression<Func<Address, LocationType>> locationTypeExpression, | |
Expression<Func<Address, int>> idExpression) | |
{ | |
var visitor = new AddressKeySelectorExpressionVisitor( | |
templateExpression.Parameters.First(), | |
locationTypeExpression, | |
idExpression); | |
return visitor.VisitAndConvert(templateExpression, null); | |
} | |
private class AddressKeySelectorExpressionVisitor : ExpressionVisitor | |
{ | |
private readonly ParameterExpression _addressParameterExpression; | |
private readonly Expression<Func<Address, LocationType>> _locationTypeExpression; | |
private readonly Expression<Func<Address, int>> _idExpression; | |
public AddressKeySelectorExpressionVisitor( | |
ParameterExpression addressParameterExpression, | |
Expression<Func<Address, LocationType>> locationTypeExpression, | |
Expression<Func<Address, int>> idExpression) | |
{ | |
_addressParameterExpression = addressParameterExpression; | |
_locationTypeExpression = locationTypeExpression; | |
_idExpression = idExpression; | |
} | |
protected override Expression VisitNew(NewExpression node) | |
{ | |
// replacing both parameters construction with the one provided by the settings | |
var updated = node.Update( | |
[ | |
_locationTypeExpression.Body, | |
_idExpression.Body | |
]); | |
// visiting the NewExpression as usual to replace addess parameter usages | |
return base.VisitNew(updated); | |
} | |
protected override Expression VisitParameter(ParameterExpression node) | |
{ | |
// replace every ParameterExpression that match the addressParameterExpression type (~Address) | |
// by the one originally declared on the template expression | |
if (node.Type == _addressParameterExpression.Type) | |
return _addressParameterExpression; | |
return base.VisitParameter(node); | |
} | |
} | |
} | |
internal class MyDbContext : DbContext | |
{ | |
public MyDbContext(DbContextOptions<MyDbContext> optionsBuilder) | |
: base(optionsBuilder) | |
{ } | |
public DbSet<Place> Places { get; set; } | |
public DbSet<Address> Addresses { get; set; } | |
protected override void OnModelCreating(ModelBuilder modelBuilder) | |
{ | |
modelBuilder.Entity<Place>(); | |
modelBuilder.Entity<Address>(); | |
} | |
} | |
public record Place(int Id, string Name, LocationType Type); | |
public record Address(int Id, string Value, LocationType Type); | |
public enum LocationType | |
{ | |
Home, | |
Business | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment