From d827c7677543e984662886a2fbc1346e7ce1c46a Mon Sep 17 00:00:00 2001 From: 丁松杰 <377973147@qq.com> Date: Fri, 14 Feb 2020 22:51:56 +0800 Subject: [PATCH] 添加 orleans.provider. entityframeworkcore 部分代码 --- Pole.sln | 11 +++++++++-- src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureEntryStateDelegate.cs | 10 ++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureSaveEntryContext.cs | 21 +++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConvention.cs | 687 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConventionOptions.cs | 15 +++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/IGrainStorageConvention.cs | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/DefaultGrainStateEntryConfigurator.cs | 23 +++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/EntityFrameworkGrainStorage.cs | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/EntityTypeResolver.cs | 23 +++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/EntityWapper.cs | 13 +++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorage.cs | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageConfigurationException.cs | 22 ++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageContext.cs | 41 +++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptions.cs | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptionsExtensions.cs | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/GrainStoragePostConfigureOptions.cs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/IEntityTypeResolver.cs | 13 +++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/IGrainStateEntryConfigurator.cs | 15 +++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/Pole.Orleans.Provider.EntityframeworkCore.csproj | 15 +++++++++++++++ src/Pole.Orleans.Provider.EntityframeworkCore/Utils/ReflectionHelper.cs | 38 ++++++++++++++++++++++++++++++++++++++ 20 files changed, 1722 insertions(+), 2 deletions(-) create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureEntryStateDelegate.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureSaveEntryContext.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConvention.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConventionOptions.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/IGrainStorageConvention.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/DefaultGrainStateEntryConfigurator.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/EntityFrameworkGrainStorage.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/EntityTypeResolver.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/EntityWapper.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorage.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageConfigurationException.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageContext.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptions.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptionsExtensions.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/GrainStoragePostConfigureOptions.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/IEntityTypeResolver.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/IGrainStateEntryConfigurator.cs create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/Pole.Orleans.Provider.EntityframeworkCore.csproj create mode 100644 src/Pole.Orleans.Provider.EntityframeworkCore/Utils/ReflectionHelper.cs diff --git a/Pole.sln b/Pole.sln index 973006e..5a84a83 100644 --- a/Pole.sln +++ b/Pole.sln @@ -47,9 +47,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Product.IntegrationEvents", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pole.EventBus.Rabbitmq", "src\Pole.EventBus.Rabbitmq\Pole.EventBus.Rabbitmq.csproj", "{BDF62A19-FFBD-4EE1-A07A-68472E680A95}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.Core.Test", "test\Pole.Core.Test\Pole.Core.Test.csproj", "{23EA8735-DB2E-4599-8902-8FCBCBE4799C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pole.Core.Test", "test\Pole.Core.Test\Pole.Core.Test.csproj", "{23EA8735-DB2E-4599-8902-8FCBCBE4799C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.EventStorage.PostgreSql", "src\Pole.EventStorage.PostgreSql\Pole.EventStorage.PostgreSql.csproj", "{548EFDBB-252F-48DD-87F4-58ABFBD4963C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pole.EventStorage.PostgreSql", "src\Pole.EventStorage.PostgreSql\Pole.EventStorage.PostgreSql.csproj", "{548EFDBB-252F-48DD-87F4-58ABFBD4963C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pole.Orleans.Provider.EntityframeworkCore", "src\Pole.Orleans.Provider.EntityframeworkCore\Pole.Orleans.Provider.EntityframeworkCore.csproj", "{0DA75F4A-BF47-4B52-B932-48BB6A709934}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -129,6 +131,10 @@ Global {548EFDBB-252F-48DD-87F4-58ABFBD4963C}.Debug|Any CPU.Build.0 = Debug|Any CPU {548EFDBB-252F-48DD-87F4-58ABFBD4963C}.Release|Any CPU.ActiveCfg = Release|Any CPU {548EFDBB-252F-48DD-87F4-58ABFBD4963C}.Release|Any CPU.Build.0 = Release|Any CPU + {0DA75F4A-BF47-4B52-B932-48BB6A709934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DA75F4A-BF47-4B52-B932-48BB6A709934}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DA75F4A-BF47-4B52-B932-48BB6A709934}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DA75F4A-BF47-4B52-B932-48BB6A709934}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -155,6 +161,7 @@ Global {BDF62A19-FFBD-4EE1-A07A-68472E680A95} = {9932C965-8B38-4F70-9E43-86DC56860E2B} {23EA8735-DB2E-4599-8902-8FCBCBE4799C} = {655E719B-4A3E-467C-A541-E0770AB81DE1} {548EFDBB-252F-48DD-87F4-58ABFBD4963C} = {9932C965-8B38-4F70-9E43-86DC56860E2B} + {0DA75F4A-BF47-4B52-B932-48BB6A709934} = {9932C965-8B38-4F70-9E43-86DC56860E2B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DB0775A3-F293-4043-ADB7-72BAC081E87E} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureEntryStateDelegate.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureEntryStateDelegate.cs new file mode 100644 index 0000000..7851d34 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureEntryStateDelegate.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore.ChangeTracking; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public delegate void ConfigureEntryStateDelegate(EntityEntry entry) + where TGrainState : class; +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureSaveEntryContext.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureSaveEntryContext.cs new file mode 100644 index 0000000..03696b1 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/ConfigureSaveEntryContext.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public class ConfigureSaveEntryContext + { + public ConfigureSaveEntryContext(TContext dbContext, TEntity entity) + { + DbContext = dbContext; + Entity = entity; + } + + public TContext DbContext { get; } + + public TEntity Entity { get; } + + public bool IsPersisted { get; set; } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConvention.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConvention.cs new file mode 100644 index 0000000..ada275b --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConvention.cs @@ -0,0 +1,687 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans; +using Orleans.Runtime; +using Pole.Orleans.Provider.EntityframeworkCore.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Orleans.Provider.EntityframeworkCore.Conventions +{ + public class GrainStorageConvention : IGrainStorageConvention + { + private readonly GrainStorageConventionOptions _options; + private readonly IServiceScopeFactory _serviceScopeFactory; + + public GrainStorageConvention(IOptions options, IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + _options = options.Value; + } + + public virtual Action GetSetterFunc() where TEntity : class + { + return (state, entity) => state.State = entity; + } + + public virtual Func GetGetterFunc() where TEntity : class + { + return state => state.State as TEntity; + } + + + #region Default Query + + public virtual Func> CreateDefaultDbSetAccessorFunc() + where TContext : DbContext + where TEntity : class + { + Type contextType = typeof(TContext); + + // Find a dbSet as default + PropertyInfo dbSetPropertyInfo = + contextType + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(pInfo => pInfo.PropertyType == typeof(DbSet)); + + if (dbSetPropertyInfo == null) + throw new GrainStorageConfigurationException($"Could not find A property of type \"{typeof(DbSet).FullName}\" " + + $"on context with type \"{typeof(TContext).FullName}\""); + + var dbSetDelegate = (Func>)Delegate.CreateDelegate( + typeof(Func>), + null, + dbSetPropertyInfo.GetMethod); + + // set queries as no tracking + MethodInfo noTrackingMethodInfo = (typeof(GrainStorageConvention).GetMethod(nameof(AsNoTracking)) + ?? throw new Exception("Impossible")) + .MakeGenericMethod(typeof(TContext), typeof(TEntity)); + + // create final delegate which chains dbSet getter and no tracking delegates + return (Func>)Delegate.CreateDelegate( + typeof(Func>), + dbSetDelegate, + noTrackingMethodInfo); + } + + public static IQueryable AsNoTracking( + Func> func, + TContext context) + where TContext : DbContext + where TEntity : class + => func(context).AsNoTracking(); + + public virtual Func> + CreateDefaultReadStateFunc( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (typeof(IGrainWithGuidKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.GuidKeySelector == null) + throw new GrainStorageConfigurationException($"GuidKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + return (TContext context, IAddressable grainRef) => + { + Guid key = grainRef.GetPrimaryKey(); + return options.DbSetAccessor(context) + .SingleOrDefaultAsync( + state => options.GuidKeySelector(state) == key); + }; + } + + if (typeof(IGrainWithGuidCompoundKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.GuidKeySelector == null) + throw new GrainStorageConfigurationException($"GuidKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + if (options.KeyExtSelector == null) + throw new GrainStorageConfigurationException($"KeyExtSelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + return (TContext context, IAddressable grainRef) => + { + Guid key = grainRef.GetPrimaryKey(out string keyExt); + return + options.DbSetAccessor(context) + .SingleOrDefaultAsync(state => + options.GuidKeySelector(state) == key + && options.KeyExtSelector(state) == keyExt); + }; + } + + if (typeof(IGrainWithIntegerKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.LongKeySelector == null) + throw new GrainStorageConfigurationException($"LongKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + return (TContext context, IAddressable grainRef) => + { + long key = grainRef.GetPrimaryKeyLong(); + return options.DbSetAccessor(context) + .SingleOrDefaultAsync(state => options.LongKeySelector(state) == key); + + }; + } + + if (typeof(IGrainWithIntegerCompoundKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.LongKeySelector == null) + throw new GrainStorageConfigurationException($"LongKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + if (options.KeyExtSelector == null) + throw new GrainStorageConfigurationException($"KeyExtSelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + return (TContext context, IAddressable grainRef) => + { + long key = grainRef.GetPrimaryKeyLong(out string keyExt); + return options.DbSetAccessor(context) + .SingleOrDefaultAsync(state => + options.LongKeySelector(state) == key + && options.KeyExtSelector(state) == keyExt); + }; + } + + if (typeof(IGrainWithStringKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.KeyExtSelector == null) + throw new GrainStorageConfigurationException($"KeyExtSelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + var compiledQuery = EF.CompileAsyncQuery((TContext context, string keyExt) + => options.DbSetAccessor(context) + .SingleOrDefault(state => + options.KeyExtSelector(state) == keyExt)); + + return (TContext context, IAddressable grainRef) => + { + string keyExt = grainRef.GetPrimaryKeyString(); + return options.DbSetAccessor(context) + .SingleOrDefaultAsync(state => + options.KeyExtSelector(state) == keyExt); + }; + } + + throw new InvalidOperationException($"Unexpected grain type \"{typeof(TGrain).FullName}\""); + } + + public virtual Func> + CreatePreCompiledDefaultReadStateFunc( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (typeof(IGrainWithGuidKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.GuidKeySelector == null) + throw new GrainStorageConfigurationException($"GuidKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + Func> compiledQuery + = CreateCompiledQuery(options); + + return (TContext context, IAddressable grainRef) => + { + Guid key = grainRef.GetPrimaryKey(); + return compiledQuery(context, key); + }; + } + + if (typeof(IGrainWithGuidCompoundKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.GuidKeySelector == null) + throw new GrainStorageConfigurationException($"GuidKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + if (options.KeyExtSelector == null) + throw new GrainStorageConfigurationException($"KeyExtSelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + Func> compiledQuery + = CreateCompiledCompoundQuery(options); + + return (TContext context, IAddressable grainRef) => + { + Guid key = grainRef.GetPrimaryKey(out string keyExt); + return compiledQuery(context, key, keyExt); + }; + } + + if (typeof(IGrainWithIntegerKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.LongKeySelector == null) + throw new GrainStorageConfigurationException($"LongKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + Func> compiledQuery + = CreateCompiledQuery(options); + + return (TContext context, IAddressable grainRef) => + { + long key = grainRef.GetPrimaryKeyLong(); + return compiledQuery(context, key); + }; + } + + if (typeof(IGrainWithIntegerCompoundKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.LongKeySelector == null) + throw new GrainStorageConfigurationException($"LongKeySelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + if (options.KeyExtSelector == null) + throw new GrainStorageConfigurationException($"KeyExtSelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + Func> compiledQuery + = CreateCompiledCompoundQuery(options); + + return (TContext context, IAddressable grainRef) => + { + long key = grainRef.GetPrimaryKeyLong(out string keyExt); + return compiledQuery(context, key, keyExt); + }; + } + + if (typeof(IGrainWithStringKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.KeyExtSelector == null) + throw new GrainStorageConfigurationException($"KeyExtSelector is not defined for " + + $"{typeof(GrainStorageOptions).FullName}"); + + Func> compiledQuery + = CreateCompiledQuery(options); + + return (TContext context, IAddressable grainRef) => + { + string keyExt = grainRef.GetPrimaryKeyString(); + return compiledQuery(context, keyExt); + }; + } + + throw new InvalidOperationException($"Unexpected grain type \"{typeof(TGrain).FullName}\""); + } + + public virtual void SetDefaultKeySelectors( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (options.KeyPropertyName == null) + options.KeyPropertyName = _options.DefaultGrainKeyPropertyName; + + if (options.KeyExtPropertyName == null) + options.KeyExtPropertyName = _options.DefaultGrainKeyExtPropertyName; + + + PropertyInfo idProperty = ReflectionHelper.GetPropertyInfo( + options.KeyPropertyName ?? _options.DefaultGrainKeyPropertyName); + + Type idType = idProperty.PropertyType; + + if (typeof(IGrainWithGuidKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.GuidKeySelector != null) + return; + + if (idType != typeof(Guid)) + throw new GrainStorageConfigurationException( + $"Incompatible grain and state. \"{typeof(TGrain).FullName}\" expects a Guid key " + + $"but the type {typeof(TEntity).FullName}.{idProperty.Name} " + + $"is of type {idType.FullName}."); + + + options.GuidKeySelector = ReflectionHelper.GetAccessorDelegate(idProperty); + return; + } + + + if (typeof(IGrainWithGuidCompoundKey).IsAssignableFrom(typeof(TGrain))) + { + PropertyInfo keyExtProperty + = ReflectionHelper.GetPropertyInfo( + options.KeyExtPropertyName ?? _options.DefaultGrainKeyExtPropertyName); + + if (idType != typeof(Guid)) + throw new GrainStorageConfigurationException( + $"Incompatible grain and state. \"{typeof(TGrain).FullName}\" expects a Guid key " + + $"but the type {typeof(TEntity).FullName}.{idProperty.Name} " + + $"is of type {idType.FullName}."); + + if (keyExtProperty.PropertyType != typeof(string)) + throw new GrainStorageConfigurationException($"Can not use property \"{keyExtProperty.Name}\" " + + $"on grain state type \"{typeof(TEntity)}\". " + + "KeyExt property must be of type string."); + + if (options.GuidKeySelector == null) + options.GuidKeySelector = ReflectionHelper.GetAccessorDelegate(idProperty); + if (options.KeyExtSelector == null) + options.KeyExtSelector = ReflectionHelper.GetAccessorDelegate(keyExtProperty); + + return; + } + + if (typeof(IGrainWithIntegerKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.LongKeySelector != null) + return; + + if (idType != typeof(long)) + throw new GrainStorageConfigurationException( + $"Incompatible grain and state. \"{typeof(TGrain).FullName}\" expects a long key " + + $"but the type {typeof(TEntity).FullName}.{idProperty.Name} " + + $"is of type {idType.FullName}."); + + options.LongKeySelector = ReflectionHelper.GetAccessorDelegate(idProperty); + return; + } + + if (typeof(IGrainWithIntegerCompoundKey).IsAssignableFrom(typeof(TGrain))) + { + PropertyInfo keyExtProperty + = ReflectionHelper.GetPropertyInfo( + options.KeyExtPropertyName ?? _options.DefaultGrainKeyExtPropertyName); + + if (keyExtProperty.PropertyType != typeof(string)) + throw new GrainStorageConfigurationException($"Can not use property \"{keyExtProperty.Name}\" " + + $"on grain state type \"{typeof(TEntity)}\". " + + "KeyExt property must be of type string."); + + if (options.LongKeySelector == null) + options.LongKeySelector = ReflectionHelper.GetAccessorDelegate(idProperty); + if (options.KeyExtSelector == null) + options.KeyExtSelector = ReflectionHelper.GetAccessorDelegate(keyExtProperty); + return; + } + + if (typeof(IGrainWithStringKey).IsAssignableFrom(typeof(TGrain))) + { + if (options.KeyExtSelector != null) + return; + + if (idType != typeof(string)) + throw new GrainStorageConfigurationException( + $"Incompatible grain and state. \"{typeof(TGrain).FullName}\" expects a string key " + + $"but the type {typeof(TEntity).FullName}.{idProperty.Name} " + + $"is of type {idType.FullName}."); + + options.KeyExtSelector = ReflectionHelper.GetAccessorDelegate(idProperty); + return; + } + + throw new InvalidOperationException($"Unexpected grain type \"{typeof(TGrain).FullName}\""); + } + + private static Func> CreateCompiledQuery( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + var contextParameter = Expression.Parameter(typeof(TContext), "context"); + var keyParameter = Expression.Parameter(typeof(TKey), "grainKey"); + var predicate = CreateKeyPredicate(options, keyParameter); + + var queryable = Expression.Call( + options.DbSetAccessor.Method, + Expression.Constant(options.DbSetAccessor), + contextParameter); + + var compiledLambdaBody = Expression.Call( + typeof(Queryable).GetMethods().Single(mi => + mi.Name == nameof(Queryable.SingleOrDefault) && mi.GetParameters().Count() == 2) + .MakeGenericMethod(typeof(TEntity)), + queryable, + Expression.Quote(predicate)); + + var lambdaExpression = Expression.Lambda>( + compiledLambdaBody, contextParameter, keyParameter); + + return EF.CompileAsyncQuery(lambdaExpression); + } + + private static Expression> CreateKeyPredicate( + GrainStorageOptions options, + ParameterExpression grainKeyParameter) + { + ParameterExpression stateParam = Expression.Parameter(typeof(TEntity), "state"); + MemberExpression stateKeyParam = Expression.Property(stateParam, options.KeyPropertyName); + + BinaryExpression equals = Expression.Equal(grainKeyParameter, stateKeyParam); + + return Expression.Lambda>(equals, stateParam); + } + + private static Func> CreateCompiledCompoundQuery( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + var contextParameter = Expression.Parameter(typeof(TContext), "context"); + var keyParameter = Expression.Parameter(typeof(TKey), "grainKey"); + var keyExtParameter = Expression.Parameter(typeof(string), "grainKeyExt"); + var predicate = CreateCompoundKeyPredicate( + options, + keyParameter, + keyExtParameter); + + var queryable = Expression.Call( + options.DbSetAccessor.Method, + Expression.Constant(options.DbSetAccessor), + contextParameter); + + var compiledLambdaBody = Expression.Call( + typeof(Queryable).GetMethods().Single(mi => + mi.Name == nameof(Queryable.SingleOrDefault) && mi.GetParameters().Count() == 2) + .MakeGenericMethod(typeof(TEntity)), + queryable, + Expression.Quote(predicate)); + + var lambdaExpression = Expression.Lambda>( + compiledLambdaBody, contextParameter, keyParameter, keyExtParameter); + + return EF.CompileAsyncQuery(lambdaExpression); + } + + + private static Expression> CreateCompoundKeyPredicate( + GrainStorageOptions options, + ParameterExpression grainKeyParam, + ParameterExpression grainKeyExtParam) + { + ParameterExpression stateParam = Expression.Parameter(typeof(TEntity), "state"); + MemberExpression stateKeyParam = Expression.Property(stateParam, options.KeyPropertyName); + MemberExpression stateKeyExtParam = Expression.Property(stateParam, options.KeyExtPropertyName); + + BinaryExpression equals = Expression.And( + Expression.Equal(grainKeyParam, stateKeyParam), + Expression.Equal(grainKeyExtParam, stateKeyExtParam) + ); + + return Expression.Lambda>(equals, stateParam); + } + + #endregion + + #region IsPersisted + + /// + /// Creates a method that tests the value of the Id property to default of its type. + /// + /// + /// + /// + public virtual Func CreateIsPersistedFunc(GrainStorageOptions options) + where TEntity : class + { + PropertyInfo idProperty + = ReflectionHelper.GetPropertyInfo( + options.PersistenceCheckPropertyName ?? _options.DefaultPersistenceCheckPropertyName); + + if (!idProperty.CanRead) + throw new GrainStorageConfigurationException( + $"Property \"{idProperty.Name}\" of type \"{idProperty.PropertyType.FullName}\" " + + "must have a public getter."); + + MethodInfo methodInfo = typeof(GrainStorageConvention).GetMethod( + idProperty.PropertyType.IsValueType + ? nameof(IsNotDefaultValueType) + : nameof(IsNotDefaultReferenceType), + BindingFlags.Static | BindingFlags.Public); + if (methodInfo == null) + throw new Exception("Impossible"); + + return (Func) + Delegate.CreateDelegate(typeof(Func), + idProperty, + methodInfo.MakeGenericMethod(typeof(TEntity), idProperty.PropertyType)); + } + + public static bool IsNotDefaultValueType( + PropertyInfo propertyInfo, TEntity state) + where TProperty : struct + { + return !((TProperty)propertyInfo.GetValue(state)).Equals(default(TProperty)); + } + + public static bool IsNotDefaultReferenceType( + PropertyInfo propertyInfo, TEntity state) + where TProperty : class + { + return !((TProperty)propertyInfo.GetValue(state)).Equals(default(TProperty)); + } + + #endregion + + #region ETag + + public virtual void FindAndConfigureETag( + GrainStorageOptions options, + bool throwIfNotFound) + where TContext : DbContext + where TEntity : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + using (IServiceScope scope = _serviceScopeFactory.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + + IEntityType entityType = context.Model.FindEntityType(typeof(TEntity)); + + if (entityType == null) + return; + + if (!FindAndConfigureETag(entityType, options) && throwIfNotFound) + throw new GrainStorageConfigurationException( + $"Could not find a valid ETag property on type \"{typeof(TEntity).FullName}\"."); + } + } + + public virtual void ConfigureETag( + string propertyName, + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (options == null) throw new ArgumentNullException(nameof(options)); + + using (IServiceScope scope = _serviceScopeFactory.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + + IEntityType entityType = context.Model.FindEntityType(typeof(TEntity)); + + if (entityType == null) + return; + + ConfigureETag(entityType, propertyName, options); + } + } + + private static bool FindAndConfigureETag( + IEntityType entityType, + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (entityType == null) throw new ArgumentNullException(nameof(entityType)); + if (options == null) throw new ArgumentNullException(nameof(options)); + + IEnumerable properties = entityType.GetProperties(); + + foreach (IProperty property in properties) + { + if (!property.IsConcurrencyToken) + continue; + + ConfigureETag(property, options); + + return true; + } + + return false; + } + + + private static void ConfigureETag( + IEntityType entityType, + string propertyName, + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (entityType == null) throw new ArgumentNullException(nameof(entityType)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + if (options == null) throw new ArgumentNullException(nameof(options)); + + IProperty property = entityType.FindProperty(propertyName); + + if (property == null) + throw new GrainStorageConfigurationException( + $"Property {propertyName} on model{typeof(TEntity).FullName} not found."); + + ConfigureETag(property, options); + } + + + private static void ConfigureETag( + IProperty property, + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class + { + if (property == null) throw new ArgumentNullException(nameof(property)); + + if (!property.IsConcurrencyToken) + throw new GrainStorageConfigurationException($"Property {property.Name} is not a concurrency token."); + + options.CheckForETag = true; + options.ETagPropertyName = property.Name; + options.ETagProperty = property; + options.ETagType = property.ClrType; + + options.GetETagFunc = CreateGetETagFunc(property.Name); + options.ConvertETagObjectToStringFunc + = CreateConvertETagObjectToStringFunc(); + } + + private static Func CreateGetETagFunc(string propertyName) + { + PropertyInfo propertyInfo = ReflectionHelper.GetPropertyInfo(propertyName); + + var getterDelegate = (Func)Delegate.CreateDelegate( + typeof(Func), + null, + propertyInfo.GetMethod); + + return state => ConvertETagObjectToString(getterDelegate(state)); + } + + private static Func CreateConvertETagObjectToStringFunc() + { + return ConvertETagObjectToString; + } + + private static string ConvertETagObjectToString(object obj) + { + if (obj == null) + return null; + switch (obj) + { + case byte[] bytes: + return ByteToHexBitFiddle(bytes); + default: + return obj.ToString(); + } + + } + + private static string ByteToHexBitFiddle(byte[] bytes) + { + var c = new char[bytes.Length * 2]; + for (var i = 0; i < bytes.Length; i++) + { + int b = bytes[i] >> 4; + c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7)); + b = bytes[i] & 0xF; + c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7)); + } + return new string(c); + } + + #endregion + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConventionOptions.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConventionOptions.cs new file mode 100644 index 0000000..7d1aead --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/GrainStorageConventionOptions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore.Conventions +{ + public class GrainStorageConventionOptions + { + public string DefaultGrainKeyPropertyName { get; set; } = "Id"; + + public string DefaultGrainKeyExtPropertyName { get; set; } = "KeyExt"; + + public string DefaultPersistenceCheckPropertyName { get; set; } = "Id"; + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/IGrainStorageConvention.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/IGrainStorageConvention.cs new file mode 100644 index 0000000..8c1b351 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/Conventions/IGrainStorageConvention.cs @@ -0,0 +1,125 @@ +using Microsoft.EntityFrameworkCore; +using Orleans; +using Orleans.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Orleans.Provider.EntityframeworkCore.Conventions +{ + public interface IGrainStorageConvention + { + /// + /// Creates a method that returns an IQueryable' + /// against type. + /// + /// + /// + /// + Func> + CreateDefaultDbSetAccessorFunc() + where TContext : DbContext + where TEntity : class; + + Func> + CreateDefaultReadStateFunc( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class; + + Func> + CreatePreCompiledDefaultReadStateFunc( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class; + + void SetDefaultKeySelectors( + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class; + + // todo: support composite key grains + + /// + /// Creates a method that determines if a state object is persisted in the database. + /// This is used to decide whether an insert or an update operation is needed. + /// + /// + /// + /// + Func CreateIsPersistedFunc(GrainStorageOptions options) + where TEntity : class; + + /// + /// Tries to find and configure an ETag property on the state model + /// + /// + /// Indicates if failure of finding an ETag property should throw + /// + /// + /// + void FindAndConfigureETag( + GrainStorageOptions options, + bool throwIfNotFound) + where TContext : DbContext + where TEntity : class; + + /// + /// Configures the ETag property using the provided property name + /// + /// + /// + /// + /// + /// + void ConfigureETag( + string propertyName, + GrainStorageOptions options) + where TContext : DbContext + where TEntity : class; + + Action GetSetterFunc() + where TEntity : class; + + Func GetGetterFunc() + where TEntity : class; + } + + public interface IGrainStorageConvention + where TContext : DbContext + where TEntity : class + { + /// + /// Creates a method that returns an IQueryable' + /// against type. + /// + /// + /// + /// + Func> + CreateDefaultDbSetAccessorFunc(); + + /// + /// Creates a method that generates an expression to be used by entity framework to + /// fetch a single state. + /// + /// Type of grain state + /// + Func>> + CreateGrainStateQueryExpressionGeneratorFunc(); + + Func> CreateDefaultReadStateFunc(); + + Func> CreatePreCompiledDefaultReadStateFunc( + GrainStorageOptions options); + + void SetDefaultKeySelector(GrainStorageOptions options); + + Action GetSetterFunc(); + + Func GetGetterFunc(); + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/DefaultGrainStateEntryConfigurator.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/DefaultGrainStateEntryConfigurator.cs new file mode 100644 index 0000000..3a36a79 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/DefaultGrainStateEntryConfigurator.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public class DefaultGrainStateEntryConfigurator + : IGrainStateEntryConfigurator + where TContext : DbContext + where TEntity : class + { + public void ConfigureSaveEntry(ConfigureSaveEntryContext context) + { + EntityEntry entry = context.DbContext.Entry(context.Entity); + + entry.State = context.IsPersisted + ? EntityState.Modified + : EntityState.Added; + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/EntityFrameworkGrainStorage.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/EntityFrameworkGrainStorage.cs new file mode 100644 index 0000000..da56d53 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/EntityFrameworkGrainStorage.cs @@ -0,0 +1,89 @@ +using Microsoft.EntityFrameworkCore; +using Orleans; +using Orleans.Runtime; +using Orleans.Storage; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public class EntityFrameworkGrainStorage : IGrainStorage + where TContext : DbContext + { + private readonly IServiceProvider _serviceProvider; + private readonly ITypeResolver _typeResolver; + private readonly IEntityTypeResolver _entityTypeResolver; + + private readonly ConcurrentDictionary _storage + = new ConcurrentDictionary(); + + public EntityFrameworkGrainStorage( + IServiceProvider serviceProvider, + ITypeResolver typeResolver, + IEntityTypeResolver entityTypeResolver) + { + _serviceProvider = serviceProvider; + _entityTypeResolver = entityTypeResolver; + _typeResolver = typeResolver; + } + + public Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + if (!_storage.TryGetValue(grainType, out IGrainStorage storage)) + storage = CreateStorage(grainType, grainState); + + return storage.ReadStateAsync(grainType, grainReference, grainState); + } + + public Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + if (!_storage.TryGetValue(grainType, out IGrainStorage storage)) + storage = CreateStorage(grainType, grainState); + + return storage.WriteStateAsync(grainType, grainReference, grainState); + } + + public Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + if (!_storage.TryGetValue(grainType, out IGrainStorage storage)) + storage = CreateStorage(grainType, grainState); + + return storage.ClearStateAsync(grainType, grainReference, grainState); + } + + private IGrainStorage CreateStorage( + string grainType + , IGrainState grainState) + { + Type grainImplType = _typeResolver.ResolveType(grainType); + Type stateType = _entityTypeResolver.ResolveStateType(grainType, grainState); + Type entityType = _entityTypeResolver.ResolveEntityType(grainType, grainState); + + Type storageType = typeof(GrainStorage<,,,>) + .MakeGenericType(typeof(TContext), + grainImplType, stateType, entityType); + + IGrainStorage storage; + + try + { + storage = (IGrainStorage)Activator.CreateInstance(storageType, grainType, _serviceProvider); + } + catch (Exception e) + { + if (e.InnerException == null) + throw; + // Unwrap target invocation exception + + throw e.InnerException; + } + + + _storage.TryAdd(grainType, storage); + return storage; + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/EntityTypeResolver.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/EntityTypeResolver.cs new file mode 100644 index 0000000..cfabdb8 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/EntityTypeResolver.cs @@ -0,0 +1,23 @@ +using Orleans; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public class EntityTypeResolver : IEntityTypeResolver + { + public virtual Type ResolveEntityType(string grainType, IGrainState grainState) + { + return ResolveStateType(grainType, grainState); + } + + public virtual Type ResolveStateType(string grainType, IGrainState grainState) + { + // todo: hack, the declared type of the grain state is only accessible like so + return grainState.GetType().IsGenericType + ? grainState.GetType().GenericTypeArguments[0] + : grainState.State.GetType(); + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/EntityWapper.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/EntityWapper.cs new file mode 100644 index 0000000..1d8dfb1 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/EntityWapper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public class EntityWapper where TEntity : class + { + public TEntity Entity { get; set; } + public bool ToAdd { get; set; } + public bool IsAdded { get; set; } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorage.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorage.cs new file mode 100644 index 0000000..aaf7c9f --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorage.cs @@ -0,0 +1,152 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Orleans; +using Orleans.Runtime; +using Orleans.Storage; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + internal class GrainStorage : IGrainStorage + where TContext : DbContext + where TGrain : Grain + where TGrainState : class, new() + where TEntity : class + { + private readonly GrainStorageOptions _options; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger> _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IGrainStateEntryConfigurator _entryConfigurator; + + public GrainStorage(string grainType, IServiceProvider serviceProvider) + { + if (grainType == null) throw new ArgumentNullException(nameof(grainType)); + + _serviceProvider = serviceProvider + ?? throw new ArgumentNullException(nameof(serviceProvider)); + + _entryConfigurator = (IGrainStateEntryConfigurator)serviceProvider.GetRequiredService( + typeof(IGrainStateEntryConfigurator)); + + var loggerFactory = _serviceProvider.GetService(); + _logger = loggerFactory?.CreateLogger>() + ?? NullLogger>.Instance; + + _scopeFactory = serviceProvider.GetRequiredService(); + _options = GetOrCreateDefaultOptions(grainType); + } + + public async Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + using (IServiceScope scope = _scopeFactory.CreateScope()) + using (var context = scope.ServiceProvider.GetRequiredService()) + { + TEntity entity = await _options.ReadStateAsync(context, grainReference) + .ConfigureAwait(false); + + _options.SetEntity(grainState, entity); + + if (entity != null && _options.CheckForETag) + grainState.ETag = _options.GetETagFunc(entity); + } + } + + public async Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + TEntity entity = _options.GetEntity(grainState); + + using (IServiceScope scope = _scopeFactory.CreateScope()) + using (var context = scope.ServiceProvider.GetRequiredService()) + { + if (GrainStorageContext.IsConfigured) + { + EntityEntry entry = context.Entry(entity); + GrainStorageContext.ConfigureStateDelegate(entry); + } + else + { + bool isPersisted = _options.IsPersistedFunc(entity); + + _entryConfigurator.ConfigureSaveEntry( + new ConfigureSaveEntryContext( + context, entity) + { + IsPersisted = isPersisted + }); + } + + try + { + await context.SaveChangesAsync() + .ConfigureAwait(false); + + if (_options.CheckForETag) + grainState.ETag = _options.GetETagFunc(entity); + } + catch (DbUpdateConcurrencyException e) + { + if (!_options.CheckForETag) + throw new InconsistentStateException(e.Message, e); + + object storedETag = e.Entries.First().OriginalValues[_options.ETagProperty]; + throw new InconsistentStateException(e.Message, + _options.ConvertETagObjectToStringFunc(storedETag), + grainState.ETag, + e); + } + } + } + + public async Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState) + { + TEntity entity = _options.GetEntity(grainState); + using (IServiceScope scope = _scopeFactory.CreateScope()) + using (var context = scope.ServiceProvider.GetRequiredService()) + { + context.Remove(entity); + await context.SaveChangesAsync() + .ConfigureAwait(false); + } + } + + + private GrainStorageOptions GetOrCreateDefaultOptions(string grainType) + { + var options + = _serviceProvider.GetOptionsByName>(grainType); + + if (options.IsConfigured) + return options; + + // Try generating a default options for the grain + + Type optionsType = typeof(GrainStoragePostConfigureOptions<,,,>) + .MakeGenericType( + typeof(TContext), + typeof(TGrain), + typeof(TGrainState), + typeof(TEntity)); + + var postConfigure = (IPostConfigureOptions>) + Activator.CreateInstance(optionsType, _serviceProvider); + + postConfigure.PostConfigure(grainType, options); + + _logger.LogInformation($"GrainStorageOptions is not configured for grain {grainType} " + + "and default options will be used. If default configuration is not desired, " + + "consider configuring options for grain using " + + "using IServiceCollection.ConfigureGrainStorageOptions extension method."); + + return options; + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageConfigurationException.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageConfigurationException.cs new file mode 100644 index 0000000..b32f80e --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageConfigurationException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + // todo: Use for configuration errors + public class GrainStorageConfigurationException : Exception + { + public GrainStorageConfigurationException() + { + } + + public GrainStorageConfigurationException(string message) : base(message) + { + } + + public GrainStorageConfigurationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageContext.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageContext.cs new file mode 100644 index 0000000..d2dcef3 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageContext.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public static class GrainStorageContext + where TEntity : class + { + // ReSharper disable once StaticMemberInGenericType + private static readonly AsyncLocal IsConfiguredLocal + = new AsyncLocal(); + + private static readonly AsyncLocal> + ConfigureStateDelegateLocal + = new AsyncLocal>(); + + internal static bool IsConfigured => IsConfiguredLocal.Value; + + internal static ConfigureEntryStateDelegate ConfigureStateDelegate + => ConfigureStateDelegateLocal.Value; + + /// + /// Configures the entry state. + /// Use it to modify what gets changed during the write operations. + /// + /// The delegate to be called before saving context's state. + public static void ConfigureEntryState(ConfigureEntryStateDelegate configureState) + { + ConfigureStateDelegateLocal.Value = configureState; + IsConfiguredLocal.Value = true; + } + + public static void Clear() + { + ConfigureStateDelegateLocal.Value = null; + IsConfiguredLocal.Value = false; + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptions.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptions.cs new file mode 100644 index 0000000..ff3795c --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptions.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Orleans; +using Orleans.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public abstract class GrainStorageOptions + { + internal string KeyPropertyName { get; set; } + + internal string KeyExtPropertyName { get; set; } + + internal string ETagPropertyName { get; set; } + + internal string PersistenceCheckPropertyName { get; set; } + + internal IProperty ETagProperty { get; set; } + + internal bool CheckForETag { get; set; } + + internal Func ConvertETagObjectToStringFunc { get; set; } + + internal Type ETagType { get; set; } + + public bool ShouldUseETag { get; set; } + + internal bool IsConfigured { get; set; } + + internal bool PreCompileReadQuery { get; set; } = true; + } + public class GrainStorageOptions : GrainStorageOptions + where TContext : DbContext + where TEntity : class + { + internal Func> DbSetAccessor { get; set; } + + internal Func IsPersistedFunc { get; set; } + + internal Func GetETagFunc { get; set; } + + internal Func GuidKeySelector { get; set; } + + internal Func KeyExtSelector { get; set; } + + internal Func LongKeySelector { get; set; } + + internal Func> ReadStateAsync { get; set; } + + internal Action SetEntity { get; set; } + + internal Func GetEntity { get; set; } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptionsExtensions.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptionsExtensions.cs new file mode 100644 index 0000000..49f9b34 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStorageOptionsExtensions.cs @@ -0,0 +1,262 @@ +using Microsoft.EntityFrameworkCore; +using Orleans.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public static class GrainStorageOptionsExtensions + { + public static GrainStorageOptions UseQuery( + this GrainStorageOptions options, + Func> queryFunc) + where TContext : DbContext + where TGrainState : class + { + options.DbSetAccessor = queryFunc; + return options; + } + + public static GrainStorageOptions ConfigureIsPersisted( + this GrainStorageOptions options, + Func isPersistedFunc) + where TContext : DbContext + where TGrainState : class + { + options.IsPersistedFunc = isPersistedFunc; + return options; + } + + /// + /// Instructs the storage provider to precompile read query. + /// This will lead to better performance for complex queries. + /// Default is to precompile. + /// + /// + /// + /// + /// + /// + /// + public static GrainStorageOptions PreCompileReadQuery( + this GrainStorageOptions options, + bool value = true) + where TContext : DbContext + where TGrainState : class + { + options.PreCompileReadQuery = value; + return options; + } + + + /// + /// Overrides the default implementation used to query grain state from database. + /// + /// + /// + /// + /// + /// + /// + public static GrainStorageOptions ConfigureReadState( + this GrainStorageOptions options, + Func> readStateAsyncFunc) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + options.ReadStateAsync = readStateAsyncFunc ?? throw new ArgumentNullException(nameof(readStateAsyncFunc)); + return options; + } + + + /// + /// Instruct the storage that the current entity should use etags. + /// If no valid properties were found on the entity and exception would be thrown. + /// + /// + /// + /// + /// + /// + public static GrainStorageOptions UseETag( + this GrainStorageOptions options) + where TContext : DbContext + where TGrainState : class + { + options.ShouldUseETag = true; + return options; + } + + public static GrainStorageOptions UseETag( + this GrainStorageOptions options, + Expression> expression) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var memberExpression = expression.Body as MemberExpression + ?? throw new ArgumentException( + $"{nameof(expression)} must be a MemberExpression."); + + options.ETagPropertyName = memberExpression.Member.Name; + options.ShouldUseETag = true; + + return options; + } + + public static GrainStorageOptions UseETag( + this GrainStorageOptions options, + string propertyName) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + options.ETagPropertyName = propertyName; + options.ShouldUseETag = true; + + return options; + } + + public static GrainStorageOptions UseKey( + this GrainStorageOptions options, + Expression> expression) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var memberExpression = expression.Body as MemberExpression + ?? throw new ArgumentException( + $"{nameof(expression)} must be a MemberExpression."); + + options.KeyPropertyName = memberExpression.Member.Name; + + return options; + } + + public static GrainStorageOptions UseKey( + this GrainStorageOptions options, + Expression> expression) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var memberExpression = expression.Body as MemberExpression + ?? throw new GrainStorageConfigurationException( + $"{nameof(expression)} must be a MemberExpression."); + + options.KeyPropertyName = memberExpression.Member.Name; + + return options; + } + + public static GrainStorageOptions UseKey( + this GrainStorageOptions options, + Expression> expression) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var memberExpression = expression.Body as MemberExpression + ?? throw new ArgumentException( + $"{nameof(expression)} must be a MemberExpression."); + + options.KeyPropertyName = memberExpression.Member.Name; + + return options; + } + + public static GrainStorageOptions UseKey( + this GrainStorageOptions options, + string propertyName) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + options.KeyPropertyName = propertyName; + + return options; + } + + public static GrainStorageOptions UseKeyExt( + this GrainStorageOptions options, + Expression> expression) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var memberExpression = expression.Body as MemberExpression + ?? throw new ArgumentException( + $"{nameof(expression)} must be a MemberExpression."); + + options.KeyExtPropertyName = memberExpression.Member.Name; + + return options; + } + + public static GrainStorageOptions UseKeyExt( + this GrainStorageOptions options, + string propertyName) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + options.KeyExtPropertyName = propertyName; + + return options; + } + + public static GrainStorageOptions CheckPersistenceOn( + this GrainStorageOptions options, + Expression> expression) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var memberExpression = expression.Body as MemberExpression + ?? throw new ArgumentException( + $"{nameof(expression)} must be a MemberExpression."); + + options.PersistenceCheckPropertyName = memberExpression.Member.Name; + + return options; + } + + public static GrainStorageOptions CheckPersistenceOn( + this GrainStorageOptions options, + string propertyName) + where TContext : DbContext + where TGrainState : class + { + if (options == null) throw new ArgumentNullException(nameof(options)); + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + options.PersistenceCheckPropertyName = propertyName; + + return options; + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStoragePostConfigureOptions.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStoragePostConfigureOptions.cs new file mode 100644 index 0000000..d68feba --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/GrainStoragePostConfigureOptions.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Orleans; +using Pole.Orleans.Provider.EntityframeworkCore.Conventions; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public class GrainStoragePostConfigureOptions + : IPostConfigureOptions> + where TContext : DbContext + where TGrain : Grain + where TGrainState : new() + where TEntity : class + { + public IGrainStorageConvention Convention { get; } + public IGrainStorageConvention DefaultConvention { get; } + + public GrainStoragePostConfigureOptions(IServiceProvider serviceProvider) + { + DefaultConvention = + (IGrainStorageConvention)serviceProvider.GetRequiredService(typeof(IGrainStorageConvention)); + Convention = (IGrainStorageConvention) + serviceProvider.GetService(typeof(IGrainStorageConvention)); + } + + public void PostConfigure(string name, GrainStorageOptions options) + { + if (!string.Equals(name, typeof(TGrain).FullName)) + throw new Exception("Post configure on wrong grain type."); + + if (options.IsPersistedFunc == null) + options.IsPersistedFunc = + DefaultConvention.CreateIsPersistedFunc(options); + + // Configure ETag + if (options.ShouldUseETag) + { + if (!string.IsNullOrWhiteSpace(options.ETagPropertyName)) + DefaultConvention.ConfigureETag(options.ETagPropertyName, options); + } + + if (options.ReadStateAsync == null) + { + if (options.DbSetAccessor == null) + options.DbSetAccessor = Convention?.CreateDefaultDbSetAccessorFunc() + ?? DefaultConvention.CreateDefaultDbSetAccessorFunc(); + + if (Convention != null) + Convention.SetDefaultKeySelector(options); + else + DefaultConvention.SetDefaultKeySelectors(options); + + if (options.PreCompileReadQuery) + { + options.ReadStateAsync + = Convention?.CreatePreCompiledDefaultReadStateFunc(options) + ?? DefaultConvention + .CreatePreCompiledDefaultReadStateFunc(options); + } + else + { + options.ReadStateAsync + = Convention?.CreateDefaultReadStateFunc() + ?? DefaultConvention + .CreateDefaultReadStateFunc(options); + } + } + + if (options.SetEntity == null) + options.SetEntity = + Convention?.GetSetterFunc() + ?? DefaultConvention.GetSetterFunc(); + + if (options.GetEntity == null) + options.GetEntity = + Convention?.GetGetterFunc() + ?? DefaultConvention.GetGetterFunc(); + + DefaultConvention.FindAndConfigureETag(options, options.ShouldUseETag); + + // todo: Validate options + + options.IsConfigured = true; + } + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/IEntityTypeResolver.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/IEntityTypeResolver.cs new file mode 100644 index 0000000..15c71d3 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/IEntityTypeResolver.cs @@ -0,0 +1,13 @@ +using Orleans; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public interface IEntityTypeResolver + { + Type ResolveEntityType(string grainType, IGrainState grainState); + Type ResolveStateType(string grainType, IGrainState grainState); + } +} diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/IGrainStateEntryConfigurator.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/IGrainStateEntryConfigurator.cs new file mode 100644 index 0000000..5d256d7 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/IGrainStateEntryConfigurator.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore +{ + public interface IGrainStateEntryConfigurator + where TContext : DbContext + where TEntity : class + { + void ConfigureSaveEntry(ConfigureSaveEntryContext context); + } +} + diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/Pole.Orleans.Provider.EntityframeworkCore.csproj b/src/Pole.Orleans.Provider.EntityframeworkCore/Pole.Orleans.Provider.EntityframeworkCore.csproj new file mode 100644 index 0000000..3467f5f --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/Pole.Orleans.Provider.EntityframeworkCore.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.1 + + + + + + + + + + + diff --git a/src/Pole.Orleans.Provider.EntityframeworkCore/Utils/ReflectionHelper.cs b/src/Pole.Orleans.Provider.EntityframeworkCore/Utils/ReflectionHelper.cs new file mode 100644 index 0000000..9df7891 --- /dev/null +++ b/src/Pole.Orleans.Provider.EntityframeworkCore/Utils/ReflectionHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Pole.Orleans.Provider.EntityframeworkCore.Utils +{ + internal static class ReflectionHelper + { + public static PropertyInfo GetPropertyInfo(string propertyName) + { + if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); + + Type statetype = typeof(T); + + PropertyInfo idProperty + = statetype.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public); + + if (idProperty == null) + throw new GrainStorageConfigurationException( + $"Could not find \"{propertyName}\" property on type \"{statetype.FullName}\". Either configure the state locator predicate manually or update your model."); + + if (!idProperty.CanRead) + throw new GrainStorageConfigurationException( + $"The property \"{propertyName}\" of type \"{statetype.FullName}\" must have a public getter."); + + return idProperty; + } + + public static Func GetAccessorDelegate(PropertyInfo pInfo) + { + return (Func)Delegate.CreateDelegate( + typeof(Func), + null, + pInfo.GetMethod); + } + } +} -- libgit2 0.25.0