Wire selectable persistence backend for cloud state stores
This commit is contained in:
@@ -56,8 +56,21 @@ public static class ServiceCollectionExtensions
|
||||
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "cloud-state.json");
|
||||
var personalMemoryPersistencePath = configuration?["OpenJibo:PersonalMemory:PersistencePath"]
|
||||
?? Path.Combine(AppContext.BaseDirectory, "App_Data", "personal-memory.json");
|
||||
services.AddSingleton<ICloudStateStore>(_ => new InMemoryCloudStateStore(statePersistencePath));
|
||||
services.AddSingleton<IPersonalMemoryStore>(_ => new InMemoryPersonalMemoryStore(personalMemoryPersistencePath));
|
||||
var stateBackendKind = ParseBackendKind(configuration?["OpenJibo:State:Backend"]);
|
||||
var personalMemoryBackendKind = ParseBackendKind(configuration?["OpenJibo:PersonalMemory:Backend"]);
|
||||
var stateConnectionString = configuration?["OpenJibo:State:ConnectionString"] ?? Environment.GetEnvironmentVariable("OPENJIBO_STATE_SQL_CONNECTION_STRING");
|
||||
var personalMemoryConnectionString = configuration?["OpenJibo:PersonalMemory:ConnectionString"] ?? Environment.GetEnvironmentVariable("OPENJIBO_PERSONAL_MEMORY_SQL_CONNECTION_STRING");
|
||||
services.AddSingleton<IPersistenceSnapshotStoreFactory, PersistenceSnapshotStoreFactory>();
|
||||
services.AddSingleton<ICloudStateStore>(provider =>
|
||||
{
|
||||
var snapshotFactory = provider.GetRequiredService<IPersistenceSnapshotStoreFactory>();
|
||||
return new InMemoryCloudStateStore(snapshotFactory.Create(statePersistencePath, stateBackendKind, "cloud-state", stateConnectionString));
|
||||
});
|
||||
services.AddSingleton<IPersonalMemoryStore>(provider =>
|
||||
{
|
||||
var snapshotFactory = provider.GetRequiredService<IPersistenceSnapshotStoreFactory>();
|
||||
return new InMemoryPersonalMemoryStore(snapshotFactory.Create(personalMemoryPersistencePath, personalMemoryBackendKind, "personal-memory", personalMemoryConnectionString));
|
||||
});
|
||||
services.AddSingleton<IJiboExperienceContentRepository, InMemoryJiboExperienceContentRepository>();
|
||||
services.AddSingleton<JiboExperienceContentCache>();
|
||||
services.AddSingleton<IJiboRandomizer, DefaultJiboRandomizer>();
|
||||
@@ -78,4 +91,11 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static PersistenceBackendKind ParseBackendKind(string? value)
|
||||
{
|
||||
return Enum.TryParse<PersistenceBackendKind>(value, ignoreCase: true, out var backendKind)
|
||||
? backendKind
|
||||
: PersistenceBackendKind.File;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Persistence;
|
||||
|
||||
internal sealed class AzureSqlSnapshotStore : ISnapshotStore
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly string _connectionString;
|
||||
private readonly string _snapshotName;
|
||||
private readonly string _tableName;
|
||||
|
||||
public AzureSqlSnapshotStore(string connectionString, string snapshotName, string tableName = "PersistenceSnapshots")
|
||||
{
|
||||
_connectionString = string.IsNullOrWhiteSpace(connectionString)
|
||||
? throw new InvalidOperationException("Azure SQL persistence requires a connection string.")
|
||||
: connectionString;
|
||||
_snapshotName = string.IsNullOrWhiteSpace(snapshotName)
|
||||
? throw new ArgumentException("A snapshot name is required for Azure SQL persistence.", nameof(snapshotName))
|
||||
: snapshotName;
|
||||
_tableName = string.IsNullOrWhiteSpace(tableName) ? "PersistenceSnapshots" : tableName;
|
||||
}
|
||||
|
||||
public TSnapshot? Load<TSnapshot>() where TSnapshot : class
|
||||
{
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
connection.Open();
|
||||
EnsureTable(connection);
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = $"""
|
||||
SELECT SnapshotJson
|
||||
FROM dbo.[{_tableName}]
|
||||
WHERE SnapshotName = @snapshotName
|
||||
""";
|
||||
command.Parameters.Add(new SqlParameter("@snapshotName", SqlDbType.NVarChar, 200) { Value = _snapshotName });
|
||||
|
||||
var result = command.ExecuteScalar();
|
||||
if (result is not string json || string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<TSnapshot>(json, JsonOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public void Save<TSnapshot>(TSnapshot snapshot) where TSnapshot : class
|
||||
{
|
||||
using var connection = new SqlConnection(_connectionString);
|
||||
connection.Open();
|
||||
EnsureTable(connection);
|
||||
|
||||
var json = JsonSerializer.Serialize(snapshot, JsonOptions);
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = $"""
|
||||
MERGE dbo.[{_tableName}] AS target
|
||||
USING (SELECT @snapshotName AS SnapshotName) AS source
|
||||
ON target.SnapshotName = source.SnapshotName
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET SnapshotJson = @snapshotJson,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (SnapshotName, SnapshotJson, CreatedUtc, UpdatedUtc)
|
||||
VALUES (@snapshotName, @snapshotJson, SYSUTCDATETIME(), SYSUTCDATETIME());
|
||||
""";
|
||||
command.Parameters.Add(new SqlParameter("@snapshotName", SqlDbType.NVarChar, 200) { Value = _snapshotName });
|
||||
command.Parameters.Add(new SqlParameter("@snapshotJson", SqlDbType.NVarChar, -1) { Value = json });
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private void EnsureTable(SqlConnection connection)
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = $"""
|
||||
IF OBJECT_ID(N'dbo.[{_tableName}]', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.[{_tableName}] (
|
||||
SnapshotName nvarchar(200) NOT NULL CONSTRAINT PK_{_tableName}_SnapshotName PRIMARY KEY,
|
||||
SnapshotJson nvarchar(max) NOT NULL,
|
||||
CreatedUtc datetimeoffset NOT NULL,
|
||||
UpdatedUtc datetimeoffset NOT NULL
|
||||
);
|
||||
END
|
||||
""";
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Jibo.Cloud.Infrastructure.Persistence;
|
||||
|
||||
public interface IPersistenceSnapshotStoreFactory
|
||||
{
|
||||
ISnapshotStore Create(string? persistencePath, PersistenceBackendKind backendKind, string snapshotName, string? connectionString = null);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Jibo.Cloud.Infrastructure.Persistence;
|
||||
|
||||
public enum PersistenceBackendKind
|
||||
{
|
||||
File,
|
||||
AzureSql
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Jibo.Cloud.Infrastructure.Persistence;
|
||||
|
||||
public sealed class PersistenceSnapshotStoreFactory : IPersistenceSnapshotStoreFactory
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public ISnapshotStore Create(string? persistencePath, PersistenceBackendKind backendKind, string snapshotName, string? connectionString = null)
|
||||
{
|
||||
return backendKind switch
|
||||
{
|
||||
PersistenceBackendKind.File => new JsonFileSnapshotStore(persistencePath, JsonOptions),
|
||||
PersistenceBackendKind.AzureSql => new AzureSqlSnapshotStore(
|
||||
connectionString ?? throw new InvalidOperationException("Azure SQL persistence requires a connection string."),
|
||||
snapshotName),
|
||||
_ => new JsonFileSnapshotStore(persistencePath, JsonOptions)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user