Skip to content

Commit e1b6dfd

Browse files
committed
Add Garnet data protection feature
1 parent 0e6180e commit e1b6dfd

7 files changed

Lines changed: 229 additions & 5 deletions

File tree

src/OrchardCoreContrib.Garnet/Extensions/GarnetClientExtensions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,35 @@ public static async Task SetSetAsync(this GarnetClient client, string key, strin
3636

3737
await client.ExecuteForStringResultAsync("SADD", [key, value]);
3838
}
39+
40+
/// <summary>
41+
/// Gets the values of the specified list key.
42+
/// </summary>
43+
/// <param name="client">The <see cref="GarnetClient"/>.</param>
44+
/// <param name="key">The list key.</param>
45+
/// <param name="start">The offset start.</param>
46+
/// <param name="stop">The offset stop.</param>
47+
/// <returns></returns>
48+
public static async Task<string[]> ListRangeAsync(this GarnetClient client, string key, int start, int stop)
49+
{
50+
Guard.ArgumentNotNull(client, nameof(client));
51+
52+
return await client.ExecuteForStringArrayResultAsync("LRANGE", [key, start.ToString(), stop.ToString()]);
53+
}
54+
55+
/// <summary>
56+
/// Pushes a value to the tail of the list.
57+
/// </summary>
58+
/// <param name="client">The <see cref="GarnetClient"/>.</param>
59+
/// <param name="key">The list key.</param>
60+
/// <param name="value">The value to be added.</param>
61+
/// <returns></returns>
62+
public static async Task ListRightPushAsync(this GarnetClient client, string key, string value)
63+
{
64+
Guard.ArgumentNotNull(client, nameof(client));
65+
Guard.ArgumentNotNullOrEmpty(key, nameof(key));
66+
Guard.ArgumentNotNullOrEmpty(value, nameof(value));
67+
68+
await client.ExecuteForStringResultAsync("RPUSH", [key, value]);
69+
}
3970
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Microsoft.AspNetCore.DataProtection.KeyManagement;
2+
using Microsoft.Extensions.Options;
3+
using OrchardCore.Environment.Shell;
4+
using OrchardCoreContrib.Garnet.Services;
5+
6+
namespace OrchardCoreContrib.Garnet;
7+
8+
/// <summary>
9+
/// Represents a setup to configure the <see cref="KeyManagementOptions"/> for Garnet data protection feature.
10+
/// </summary>
11+
/// <param name="garnetService">The <see cref="IGarnetService"/>.</param>
12+
/// <param name="shellSettings">The <see cref="ShellSettings"/>.</param>
13+
public class GarnetKeyManagementOptionsSetup(IGarnetService garnetService, ShellSettings shellSettings)
14+
: IConfigureOptions<KeyManagementOptions>
15+
{
16+
private readonly string _tenant = shellSettings.Name;
17+
18+
/// <inheritdoc/>
19+
public void Configure(KeyManagementOptions options)
20+
{
21+
options.XmlRepository = new GarnetXmlRepository(() =>
22+
{
23+
if (garnetService.Client == null)
24+
{
25+
garnetService.ConnectAsync().GetAwaiter().GetResult();
26+
}
27+
28+
return garnetService.Client;
29+
}, $"({garnetService.InstancePrefix}{_tenant}:DataProtection-Keys");
30+
}
31+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Garnet.client;
2+
using Microsoft.AspNetCore.DataProtection.Repositories;
3+
using System.Xml.Linq;
4+
5+
namespace OrchardCoreContrib.Garnet;
6+
7+
/// <summary>
8+
/// Represents a repository to store data protection keys in Garnet.
9+
/// </summary>
10+
/// <param name="garnetClientFactory">The <see cref="GarnetClient"/> factory.</param>
11+
/// <param name="key">The key.</param>
12+
public class GarnetXmlRepository(Func<GarnetClient> garnetClientFactory, string key) : IXmlRepository
13+
{
14+
/// <inheritdoc/>
15+
public IReadOnlyCollection<XElement> GetAllElements() => GetAllElementsCore().ToList().AsReadOnly();
16+
17+
/// <inheritdoc/>
18+
public void StoreElement(XElement element, string friendlyName) => garnetClientFactory()
19+
.ListRightPushAsync(key, element.ToString(SaveOptions.DisableFormatting))
20+
.GetAwaiter()
21+
.GetResult();
22+
23+
private IEnumerable<XElement> GetAllElementsCore()
24+
{
25+
var elements = garnetClientFactory()
26+
.ListRangeAsync(key, 0, -1)
27+
.GetAwaiter()
28+
.GetResult();
29+
30+
foreach (var element in elements)
31+
{
32+
yield return XElement.Parse(element);
33+
}
34+
}
35+
}

src/OrchardCoreContrib.Garnet/Manifest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@
3030
Dependencies = ["OrchardCoreContrib.Garnet"],
3131
Category = "Distributed Caching"
3232
)]
33+
34+
[assembly: Feature(
35+
Id = "OrchardCoreContrib.Garnet.DataProtection",
36+
Name = "Garnet DataProtection",
37+
Description = "Distributed DataProtection using Garnet.",
38+
Dependencies = ["OrchardCoreContrib.Garnet"],
39+
Category = "Distributed Caching"
40+
)]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Microsoft.AspNetCore.DataProtection.KeyManagement;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Options;
4+
using OrchardCore.Modules;
5+
using OrchardCoreContrib.Garnet.Services;
6+
7+
namespace OrchardCoreContrib.Garnet;
8+
9+
/// <summary>
10+
/// Represensts a startup point to register the required services by Garnet data protection feature.
11+
/// </summary>
12+
[Feature("OrchardCoreContrib.Garnet.DataProtection")]
13+
public class GarnetDataProtectionStartup : StartupBase
14+
{
15+
/// <inheritdoc/>
16+
public override void ConfigureServices(IServiceCollection services)
17+
{
18+
if (services.Any(d => d.ServiceType == typeof(IGarnetService)))
19+
{
20+
services.AddTransient<IConfigureOptions<KeyManagementOptions>, GarnetKeyManagementOptionsSetup>();
21+
}
22+
}
23+
}

test/OrchardCoreContrib.Garnet.Tests/Extensions/GarnetClientExtensionsTests.cs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ public class GarnetClientExtensionsTests : TestBase
1010
public override async Task InitializeAsync()
1111
{
1212
_garnetClient = (await Utilities.CreateGarnetServiceAsync()).Client;
13-
14-
await _garnetClient.ExecuteForStringResultAsync("SADD", ["sets_key1", "foo", "bar", "baz"]);
15-
await _garnetClient.KeyDeleteAsync(["sets_key2"]);
13+
14+
await _garnetClient.KeyDeleteAsync(["set_key1"]);
15+
await _garnetClient.KeyDeleteAsync(["list_key1"]);
16+
await _garnetClient.KeyDeleteAsync(["set_key2"]);
17+
await _garnetClient.KeyDeleteAsync(["list_key2"]);
18+
await _garnetClient.ExecuteForStringResultAsync("SADD", ["set_key1", "foo", "bar", "baz"]);
19+
await _garnetClient.ExecuteForStringResultAsync("RPUSH", ["list_key1", "foo", "bar", "baz"]);
1620
}
1721

1822
[Fact]
1923
public async Task GetSetElements()
2024
{
2125
// Arrange
22-
var key = "sets_key1";
26+
var key = "set_key1";
2327

2428
// Act
2529
var values = await _garnetClient.SetGetAsync(key);
@@ -33,7 +37,7 @@ public async Task GetSetElements()
3337
public async Task AddSetElements()
3438
{
3539
// Arrange
36-
var key = "sets_key2";
40+
var key = "set_key2";
3741

3842
// Act
3943
await _garnetClient.SetSetAsync(key, "foo");
@@ -44,4 +48,41 @@ public async Task AddSetElements()
4448
Assert.NotEmpty(values);
4549
Assert.Equal(["foo", "bar"], values);
4650
}
51+
52+
[Theory]
53+
[InlineData(0, -1, new string[] { "foo", "bar", "baz" })]
54+
[InlineData(0, 0, new string[] { "foo" })]
55+
[InlineData(1, 2, new string[] { "bar", "baz" })]
56+
[InlineData(-3, 1, new string[] { "foo", "bar" })]
57+
[InlineData(-3, 2, new string[] { "foo", "bar", "baz" })]
58+
[InlineData(-100, 100, new string[] { "foo", "bar", "baz" })]
59+
public async Task GetListElements(int start, int stop, string[] expectedValues)
60+
{
61+
// Arrange
62+
var key = "list_key1";
63+
64+
// Act
65+
var values = await _garnetClient.ListRangeAsync(key, start, stop);
66+
67+
// Assert
68+
Assert.NotEmpty(values);
69+
Assert.Equal(expectedValues, values);
70+
}
71+
72+
[Fact]
73+
public async Task PushListElement()
74+
{
75+
// Arrange
76+
var key = "list_key2";
77+
string[] items = ["foo", "bar"];
78+
79+
// Act
80+
await _garnetClient.ListRightPushAsync(key, items[0]);
81+
await _garnetClient.ListRightPushAsync(key, items[1]);
82+
83+
// Assert
84+
var values = await _garnetClient.ExecuteForStringArrayResultAsync("LRANGE", [key, "0", "-1"]);
85+
Assert.NotEmpty(values);
86+
Assert.Equal(items, values);
87+
}
4788
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using OrchardCoreContrib.Garnet.Services;
2+
using System.Xml.Linq;
3+
4+
namespace OrchardCoreContrib.Garnet.Tests;
5+
6+
public class GarnetXmlRepositoryTests : TestBase
7+
{
8+
private static IGarnetService _garnetService;
9+
10+
public override async Task InitializeAsync()
11+
{
12+
_garnetService = await Utilities.CreateGarnetServiceAsync();
13+
14+
await _garnetService.Client.KeyDeleteAsync(["xmlKey1", "xmlKey2", "xmlKey4"]);
15+
16+
await _garnetService.Client.ExecuteForStringResultAsync("RPUSH", ["xmlKey1", "<element><child1>value1</child1></element>"]);
17+
18+
for (int i = 1; i <= 3; i++)
19+
{
20+
await _garnetService.Client.ExecuteForStringResultAsync("RPUSH", ["xmlKey2", "<element><child1>value1</child1></element>"]);
21+
}
22+
}
23+
24+
[Theory]
25+
[InlineData("xmlKey1", 1)]
26+
[InlineData("xmlKey2", 3)]
27+
[InlineData("xmlKey3", 0)]
28+
public void GetAllElements(string key, int expectedElementsCount)
29+
{
30+
// Arrange
31+
var garnetXmlRepository = new GarnetXmlRepository(() => _garnetService.Client, key);
32+
33+
// Act
34+
var elements = garnetXmlRepository.GetAllElements();
35+
36+
// Assert
37+
Assert.Equal(expectedElementsCount, elements.Count);
38+
}
39+
40+
[Fact]
41+
public void StoreElement()
42+
{
43+
// Arrange
44+
var element = XElement.Parse("<element><child1>value1</child1></element>");
45+
var garnetXmlRepository = new GarnetXmlRepository(() => _garnetService.Client, "xmlKey4");
46+
47+
// Act
48+
garnetXmlRepository.StoreElement(element, null);
49+
50+
// Assert
51+
var elements = garnetXmlRepository.GetAllElements();
52+
Assert.Single(elements);
53+
Assert.Equal(element.ToString(), elements.Single().ToString());
54+
}
55+
}

0 commit comments

Comments
 (0)