Skip to content

Commit

Permalink
Asset Key Value Store for entity framework. (#83)
Browse files Browse the repository at this point in the history
* Asset Key Value Store for entity framework.

* Add missing files.
  • Loading branch information
SebastianStehle authored Jan 19, 2025
1 parent 709397f commit 3ef5ac7
Show file tree
Hide file tree
Showing 43 changed files with 768 additions and 279 deletions.
13 changes: 10 additions & 3 deletions Squidex.Libs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Events", "events\Sq
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Events.EntityFramework", "events\Squidex.Events.EntityFramework\Squidex.Events.EntityFramework.csproj", "{5F4B51E1-0ADD-4C03-A93A-401BA86D08BA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Events.Tests", "events\Squidex.Events.Tests\Squidex.Events.Tests.csproj", "{1E9B31E9-EA9D-4A82-B207-00E8B275EFD4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Events.Tests", "events\Squidex.Events.Tests\Squidex.Events.Tests.csproj", "{1E9B31E9-EA9D-4A82-B207-00E8B275EFD4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Events.Mongo", "events\Squidex.Events.Mongo\Squidex.Events.Mongo.csproj", "{E36DAC0D-8A2D-49AA-B9C4-E74CAB0660B0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Events.Mongo", "events\Squidex.Events.Mongo\Squidex.Events.Mongo.csproj", "{E36DAC0D-8A2D-49AA-B9C4-E74CAB0660B0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Events.GetEventStore", "events\Squidex.Events.GetEventStore\Squidex.Events.GetEventStore.csproj", "{98156A5E-1B4A-46EF-AA84-019868425D80}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Events.GetEventStore", "events\Squidex.Events.GetEventStore\Squidex.Events.GetEventStore.csproj", "{98156A5E-1B4A-46EF-AA84-019868425D80}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Assets.EntityFramework", "assets\Squidex.Assets.EntityFramework\Squidex.Assets.EntityFramework.csproj", "{C3D35ED4-8C81-449B-B732-3F0EE1FD6C7A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -283,6 +285,10 @@ Global
{98156A5E-1B4A-46EF-AA84-019868425D80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98156A5E-1B4A-46EF-AA84-019868425D80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98156A5E-1B4A-46EF-AA84-019868425D80}.Release|Any CPU.Build.0 = Release|Any CPU
{C3D35ED4-8C81-449B-B732-3F0EE1FD6C7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3D35ED4-8C81-449B-B732-3F0EE1FD6C7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3D35ED4-8C81-449B-B732-3F0EE1FD6C7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3D35ED4-8C81-449B-B732-3F0EE1FD6C7A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -331,6 +337,7 @@ Global
{1E9B31E9-EA9D-4A82-B207-00E8B275EFD4} = {94285572-6875-4A9C-AFC4-987758DC9088}
{E36DAC0D-8A2D-49AA-B9C4-E74CAB0660B0} = {94285572-6875-4A9C-AFC4-987758DC9088}
{98156A5E-1B4A-46EF-AA84-019868425D80} = {94285572-6875-4A9C-AFC4-987758DC9088}
{C3D35ED4-8C81-449B-B732-3F0EE1FD6C7A} = {C857F3ED-A6AE-47C6-A115-87ECCB36AC02}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {060512DD-34DA-4929-A67F-2E473577FBF5}
Expand Down
30 changes: 15 additions & 15 deletions ai/Squidex.AI.Tests/EFChatStoreFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public sealed class EFChatStoreFixture : IAsyncLifetime
.WithLabel("reuse-id", "chatstore-postgres")
.Build();

private IServiceProvider services;
public IServiceProvider Services { get; private set; }

public EFChatStore<AppDbContext> Store => services.GetRequiredService<EFChatStore<AppDbContext>>();
public EFChatStore<AppDbContext> Store => Services.GetRequiredService<EFChatStore<AppDbContext>>();

public sealed class AppDbContext(DbContextOptions options) : DbContext(options)
{
Expand All @@ -38,21 +38,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
}
}

public async Task DisposeAsync()
{
foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.ReleaseAsync(default);
}

await postgresSql.StopAsync();
}

public async Task InitializeAsync()
{
await postgresSql.StartAsync();

services = new ServiceCollection()
Services = new ServiceCollection()
.AddDbContext<AppDbContext>(b =>
{
b.UseNpgsql(postgresSql.GetConnectionString());
Expand All @@ -62,15 +52,25 @@ public async Task InitializeAsync()
.Services
.BuildServiceProvider();

var factory = services.GetRequiredService<IDbContextFactory<AppDbContext>>();
var factory = Services.GetRequiredService<IDbContextFactory<AppDbContext>>();
var context = await factory.CreateDbContextAsync();
var creator = (RelationalDatabaseCreator)context.Database.GetService<IDatabaseCreator>();

await creator.EnsureCreatedAsync();

foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
foreach (var service in Services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.InitializeAsync(default);
}
}

public async Task DisposeAsync()
{
foreach (var service in Services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.ReleaseAsync(default);
}

await postgresSql.StopAsync();
}
}
30 changes: 15 additions & 15 deletions ai/Squidex.AI.Tests/MongoChatStoreFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,37 @@ public sealed class MongoChatStoreFixture : IAsyncLifetime
new MongoDbBuilder()
.WithReuse(Debugger.IsAttached)
.WithLabel("reuse-id", "chatstore-mongo")
.Build();
.Build();

private IServiceProvider services;
public IServiceProvider Services { get; private set; }

public MongoChatStore Store => services.GetRequiredService<MongoChatStore>();

public async Task DisposeAsync()
{
foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.ReleaseAsync(default);
}

await mongoDb.StopAsync();
}
public MongoChatStore Store => Services.GetRequiredService<MongoChatStore>();

public async Task InitializeAsync()
{
await mongoDb.StartAsync();

services = new ServiceCollection()
Services = new ServiceCollection()
.AddSingleton<IMongoClient>(_ => new MongoClient(mongoDb.GetConnectionString()))
.AddSingleton(c => c.GetRequiredService<IMongoClient>().GetDatabase("Test"))
.AddAI()
.AddMongoChatStore(TestHelpers.Configuration)
.Services
.BuildServiceProvider();

foreach (var service in services.GetRequiredService<IEnumerable<IInitializable>>())
foreach (var service in Services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.InitializeAsync(default);
}
}

public async Task DisposeAsync()
{
foreach (var service in Services.GetRequiredService<IEnumerable<IInitializable>>())
{
await service.ReleaseAsync(default);
}

await mongoDb.StopAsync();
}
}
30 changes: 30 additions & 0 deletions assets/Squidex.Assets.EntityFramework/AssetsServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Squidex.Assets;
using Squidex.Assets.EntityFramework;

namespace Microsoft.Extensions.DependencyInjection;

public static class AssetsServiceExtensions
{
public static IServiceCollection AddEntityFrameworkAssetKeyValueStore<TContext, TEntity>(this IServiceCollection services)
where TContext : DbContext
where TEntity : class
{
services.AddSingletonAs<EFAssetKeyValueStore<TContext, TEntity>>()
.As<IAssetKeyValueStore<TEntity>>().AsSelf();

services.AddDbContextFactory<TContext>();
services.TryAddSingleton(JsonSerializerOptions.Default);

return services;
}
}
40 changes: 40 additions & 0 deletions assets/Squidex.Assets.EntityFramework/EFAssetKeyValueEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using System.ComponentModel.DataAnnotations;
using System.Text.Json;

namespace Squidex.Assets.EntityFramework;

public sealed class EFAssetKeyValueEntity<T> where T : class
{
[Key]
public string Key { get; set; }

public string Value { get; set; }

public DateTimeOffset Expires { get; set; }

public static EFAssetKeyValueEntity<T> Create(string key, T value, DateTimeOffset expires,
JsonSerializerOptions options)
{
var serialized = JsonSerializer.Serialize(value, options);

return new EFAssetKeyValueEntity<T> { Key = key, Value = serialized, Expires = expires.ToUniversalTime() };
}

public void Update(T value, JsonSerializerOptions options)
{
Value = JsonSerializer.Serialize(value, options);
}

public T GetValue(JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(Value, options) ??
throw new JsonException("Failed to deserialize json.");
}
}
82 changes: 82 additions & 0 deletions assets/Squidex.Assets.EntityFramework/EFAssetKeyValueStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;

namespace Squidex.Assets.EntityFramework;

public sealed class EFAssetKeyValueStore<TContext, TEntity>(
IDbContextFactory<TContext> dbContextFactory,
JsonSerializerOptions jsonSerializerOptions)
: IAssetKeyValueStore<TEntity>
where TContext : DbContext
where TEntity : class
{
public async Task DeleteAsync(string key,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);

await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);

await dbContext.Set<EFAssetKeyValueEntity<TEntity>>().Where(x => x.Key == key)
.ExecuteDeleteAsync(ct);
}

public async Task<TEntity?> GetAsync(string key,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);

await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);

var entity = await dbContext.Set<EFAssetKeyValueEntity<TEntity>>().Where(x => x.Key == key)
.FirstOrDefaultAsync(ct);

if (entity == null)
{
return default;
}

return entity?.GetValue(jsonSerializerOptions);
}

public async IAsyncEnumerable<(string Key, TEntity Value)> GetExpiredEntriesAsync(DateTimeOffset now,
[EnumeratorCancellation] CancellationToken ct = default)
{
await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);

var query = dbContext.Set<EFAssetKeyValueEntity<TEntity>>().Where(x => x.Expires < now);

foreach (var entity in query)
{
yield return (entity.Key, entity.GetValue(jsonSerializerOptions));
}
}

public async Task SetAsync(string key, TEntity value, DateTimeOffset expiration,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(key);

await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct);

var entity = EFAssetKeyValueEntity<TEntity>.Create(key, value, expiration, jsonSerializerOptions);
try
{
await dbContext.Set<EFAssetKeyValueEntity<TEntity>>().AddAsync(entity, ct);
await dbContext.SaveChangesAsync();
}
catch (DbUpdateException)
{
dbContext.Entry(entity).State = EntityState.Modified;
await dbContext.SaveChangesAsync();
}
}
}
25 changes: 25 additions & 0 deletions assets/Squidex.Assets.EntityFramework/EFSchema.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using Squidex.Assets.EntityFramework;

namespace Microsoft.EntityFrameworkCore;

public static class EFSchema
{
public static ModelBuilder AddAssetKeyValueStore<T>(this ModelBuilder modelBuilder) where T : class
{
modelBuilder.Entity<EFAssetKeyValueEntity<T>>(b =>
{
b.ToTable($"AssetKeyValueStore_{typeof(T).Name}");

b.HasIndex(x => x.Expires);
});

return modelBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<None Include="logo-squared.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.179">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.11" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\hosting\Squidex.Hosting.Abstractions\Squidex.Hosting.Abstractions.csproj" />
<ProjectReference Include="..\Squidex.Assets\Squidex.Assets.csproj" />
</ItemGroup>

</Project>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/Squidex.Assets.Mongo/AssetsServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static IServiceCollection AddMongoAssetStore(this IServiceCollection serv

public static IServiceCollection AddMongoAssetKeyValueStore(this IServiceCollection services)
{
services.AddSingleton(typeof(MongoAssetKeyValueStore<>));
services.AddSingleton(typeof(IAssetKeyValueStore<>), typeof(MongoAssetKeyValueStore<>));

return services;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Squidex.Assets;

internal sealed class MongoAssetEntity<T>
internal sealed class MongoAssetKeyValueEntity<T>
{
[BsonId]
public string Key { get; set; }
Expand Down
Loading

0 comments on commit 3ef5ac7

Please sign in to comment.