Skip to content

Commit

Permalink
add new optimistic lock extensions (temporary)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaceWindu committed Dec 24, 2022
1 parent cb1b005 commit c5a181d
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 4 deletions.
186 changes: 186 additions & 0 deletions Source/LinqToDB/Extensions/ConcurrencyExtensions.cs
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);
}
}
}
63 changes: 63 additions & 0 deletions Source/LinqToDB/Mapping/ConcurrencyPropertyAttribute.cs
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}.";
}
}
}
22 changes: 22 additions & 0 deletions Source/LinqToDB/Mapping/VersionBehavior.cs
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
}
}
9 changes: 5 additions & 4 deletions Source/LinqToDB/Reflection/Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,12 @@ public static class GroupBy

public static class SqlExt
{
public static readonly MethodInfo ToNotNull = MemberHelper.MethodOfGeneric<int?>(i => global::LinqToDB.Sql.ToNotNull(i));
public static readonly MethodInfo ToNotNullable = MemberHelper.MethodOfGeneric<int?>(i => global::LinqToDB.Sql.ToNotNullable(i));
public static readonly MethodInfo Alias = MemberHelper.MethodOfGeneric<int?>(i => global::LinqToDB.Sql.Alias(i, ""));
public static readonly MethodInfo ToNotNull = MemberHelper.MethodOfGeneric<int?>(i => global::LinqToDB.Sql.ToNotNull(i));
public static readonly MethodInfo ToNotNullable = MemberHelper.MethodOfGeneric<int?>(i => global::LinqToDB.Sql.ToNotNullable(i));
public static readonly MethodInfo Alias = MemberHelper.MethodOfGeneric<int?>(i => global::LinqToDB.Sql.Alias(i, ""));
// don't use MethodOfGeneric here (Sql.Property treatened in special way by it)
public static readonly MethodInfo Property = typeof(global::LinqToDB.Sql).GetMethodEx(nameof(global::LinqToDB.Sql.Property))!.GetGenericMethodDefinition();
public static readonly MethodInfo Property = typeof(global::LinqToDB.Sql).GetMethodEx(nameof(global::LinqToDB.Sql.Property))!.GetGenericMethodDefinition();
public static readonly MethodInfo CurrentTimestamp = typeof(global::LinqToDB.Sql).GetMethodEx(nameof(global::LinqToDB.Sql.CurrentTimestamp))!.GetGenericMethodDefinition();
}

public static class Update
Expand Down

0 comments on commit c5a181d

Please sign in to comment.