-
Notifications
You must be signed in to change notification settings - Fork 453
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add new optimistic lock extensions (temporary)
- Loading branch information
Showing
4 changed files
with
276 additions
and
4 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using LinqToDB.Expressions; | ||
using LinqToDB.Extensions; | ||
using LinqToDB.Linq; | ||
using LinqToDB.Mapping; | ||
using LinqToDB.Reflection; | ||
|
||
namespace LinqToDB | ||
{ | ||
public static class ConcurrencyExtensions | ||
{ | ||
private static IQueryable<T> FilterByColumns<T>(IQueryable<T> query, T obj, ColumnDescriptor[] columns) | ||
where T : class | ||
{ | ||
var objType = typeof(T); | ||
var methodInfo = Methods.Queryable.Where.MakeGenericMethod(objType); | ||
var param = Expression.Parameter(typeof(T), "obj"); | ||
var instance = Expression.Constant(obj); | ||
Expression? predicate = null; | ||
|
||
foreach (var cd in columns) | ||
{ | ||
var equality = Expression.Equal( | ||
Expression.MakeMemberAccess(param, cd.MemberInfo), | ||
cd.MemberAccessor.GetterExpression.GetBody(instance)); | ||
|
||
predicate = predicate == null ? equality : Expression.AndAlso(predicate, equality); | ||
} | ||
|
||
if (predicate != null) | ||
query = (IQueryable<T>)methodInfo.Invoke(null, new object[] { query, Expression.Lambda(predicate, param) })!; | ||
|
||
return query; | ||
} | ||
|
||
private static IQueryable<T> FilterByPrimaryKey<T>(this IDataContext dc, T obj, EntityDescriptor ed) | ||
where T : class | ||
{ | ||
var objType = typeof(T); | ||
var pks = ed.Columns.Where(c => c.IsPrimaryKey).ToArray(); | ||
|
||
if (pks.Length == 0) | ||
throw new LinqToDBException($"Entity of type {objType} does not have primary key defined."); | ||
|
||
return FilterByColumns(dc.GetTable<T>(), obj, pks); | ||
} | ||
|
||
private static IQueryable<T> MakeConcurrentFilter<T>(IDataContext dc, T obj, Type objType, EntityDescriptor ed) | ||
where T : class | ||
{ | ||
var query = FilterByPrimaryKey(dc, obj, ed); | ||
|
||
var concurrencyColumns = ed.Columns | ||
.Select(c => new | ||
{ | ||
Column = c, | ||
Attr = dc.MappingSchema.GetAttribute<ConcurrencyPropertyAttribute>(objType, c.MemberInfo) | ||
}) | ||
.Where(_ => _.Attr != null) | ||
.Select(_ => _.Column) | ||
.ToArray(); | ||
|
||
if (concurrencyColumns.Length > 0) | ||
query = FilterByColumns(query, obj, concurrencyColumns); | ||
|
||
return query; | ||
} | ||
|
||
private static IUpdatable<T> MakeUpdateConcurrent<T>(IDataContext dc, T obj) | ||
where T : class | ||
{ | ||
var objType = typeof(T); | ||
var ed = dc.MappingSchema.GetEntityDescriptor(objType); | ||
var query = MakeConcurrentFilter(dc, obj, objType, ed); | ||
|
||
var updatable = query.AsUpdatable(); | ||
var columnsToUpdate = ed.Columns.Where(c => !c.IsPrimaryKey && !c.IsIdentity && !c.SkipOnUpdate && !c.ShouldSkip(obj, ed, SkipModification.Update)); | ||
|
||
var param = Expression.Parameter(objType, "u"); | ||
var instance = Expression.Constant(obj); | ||
|
||
foreach (var cd in columnsToUpdate) | ||
{ | ||
var updateMethod = Methods.LinqToDB.Update.SetUpdatablePrev.MakeGenericMethod(objType, cd.MemberInfo.GetMemberType()); | ||
var propExpression = Expression.Lambda(Expression.MakeMemberAccess(param, cd.MemberInfo), param); | ||
|
||
var concurrencyAttribute = dc.MappingSchema.GetAttribute<ConcurrencyPropertyAttribute>(objType, cd.MemberInfo); | ||
|
||
LambdaExpression? valueExpression; | ||
if (concurrencyAttribute != null) | ||
{ | ||
valueExpression = concurrencyAttribute?.GetNextValue(cd, param); | ||
|
||
if (valueExpression == null) | ||
continue; | ||
} | ||
else | ||
valueExpression = Expression.Lambda(cd.MemberAccessor.GetterExpression.GetBody(instance), param); | ||
|
||
updatable = (IUpdatable<T>)updateMethod.Invoke(null, new object[] { updatable, propExpression, valueExpression })!; | ||
} | ||
|
||
return updatable; | ||
} | ||
|
||
private static IQueryable<T> MakeDeleteConcurrent<T>(IDataContext dc, T obj) | ||
where T : class | ||
{ | ||
var objType = typeof(T); | ||
var ed = dc.MappingSchema.GetEntityDescriptor(objType); | ||
var query = MakeConcurrentFilter(dc, obj, objType, ed); | ||
|
||
return query; | ||
} | ||
|
||
/// <summary> | ||
/// Performs record update using optimistic lock strategy. | ||
/// Entity should have column annotated with <see cref="ConcurrencyPropertyAttribute" />, otherwise regular update operation will be performed. | ||
/// </summary> | ||
/// <typeparam name="T">Entity type.</typeparam> | ||
/// <param name="dc">Database context.</param> | ||
/// <param name="obj">Entity instance to update.</param> | ||
/// <returns>Number of updated records.</returns> | ||
public static int UpdateConcurrent<T>(this IDataContext dc, T obj) | ||
where T : class | ||
{ | ||
if (obj == null) throw new ArgumentNullException(nameof(obj)); | ||
|
||
return MakeUpdateConcurrent(dc, obj).Update(); | ||
} | ||
|
||
/// <summary> | ||
/// Performs record update using optimistic lock strategy asynchronously. | ||
/// Entity should have column annotated with <see cref="ConcurrencyPropertyAttribute" />, otherwise regular update operation will be performed. | ||
/// </summary> | ||
/// <typeparam name="T">Entity type.</typeparam> | ||
/// <param name="dc">Database context.</param> | ||
/// <param name="obj">Entity instance to update.</param> | ||
/// <param name="cancellationToken">Asynchronous operation cancellation token.</param> | ||
/// <returns>Number of updated records.</returns> | ||
public static Task<int> UpdateConcurrentAsync<T>(this IDataContext dc, T obj, CancellationToken cancellationToken = default) | ||
where T : class | ||
{ | ||
if (obj == null) throw new ArgumentNullException(nameof(obj)); | ||
|
||
return MakeUpdateConcurrent(dc, obj).UpdateAsync(cancellationToken); | ||
} | ||
|
||
/// <summary> | ||
/// Performs record delete using optimistic lock strategy. | ||
/// Entity should have column annotated with <see cref="ConcurrencyPropertyAttribute" />, otherwise regular delete operation will be performed. | ||
/// </summary> | ||
/// <typeparam name="T">Entity type.</typeparam> | ||
/// <param name="dc">Database context.</param> | ||
/// <param name="obj">Entity instance to delete.</param> | ||
/// <returns>Number of deleted records.</returns> | ||
public static int DeleteConcurrent<T>(this IDataContext dc, T obj) | ||
where T : class | ||
{ | ||
if (obj == null) throw new ArgumentNullException(nameof(obj)); | ||
|
||
return MakeDeleteConcurrent(dc, obj).Delete(); | ||
} | ||
|
||
/// <summary> | ||
/// Performs record delete using optimistic lock strategy asynchronously. | ||
/// Entity should have column annotated with <see cref="ConcurrencyPropertyAttribute" />, otherwise regular delete operation will be performed. | ||
/// </summary> | ||
/// <typeparam name="T">Entity type.</typeparam> | ||
/// <param name="dc">Database context.</param> | ||
/// <param name="obj">Entity instance to delete.</param> | ||
/// <param name="cancellationToken">Asynchronous operation cancellation token.</param> | ||
/// <returns>Number of deleted records.</returns> | ||
public static Task<int> DeleteConcurrentAsync<T>(this IDataContext dc, T obj, CancellationToken cancellationToken = default) | ||
where T : class | ||
{ | ||
if (obj == null) throw new ArgumentNullException(nameof(obj)); | ||
|
||
return MakeDeleteConcurrent(dc, obj).DeleteAsync(cancellationToken); | ||
} | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
using System; | ||
using System.Linq.Expressions; | ||
using LinqToDB.Expressions; | ||
using LinqToDB.Reflection; | ||
|
||
namespace LinqToDB.Mapping | ||
{ | ||
/// <summary> | ||
/// Identifies optimistic concurrency column behavior/strategy column and strategy. | ||
/// Used with <see cref="ConcurrencyExtensions" /> extensions, e.g. <see cref="ConcurrencyExtensions.UpdateConcurrent{T}(IDataContext, T)"/> or <see cref="ConcurrencyExtensions.UpdateConcurrentAsync{T}(IDataContext, T, System.Threading.CancellationToken)"/> methods. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] | ||
public class ConcurrencyPropertyAttribute : MappingAttribute | ||
{ | ||
public ConcurrencyPropertyAttribute(VersionBehavior behavior) | ||
{ | ||
Behavior = behavior; | ||
} | ||
|
||
/// <summary> | ||
/// Versioning strategy. | ||
/// </summary> | ||
public VersionBehavior Behavior { get; } | ||
|
||
/// <summary> | ||
/// Optional mapping configuration name. | ||
/// </summary> | ||
public string? Configuration { get; set; } | ||
|
||
/// <summary> | ||
/// Implements generation of update value expression for current optimistic lock column. | ||
/// </summary> | ||
/// <param name="column">Column mapping descriptor.</param> | ||
/// <param name="record">Updated record.</param> | ||
/// <returns><c>null</c> to skip explicit column update or update expression.</returns> | ||
public virtual LambdaExpression? GetNextValue(ColumnDescriptor column, ParameterExpression record) | ||
{ | ||
switch (Behavior) | ||
{ | ||
case VersionBehavior.Auto: | ||
return null; | ||
|
||
case VersionBehavior.AutoIncrement: | ||
return Expression.Lambda( | ||
Expression.Add(column.MemberAccessor.GetterExpression.GetBody(record), Expression.Constant(1)), | ||
record); | ||
|
||
case VersionBehavior.CurrentTimestamp: | ||
return Expression.Lambda( | ||
Expression.Call(Methods.LinqToDB.SqlExt.CurrentTimestamp), | ||
record); | ||
|
||
default: | ||
throw new ArgumentOutOfRangeException($"Unsupported {nameof(VersionBehavior)} value: {Behavior}"); | ||
} | ||
} | ||
|
||
public override string GetObjectID() | ||
{ | ||
return $".{Configuration}.{(int)Behavior}."; | ||
} | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
namespace LinqToDB.Mapping | ||
{ | ||
/// <summary> | ||
/// Defines optimistic concurrency column modification strategy. Used with <see cref="ConcurrencyPropertyAttribute" /> attribute | ||
/// and <see cref="ConcurrencyExtensions" /> extensions, e.g. <see cref="ConcurrencyExtensions.UpdateConcurrent{T}(IDataContext, T)"/> or <see cref="ConcurrencyExtensions.UpdateConcurrentAsync{T}(IDataContext, T, System.Threading.CancellationToken)"/> methods. | ||
/// </summary> | ||
public enum VersionBehavior | ||
{ | ||
/// <summary> | ||
/// Column value modified by server automatically on update. E.g. SQL Server rowversion/timestamp column or database trigger. | ||
/// </summary> | ||
Auto, | ||
/// <summary> | ||
/// Column value should be incremented by 1. | ||
/// </summary> | ||
AutoIncrement, | ||
/// <summary> | ||
/// Use current timestamp value (provided by <see cref="Sql.CurrentTimestamp" /> helper). | ||
/// </summary> | ||
CurrentTimestamp | ||
} | ||
} |
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