Orleans.Providers.EntityFramework
An Entity Framework Core implementation of Orleans Grain Storage. More providers to come later.
Install / Use
/learn @OrleansContrib/Orleans.Providers.EntityFrameworkREADME
Orleans.Providers.EntityFramework
An Entity Framework Core implementation of Orleans Grain Storage.
There are some nice to have features missing. I didn't needed them particularly but If you have suggestions or want to help out, it would be much appreciated.
Usage
Nuget: https://www.nuget.org/packages/Orleans.Providers.EntityFramework/
or
dotnet add package Orleans.Providers.EntityFramework --version 0.15.1
or
Install-Package Orleans.Providers.EntityFramework --version 0.15.1
And configure the storage provider using SiloHostBuilder:
ISiloHostBuilder builder = new SiloHostBuilder();
builder.AddEfGrainStorage<FrogsDbContext>("ef");
This requires your DbContext to be registered as well
services
.AddDbContextPool<FatDbContext>(
(sp, options) => {});
The GrainStorage will resolve and releases contexts per operation so you won't have many context in use. Hence it's better to use the context pool provided in the entity framework package or use your own.
Configuration
By default the provider will search for key properties on your data models that match your grain interfaces, but you can change the default behavior like so:
services.Configure<GrainStorageConventionOptions>(options =>
{
options.DefaultGrainKeyPropertyName = "Id";
options.DefaultPersistenceCheckPropertyName = "Id";
options.DefaultGrainKeyExtPropertyName = "KeyExt";
});
DefaultPersistenceCheckPropertyName is used to check if a model needs to be inserted into the database or updated. The value of said property will be checked against the default value of the type.
The following sample model would work out of the box for a grain that implements IGrainWithGuidCompoundKey and requires no configuration:
public class Box {
public Guid Id { get; set; }
public string KeyExt { get; set; }
public byte[] ETag { get; set; }
}
If you use conventions (as described, configuring GrainStorageConventionOptions) your context should contain DbSets for your models.
public DbSet<Box> Boxes { get; set; }
Querying models using custom expressions
To configure a special model you can do:
public class SpecialBox {
public long WeirdId { get; set; }
public string Type { get; set; }
public long ClusterIndexId { get; set; }
}
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.UseQueryExpression(grainRef =>
{
long key = grainRef.GetPrimaryKeyLong(out string keyExt);
return (box => box.WeirdId == key && box.Type == keyExt);
})
)
or
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.UseKey(box => box.WeirdId)
.UseKeyExt(box => box.Type)
)
The UseQueryExpression method instructs the sotrage to use the provided expression to query the database.
Loading additional data on read state
You can load additional data while reading the state. Using the SpecialBox model:
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.UseQuery(context => context.SpecialBoxes.AsNoTracking()
.Include(box => box.Gems)
.ThenInclude(gems => gems.Map))
)
Using custom persistence check
When using Guids as primary keys you're most likely to add a cluster index that is auto incremented. That field can be used to check if the state is already inserted into the database or not:
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.ConfigureIsPersisted(box => box.ClusterIndexId > 0)
)
or
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.CheckPersistenceOn(box => box.ClusterIndexId)
)
If you use different cluster indices (In case of mssqlserver) than your primary keys you can configure the dafaults to write less configuration code:
services.Configure<GrainStorageConventionOptions>(options =>
{
options.DefaultPersistenceCheckPropertyName = "ClusterIndexId";
});
ETags
By default models are searched for Etags and if a property on a model is marked as ConcurrencyToken the storage will pick that up.
Using the fluent API that would be:
builder.Entity<SpecialBox>()
.Property(e => e.ETag)
.IsConcurrencyToken();
Models can be further configured using extensions:
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.UseETag()
)
Using UseETag overload with no params instructs the storage to find an ETag property. If no valid property was found, an exception would be thrown.
Use the following overload to explicitly configure the storage to use the provided property. If the property is not marked as ConcurrencyCheck an exception would be thrown.
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.UseETag(box => box.ETag)
)
Controlling how the state is saved
When calling writeState, the state object is attached to a context and its state (EF entry state) would be set to Added or Modified.
There are two ways to change the behavior:
GrainStorageContext
GrainStorageContext<Box>.ConfigureEntryState(
entry => entry.Property(e => e.Title).IsModified = true);
This way only the Title field would be updated.
Things to consider:
- When configuring the entry manually, the storage provider only attaches the state to the context and doesn't set the entry state. So for example if you call this
GrainStorageContext<Box>.ConfigureEntryState(entry => {});the write operation does nothing. - Because GrainStorageContext uses async locals you have to call
GrainStorageContext<Box>.Clear()if you want to do multiple writes on the same asynchronous operation.
IGrainStateEntryConfigurator
By implementing IGrainStateEntryConfigurator<TContext, TGrain, TEntity> and registering it.
The default implementation is DefaultGrainStateEntryConfigurator and it just does the following:
public void ConfigureSaveEntry(ConfigureSaveEntryContext<TContext, TEntity> context)
{
EntityEntry<TEntity> entry = context.DbContext.Entry(context.Entity);
entry.State = context.IsPersisted
? EntityState.Modified
: EntityState.Added;
}
Precompiled Queries
By default all queries are precompiled, unless using ConfigureReadState extension.
You can disable precompilation using
services
.ConfigureGrainStorageOptions<FatDbContext, SpecialBoxGrain, SpecialBox>(
options => options
.PreCompileReadQuery(false)
)
Conventions
You can change the conventions by implementing IGrainStorageConvention or inheriting from GrainStorageConvention which is used for all types and IGrainStorageConvention<TContext, TGrain, TEntity> for a specific grain type which has no default implementation.
Custom Grain State Setter/Getter
You can implement IEntityTypeResolver or inheriting from EntityTypeResolver so you can have different grain state and storage model. This is particularly useful if you have abstract states or models without public default constructors which is a constraint on orleans grain states.
For example you can have the following class
class GenericGrainState<TEntity>
{
public TEntity Value { get; set;}
}
Using a custom EntityTypeResolver you can tell the storage TEntity is the persistent model.
Compatibility
To build for specific dependency versions use:
dotnet test /p:ORLEANS_VERSION=3.0.0 /p:EF_VERSION=3.0.0 /p:MSEXT_VERSION=3.0.0
You can run tests for a specific version using build parameters:
dotnet test /p:ORLEANS_VERSION=3.0.0 /p:EF_VERSION=3.0.0 /p:MSEXT_VERSION=3.0.0
Known Issues and Limitations
- As types has to be configured in dbcontext, arbitrary types can't use this provider. This specially causes issues with Orleans VersionStoreGrain internal grain, hence this GrainStorage can't be used as default grain storage. I'll handle that special case if I get the time needed.
Related Skills
node-connect
352.5kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.3kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
352.5kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
352.5kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
Languages
Security Score
Audited on Feb 26, 2025
