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

feat: add support for pivot conditions in EfSaveEngine and related ex… #521

Merged
merged 1 commit into from
Jan 13, 2025
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
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
Loading