Skip to content

Commit

Permalink
Merge pull request #521 from paillave/dotnet9-migration
Browse files Browse the repository at this point in the history
feat: add support for pivot conditions in EfSaveEngine and related ex…
  • Loading branch information
paillave authored Jan 13, 2025
2 parents 9d7edff + 588f37d commit cfc8ebc
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 54 deletions.
84 changes: 52 additions & 32 deletions src/Paillave.EntityFrameworkCoreExtension/Core/EfExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,19 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace Paillave.EntityFrameworkCoreExtension.Core;

public static class EfExtensions
{
// #region Remove from ef core version 5
// // see here: https://blog.oneunicorn.com/2020/01/12/toquerystring/ and https://github.com/dotnet/efcore/issues/6482
// public static string ToQueryString<TEntity>(this IQueryable<TEntity> query) where TEntity : class
// {
// var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
// var relationalCommandCache = enumerator.Private("_relationalCommandCache");
// var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression");
// var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");

// var sqlGenerator = factory.Create();
// var command = sqlGenerator.GetCommand(selectExpression);

// string sql = command.CommandText;
// return sql;
// }
// private static object? Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
// private static T? Private<T>(this object obj, string privateField) => (T?)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
// #endregion

public static EntityEntry<TEntity> EntryWithoutDetectChanges<TEntity>(this DbContext context, TEntity entity)
where TEntity : class
{
MethodInfo entryWithoutDetectChangesMethodInfo = context.GetType().GetMethod("EntryWithoutDetectChanges", BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(TEntity) }, null)
?? throw new InvalidOperationException("The method EntryWithoutDetectChanges is not found in the context");
var entityEntryResult = entryWithoutDetectChangesMethodInfo.Invoke(context, new object[] { entity }) as EntityEntry<TEntity>
?? throw new InvalidOperationException("The method EntryWithoutDetectChanges did not return an EntityEntry<TEntity>");
return entityEntryResult;
}
// private static Regex regex = new Regex(@"SELECT\s+(?<ref>[[]?.+?[]]?)[.].+?\sFROM", RegexOptions.Singleline & RegexOptions.IgnoreCase);
/// <summary>
/// Applies a partial function application by replacing the second parameter of the given expression with a specified expression value.
/// </summary>
/// <typeparam name="T1">The type of the first parameter of the original expression.</typeparam>
/// <typeparam name="T2">The type of the second parameter of the original expression.</typeparam>
/// <typeparam name="TResult">The return type of the original expression.</typeparam>
/// <param name="expression">The original expression with two parameters.</param>
/// <param name="expressionValue">The expression value to replace the second parameter of the original expression.</param>
/// <returns>A new expression with the second parameter replaced by the specified expression value.</returns>
public static Expression<Func<T1, TResult>> ApplyPartialRight<T1, T2, TResult>(this Expression<Func<T1, T2, TResult>> expression, Expression expressionValue)
{
var parameterToBeReplaced = expression.Parameters[1];
Expand All @@ -45,32 +22,75 @@ public static Expression<Func<T1, TResult>> ApplyPartialRight<T1, T2, TResult>(t
return Expression.Lambda<Func<T1, TResult>>(newBody, expression.Parameters[0]);
}

/// <summary>
/// Partially applies a value to the second parameter of a given expression.
/// </summary>
/// <typeparam name="T1">The type of the first parameter of the expression.</typeparam>
/// <typeparam name="T2">The type of the second parameter of the expression.</typeparam>
/// <typeparam name="TResult">The type of the result of the expression.</typeparam>
/// <param name="expression">The expression to which the value will be partially applied.</param>
/// <param name="value">The value to be applied to the second parameter of the expression.</param>
/// <returns>An expression with the second parameter replaced by the specified value.</returns>
public static Expression<Func<T1, TResult>> ApplyPartialRight<T1, T2, TResult>(this Expression<Func<T1, T2, TResult>> expression, T2 value)
{
var parameterToBeReplaced = expression.Parameters[1];
var constant = Expression.Constant(value, parameterToBeReplaced.Type);
return ApplyPartialRight(expression, constant);
}
/// <summary>
/// Applies a partial function by replacing the first parameter of the given expression with the specified expression value.
/// </summary>
/// <typeparam name="T1">The type of the first parameter of the original expression.</typeparam>
/// <typeparam name="T2">The type of the second parameter of the original expression and the first parameter of the resulting expression.</typeparam>
/// <typeparam name="TResult">The return type of the expression.</typeparam>
/// <param name="expression">The original expression with two parameters.</param>
/// <param name="expressionValue">The expression value to replace the first parameter of the original expression.</param>
/// <returns>A new expression with the first parameter replaced by the specified expression value.</returns>
public static Expression<Func<T2, TResult>> ApplyPartialLeft<T1, T2, TResult>(this Expression<Func<T1, T2, TResult>> expression, Expression expressionValue)
{
var parameterToBeReplaced = expression.Parameters[0];
var visitor = new ReplacementVisitor(parameterToBeReplaced, expressionValue);
var newBody = visitor.Visit(expression.Body) ?? throw new InvalidOperationException("The expression could not be applied");
return Expression.Lambda<Func<T2, TResult>>(newBody, expression.Parameters[1]);
}
/// <summary>
/// Partially applies the first parameter of a given expression with a specified value.
/// </summary>
/// <typeparam name="T1">The type of the first parameter of the expression.</typeparam>
/// <typeparam name="T2">The type of the second parameter of the expression.</typeparam>
/// <typeparam name="TResult">The type of the result of the expression.</typeparam>
/// <param name="expression">The expression to partially apply.</param>
/// <param name="value">The value to apply to the first parameter of the expression.</param>
/// <returns>An expression with the first parameter replaced by the specified value.</returns>
public static Expression<Func<T2, TResult>> ApplyPartialLeft<T1, T2, TResult>(this Expression<Func<T1, T2, TResult>> expression, T1 value)
{
var parameterToBeReplaced = expression.Parameters[0];
var constant = Expression.Constant(value, parameterToBeReplaced.Type);
return ApplyPartialLeft(expression, constant);
}
/// <summary>
/// Applies a partial evaluation to the given expression by replacing the first parameter with the specified expression value.
/// </summary>
/// <typeparam name="T1">The type of the first parameter in the original expression.</typeparam>
/// <typeparam name="TResult">The return type of the expression.</typeparam>
/// <param name="expression">The original expression to be partially evaluated.</param>
/// <param name="expressionValue">The expression value to replace the first parameter in the original expression.</param>
/// <returns>A new expression with the first parameter replaced by the specified expression value.</returns>
public static Expression<Func<TResult>> ApplyPartial<T1, TResult>(this Expression<Func<T1, TResult>> expression, Expression expressionValue)
{
var parameterToBeReplaced = expression.Parameters[0];
var visitor = new ReplacementVisitor(parameterToBeReplaced, expressionValue);
var newBody = visitor.Visit(expression.Body) ?? throw new InvalidOperationException("The expression could not be applied");
return Expression.Lambda<Func<TResult>>(newBody, expression.Parameters[1]);
}
/// <summary>
/// Applies a partial evaluation to the given expression by replacing the first parameter with a constant value.
/// </summary>
/// <typeparam name="T1">The type of the parameter to be replaced.</typeparam>
/// <typeparam name="TResult">The return type of the expression.</typeparam>
/// <param name="expression">The expression to be partially evaluated.</param>
/// <param name="value">The constant value to replace the first parameter in the expression.</param>
/// <returns>A new expression with the first parameter replaced by the given constant value.</returns>
public static Expression<Func<TResult>> ApplyPartial<T1, TResult>(this Expression<Func<T1, TResult>> expression, T1 value)
{
var parameterToBeReplaced = expression.Parameters[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,38 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Paillave.EntityFrameworkCoreExtension.EfSave
namespace Paillave.EntityFrameworkCoreExtension.EfSave;

public static class DbContextSaveExtensions
{
public static class DbContextSaveExtensions
public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>> pivotKey, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync(context, entities, new Expression<Func<T, object>>[] { pivotKey }, CancellationToken.None, doNotUpdateIfExists, insertOnly);
public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>> pivotKey, CancellationToken cancellationToken, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync(context, entities, new Expression<Func<T, object>>[] { pivotKey }, cancellationToken, doNotUpdateIfExists, insertOnly);
public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>>[] pivotKeys, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync<T>(context, entities, pivotKeys, CancellationToken.None, doNotUpdateIfExists, insertOnly);
public static async Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>>[] pivotKeys, CancellationToken? cancellationToken = null, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
{
if (cancellationToken == null)
{
cancellationToken = CancellationToken.None;
}
EfSaveEngine<T> efSaveEngine = new EfSaveEngine<T>(context, cancellationToken.Value, pivotKeys);
await efSaveEngine.SaveAsync(entities, doNotUpdateIfExists, insertOnly);
}

public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, T, bool>> pivotCondition, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync(context, entities, pivotCondition, CancellationToken.None, doNotUpdateIfExists, insertOnly);
public static async Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, T, bool>> pivotCondition, CancellationToken? cancellationToken = null, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
{
public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>> pivotKey, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync(context, entities, new Expression<Func<T, object>>[] { pivotKey }, CancellationToken.None, doNotUpdateIfExists, insertOnly);
public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>> pivotKey, CancellationToken cancellationToken, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync(context, entities, new Expression<Func<T, object>>[] { pivotKey }, cancellationToken, doNotUpdateIfExists, insertOnly);
public static Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>>[] pivotKeys, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
=> EfSaveAsync<T>(context, entities, pivotKeys, CancellationToken.None, doNotUpdateIfExists, insertOnly);
public static async Task EfSaveAsync<T>(this DbContext context, IList<T> entities, Expression<Func<T, object>>[] pivotKeys, CancellationToken? cancellationToken = null, bool doNotUpdateIfExists = false, bool insertOnly = false) where T : class
if (cancellationToken == null)
{
if (cancellationToken == null)
{
cancellationToken = CancellationToken.None;
}
EfSaveEngine<T> efSaveEngine = new EfSaveEngine<T>(context, cancellationToken.Value, pivotKeys);
await efSaveEngine.SaveAsync(entities, doNotUpdateIfExists, insertOnly);
cancellationToken = CancellationToken.None;
}
EfSaveEngine<T> efSaveEngine = new EfSaveEngine<T>(context, cancellationToken.Value, pivotCondition);
await efSaveEngine.SaveAsync(entities, doNotUpdateIfExists, insertOnly);
}
}
17 changes: 17 additions & 0 deletions src/Paillave.EntityFrameworkCoreExtension/EfSave/EfSaveEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;

namespace Paillave.EntityFrameworkCoreExtension.EfSave;

public class EfSaveEngine<T> where T : class
{
private readonly Expression<Func<T, T, bool>> _findConditionExpression;
Expand Down Expand Up @@ -52,6 +53,18 @@ public EfSaveEngine(DbContext context, CancellationToken cancellationToken, para

_findConditionExpression = CreateFindConditionExpression(propertyInfosForPivot);
}
public EfSaveEngine(DbContext context, CancellationToken cancellationToken, Expression<Func<T, T, bool>> pivotCondition)
{
this._cancellationToken = cancellationToken;
_context = context;
var entityType = context.Model.FindEntityType(typeof(T)) ?? throw new InvalidOperationException("DbContext does not contain EntitySet for Type: " + typeof(T).Name);
_keyPropertyInfos = entityType.GetProperties()
.Where(i => !i.IsShadowProperty() && i.IsPrimaryKey())
.Where(i => i.PropertyInfo != null)
.Select(i => i.PropertyInfo!)
.ToList();
_findConditionExpression = pivotCondition;
}
private Expression<Func<T, T, bool>> CreateFindConditionExpression(List<List<PropertyInfo>> propertyInfosForPivotSet)
{
ParameterExpression leftParam = Expression.Parameter(typeof(T), "i");
Expand Down Expand Up @@ -134,3 +147,7 @@ private void InsertOrUpdateEntity(bool doNotUpdateIfExists, DbSet<T> contextSet,
}
}
}




30 changes: 24 additions & 6 deletions src/Paillave.Etl.EntityFrameworkCore/EfCoreSaveStreamNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ public EfCoreSaveArgsBuilder<TNewInEf, TIn, TNewInEf> Entity<TNewInEf>(Func<TIn,
GetOutput = (i, j) => j
}));

public EfCoreSaveArgsBuilder<TInEf, TIn, TOut> SeekOn(Expression<Func<TInEf, TInEf, bool>> pivot)
{
this.Args.PivotCriteria = pivot;
return this;
}
public EfCoreSaveArgsBuilder<TInEf, TIn, TOut> SeekOn(Expression<Func<TInEf, object>> pivot)
{
this.Args.PivotKeys = new List<Expression<Func<TInEf, object>>> { pivot };
Expand Down Expand Up @@ -146,6 +151,11 @@ public EfCoreSaveCorrelatedArgsBuilder<TInEf, TIn, TOut> AlternativelySeekOn(Exp
this.Args.PivotKeys.Add(pivot);
return this;
}
public EfCoreSaveCorrelatedArgsBuilder<TInEf, TIn, TOut> SeekOn(Expression<Func<TInEf, TInEf, bool>> pivot)
{
this.Args.PivotCriteria = pivot;
return this;
}
public EfCoreSaveCorrelatedArgsBuilder<TInEf, TIn, TNewOut> Output<TNewOut>(Func<TIn, TInEf, TNewOut> getOutput)
=> new EfCoreSaveCorrelatedArgsBuilder<TInEf, TIn, TNewOut>(UpdateArgs(new EfCoreSaveArgs<TInEf, Correlated<TIn>, Correlated<TNewOut>>
{
Expand Down Expand Up @@ -210,6 +220,7 @@ internal EfCoreSaveArgs() { }
public string? KeyedConnection { get; set; } = null;
public bool KeepChangeTracker { get; set; } = false;
public Type? DbContextType { get; set; } = null;
public Expression<Func<TInEf, TInEf, bool>> PivotCriteria { get; internal set; }
}
public enum SaveMode
{
Expand Down Expand Up @@ -279,17 +290,24 @@ private DisposeWrapper<DbContext> ResolveDbContext()
public async Task ProcessBatchAsync(List<(TIn Input, TInEf Entity)> items, DbContext dbContext, SaveMode bulkLoadMode)
{
var entities = items.Select(i => i.Item2).ToArray();
var pivotKeys = Args.PivotKeys == null ? (Expression<Func<TInEf, object>>[])null : Args.PivotKeys.ToArray();
if (bulkLoadMode == SaveMode.EntityFrameworkCore)
if (Args.PivotCriteria != null)
{
dbContext.EfSaveAsync(entities, pivotKeys, Args.SourceStream.Observable.CancellationToken, Args.DoNotUpdateIfExists, Args.InsertOnly).Wait();
dbContext.EfSaveAsync(entities, Args.PivotCriteria, Args.SourceStream.Observable.CancellationToken, Args.DoNotUpdateIfExists, Args.InsertOnly).Wait();
}
else
{
if (dbContext.Database.IsSqlServer())
dbContext.BulkSave(entities, pivotKeys, Args.SourceStream.Observable.CancellationToken, Args.DoNotUpdateIfExists, Args.InsertOnly);
else
var pivotKeys = Args.PivotKeys == null ? (Expression<Func<TInEf, object>>[])null : Args.PivotKeys.ToArray();
if (bulkLoadMode == SaveMode.EntityFrameworkCore)
{
dbContext.EfSaveAsync(entities, pivotKeys, Args.SourceStream.Observable.CancellationToken, Args.DoNotUpdateIfExists, Args.InsertOnly).Wait();
}
else
{
if (dbContext.Database.IsSqlServer())
dbContext.BulkSave(entities, pivotKeys, Args.SourceStream.Observable.CancellationToken, Args.DoNotUpdateIfExists, Args.InsertOnly);
else
dbContext.EfSaveAsync(entities, pivotKeys, Args.SourceStream.Observable.CancellationToken, Args.DoNotUpdateIfExists, Args.InsertOnly).Wait();
}
}
DetachAllEntities(dbContext);
}
Expand Down

0 comments on commit cfc8ebc

Please sign in to comment.