Skip to content

Commit

Permalink
Merge pull request #9 from mkolumb/feature/aftersavebehavior
Browse files Browse the repository at this point in the history
Change accept changes flag to after save behavior enum
  • Loading branch information
mkolumb authored Jul 15, 2022
2 parents 4b6f946 + bc0e02a commit 9ef7dc0
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class QueryExecutionConfiguration

public bool? AutoTransactionEnabled { get; set; }

public bool? AcceptAllChangesOnSuccess { get; set; }
public AfterSaveBehavior? AfterSaveBehavior { get; set; }

public IsolationLevel? AutoTransactionIsolationLevel { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace EFCore.Extensions.SaveOptimizer.Internal.Enums;

public enum AfterSaveBehavior
{
/// <summary>
/// It is default behavior, it will call ChangeTracker.Clear to prevent double save
/// </summary>
ClearChanges,

/// <summary>
/// It will call ChangeTracker.AcceptAllChanges, however it will not refresh temporary values - if you have auto increment key you should not use this
/// </summary>
AcceptChanges,

/// <summary>
/// It will mark all saved entities as detached
/// </summary>
DetachSaved,

/// <summary>
/// Do nothing
/// </summary>
DoNothing
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

public enum CaseType
{
/// <summary>
/// It will not change column name case during execution
/// </summary>
Normal,

/// <summary>
/// It will lowercase column name during execution
/// </summary>
Lowercase,

/// <summary>
/// It will uppercase column name during execution
/// </summary>
Uppercase
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
namespace EFCore.Extensions.SaveOptimizer.Internal.Enums;
// ReSharper disable UnusedMember.Global

namespace EFCore.Extensions.SaveOptimizer.Internal.Enums;

public enum ConcurrencyTokenBehavior
{
/// <summary>
/// It will throws exception when expected rows != affected rows
/// </summary>
ThrowException,

/// <summary>
/// It will skip entities with concurrency token changed, however will not throws exception
/// </summary>
SaveWhatPossible
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public int SaveChangesOptimized(DbContext context, QueryExecutionConfiguration?
transaction.Commit();
}

PrepareAfterSave(queries, configuration, context);
PrepareAfterSave(queries.Entries, configuration, context);

return rows;
}
Expand Down Expand Up @@ -166,7 +166,7 @@ public async Task<int> SaveChangesOptimizedAsync(DbContext context,
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}

PrepareAfterSave(queries, configuration, context);
PrepareAfterSave(queries.Entries, configuration, context);

return rows;
}
Expand Down Expand Up @@ -199,32 +199,52 @@ private QueryExecutionConfiguration Init(DbContext context, QueryExecutionConfig
return configuration;
}

private static void PrepareAfterSave(QueryPreparationModel queries, QueryExecutionConfiguration configuration,
private static void PrepareAfterSave(IEnumerable<EntityEntry> entries,
QueryExecutionConfiguration configuration,
DbContext context)
{
foreach (EntityEntry entry in queries.Entries)
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (configuration.AfterSaveBehavior)
{
if (entry.State != EntityState.Added)
{
continue;
}
case AfterSaveBehavior.ClearChanges:
ClearChanges(context);
return;
case AfterSaveBehavior.DetachSaved:
DetachEntries(entries);
return;
case AfterSaveBehavior.AcceptChanges:
FixTemporaryProperties(entries);
AcceptChanges(context);
return;
default:
FixTemporaryProperties(entries);
return;
}
}

private static void ClearChanges(DbContext context) => context.ChangeTracker.Clear();

private static void AcceptChanges(DbContext context) => context.ChangeTracker.AcceptAllChanges();

private static void DetachEntries(IEnumerable<EntityEntry> entries)
{
foreach (EntityEntry entry in entries)
{
entry.State = EntityState.Detached;
}
}

private static void FixTemporaryProperties(IEnumerable<EntityEntry> entries)
{
foreach (EntityEntry entry in entries)
{
foreach (PropertyEntry property in entry.Properties)
{
if (!property.IsTemporary)
if (property.IsTemporary)
{
continue;
property.IsTemporary = false;
}

property.IsTemporary = false;
}
}

if (configuration.AcceptAllChangesOnSuccess != true)
{
return;
}

context.ChangeTracker.AcceptAllChanges();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public QueryExecutionConfiguration Get(string providerName, QueryExecutionConfig

config.AutoTransactionEnabled ??= true;

config.AcceptAllChangesOnSuccess ??= true;
config.AfterSaveBehavior ??= AfterSaveBehavior.ClearChanges;

config.AutoTransactionIsolationLevel ??= IsolationLevel.Serializable;

Expand All @@ -57,7 +57,7 @@ private static QueryExecutionConfiguration Clone(QueryExecutionConfiguration? co
_ => new QueryExecutionConfiguration
{
AutoTransactionEnabled = configuration.AutoTransactionEnabled,
AcceptAllChangesOnSuccess = configuration.AcceptAllChangesOnSuccess,
AfterSaveBehavior = configuration.AfterSaveBehavior,
AutoTransactionIsolationLevel = configuration.AutoTransactionIsolationLevel,
BatchSize = configuration.BatchSize,
ConcurrencyTokenBehavior = configuration.ConcurrencyTokenBehavior,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void Init(DbContext context)
_wrappers[name] = wrapper;
}

// ReSharper disable once InvertIf
if (!_orders.ContainsKey(name))
{
IDictionary<Type, int> executeOrder = context.Model.ResolveEntityHierarchy();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Data;
using EFCore.Extensions.SaveOptimizer.Dapper;
using EFCore.Extensions.SaveOptimizer.Internal.Configuration;
using EFCore.Extensions.SaveOptimizer.Internal.Enums;
using EFCore.Extensions.SaveOptimizer.Model;
using EFCore.Extensions.SaveOptimizer.Shared.Tests.Enums;
using EFCore.Extensions.SaveOptimizer.Shared.Tests.Extensions;
Expand Down Expand Up @@ -43,6 +44,25 @@ public DbContextWrapper(ITestTimeDbContextFactory<EntitiesContext> factory, ITes

public void Dispose() => Context.Dispose();

private static QueryExecutionConfiguration GetConfig(SaveVariant variant, int? batchSize)
{
QueryExecutionConfiguration configuration =
batchSize.HasValue
? new QueryExecutionConfiguration { BatchSize = batchSize }
: new QueryExecutionConfiguration();

if (variant.HasFlag(SaveVariant.NoAutoTransaction))
{
configuration.AutoTransactionEnabled = false;
}

configuration.AfterSaveBehavior = variant.HasFlag(SaveVariant.WithTransaction)
? AfterSaveBehavior.DoNothing
: AfterSaveBehavior.AcceptChanges;

return configuration;
}

public void RecreateContext()
{
var connectionString = Context.Database.GetConnectionString();
Expand Down Expand Up @@ -91,22 +111,7 @@ public void CleanDb(string truncateFormat, string? resetSequenceFormat = null)

private async Task TrySaveAsync(SaveVariant variant, int? batchSize)
{
QueryExecutionConfiguration? configuration =
batchSize.HasValue ? new QueryExecutionConfiguration { BatchSize = batchSize } : null;

if (variant.HasFlag(SaveVariant.NoAutoTransaction))
{
configuration ??= new QueryExecutionConfiguration();

configuration.AutoTransactionEnabled = false;
}

if (variant.HasFlag(SaveVariant.WithTransaction))
{
configuration ??= new QueryExecutionConfiguration();

configuration.AcceptAllChangesOnSuccess = false;
}
QueryExecutionConfiguration configuration = GetConfig(variant, batchSize);

async Task InternalSave()
{
Expand All @@ -120,7 +125,8 @@ async Task InternalSave()
}
else if (variant.HasFlag(SaveVariant.EfCore))
{
await Context.SaveChangesAsync(configuration?.AcceptAllChangesOnSuccess == true).ConfigureAwait(false);
await Context.SaveChangesAsync(configuration.AfterSaveBehavior == AfterSaveBehavior.AcceptChanges)
.ConfigureAwait(false);
}
}

Expand Down Expand Up @@ -206,22 +212,7 @@ public void Save(SaveVariant variant, int? batchSize, int retries = RunTry)

private void TrySave(SaveVariant variant, int? batchSize)
{
QueryExecutionConfiguration? configuration =
batchSize.HasValue ? new QueryExecutionConfiguration { BatchSize = batchSize } : null;

if (variant.HasFlag(SaveVariant.NoAutoTransaction))
{
configuration ??= new QueryExecutionConfiguration();

configuration.AutoTransactionEnabled = false;
}

if (variant.HasFlag(SaveVariant.WithTransaction))
{
configuration ??= new QueryExecutionConfiguration();

configuration.AcceptAllChangesOnSuccess = false;
}
QueryExecutionConfiguration configuration = GetConfig(variant, batchSize);

void InternalSave()
{
Expand All @@ -235,7 +226,7 @@ void InternalSave()
}
else if (variant.HasFlag(SaveVariant.EfCore))
{
Context.SaveChanges(configuration?.AcceptAllChangesOnSuccess == true);
Context.SaveChanges(configuration.AfterSaveBehavior == AfterSaveBehavior.AcceptChanges);
}
}

Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,14 @@ Please note it is not working exactly as SaveChanges, so you should verify it wo
### Refresh data after save

SaveOptimizer approach makes almost impossible refresh data after save, it is on your side.
If you will use auto increment primary key it will not retrieve this key from db.
I recommend to generate values for primary keys in code, not in db.
This will make much easier refresh data after save if necessary, you will be able to use this values for query.
Also DatabaseValues for entry will not be retrieved from db when ConcurrencyTokenException is thrown.

Basically - after save you should not use this context anymore as it could be invalid, you should use new context for another operation.
However if you need you can experiment with AfterSaveBehavior.

## Known issues

### Oracle serializable transaction
Expand Down Expand Up @@ -169,8 +173,8 @@ This is not a SaveOptimizer issue, however I experienced some problems with Fire
| | | _Postgres - 31768_ |
| | | _Other - 15384_ |
| Concurrency token behavior | When concurrency token is defined for entity it is included in update / delete statements. When flag is set to throws exception it will throws exception when statements affected less / more rows than expected. | _All - throw exception_ |
| Auto transaction enabled | If enabled it will start transaction when no transaction attached to DbContext | _All - enabled_ |
| Accept all changes on success | If enabled it will accept all changes after successfull save | _All - enabled_ |
| Auto transaction enabled | If enabled it will start transaction when no transaction attached to DbContext | _All - true_ |
| After save behavior | It will behavior after successful save, possible values (ClearChanges, AcceptChanges, DetachSaved, DoNothing) | _All - ClearChanges_ |
| Auto transaction isolation level | Isolation level for auto transaction | _All - serializable_ |
| Builder configuration -> case type | Case type used when building statements, if normal it will not change case to upper / lower | _All - normal_ |
| Builder configuration -> optimize parameters | Optimize parameters usage in statements, sometimes can lead to unexpected exception in db | _All - true_ |
Expand Down

0 comments on commit 9ef7dc0

Please sign in to comment.