Skip to content

Commit 318ead9

Browse files
authored
Add Support for passing state between Before and After state operations (#22)
1 parent 5ecd7d7 commit 318ead9

File tree

8 files changed

+188
-45
lines changed

8 files changed

+188
-45
lines changed

.devcontainer/Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/dotnet/.devcontainer/base.Dockerfile
2+
3+
# [Choice] .NET version: 6.0, 3.1, 6.0-bullseye, 3.1-bullseye, 6.0-focal, 3.1-focal
4+
ARG VARIANT="6.0-bullseye-slim"
5+
FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT}
6+
7+
# [Choice] Node.js version: none, lts/*, 18, 16, 14
8+
ARG NODE_VERSION="none"
9+
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
10+
11+
# [Optional] Uncomment this section to install additional OS packages.
12+
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
13+
# && apt-get -y install --no-install-recommends <your-package-list-here>
14+
15+
# [Optional] Uncomment this line to install global node packages.
16+
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

.devcontainer/devcontainer.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2+
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/dotnet
3+
{
4+
"name": "C# (.NET)",
5+
"build": {
6+
"dockerfile": "Dockerfile",
7+
"args": {
8+
// Update 'VARIANT' to pick a .NET Core version: 3.1, 6.0
9+
// Append -bullseye or -focal to pin to an OS version.
10+
"VARIANT": "6.0-bullseye",
11+
// Options
12+
"NODE_VERSION": "none"
13+
}
14+
},
15+
// Configure tool-specific properties.
16+
"customizations": {
17+
// Configure properties specific to VS Code.
18+
"vscode": {
19+
// Add the IDs of extensions you want installed when the container is created.
20+
"extensions": [
21+
"ms-dotnettools.csharp"
22+
]
23+
}
24+
},
25+
// Use 'forwardPorts' to make a list of ports inside the container available locally.
26+
// "forwardPorts": [5000, 5001],
27+
// [Optional] To reuse of your local HTTPS dev cert:
28+
//
29+
// 1. Export it locally using this command:
30+
// * Windows PowerShell:
31+
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
32+
// * macOS/Linux terminal:
33+
// dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere"
34+
//
35+
// 2. Uncomment these 'remoteEnv' lines:
36+
// "remoteEnv": {
37+
// "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere",
38+
// "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx",
39+
// },
40+
//
41+
// 3. Do one of the following depending on your scenario:
42+
// * When using GitHub Codespaces and/or Remote - Containers:
43+
// 1. Start the container
44+
// 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer
45+
// 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https"
46+
//
47+
// * If only using Remote - Containers with a local container, uncomment this line instead:
48+
// "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" ],
49+
// Use 'postCreateCommand' to run commands after the container is created.
50+
// "postCreateCommand": "dotnet restore",
51+
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
52+
"remoteUser": "vscode",
53+
"features": {
54+
"ghcr.io/devcontainers/features/dotnet:1": {
55+
"version": "latest"
56+
},
57+
"git": "latest",
58+
"git-lfs": "latest"
59+
}
60+
}

.vscode/launch.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": ".NET Core Launch (console)",
5+
"type": "coreclr",
6+
"request": "launch",
7+
"preLaunchTask": "build",
8+
"program": "${workspaceFolder}/Source/Sample/bin/Debug/net6.0/Sample.dll",
9+
"args": [],
10+
"cwd": "${workspaceFolder}",
11+
"stopAtEntry": false,
12+
"console": "internalConsole"
13+
}
14+
]
15+
}

.vscode/tasks.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
// See https://go.microsoft.com/fwlink/?LinkId=733558
3+
// for the documentation about the tasks.json format
4+
"version": "2.0.0",
5+
"tasks": [
6+
{
7+
"label": "build",
8+
"command": "dotnet",
9+
"type": "shell",
10+
"args": [
11+
"build",
12+
// Ask dotnet build to generate full paths for file names.
13+
"/property:GenerateFullPaths=true",
14+
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
15+
"/consoleloggerparameters:NoSummary"
16+
],
17+
"group": "build",
18+
"presentation": {
19+
"reveal": "silent"
20+
},
21+
"problemMatcher": "$msCompile"
22+
}
23+
]
24+
}

Source/Orleans.StorageProviderInterceptors/Infrastructure/NamedStorageInterceptorFactory.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,30 +292,33 @@ public TState State
292292
/// <inheritdoc/>
293293
public async Task ClearStateAsync()
294294
{
295-
if (!await this.options.OnBeforeClearStateAsync(this.context, this))
295+
var (preventOperation, state) = await this.options.OnBeforeClearStateAsync(this.context, this);
296+
if (!preventOperation)
296297
{
297298
await this.storage.ClearStateAsync();
298-
await this.options.OnAfterClearStateAsync(this.context, this);
299+
await this.options.OnAfterClearStateAsync(this.context, this, state);
299300
}
300301
}
301302

302303
/// <inheritdoc/>
303304
public async Task WriteStateAsync()
304305
{
305-
if (!await this.options.OnBeforeWriteStateFunc(this.context, this))
306+
var (preventOperation, state) = await this.options.OnBeforeWriteStateFunc(this.context, this);
307+
if (!preventOperation)
306308
{
307309
await this.storage.WriteStateAsync();
308-
await this.options.OnAfterWriteStateFunc(this.context, this);
310+
await this.options.OnAfterWriteStateFunc(this.context, this, state);
309311
}
310312
}
311313

312314
/// <inheritdoc/>
313315
public async Task ReadStateAsync()
314316
{
315-
if (!await this.options.OnBeforeReadStateAsync(this.context, this))
317+
var (preventOperation, state) = await this.options.OnBeforeReadStateAsync(this.context, this);
318+
if (!preventOperation)
316319
{
317320
await this.storage.ReadStateAsync();
318-
await this.options.OnAfterReadStateFunc.Invoke(this.context, this);
321+
await this.options.OnAfterReadStateFunc.Invoke(this.context, this, state);
319322
}
320323
}
321324

Source/Orleans.StorageProviderInterceptors/Infrastructure/StorageInterceptorOptions.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,30 @@ public class StorageInterceptorOptions<TState>
1010
/// <summary>
1111
/// Called before a ClearStateAsync(); return false to prevent writing.
1212
/// </summary>
13-
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask<bool>> OnBeforeClearStateAsync { get; set; } = (_, _) => ValueTask.FromResult(false);
13+
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask<(bool PreventOperation, object? SharedState)>> OnBeforeClearStateAsync { get; set; } = (_, _) => ValueTask.FromResult((false, (object?)null));
1414

1515
/// <summary>
1616
/// Called after ClearStateAsync();
1717
/// </summary>
18-
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask> OnAfterClearStateAsync { get; set; } = (_, _) => ValueTask.CompletedTask;
18+
public Func<IGrainActivationContext, IPersistentState<TState>, object?, ValueTask> OnAfterClearStateAsync { get; set; } = (_, _, _) => ValueTask.CompletedTask;
1919

2020
/// <summary>
2121
/// Called before ReadStateAsync(); return false to prevent Reading
2222
/// </summary>
23-
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask<bool>> OnBeforeReadStateAsync { get; set; } = (_, _) => ValueTask.FromResult(false);
23+
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask<(bool PreventOperation, object? SharedState)>> OnBeforeReadStateAsync { get; set; } = (_, _) => ValueTask.FromResult((false, (object?)null));
2424

2525
/// <summary>
2626
/// Called after ReadStateAsync()
2727
/// </summary>
28-
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask> OnAfterReadStateFunc { get; set; } = (_, _) => ValueTask.CompletedTask;
28+
public Func<IGrainActivationContext, IPersistentState<TState>, object?, ValueTask> OnAfterReadStateFunc { get; set; } = (_, _, _) => ValueTask.CompletedTask;
2929

3030
/// <summary>
3131
/// Called before WriteStateAsync(); return false to prevent writing
3232
/// </summary>
33-
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask<bool>> OnBeforeWriteStateFunc { get; set; } = (_, _) => ValueTask.FromResult(true);
33+
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask<(bool PreventOperation, object? SharedState)>> OnBeforeWriteStateFunc { get; set; } = (_, _) => ValueTask.FromResult((false, (object?)null));
3434

3535
/// <summary>
3636
/// Called after WriteStateAsync()
3737
/// </summary>
38-
public Func<IGrainActivationContext, IPersistentState<TState>, ValueTask> OnAfterWriteStateFunc { get; set; } = (_, _) => ValueTask.CompletedTask;
38+
public Func<IGrainActivationContext, IPersistentState<TState>, object?, ValueTask> OnAfterWriteStateFunc { get; set; } = (_, _, _) => ValueTask.CompletedTask;
3939
}

Source/Sample/Program.cs

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,51 +20,74 @@
2020
.AddStorageInterceptors()
2121
.UseGenericStorageInterceptor<Dictionary<string, string>>("SecretsStorage", "secretsState", c =>
2222
{
23-
c.OnBeforeReadStateAsync = (grainActivationContext, currentState) =>
24-
{
25-
Console.WriteLine($"OnBeforeReadState: {grainActivationContext.GrainIdentity.IdentityString}: Count Is {currentState.State.Count}");
26-
return ValueTask.FromResult(true);
27-
};
28-
29-
c.OnAfterReadStateFunc = (grainActivationContext, currentState) =>
30-
{
31-
Console.WriteLine($"OnAfterReadState: {grainActivationContext.GrainIdentity.IdentityString}: Count Is {currentState.State.Count}");
32-
33-
// Do a deep copy
34-
// Dictionary<string, string>? stateToModify = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(currentState));
35-
36-
foreach (var (key, value) in currentState.State)
37-
{
38-
Console.WriteLine($"Intercepted: {key}: {value}");
39-
40-
// Decrypt the data
41-
currentState.State[key] = currentState.State[key].Replace('3', 'e');
42-
}
43-
return ValueTask.CompletedTask;
44-
};
4523

4624
c.OnBeforeWriteStateFunc = (grainActivationContext, currentState) =>
4725
{
26+
var unencryptedValues = new Dictionary<string, string>(currentState.State.Count);
4827
Console.WriteLine($"OnBeforeWriteState: {grainActivationContext.GrainIdentity.IdentityString}: Count Is {currentState.State.Count}");
4928
foreach (var (key, value) in currentState.State)
5029
{
5130
Console.WriteLine($"Intercepted: {key}: {value}");
5231

32+
// Save the original state to return to the grain
33+
unencryptedValues.Add(key, value);
34+
5335
// Encrypt the data
5436
currentState.State[key] = currentState.State[key].Replace('e', '3');
5537
}
56-
return ValueTask.FromResult(true);
38+
return ValueTask.FromResult((false, (object?)unencryptedValues));
5739
};
5840

59-
c.OnAfterWriteStateFunc = (grainActivationContext, currentState) =>
41+
c.OnAfterWriteStateFunc = (grainActivationContext, currentState, sharedState) =>
6042
{
43+
var unencryptedValues = (Dictionary<string, string>)sharedState!;
6144
Console.WriteLine($"OnAfterWriteState: {grainActivationContext.GrainIdentity.IdentityString}: Count Is {currentState.State.Count}");
6245
foreach (var (key, value) in currentState.State)
6346
{
6447
Console.WriteLine($"What was actually persisted: {key}: {value}");
48+
49+
currentState.State[key] = unencryptedValues[key];
50+
Console.WriteLine($"What will be returned to grain: {key}: {unencryptedValues[key]}");
6551
}
6652
return ValueTask.CompletedTask;
6753
};
54+
55+
c.OnBeforeReadStateAsync = (grainActivationContext, currentState) =>
56+
{
57+
Console.WriteLine($"OnBeforeReadState: {grainActivationContext.GrainIdentity.IdentityString}: Count Is {currentState.State.Count}");
58+
59+
var unencryptedValues = new Dictionary<string, string>(currentState.State.Count);
60+
foreach (var (key, value) in currentState.State)
61+
{
62+
Console.WriteLine($"Unencrypted Values: {key}: {value}");
63+
64+
// Save the original state to return to the grain
65+
unencryptedValues.Add(key, value);
66+
}
67+
return ValueTask.FromResult((false, (object?)unencryptedValues));
68+
};
69+
70+
c.OnAfterReadStateFunc = (grainActivationContext, currentState, sharedState) =>
71+
{
72+
var unencryptedValues = (Dictionary<string, string>)sharedState!;
73+
if (!currentState.RecordExists)
74+
{
75+
return ValueTask.CompletedTask;
76+
}
77+
78+
var list = sharedState as List<string>;
79+
Console.WriteLine($"OnAfterReadState: {grainActivationContext.GrainIdentity.IdentityString}: Count Is {currentState.State.Count}");
80+
81+
foreach (var (key, value) in currentState.State)
82+
{
83+
Console.WriteLine($"Encrypted Values: {key}: {value}");
84+
85+
// Decrypt the data
86+
currentState.State[key] = currentState.State[key].Replace('3', 'e');
87+
}
88+
return ValueTask.CompletedTask;
89+
};
90+
6891
}))
6992
.Build();
7093

@@ -81,12 +104,16 @@
81104
var secretStore = grainFactory.GetGrain<ISecretStorageGrain>("friend");
82105

83106
// Call the grain and print the result to the console
84-
await secretStore.AddSecret("Password", "Now you See the secrets and now they are seen as safe!");
107+
await secretStore.AddOrUpdateSecret("Password", "Now you See the secrets and now they are seen as safe!");
85108

86109
var result = await secretStore.GetSecret("Password");
87110

88-
Console.WriteLine("\n\n{0}\n\n", result);
111+
Console.WriteLine($"Original Value: {result}");
89112

113+
await secretStore.AddOrUpdateSecret("Password", "Seeeeeeeeeecrets");
114+
result = await secretStore.GetSecret("Password");
115+
116+
Console.WriteLine($"Updated Value: {result}");
90117
Console.ReadLine();
91118

92119
await host.StopAsync();
@@ -98,18 +125,19 @@ internal class SecretStorageGrain : Grain, ISecretStorageGrain
98125
private readonly IPersistentState<Dictionary<string, string>> secrets;
99126

100127
public SecretStorageGrain([StorageInterceptor("SecretsStorage", "secretsState")] IPersistentState<Dictionary<string, string>> state) => this.secrets = state;
101-
public async Task AddSecret(string name, string value)
128+
public async Task AddOrUpdateSecret(string name, string value)
102129
{
103-
this.secrets.State.Add(name, value);
130+
this.secrets.State[name] = value;
104131
await this.secrets.WriteStateAsync();
105132
}
106133

134+
107135
public Task<string> GetSecret(string name) => Task.FromResult(this.secrets.State[name]);
108136
}
109137

110138
internal interface ISecretStorageGrain : IGrainWithStringKey
111139
{
112-
Task AddSecret(string name, string value);
140+
Task AddOrUpdateSecret(string name, string value);
113141
Task<string> GetSecret(string name);
114142
}
115143
}

Tests/Orleans.StorageProviderInterceptors.Test/Class1Test.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,5 @@ namespace Orleans.StorageProviderInterceptors.Test;
55
public class Class1Test
66
{
77
[Fact]
8-
public void Given_When_Then() =>
9-
//var class1 = new Class1();
10-
11-
Assert.True(true);
8+
public void Given_When_Then() => Assert.True(true);
129
}

0 commit comments

Comments
 (0)