diff --git a/Source/Orleans.StorageProviderInterceptors/Orleans.StorageProviderInterceptors.csproj b/Source/Orleans.StorageProviderInterceptors/Orleans.StorageProviderInterceptors.csproj
index 5fd758e..e940518 100644
--- a/Source/Orleans.StorageProviderInterceptors/Orleans.StorageProviderInterceptors.csproj
+++ b/Source/Orleans.StorageProviderInterceptors/Orleans.StorageProviderInterceptors.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/BasicTests.cs b/Tests/Orleans.StorageProviderInterceptors.Test/BasicTests.cs
new file mode 100644
index 0000000..591409f
--- /dev/null
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/BasicTests.cs
@@ -0,0 +1,26 @@
+namespace Orleans.StorageProviderInterceptors.Test;
+
+using Xunit;
+using Xunit.Abstractions;
+
+[Collection(TestClusterCollection.Name)]
+public class BasicTests
+{
+ private readonly TestClusterFixture fixture;
+ private readonly ITestOutputHelper helper;
+
+ public BasicTests(TestClusterFixture fixture, ITestOutputHelper helper)
+ {
+ this.fixture = fixture;
+ this.helper = helper;
+ }
+
+ [Fact]
+ public async Task Validate_SecretStorageGrain_ReadWrite()
+ {
+ var grain = this.fixture.Cluster.GrainFactory.GetGrain("foo");
+ await grain.AddOrUpdateSecret("bar", "meh");
+ var secret = await grain.GetSecret("bar");
+ Assert.Equal("meh", secret);
+ }
+}
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/Class1Test.cs b/Tests/Orleans.StorageProviderInterceptors.Test/Class1Test.cs
deleted file mode 100644
index dedfe94..0000000
--- a/Tests/Orleans.StorageProviderInterceptors.Test/Class1Test.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Orleans.StorageProviderInterceptors.Test;
-
-using Xunit;
-
-///
-/// Class1 test
-///
-public class Class1Test
-{
- ///
- /// Given when true
- ///
- [Fact]
- public void Given_When_Then() => Assert.True(true);
-}
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/ISecretStorageGrain.cs b/Tests/Orleans.StorageProviderInterceptors.Test/ISecretStorageGrain.cs
new file mode 100644
index 0000000..3c56bef
--- /dev/null
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/ISecretStorageGrain.cs
@@ -0,0 +1,7 @@
+namespace Orleans.StorageProviderInterceptors.Test;
+
+internal interface ISecretStorageGrain : IGrainWithStringKey
+{
+ Task AddOrUpdateSecret(string name, string value);
+ Task GetSecret(string name);
+}
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/Orleans.StorageProviderInterceptors.Test.csproj b/Tests/Orleans.StorageProviderInterceptors.Test/Orleans.StorageProviderInterceptors.Test.csproj
index 59d8f01..37a4641 100644
--- a/Tests/Orleans.StorageProviderInterceptors.Test/Orleans.StorageProviderInterceptors.Test.csproj
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/Orleans.StorageProviderInterceptors.Test.csproj
@@ -4,6 +4,11 @@
net7.0
true
+
+
+ 1701;1702;NU5105;1591
+ false
+
@@ -27,6 +32,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/SecretStorageGrain.cs b/Tests/Orleans.StorageProviderInterceptors.Test/SecretStorageGrain.cs
new file mode 100644
index 0000000..4e8318b
--- /dev/null
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/SecretStorageGrain.cs
@@ -0,0 +1,22 @@
+namespace Orleans.StorageProviderInterceptors.Test;
+
+using Abstractions;
+using Runtime;
+
+internal sealed class SecretStorageGrain : Grain, ISecretStorageGrain
+{
+ private readonly IPersistentState> secrets;
+
+ public SecretStorageGrain(
+ [StorageInterceptor(
+ TestSiloConfigurations.StorageName, "secretsState")]
+ IPersistentState> state) => this.secrets = state;
+
+ public async Task AddOrUpdateSecret(string name, string value)
+ {
+ this.secrets.State[name] = value;
+ await this.secrets.WriteStateAsync();
+ }
+
+ public Task GetSecret(string name) => Task.FromResult(this.secrets.State[name]);
+}
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/TestCluster.cs b/Tests/Orleans.StorageProviderInterceptors.Test/TestCluster.cs
new file mode 100644
index 0000000..a291c90
--- /dev/null
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/TestCluster.cs
@@ -0,0 +1,36 @@
+namespace Orleans.StorageProviderInterceptors.Test;
+
+using System;
+using Microsoft.Extensions.Configuration;
+using Orleans;
+using Orleans.TestingHost;
+
+public sealed class TestClusterFixture : IDisposable
+{
+ public string TestClusterId = new Guid().ToString();
+
+ public Orleans.TestingHost.TestCluster Cluster { get; }
+
+ public IClusterClient? ClusterClient { get; }
+
+
+ public TestClusterFixture()
+ {
+ var builder = new TestClusterBuilder();
+
+ builder.ConfigureHostConfiguration(config => config.AddInMemoryCollection(
+ new Dictionary
+ {
+ { nameof(this.TestClusterId), this.TestClusterId }
+ }!));
+
+ builder.AddSiloBuilderConfigurator();
+
+ this.Cluster = builder.Build();
+ this.Cluster.Deploy();
+
+ this.ClusterClient = (IClusterClient)this.Cluster.ServiceProvider.GetService(typeof(IClusterClient))!;
+ }
+
+ public void Dispose() => this.Cluster.StopAllSilos();
+}
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/TestClusterCollection.cs b/Tests/Orleans.StorageProviderInterceptors.Test/TestClusterCollection.cs
new file mode 100644
index 0000000..e969325
--- /dev/null
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/TestClusterCollection.cs
@@ -0,0 +1,11 @@
+namespace Orleans.StorageProviderInterceptors.Test;
+
+using Xunit;
+
+// Important note: Fixtures can be shared across assemblies, but collection definitions must be in the same assembly as the test that uses them.
+// https://xunit.net/docs/shared-context
+[CollectionDefinition(Name)]
+public class TestClusterCollection : ICollectionFixture
+{
+ public const string Name = "TestClusterCollection";
+}
diff --git a/Tests/Orleans.StorageProviderInterceptors.Test/TestSiloConfigurations.cs b/Tests/Orleans.StorageProviderInterceptors.Test/TestSiloConfigurations.cs
new file mode 100644
index 0000000..72776f5
--- /dev/null
+++ b/Tests/Orleans.StorageProviderInterceptors.Test/TestSiloConfigurations.cs
@@ -0,0 +1,128 @@
+namespace Orleans.StorageProviderInterceptors.Test;
+
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Orleans.Streams;
+using Orleans.TestingHost;
+
+///
+public class TestSiloConfigurations : ISiloConfigurator, IHostConfigurator
+{
+ internal const string StorageName = "SecretsStorage";
+
+ ///
+ public void Configure(ISiloBuilder siloBuilder)
+ {
+ siloBuilder.UseInMemoryReminderService();
+ siloBuilder.AddMemoryStreams("Default", config =>
+ {
+ config.ConfigureStreamPubSub(StreamPubSubType.ImplicitOnly);
+ config.ConfigureCacheEviction(builder => builder.Configure(options =>
+ {
+ options.DataMinTimeInCache = TimeSpan.FromMinutes(2);
+ options.DataMaxAgeInCache = TimeSpan.FromMinutes(10);
+ options.MetadataMinTimeInCache = TimeSpan.FromMinutes(60);
+ }));
+ });
+ siloBuilder
+ .AddStorageInterceptors()
+ .AddMemoryGrainStorage(StorageName)
+ .AddStorageInterceptors()
+ .UseGenericStorageInterceptor>(
+ StorageName, "secretsState", c =>
+ {
+ c.OnBeforeWriteStateFunc = (grainActivationContext, currentState) =>
+ {
+ var unencryptedValues = new Dictionary(currentState.State.Count);
+ Console.WriteLine(
+ $"OnBeforeWriteState: {grainActivationContext}: Count Is {currentState.State.Count}");
+ foreach (var (key, value) in currentState.State)
+ {
+ Console.WriteLine($"Intercepted: {key}: {value}");
+
+ // Save the original state to return to the grain
+ unencryptedValues.Add(key, value);
+
+ // Encrypt the data
+ currentState.State[key] = currentState.State[key].Replace('e', '3');
+ }
+
+ return ValueTask.FromResult((false, (object?)unencryptedValues));
+ };
+
+ c.OnAfterWriteStateFunc = (grainActivationContext, currentState, sharedState) =>
+ {
+ var unencryptedValues = (Dictionary)sharedState!;
+ Console.WriteLine(
+ $"OnAfterWriteState: {grainActivationContext}: Count Is {currentState.State.Count}");
+ foreach (var (key, value) in currentState.State)
+ {
+ Console.WriteLine($"What was actually persisted: {key}: {value}");
+
+ currentState.State[key] = unencryptedValues[key];
+ Console.WriteLine($"What will be returned to grain: {key}: {unencryptedValues[key]}");
+ }
+
+ return ValueTask.CompletedTask;
+ };
+
+ c.OnBeforeReadStateAsync = (grainActivationContext, currentState) =>
+ {
+ Console.WriteLine(
+ $"OnBeforeReadState: {grainActivationContext}: Count Is {currentState.State.Count}");
+
+ var unencryptedValues = new Dictionary(currentState.State.Count);
+ foreach (var (key, value) in currentState.State)
+ {
+ Console.WriteLine($"Unencrypted Values: {key}: {value}");
+
+ // Save the original state to return to the grain
+ unencryptedValues.Add(key, value);
+ }
+
+ return ValueTask.FromResult((false, (object?)unencryptedValues));
+ };
+
+ c.OnAfterReadStateFunc = (grainActivationContext, currentState, sharedState) =>
+ {
+ var unencryptedValues = (Dictionary)sharedState!;
+ if (!currentState.RecordExists)
+ {
+ return ValueTask.CompletedTask;
+ }
+
+ var list = sharedState as List;
+ Console.WriteLine(
+ $"OnAfterReadState: {grainActivationContext}: Count Is {currentState.State.Count}");
+
+ foreach (var (key, value) in currentState.State)
+ {
+ Console.WriteLine($"Encrypted Values: {key}: {value}");
+
+ // Decrypt the data
+ currentState.State[key] = currentState.State[key].Replace('3', 'e');
+ }
+
+ return ValueTask.CompletedTask;
+ };
+ });
+ }
+
+ ///
+ public virtual void Configure(IHostBuilder hostBuilder)
+ {
+ AppContext.SetSwitch("System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization", true);
+
+ hostBuilder.ConfigureLogging(logging =>
+ {
+ logging.ClearProviders();
+ logging.AddConsole();
+ });
+
+ hostBuilder.ConfigureServices((context, services) =>
+ {
+ // grab the cluster id that owns this silo
+ var clusterId = context.Configuration["TestClusterId"];
+ });
+ }
+}