Skip to content

Instantly share code, notes, and snippets.

@blemasle
Last active August 23, 2024 15:42
Show Gist options
  • Save blemasle/03a0dde89b9d27536bf9f470d5413fe7 to your computer and use it in GitHub Desktop.
Save blemasle/03a0dde89b9d27536bf9f470d5413fe7 to your computer and use it in GitHub Desktop.
Using expressions parameters to join on a composite key
// 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