From 3ef5ac7b1b29085ba3b338c03a833f954db4fc0c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 20 Jan 2025 00:53:16 +0100 Subject: [PATCH] Asset Key Value Store for entity framework. (#83) * Asset Key Value Store for entity framework. * Add missing files. --- Squidex.Libs.sln | 13 ++- ai/Squidex.AI.Tests/EFChatStoreFixture.cs | 30 +++---- ai/Squidex.AI.Tests/MongoChatStoreFixture.cs | 30 +++---- .../AssetsServiceExtensions.cs | 30 +++++++ .../EFAssetKeyValueEntity.cs | 40 +++++++++ .../EFAssetKeyValueStore.cs | 82 +++++++++++++++++ .../EFSchema.cs | 25 ++++++ .../Squidex.Assets.EntityFramework.csproj | 38 ++++++++ .../logo-squared.png | Bin 0 -> 19430 bytes .../AssetsServiceExtensions.cs | 1 + ...tEntity.cs => MongoAssetKeyValueEntity.cs} | 2 +- .../MongoAssetKeyValueStore.cs | 24 +++-- .../logo-squared.png | Bin 0 -> 19430 bytes .../AmazonS3AssetStoreFixture.cs | 24 ++--- .../AzureBlobAssetStoreFixture.cs | 24 ++--- .../CloudflareR2Fixture.cs | 8 +- .../FTPAssetStoreFixture.cs | 24 ++--- .../FolderAssetStoreFixture.cs | 34 +++---- .../GoogleCloudAssetStoreFixture.cs | 24 ++--- .../KeyValueStore/KeyValueStoreTests.cs | 85 ++++++++++++++++++ .../MongoKeyValueStoreFixture.cs | 55 ++++++++++++ .../KeyValueStore/MongoKeyValueStoreTests.cs | 19 ++++ .../PostgresKeyValueStoreFixture.cs | 72 +++++++++++++++ .../PostgresKeyValueStoreTests.cs | 19 ++++ .../KeyValueStore/TestValue.cs | 13 +++ .../MongoGridFSAssetStoreFixture.cs | 30 +++---- .../MongoGridFsAssetStoreTests.cs | 1 - .../Squidex.Assets.Tests.csproj | 3 + assets/Squidex.Assets/IAssetKeyValueStore.cs | 2 +- .../EFEventStore_Writer.cs | 5 +- .../GetEventStoreFixture.cs | 28 +++--- .../MariaDbEventStoreFixture.cs | 32 ++++--- .../MongoEventStoreReplicaFixture.cs | 28 +++--- .../MongoEventStoreStandaloneFixture.cs | 28 +++--- .../MysqlEventStoreFixture.cs | 32 ++++--- .../PostgresEventStoreFixture.cs | 32 ++++--- .../SqlServerEventStoreFixture.cs | 32 ++++--- .../EFMessagingDataStoreFixture.cs | 12 +-- .../EFMessagingFixture.cs | 10 +-- .../Squidex.Messaging.Tests/KafkaFixture.cs | 10 +-- .../MongoMessagingDataStoreFixture.cs | 28 +++--- .../RabbitMqFixture.cs | 8 +- .../Squidex.Messaging.Tests/RedisFixture.cs | 10 +-- 43 files changed, 768 insertions(+), 279 deletions(-) create mode 100644 assets/Squidex.Assets.EntityFramework/AssetsServiceExtensions.cs create mode 100644 assets/Squidex.Assets.EntityFramework/EFAssetKeyValueEntity.cs create mode 100644 assets/Squidex.Assets.EntityFramework/EFAssetKeyValueStore.cs create mode 100644 assets/Squidex.Assets.EntityFramework/EFSchema.cs create mode 100644 assets/Squidex.Assets.EntityFramework/Squidex.Assets.EntityFramework.csproj create mode 100644 assets/Squidex.Assets.EntityFramework/logo-squared.png rename assets/Squidex.Assets.Mongo/{MongoAssetEntity.cs => MongoAssetKeyValueEntity.cs} (92%) create mode 100644 assets/Squidex.Assets.ResizeService/logo-squared.png create mode 100644 assets/Squidex.Assets.Tests/KeyValueStore/KeyValueStoreTests.cs create mode 100644 assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreFixture.cs create mode 100644 assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreTests.cs create mode 100644 assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreFixture.cs create mode 100644 assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreTests.cs create mode 100644 assets/Squidex.Assets.Tests/KeyValueStore/TestValue.cs diff --git a/Squidex.Libs.sln b/Squidex.Libs.sln index bf777ef..a9c03a1 100644 --- a/Squidex.Libs.sln +++ b/Squidex.Libs.sln @@ -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 @@ -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 @@ -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} diff --git a/ai/Squidex.AI.Tests/EFChatStoreFixture.cs b/ai/Squidex.AI.Tests/EFChatStoreFixture.cs index e180083..de300a3 100644 --- a/ai/Squidex.AI.Tests/EFChatStoreFixture.cs +++ b/ai/Squidex.AI.Tests/EFChatStoreFixture.cs @@ -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 Store => services.GetRequiredService>(); + public EFChatStore Store => Services.GetRequiredService>(); public sealed class AppDbContext(DbContextOptions options) : DbContext(options) { @@ -38,21 +38,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await postgresSql.StopAsync(); - } - public async Task InitializeAsync() { await postgresSql.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddDbContext(b => { b.UseNpgsql(postgresSql.GetConnectionString()); @@ -62,15 +52,25 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - var factory = services.GetRequiredService>(); + var factory = Services.GetRequiredService>(); var context = await factory.CreateDbContextAsync(); var creator = (RelationalDatabaseCreator)context.Database.GetService(); await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await postgresSql.StopAsync(); + } } diff --git a/ai/Squidex.AI.Tests/MongoChatStoreFixture.cs b/ai/Squidex.AI.Tests/MongoChatStoreFixture.cs index 032bd70..9a4ba58 100644 --- a/ai/Squidex.AI.Tests/MongoChatStoreFixture.cs +++ b/ai/Squidex.AI.Tests/MongoChatStoreFixture.cs @@ -21,27 +21,17 @@ 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(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mongoDb.StopAsync(); - } + public MongoChatStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mongoDb.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddSingleton(_ => new MongoClient(mongoDb.GetConnectionString())) .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) .AddAI() @@ -49,9 +39,19 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mongoDb.StopAsync(); + } } diff --git a/assets/Squidex.Assets.EntityFramework/AssetsServiceExtensions.cs b/assets/Squidex.Assets.EntityFramework/AssetsServiceExtensions.cs new file mode 100644 index 0000000..23079a7 --- /dev/null +++ b/assets/Squidex.Assets.EntityFramework/AssetsServiceExtensions.cs @@ -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(this IServiceCollection services) + where TContext : DbContext + where TEntity : class + { + services.AddSingletonAs>() + .As>().AsSelf(); + + services.AddDbContextFactory(); + services.TryAddSingleton(JsonSerializerOptions.Default); + + return services; + } +} diff --git a/assets/Squidex.Assets.EntityFramework/EFAssetKeyValueEntity.cs b/assets/Squidex.Assets.EntityFramework/EFAssetKeyValueEntity.cs new file mode 100644 index 0000000..478119c --- /dev/null +++ b/assets/Squidex.Assets.EntityFramework/EFAssetKeyValueEntity.cs @@ -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 where T : class +{ + [Key] + public string Key { get; set; } + + public string Value { get; set; } + + public DateTimeOffset Expires { get; set; } + + public static EFAssetKeyValueEntity Create(string key, T value, DateTimeOffset expires, + JsonSerializerOptions options) + { + var serialized = JsonSerializer.Serialize(value, options); + + return new EFAssetKeyValueEntity { 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(Value, options) ?? + throw new JsonException("Failed to deserialize json."); + } +} diff --git a/assets/Squidex.Assets.EntityFramework/EFAssetKeyValueStore.cs b/assets/Squidex.Assets.EntityFramework/EFAssetKeyValueStore.cs new file mode 100644 index 0000000..2b84048 --- /dev/null +++ b/assets/Squidex.Assets.EntityFramework/EFAssetKeyValueStore.cs @@ -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( + IDbContextFactory dbContextFactory, + JsonSerializerOptions jsonSerializerOptions) + : IAssetKeyValueStore + 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>().Where(x => x.Key == key) + .ExecuteDeleteAsync(ct); + } + + public async Task GetAsync(string key, + CancellationToken ct = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + + await using var dbContext = await dbContextFactory.CreateDbContextAsync(ct); + + var entity = await dbContext.Set>().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>().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.Create(key, value, expiration, jsonSerializerOptions); + try + { + await dbContext.Set>().AddAsync(entity, ct); + await dbContext.SaveChangesAsync(); + } + catch (DbUpdateException) + { + dbContext.Entry(entity).State = EntityState.Modified; + await dbContext.SaveChangesAsync(); + } + } +} diff --git a/assets/Squidex.Assets.EntityFramework/EFSchema.cs b/assets/Squidex.Assets.EntityFramework/EFSchema.cs new file mode 100644 index 0000000..c3e1cbd --- /dev/null +++ b/assets/Squidex.Assets.EntityFramework/EFSchema.cs @@ -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(this ModelBuilder modelBuilder) where T : class + { + modelBuilder.Entity>(b => + { + b.ToTable($"AssetKeyValueStore_{typeof(T).Name}"); + + b.HasIndex(x => x.Expires); + }); + + return modelBuilder; + } +} diff --git a/assets/Squidex.Assets.EntityFramework/Squidex.Assets.EntityFramework.csproj b/assets/Squidex.Assets.EntityFramework/Squidex.Assets.EntityFramework.csproj new file mode 100644 index 0000000..abb906c --- /dev/null +++ b/assets/Squidex.Assets.EntityFramework/Squidex.Assets.EntityFramework.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + Latest + enable + enable + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/assets/Squidex.Assets.EntityFramework/logo-squared.png b/assets/Squidex.Assets.EntityFramework/logo-squared.png new file mode 100644 index 0000000000000000000000000000000000000000..3cbc19038a372bfa3b1094aedc915af1fe54b6ce GIT binary patch literal 19430 zcmdSBi93|<7cf2uMW}31w(R?^Ot$P}-}f#1PK<;sk&u04%}%lovJMGtBSU`}+%N+$)2!s;?Q@>~SsBmjBEYB_^|M2&5&E^iA@_l*ibE!8& z*Pc|?si~;kd-i~ZG2}vM;JpVd#+=xl80nl_uv#+Xiyp8SaYtY4i^E-mD9S}ZxIUD<}|bXlnGW@hWr1-dM4f{mE*?oe2JdpH*2 zbRu8{`Ng1!nJsJ4CuCHf*21Usr6To`l+1S85^CXkkn7A~mn-(X$`)uXJLo%2ZuS@0 zI7+tCbaBN=A=v^F$h<~BsgY%i?iv2R*E?9FW#s6+UI|S6Y0qH;beI$ZV-SIamw2rhu#k`M{hNDtz`$I9Cl|RZxUk3OV_#xC$c+TdegQZvc@lYB3u=v@{3gu_`r(hj{}(W3ASr?d)>Le1xBYLSc=g4eY>E9D&l8%B-FUE8Y1 zV&_lpELHP_A8m#`41iyOjKqT-YN>dYHL47ULLR|$BnMYuTz;G_@l%!WkjK)JSdo>r{8qp zD)USn5r7x~a7h^dw||FF2pdZsHE}!Y#*aQ>@8*JDjQOnIPbv|mF&1JlEx%?X;?=Qn zN4C{0tfxg`;EV|0rOb)sdah~-hcB|uF5Cc~0}bob+uho2Z`7b98NUO_`@F_s*`Ori z?>(zB=2gx=jb4zg->&(dGb@3}A)+ z3fp&?pubO8n~drc^nB{Rcb|~nNTDU;R0Qap?L0*o&hyx>q36q6Wb$N#da4$DLNzH# zGH3yCL}fnj?hwo|2kr^ap^z1rd>hmeDFgWMh(UxCx6sg}z5ZpIzeW^l@F93H&k+BO zk$Kj!q5_f#)IrMk@YUIhPV&1SXlejDe`!@Oz-rd712E!BTr9b(*}7`YSiuSF zf)~1kyhC*19!l;e8-+@waZ^l+ylax-tdDkXH?HW{O`~8w(Z-d70`uUl+Y{E1DLQ^j zPBnM?onwCPE(i3332Ujf#sUa`V?K z4;x*sSzzCxn z-S$$69~?5%Rw2ximUeT|=Xtw4lsaY;2#Eel3Qn>2klkLW#`bORjW2HsvDx)sNed|X zol0TD4!hUl@AQ+txe|BdS0>#|03O;}3^#sw9b z?zD+(QIbf=Gl-P-kBI!Ok@u>p?5Sue6|ysN3^QV`@r1#zL+HVQuv(Rkjb-Ap5H{fL zW=?zC<`$57Cm?&vK=xS2Es|#=%3DgSqR>+1xrq&(=dO_pOQBJW#% zkbFhEW)#u8R9UBvdjGH!|NOx8U<9kNqzITlunfMI+p*Y~yai*{)VfE=pV|%UX(H+_ z_*NN^%D}Ryf~SJ5t9#rddvK~CS=3%TaCU`_ysrYtgpQtgxNSrNG866&XOgLm2ueB& zTUD^V!FB=S(-dQN2MDa!?s!PK-Thy*dO?IGYwE~wz#||L?~9D~yyDO!e2TZB)CKR! zl5ma|2XlrMaS&_Geo|7lp21Kb7+!yfvDVlA7I9Ak@f9$}u|`NEmlqf`c9vu&kEWVY(6%OFngVmFk{=wMtR5KA92!Wb(TR-SS-{^D=?Otb()IaFG5KaUq?!~wR{JlQ`RU%aC1_0&pUC#J; zP_)c^Ob;y3$a4T$yUdW9r$-S=*+i!0_r3mHscdDE_%0E=Qz={7=Q&VchddmTiO7O4 zQ)3pTz;Z!*x0&kG}iYa)wjMIsW39?L_P0T5|{g}LG}P^ z9#1=xK1w1(uXG_s2y3%4wx_O)XtjhhImLcA-D&2X;EHiq=_jrIk@xAWHRrx)Zfi7n zckNp4==F`}8vv73;8^w0TfFZE21}!Oup1j9DvX!c?9Si5!9?9;yWr_s-%rYSKYU_d z^BFu#z|xbQEeTu25*g(QzfzhBEEUXicYI(KDi5VJax)z2_AmZ@-$Rj0;vR#DQ!H?W zk6{6+qa;&<_y3M`odMSdSju{poH{QCi6~h^zT3E|l3H{ac(*aw>6X+|Y~T7X_^P zuERy~lOy==0Ffq^mkFP2M4T>L#gT8Ru9GT%4BE_X^?);R(hTEs&iuQDpp+TzfG&RE zjE)Jkr&j1;z^&((zra;d^Av!WUY|r&o%rS88(HOcB=g?6xUToA>$PG{WZ)x8z>E>G z9zg)ZB%u@QjSA+!Fhl$Odf!6^CW5ilsXBTJ0INlbi5$)S+PJPV2c*_dPn@u`%_gK9 z4pgp`i0TSJTz77aT1ReBvIQRu}CKCqQ=Jfc?*ZCf4; zp;;^C!hQfUAoG)nOTwGzk}{bVxxALZR@_0DI*RaZ0-&5VkeoV$C-jEbggHQ*-n~kg z=v=hcA!wi++M8RG5g&soo_v|R%QCS((?#-wiK~>T-Oe_GNeMHg=0DJX$rKMW0U#bK zCaLWVUM07&dkrt(W&r7=%d2+w!V!M@@GG2D9TzjbBK)T`z$Vk*it~`rCLCbJ$ZY< zAx5LyV)jwP&e1k-+R|bHtHBqeh|x?q_{HgR=*#kr5o{j*i-}9hW$p12e=g=6Hg^|F zouR>3^nJ<25h&XfnPpQ33+nA!auE5`2iOn%%tQq@q5}x4mW|!z?E%V=@qp5AV?3S+ zs?@3MFY2Wo2V-L>XQ3|h*xTWft3_nhvPyyDcqlX07se0h=7enynolvakygtxp(<*K z7E=v<4hsi96q5e?ZBY6tC-j1muwB2Z=?%nlz(DLW;nB~@@2qTO)r?e31r{D((lhWo z8#f{CAK;bWuxt~OF*apRWnQa#ys*b=6zIA~Q=}d^1{q$k*u?TndI%;NaBrrjXI_t4 zJptnVBsRWk1+Ds zXAMcGF#wTx$X8^$u^$JZO{)X+ur`&Jx&yLSetS-l&iTWB)v1 zKZe0+o!$^X_<_TR8zW<$s>}jc0IF*$Fzblj51(c_{ z_S-_@XgV#49tAly&B&{;2^Ppy47Ndf{a6WbH>upI>^@j*^?UeUskQ#d>cP&&l|FUj zrt!h>axzGDO|&uyqL|o7^Dctz(I9V%&UIidB2&@dUNo8TsZfK^-|JUZ(|L9v2NRu; zhfL+^hC7pE%9{`R|2}&Vh0l1L@;skpn2B`g&UnNnIjSkn@NUsY6IGS1s4!o1>0LPCK z*uss=eO7=-iG|K^a@PV}{J`iDzvcksb8Kwjw%I8(P#^g~*3OQis{Z=QE!8^GI1wdgFM@hD*$f;{E)_dGMZ1jLdLRLzzEj1D# z594Fwr=+nofb*4!xcZHYeMn#{{CD`U(-V^55q5A?BMJ_`?BpR}rQJ5F0X0TH0cciU zPF~CmVi2h;;DvHS$zt%5nEVNrn4&T#u0=iZv@{53Hm!gNzM@kAJHO zomPe{H zpGuEu)mI+#@)n3Tf3Cj|%LFWRxJ?WEoPPs89wqvtp&CnMbe0S+lrF~1Y7PDCe+gg{ zH#p6w@7>OL>NeoH4q{j{*!Dy|WGb^}3r!QWcxT+DH!+Qjc$PHlAhEh3(=%w-n1R0q z>(=rhH40(O%0Xc%c)OYxe0o!9FQia{b=&{a-SOT|JReLv^F{jT1t${=SXa=U*Ec}c zOsAee!uK4gyQI$(ju$JfDrlBpZ8S0kcZ1~ZJA;ss;?RpmG>)*XitE&fl$YflPl^vhvg+TRfzJIRbV& zz1}HlH~xRNa24rOE)AKH%+2|3(^>|6o2XIKc&p7G!5mHjx$FKpf?)(4W0m==$U7D? zuQg}my9G?-)FX8o30hi^CYK%HvMEX}fVR00Xi|a)?gc=(|C#Ds+$vDkt*I?yMFbdA zCa_4Sh*#O5Y8J@umKiP)x3F>L-+nLj2j5IDF;W^ZiD*wJ?*NheAlhZ{Lb!Ay zPY=UDIQaoWi8er&%(u{upsZqGIQpJ$iDEES%#lanl`MG0Kt&yyR=E%CVcrXux!0Rj zYB_&iY;g9u0!02J);grJiMEmyvwwJfirEaVR5CvPh@_${MC`wH6mI`l+hrM=EQdu| zb6v13tH1H$M3Z~YTVeiFm1{n7G43k`wmY)zfhuainsR|})I?spAr4c4-E&{C_n^Vg zwE+YE8@&8ET)2C@vS-7G9S07?+keJhoMof!Jc*X{pB$}jXuPwn!^xqjhzasYxGsL- zEUaA!dQrkc*_5&`;fY z_)ggry#;I^t4Z=+xIOSJ)PVX51m@+ZE)@v9n;nT(I55?;JA4=%&^1rbgb|w`Ex<%x zBc~RW^vO)i#Cidy%e3<}G6M$=0!Ncu4?wYjL}gielvn!__}%{rcC~K>1z(s*e%t|e zBB%aY#oRMenFWaPr1EP((gSb|B2Rsm^rd;XiPkST5EBfP7CZ~aTm?QuCcW^^Yfex& z_^AgK0S0vKXQ62ZDR?c7k*<)2cnT#HKImDmP~)5zu(~+Vp${>aTm9~cW0~N}@M949 z7<3y(sWku@w-X9Orf>u8E~|=G>4td&Ph5(!D(6@CP-!=Ax>DO?J_&a9RN@;IO#~La zT1WWUk)&Gb_W$=&VM&T)3S!xG1q4$@u=gE$km+>=Hqw5uXLjx*0pS>%9}BTY04tqJ z9)Kj@${Yh5$*g9-st6_!xSf%KO09C~e<)^u-4~@-IpCG2VsZu>3rRMlk3vfSg6Mq$ z0Glq@=m*B40C2br{FKd`T>6I=I93@dCe=kr40~p)Byc!95-?}uX5W^giWP6NkrL6mbGsoGY+JLy#s&eQ zcrlI{m@}DdM_}f_wjQ9mRDG+$iHpegDjKX9EE}9%0fpn-m9*ISzz!xq?ZPW0W@9RF zNK9-zJn(=BpW)1+_$!^qmGsznGYM09z=f)DacK~G0TH4qo*V*Xv6jfMU#lb|r^YrV zP<;GgO`?Zbhugv_Q~;K|L+f)*-8mp~86Vf2lTYX51&Twn{E|QB3yak^7aWMSh+>D( z#h(%(&7G@LLfQf9vKF*hegr#+KT0LT%3lI**@6w(HFQ@958;;~G=m{aD&jX%Zh=yv zHly_7O(30QQi~tYEVb!@e7Xf1E#(Q0MBmS0+YWWxg{m75=Hf z>4IaQ`YFd1^MG);p|<>F3Tzy*2aYi2frLE=8_5h1teO^Q`)IG};ifrKMNhg+1pOAdO0=+rB}6_oqsV7tIc zp$A?yekYc{fI+(os1$u*4;+^Y;gNwZ;18(utJXGbHb)(PByEzt-C!q+A+r79Gh&=J z$q!a&XMe3AgLH$iYO?`1et@v@AyElh^oZDewsHMm^cG&xEz`n89iwMsqJ(({tSfP? zOQ1bh%{4YMm1-@UdHgI0q^*GOxiZvqXbZbdj7SIZH50EDAT?V3svrJS=Kx_O=de@S zr)^JA5RyrG5_TAw|7hoWh6ZvBxz(-=VyVv{Y72fzpR1VwA%->2WM7#lqn?w?H1KXk#A^cG`anF!L35E(LB*+WL*kSZ zGaxwdwEIL_kPb_Jw8qH_%270_byg~qf0x$^m9By8LIgzU?3T^IxFL_0fR2N}QxAv~ zO*(PapHJ*+gvTJrUfJ(8IZ4xtuA<5iMf5)Y%m(q$Jk&FNyh2uOVO0iUwV?%8u; zi-7=r_I>nm>IN|+0fE;LkSn?0CsL*NJCRn4excxC{^gV{$EEKCu;&1S8FI`ASh;Sw z3#1L;G6@*V&DSFil*(R0)Gj3RR31uUL=850b;f>N3;PP>WrhS3$vUM4yhVMZ8rkl-c(;PBdI{xfuApy9u1CTwt*9*>3Avf8b!ifNPJ}k zAfIKg^y7AzE70xyprf$%FZvw7e!0e@bXFZA1i)zofL%xXDK}VQy~G!NC6Bc^fF)H2 zW+ynO*o%=~nVE9)hZOrpN<9eoRW61K<7*sEJm3sl<2JB!w<=p#f}fk!QF@-Hjm^Q- zV-3S#ECb3Q7dyC7I)m(`rP+*;)o?kNcLf$Te|mOZTmsWk9{i(uq1A-(fOfcLH+^+% z47hqkp$@{J=7CH~0`_#(|JVtBnG@s%`4$3y>3EAk=!&T1lJ2RCMk`IFUIiwz;t5U7@1vR~?(2-RDJaJo)A=2Cbd9lwYq&2LX!TkXer%GB@ArnO%a; zP{4uKw=Xh3kgG}9HqvO(CmHu+#+2$&pvP z>h6P0AMM3&Y_z?s3meZ*KKWx;&Y@jkGP3Nfr)0U)99*u(@LXy7E<6(!kkvYN{ai8^ z*S#Z@8bZTKH0F9U|EuOAGw=nlo9=$Mdi>1OKlSwcRX+RM;h639*$?I^&o1BR5Yf{v8>3@qG3({CMDk!u z3-ODpCB2QTD83pwi{H9O>O>pq;Na(R!|gJdJDr_`+YIrgHK-c<@CCAEnLABgwV?-0)GYlyUfv9 zby0zn#~?>aHCNur8}Kon+R~F+v`Sz(H{WNO`9;_M+CP!iLE!JK)buZ%Kc?3(4WOEj z5*xH-`oFUPvUlVPu9H){$OUI|h-24mcBEaAh75$qCb@_Fn|)*<6_pXU+&xlZj5OcQ z63AEOZMeLez6Eo_y2>9TX|yiyfR2wg!Ieg9E5GxdvDHe8&mA&-;2|Zkb}L%z*CI{P z{<7?{=E(G^N1q>x00>mObz*2^v%jIyeAV!07j-yRL`=-@|) z71z}DQkYRwldiP*zK2zLfbLwk0rz=Z%Zk>N*Ebiqc1c#NnDfJ`N2f@k{GI`&chb0^rCx*m@jd~onVPKR@BT?SI^o`I0|lt>#sgu|9rXj> zzT#&SXIZ4p8t)}NNN)PJaByDkzE0m`daxMe+3Tu)8&p2-Zr-7%NsNsznj0@OEby^d zqA`UMPp>+8o1Ew_&*Ar*s$5ik!K3#a{ zuyj>c7%w=2;F+kBfJxeg=f((K<~XUD6*&W_mLD$1_&Yv>$2gO2+!Z%o)8c+G2mOb8}e z%mhwKm%HmC^dbh~uNK0#ZnA^uaR4iVy(xVf45#hr^l-~bZa1#Nm9GVS)fcwagl`S` z+bT}D)Ni(NtOY|Y(C6lote@BX4P_#v=&pzz%@qydtg><}rDu3feevF2mkmGd!%LRh z1bhX_O;P~tyx;eizMAaWDN#p7@*#b=*8^5N8a{-7X~)6&oIlxhYgI)vj&0Pv4&m#0 zY=X4GUMH?q4Fx9{G zM#Ve|=~(2&YPCU?T79Q)9)~s1zB?g}FhZW!PX7-5q$gbA9?%S?AK)Op2=@Jno|V>p z=Ln}ICRK>at6oTS@PUpFK7ZpP0v?fm>X5#pXdW8!_lLxy**=T)^w#+~Z&`vu~I;(%ivCQ_pYilRMQDB7Eb! zbJW-W+%=qLBe_uKpQ3Bmm^Lc0`BJXhGH0x<^<;&>QGa#= z+J(nKX?3S5-K3ic{=aN`F+<9gZgFG1wqS;bb?U3#gE7qooARkb+pAAwvqP#TE4Z}@ zh(q+(uNw2$i9%zn%4ap5&~j)|iPM6fWaag$U%7%IAj`I*V0*M(H5ZZQd4rBWd# zDcq}EJsN2A4*{)_jeWH^Y0yd+{vg79MuU5egTf!OVQL|RvE{sz6d^gW9RZ%14magF zngy(8AL{(rx@w5ek=go2Tl#mwrj<(Ar8a9d-HIOL**psJ9X*P)OAV-&WK`f4{fP;psE&cCUrR^-G2h;_Rl(X_B-! z=IXtct0}SB#yKDDdx9plavh!GjQ2@gwNRcn+z5}RPV9c5m;f#;ZdD%SsSYKrn^{B% zMp5b|8F=l%|9H0SlAQpTg9P1k?_*Ny#ySYQ3N)N87Iv=D+4zHupH(&N-`EJH_L(PB@0Y_!s zxlp*y_=fDgV1D0a|2ap=-K+#s_i{;%w-z?;p-&@?UrJCzDZ^Czhk#Bn=LYWKJEZJmy~GL(NcrGF1O{Z@siMSd4fCgLd0o!J@vmSCf(nS0qox zT*X+^B8GiFWqlU5lE;u;4OZMJOa{VwSt>cKg)U-TGdgRMMQR#9pMmmJK;^||ce-m1 z7riKyqZoPRE>vNDtBM-*572W_wPe5pt6 z!_{E^+Q58e7`p#zi)dpv_geYyRx`Nepwr~fiil0wl(Oe?@t5UR!8tJi$U(>qO3@Bl z>?QD7eK|d^b>E6I(O{FVm6N0@QLNFCR0N~$7hRVKeLuH%f1Q)vzvLdkJP2d-aiQ|n z*)dEXbZgeWUzK4_6?RRR5AKn0-cet@rYP?TAD$C`TJGeQ`RVO!&$Ycr#a|vpeKe|Y zy8l+hmwt(va-=BZW9;ZXgMSRB$oH`yTqvY{o_F`EG&4aGaVF^6Ij|*{`q>R;)lYbh z)Htgzon3aXB8$^(c@RhexLobrCGPA$CBDb^J}BI8YU??EvU=82FSC)w@Yi?Y<+TDf zNx9r)`Sn{&fXqGgF87+;&{&tDSa>q(`xU8$ga5cFAAkr}noy90()k6&rQmwFqIlQQ zzq#UseY+7FfJ)YzmyhK_RDPNmo=T&@h=GR5Ov{I;H+a3e3nQ9wtdS%27g_wn=M;}V z*IV99&*xn-lDA-90r} zJUxt#BDF)uV3)d}M;A0bRMD5Nw#+`{?G~`IX6y(^Lt)>S&faB=JkUWO`HRkJ|9C9Z zG!bI8lwJ^k*6r4b@|`*QQo0-ej$0;mhmt#+!IB6Un`jbe{2 z?W-LD`z5F>Eb>v6wq*2$Xq0vUP2qW{ee#L5yA4Br|0~;_LXYU=;h4`IgvYk)Pm&k4 z?2B_ka_o6~^pRN=z+B=sP)sc9{XSHUun*3d;YvQPq@)v#GThfoVa;=D5u=;c$3d>wV&JBlr0A1g~#GmoyhKmaI zFL^W?Dd)$zB4zK86@1+A@lp#+;jUW2I|BF_;$q+c-rG5vE;QVO?dg;UP>yN=Ql2#w zdmI(KXLeE&w469+(uvYhOfUe0C-h+;Ebm`k5I zHU&^zhKQvu2evI zMjV&Qf~k2O?Yt(Zfny5ElK=5VBVb9wTs#*iDppi3V=oaHnL8g>>U0l*GzzE6;aRnV znj=jyp&J23yyuz*T_gH+b2j*7d&N5;VD_qNtLlU3M2mfR8SJ&-nPqJHdRKs_wqsJ- zlQkKc>c-X%oJoE$Q<0nSSO$LL#9dV2#mU_@Li?VL+@Hj#wkprNi@mr%pT0MTscYr= zxZhF??ZG>vRHJJ;xr;H36D*+aPX5=S3x>l}!j3gOX487~!{c-VGIU=iti~hGt<(Jx z1dmYj;nrD>#aY}Xscmj-$qNO-oWY1e)%?$@^qoyL^1NzUjroPP@!U?c=c8}fOnG~T z^~FtZwS{_i8tpA<6Ot<_N6cW?Lh=N~y&Pf3zU<$W5X0F!M{Si$yT7%!(g@mMcG(P6 zgsFMYJ1hTASc9hL#-&h4nAGvWlHRd(=nXn}VgRbTL9oh@2FC1-KSek9y0j!f}*uBtPs=<}47b!q^=(V;c<1eV(*OXlr|mwo63PrLjV$1wT2T#XZin;AR} z#6laW;Bs^C*kj7Z)Du)Rw_X{-2qEy46swQS{JzA^i!I8HH;8MktF*s}z%wRCUxb?m zo9-5rYbcjr6jbeh?}De>+(%fbuX~!jtS|5QDnfIk`^Hv_{MKq|*4q(Bk zte9xZ=!B~Ujh~q#{G#jphEJe5JdLl#$vpi4=m7y#59}i@*KSm0PskU1q%gA0w zd*|QrdEDPp?}X2*v4w|%HfO{$ii2ImyBkFnSM%}xVARPHBqg9b)osR(KGyFNgKEX0 zq`fA;&KNOx0bzbA><2YP7fo2R4qFl^5BdSF$^dYKXVUJg+u8(o_={&hx(t!bjY>=3 zz&zzccThqS%XHwxc7RV}ad+uLHuQbJ#59*W-PZ$+qd?ob(LikbhKS(NK&8gH$&RcN z=sZ{)%y1yyItXCX!Q}W3rJr5)=Q~DC67FBcoB9*(;vb{@Q$`Ucy)}8o-QH=Z3ZP}o zUbi+gCY)3(iroRQ4o2E7PH}tGz7o)jtUy4PiM3j6X&n4nH}d%?La55IU7H`d)81WP z0ikaWFJ62pUBvUv)eomTrz`&2?*3Z=tLq(LVB4)!YdsH70?T(yc0%vDs*j<12BJ8M z$n#RN7Z3SY4-{Zu%Udc{ewj2cG~8~&F!F)!Y?9>u8M|-+sD*$6D*y9;6WA-}%(Eq6 zaB|wH*d;n&t~!f1yS(LT82t5d^o@;Xy8i+n>M@X0XQ7w;&P69ha-(h*y&C$ppMgUCnJ6o)(&=~Vt=Y%%5chh zr&%0K(REsvTwE?5sGTlG4!(8H(VL+NkhCL=ZtU5c)49dok`+}()> z-mXQkU9nJH<&&__jSU%>07C{dTdQvcD4_dYdzgj13J26tvc}Q#H$XqwU0AT z-z@lyjjh59DoZrhYYn#KrC_!FPopceWx+-K z2P&4>p73^FwLAXadT->W{fFPl8w&Jhl31uY+zb#MF<=+L3B7W~>o57C6Cm7tLK1<@ zoyxO}(Cd}K%uZ6s_AL16RxFpwx2aRA?011w3xTwlXxuKS&HWlutjiC3v*FwQU!tAm z>OSGW=ctE<@avp@nvI8Vy}ywv3rO}@R^8T}6NO@~pA1&=p56!{OYgqd09X>Cp-Xtx z2qVlj@1N##ixMM5PHfG@MEfr>(c6>fU(+|f3X8qz)ubdv`@<#wgn{uZIx`!*O=!dq?yUkS$nQ~;d+MwwcL*}cvL_tbRV_r<+I=8``9gkot zCeDFZ(GMTogpFm&s7=4i{Om9F3orL8r}uXE zieaO+)?kQ{bNKW8Vi7N8R56cf_qCA0DJ8tv5m_bMxCuO}O^GFkK#bhLO%h0q5V(2+ftaxzJCV=_g1L|L z)yDJY$;b2bt!2cTu{&vP*?(REizlTIm-b5Nl&$E9my$B`V>H2)o6ptb>qW&I2ky{^ z+lmDfIrE#D9Jp4L(OwCRb#N8FI3(Yt;m)4RbiJZ{y$cGj)onz`nBwsD?S*sgCm5gI ziD#-`EW3SPt-}4loHFSoDGj)RvIRy}$BfFUR}1{wKPZ2z(&F%)&*y=@9)Im<`$akE z#}fj>(pjdahu!Yqwxi9WpdQStxQBaMp}Fw#e1jLb6V)%#&gfO*9i4DDa@MQh3IJQ4 z_1-CznGm|#EM(?c^Ji~F@fKRN zi2C#|crh*m=DsbUx@XYfNPEISV|DFEyRO=nF~i!u2TvsPQR5}cRb#NZ-#YJ;W*7Bt zGcyr<>K@ybmVse+V|(7{S1`vHQ#KV>Mb`*>#6}D9T);>%KK`AD0pX4J(T6IFeB{pN zestZ$?XsC(#F?C1Rb$P(!21rt!^9fpDdu}QfhD3@25_qN5SFIRmB!M&un(iPBl z9hQ%34$Z@pPoFWz-OXNsJFPsH>E&@epI@3E`~qJEkue<@ss^3yd})PxI&Cnjqm_2V z;XRyH8z2O|s#U*hBni5y0fj#@1>=>JXI>qtuV?Udm_7bemh`InV~KLb7#*0im@>8b z{#eC64}YH=37ai|MJJBNthvjdN`h>noeA?EjMOOt6=spvjjAPHFLE)H`V^uCzbMo3 z)8f<&p#@;(I!FeWDf5FSDzSC@Owe4Jr8mMly#dto*4jb##_#1vEDf6+uwU>7?R&jE zAS*8D7EoYZ?b?70rr;{beD9-!Jw)*`J#pUGUKJP{Zbz71vu6a;GZ|x8X#`^9 zrmdNtA{a;|j)r52!>1_x33%9v`%LU|Y}_1tM(i@f9Meae<6+{0!$%)L-}hfjdjI)o zrpX)LSlmyG$%qOUPTOl(d&D=5_3Lgqn3rH?gt*A`F1U)5i4B>MZ~w1w?fZ9^-t^HY zzSh$&(HXdM;x_Py#KCbef=-+TpJTrf(pAZ?0muigbE93jBAD|fn7a^!m86bJdOJGi z7Kob06aY+X^0BfLEIcq|T`0#?cexAaJT25H;I<$m?^30umdF1(>>(%4yBWk;dYJBz zVmL8dN}`SRwGFQ_Cv+b?%u>2iq=BXmyC_q@LQbvkGnr_R-D(?{LcYo#kv?qF{8Eln zdUv7>)PQGVsQqvFKMPgD^K6cqC{B(U1q#Lg5Lp=)9dQLeO52T z+m*X;iuXzE62Kj#Bbt|VQ{bkN{ug_G%@MhpUJ$5WljXRc{=}i%0$e|J*eeeLvHLPBiKhT+c31my}rFc0}+7++sH5y}RS@dqpQ2A@!Z+h}c9TSh*7 zuT#+n3+k&9^knUOK+)#uFa@6{Hs3-LTaY*_#>OJaTn#S47L2CQ9Xo)z@4W4*My+9T zKH>k`y3a51mN3cOs0PtqT2qSX^DBASAyDXx}63Wyl zlX1IJcQBQrggi`ao_uz&o`}kFhC)5}0&uc}!CcfGZs_}<5V+jPCfRu3bQn-a6xTG{ z!7t&HK&&q1-L)Q2zkFbKN|jTe*1~=_)E$t}d8{zcQ?t|Pl3O(u!Dj^K7iHM?GqMgo z$K+=P4PGm`g-uMHMO|IAY_$HsEos@r6QU1pwT*8j?Gt))Krz!D%wX()iGPc{O*vfn zLh%{A-WhpQ!Dy$gOH`pZxcF2b?TC{N5Z?At?pNL|uJXhe_79KV8lGFBxh~Ai%dgAlE_pmtmhW8}e&=zNf3)}Z)~6~G z-P-g|3v|;n11X6MzdOsnzWzC!Jor$j*XK(TOVdqcedW>CYMe!((UT*I=PBxBIc6m? z?#(Mk?`N-~mM17N^l+C4gH7}uZoSQPNSYybX=>=RMcs~B$4WC9C)o4@;YE7y-uIm% zJfSzQEoa@EP=h&QbFJBTmNhsXH|=6BD6wVU75lu9Xk{4tuH}|SCBP>wY*-xkbm)4M z*t-G4ko6B&XZ?M@x;3oi#yxc1=6sNQDf%37S8)?PpVt{2rtVLwoIJ=V$VX#k5@M46 z*KJ!Ow?6kYv^FYc>`Lp*gh_GwAi0E#ni0HW+Pu#?_yjoufl%ub|6hRmdj9LVn(FJO zMg53P)5GY?>Yrtal z=Iq6F28!iL3%Vz=d@|o~a(JagZ9!p6RW-H*w_#$~2v#LQA-f)FGySyv;@^j9m-qr785fBpmxD-Nz}$z{$cX z9ut&2do7Qyula>x|h8kKQyZ}yhPD{!jl9NKxUK4I&q zFGb=V@QA_gv$W|qL&{H^B|1NQ4=Z$%<;jm$^o#oYIAHhV?m{xKdvzly`KI^jX^$;6 zYSSo0Y8*yeo?u~{_3z->3Gb9%qb5CG3oem~yh-@5Ie6l7Vj39b{P}}u(OL1}wS0f& zgr6n4GfO0KZN{_z6Z0H?1xCA{_J3atEOiX2iS@R^kMnc>yAT+zes(T>R@bp87ys~% z4zh@*Z}bQ2t`2>13RG?l`JZ$kJO7^j?~ee zL;c6Z`G+ z?2niPgFV^$2|Q6hU#z!_;X!cM35D*AYS_&i=?x<`Cu1s=+N!%>uL!owFP>cuEl%9| ztxIc&c+(V`-($Bi_O5!;&`0M_!G{JF`!eO|LW}je29Z$Z5$v88+nGeyz}2wO9sQS` zF2~#Lz2h8F4?0S0*B2;Xd>T1`T;C7&V|sT(ztV~*LAL)TVovdl4kS9^EbbS``i>SK zl^$I&%xb&TkTqEUU%pJB^k_q00ArQ&6CS%9Tj!S*J~s`&>XhVu?tu{0zxVy2h*QY0 z^=~v>iDOdu71Frn996JKF0Di*!&L3UwKa6`U{pOlGBb#1n;fdXo|6vw9E}@0kQGa~ zL~i<)d>uFGXC~gkqY@ zl|N-4OkqP^H3@&m+N^QPv(bZ(zutJP+%kgtNRO0ShyGZ6Rdd&!4fPP0BF1H+2Tpj? z&Ek*VW>~h=O;zx{hxliQ(8K&pN?OS4ABAa>WPjL(*024zgbI$_HHlE@8u%j9xR-7_ zRItHbkwN*8q!)5Xl5n%?mrULR=x7S}fF|$FWwN+S9;cp3+o@u={_VgA@Kd8GN!>A4 z3lH`i7EE3(=g&Iek)m&zkjEXGVCt}Q1_D0;{4|Cd7x zZrIUenvBQr%6=%kd;MLQ)pPy^UF&Q1&ci9Rr6*&Z=Itcc-m%-O{xjSo8(tl`3xD0= z=|Z9B-`v_|yFOa6S@Zth-e5N$>AZ_^KWDR7`?pd*-}i)9qU=;PNHv1O1X`|}^0MW^ z44Zw0&UQ`7({EDR`V(MFH?{5_mvQ#R3LzG1M=SRL4b1h%1(S&RRE-J6+Dw*YIq_Dq z2}N}sm=9|upIg^S$&+-AqiQA!A3zKJgd!E!jba*Ezdqg!3$g8fV|%tD!ZyL^X^?^^2Ihq zPH0fA#=9L_6#OTM|A3&Y-S`lH^>9$IcT2Gp3|0q|mKfS@u z=pV+rtwt{>vlz$QHnJ$S_?T{7@s`Wc5lT+U_UeuKfe(vP_Kzu-_b1GSQu_;v<@~GX zT-00{;1s&vFY}L%erq`C!n1Xp|6_O^WNLm+A{%V5%vAWgqR#AE!g^2RbIO2BV>FA% z)gQkiPNup@ipj;&c1#Xa?9RDtLS46=;Mpcb?|kq9`U9~c#`q)tGlk7<;C3pK6V)fd zXHGzGmE&M8EiZ`S^^TUYx0KG1IA=C2m=H?HMXotNY<2$Ytr}$`t#4d8@s%xm(e5d%&vys^o-sc<;}S_<-^W-WtX#rCr-Yg z>NHU>CpPQ6xmkhW1=rj?QR$|oF-sn1rR?>7%l^~tTzQ2BP{)pgf15cadBc^Ifnhw+ zFyrMn{$=OP8@{`jYc}1L*#7iZPHD+RuPb{l<(oW~d$4`+ZV^ZQbMv20n60vdN-FuyS!6%I(=O0SRW1Yr)dC_l?1JV-l_2+f= z?fm6-+I_mCM0PzJ~Mh`9|Z#zk3cRK3G__Be{6@U!7e(rZaCpeJ|Iv zo^d|&tVKY%yg9pn{oc8M+O$($B9qv3CURz-{k5cAtx)(_cmp?3^P$<#{`9Ke*}nDc zH<#%v({%6FEn9x))v@_Z`z7zDi^Q?~`}Ovu&5ut`)-thfOC|x+h~@(Cu%Az-J@-Eo z;ZPZ?bV_;+>%WET`|Gbnc3#?L@H$xC|JmdN@|IhI{8r1(VV3Bh@Zs$pq1!UC?n|OF zfV!J*ey_UxrEVRYUD`SK&Yd4C4GOI6 zx$@^55C65*YJ$iGrLiX|VX`8Y& p9v**w?$PoprT~`_08|eAXY8M_ww#&q_!`h@SDvnZF6*2UngFw8suutN literal 0 HcmV?d00001 diff --git a/assets/Squidex.Assets.Mongo/AssetsServiceExtensions.cs b/assets/Squidex.Assets.Mongo/AssetsServiceExtensions.cs index c4b035b..7ab52b8 100644 --- a/assets/Squidex.Assets.Mongo/AssetsServiceExtensions.cs +++ b/assets/Squidex.Assets.Mongo/AssetsServiceExtensions.cs @@ -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; diff --git a/assets/Squidex.Assets.Mongo/MongoAssetEntity.cs b/assets/Squidex.Assets.Mongo/MongoAssetKeyValueEntity.cs similarity index 92% rename from assets/Squidex.Assets.Mongo/MongoAssetEntity.cs rename to assets/Squidex.Assets.Mongo/MongoAssetKeyValueEntity.cs index 42715af..7266974 100644 --- a/assets/Squidex.Assets.Mongo/MongoAssetEntity.cs +++ b/assets/Squidex.Assets.Mongo/MongoAssetKeyValueEntity.cs @@ -10,7 +10,7 @@ namespace Squidex.Assets; -internal sealed class MongoAssetEntity +internal sealed class MongoAssetKeyValueEntity { [BsonId] public string Key { get; set; } diff --git a/assets/Squidex.Assets.Mongo/MongoAssetKeyValueStore.cs b/assets/Squidex.Assets.Mongo/MongoAssetKeyValueStore.cs index 5392397..6b377e7 100644 --- a/assets/Squidex.Assets.Mongo/MongoAssetKeyValueStore.cs +++ b/assets/Squidex.Assets.Mongo/MongoAssetKeyValueStore.cs @@ -18,13 +18,13 @@ public sealed class MongoAssetKeyValueStore : IAssetKeyValueStore, IInitia { IsUpsert = true }; - private readonly IMongoCollection> collection; + private readonly IMongoCollection> collection; public MongoAssetKeyValueStore(IMongoDatabase database) { var collectionName = $"AssetKeyValueStore_{typeof(T).Name}"; - collection = database.GetCollection>(collectionName); + collection = database.GetCollection>(collectionName); } public Task InitializeAsync( @@ -37,23 +37,31 @@ public Task InitializeAsync( }); return collection.Indexes.CreateOneAsync( - new CreateIndexModel>( - Builders>.IndexKeys.Ascending(x => x.Expires)), + new CreateIndexModel>( + Builders>.IndexKeys.Ascending(x => x.Expires)), cancellationToken: ct); } public Task DeleteAsync(string key, CancellationToken ct = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + return collection.DeleteOneAsync(x => x.Key == key, ct); } - public async Task GetAsync(string key, + public async Task GetAsync(string key, CancellationToken ct = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + var entity = await collection.Find(x => x.Key == key).FirstOrDefaultAsync(ct); + if (entity == null) + { + return default; + } - return entity != null ? entity.Value! : default!; + return entity.Value; } public async IAsyncEnumerable<(string Key, T Value)> GetExpiredEntriesAsync(DateTimeOffset now, @@ -75,10 +83,12 @@ public async Task GetAsync(string key, public Task SetAsync(string key, T value, DateTimeOffset expires, CancellationToken ct = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(key); + var utcExpires = expires.UtcDateTime; return collection.UpdateOneAsync(x => x.Key == key, - Builders>.Update + Builders>.Update .Set(x => x.Expires, utcExpires) .Set(x => x.Value, value), upsert, ct); diff --git a/assets/Squidex.Assets.ResizeService/logo-squared.png b/assets/Squidex.Assets.ResizeService/logo-squared.png new file mode 100644 index 0000000000000000000000000000000000000000..3cbc19038a372bfa3b1094aedc915af1fe54b6ce GIT binary patch literal 19430 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 literal 0 HcmV?d00001 diff --git a/assets/Squidex.Assets.Tests/AmazonS3AssetStoreFixture.cs b/assets/Squidex.Assets.Tests/AmazonS3AssetStoreFixture.cs index 20fbfaf..8a6ab4c 100644 --- a/assets/Squidex.Assets.Tests/AmazonS3AssetStoreFixture.cs +++ b/assets/Squidex.Assets.Tests/AmazonS3AssetStoreFixture.cs @@ -13,29 +13,29 @@ namespace Squidex.Assets; public sealed class AmazonS3AssetStoreFixture : IAsyncLifetime { - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public AmazonS3AssetStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - } + public AmazonS3AssetStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { // From: https://console.aws.amazon.com/iam/home?region=eu-central-1#/users/s3?section=security_credentials - services = + Services = new ServiceCollection() .AddAmazonS3AssetStore(TestHelpers.Configuration) .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + } } diff --git a/assets/Squidex.Assets.Tests/AzureBlobAssetStoreFixture.cs b/assets/Squidex.Assets.Tests/AzureBlobAssetStoreFixture.cs index a65ebeb..6b75df0 100644 --- a/assets/Squidex.Assets.Tests/AzureBlobAssetStoreFixture.cs +++ b/assets/Squidex.Assets.Tests/AzureBlobAssetStoreFixture.cs @@ -13,28 +13,28 @@ namespace Squidex.Assets; public sealed class AzureBlobAssetStoreFixture : IAsyncLifetime { - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public AzureBlobAssetStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - } + public AzureBlobAssetStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { - services = + Services = new ServiceCollection() .AddAzureBlobAssetStore(TestHelpers.Configuration) .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + } } diff --git a/assets/Squidex.Assets.Tests/CloudflareR2Fixture.cs b/assets/Squidex.Assets.Tests/CloudflareR2Fixture.cs index fc33ecd..f7e5caf 100644 --- a/assets/Squidex.Assets.Tests/CloudflareR2Fixture.cs +++ b/assets/Squidex.Assets.Tests/CloudflareR2Fixture.cs @@ -25,13 +25,13 @@ public CloudflareR2Fixture() Store = services.GetRequiredService(); } - public async Task DisposeAsync() + public async Task InitializeAsync() { - await Store.ReleaseAsync(default); + await Store.InitializeAsync(default); } - public async Task InitializeAsync() + public async Task DisposeAsync() { - await Store.InitializeAsync(default); + await Store.ReleaseAsync(default); } } diff --git a/assets/Squidex.Assets.Tests/FTPAssetStoreFixture.cs b/assets/Squidex.Assets.Tests/FTPAssetStoreFixture.cs index 54a2cb2..8f87594 100644 --- a/assets/Squidex.Assets.Tests/FTPAssetStoreFixture.cs +++ b/assets/Squidex.Assets.Tests/FTPAssetStoreFixture.cs @@ -14,29 +14,29 @@ namespace Squidex.Assets; public sealed class FTPAssetStoreFixture : IAsyncLifetime { - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public FTPAssetStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - } + public FTPAssetStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { - services = + Services = new ServiceCollection() .AddSingleton(A.Fake>()) .AddFTPAssetStore(TestHelpers.Configuration) .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + } } diff --git a/assets/Squidex.Assets.Tests/FolderAssetStoreFixture.cs b/assets/Squidex.Assets.Tests/FolderAssetStoreFixture.cs index e53b4b9..9cdbe11 100644 --- a/assets/Squidex.Assets.Tests/FolderAssetStoreFixture.cs +++ b/assets/Squidex.Assets.Tests/FolderAssetStoreFixture.cs @@ -12,28 +12,15 @@ namespace Squidex.Assets; public sealed class FolderAssetStoreFixture : IAsyncLifetime { - private IServiceProvider services; + public IServiceProvider Services { get; private set; } public string TestFolder { get; } = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - public FolderAssetStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - if (Directory.Exists(TestFolder)) - { - Directory.Delete(TestFolder, true); - } - } + public FolderAssetStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { - services = + Services = new ServiceCollection() .AddFolderAssetStore(TestHelpers.Configuration, config => { @@ -42,9 +29,22 @@ public async Task InitializeAsync() .AddLogging() .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + if (Directory.Exists(TestFolder)) + { + Directory.Delete(TestFolder, true); + } + } } diff --git a/assets/Squidex.Assets.Tests/GoogleCloudAssetStoreFixture.cs b/assets/Squidex.Assets.Tests/GoogleCloudAssetStoreFixture.cs index f86a65f..f713d9f 100644 --- a/assets/Squidex.Assets.Tests/GoogleCloudAssetStoreFixture.cs +++ b/assets/Squidex.Assets.Tests/GoogleCloudAssetStoreFixture.cs @@ -13,28 +13,28 @@ namespace Squidex.Assets; public sealed class GoogleCloudAssetStoreFixture : IAsyncLifetime { - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public GoogleCloudAssetStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - } + public GoogleCloudAssetStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { - services = + Services = new ServiceCollection() .AddGoogleCloudAssetStore(TestHelpers.Configuration) .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + } } diff --git a/assets/Squidex.Assets.Tests/KeyValueStore/KeyValueStoreTests.cs b/assets/Squidex.Assets.Tests/KeyValueStore/KeyValueStoreTests.cs new file mode 100644 index 0000000..c448433 --- /dev/null +++ b/assets/Squidex.Assets.Tests/KeyValueStore/KeyValueStoreTests.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Assets.KeyValueStore; + +public abstract class KeyValueStoreTests +{ + protected abstract Task> CreateSutAsync(); + + [Fact] + public async Task Should_insert_and_fetch_value() + { + var sut = await CreateSutAsync(); + + var key = Guid.NewGuid().ToString(); + await sut.SetAsync(key, new TestValue { Value = key }, DateTimeOffset.Now.AddHours(1)); + + var queried = await sut.GetAsync(key); + + Assert.Equal(key, queried?.Value); + } + + [Fact] + public async Task Should_update_and_fetch_value() + { + var sut = await CreateSutAsync(); + + var key = Guid.NewGuid().ToString(); + var value0 = $"{key}_v0"; + var value1 = $"{key}_v1"; + + await sut.SetAsync(key, new TestValue { Value = value0 }, DateTimeOffset.UtcNow.AddHours(1)); + await sut.SetAsync(key, new TestValue { Value = value1 }, DateTimeOffset.UtcNow.AddHours(1)); + + var queried = await sut.GetAsync(key); + + Assert.Equal(value1, queried?.Value); + } + + [Fact] + public async Task Should_delete_entity() + { + var sut = await CreateSutAsync(); + + var key = Guid.NewGuid().ToString(); + await sut.SetAsync(key, new TestValue { Value = key }, DateTimeOffset.UtcNow.AddHours(1)); + await sut.DeleteAsync(key); + + var queried = await sut.GetAsync(key); + + Assert.Null(queried); + } + + [Fact] + public async Task Should_query_expired_items() + { + var sut = await CreateSutAsync(); + + var key1 = Guid.NewGuid().ToString(); + var key2 = Guid.NewGuid().ToString(); + var key3 = Guid.NewGuid().ToString(); + + await sut.SetAsync(key1, new TestValue { Value = key1 }, DateTimeOffset.UtcNow.AddHours(1)); + await sut.SetAsync(key2, new TestValue { Value = key2 }, DateTimeOffset.UtcNow.AddHours(-3)); + await sut.SetAsync(key3, new TestValue { Value = key3 }, DateTimeOffset.UtcNow.AddHours(1)); + + var query = sut.GetExpiredEntriesAsync(DateTimeOffset.UtcNow); + var expired = new List(); + + await foreach (var (key, _) in query) + { + expired.Add(key); + } + + Assert.Contains(key2, expired); + Assert.DoesNotContain(key1, expired); + Assert.DoesNotContain(key3, expired); + } +} diff --git a/assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreFixture.cs b/assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreFixture.cs new file mode 100644 index 0000000..c78877c --- /dev/null +++ b/assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreFixture.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Diagnostics; +using MongoDB.Driver; +using Squidex.Assets.Mongo; +using Squidex.Hosting; +using Testcontainers.MongoDb; +using Xunit; + +namespace Squidex.Assets.KeyValueStore; + +public class MongoKeyValueStoreFixture : IAsyncLifetime +{ + private readonly MongoDbContainer mongoDb = + new MongoDbBuilder() + .WithReuse(Debugger.IsAttached) + .WithLabel("reuse-id", "asset-kvp-mongo") + .Build(); + + public IServiceProvider Services { get; private set; } + + public MongoAssetKeyValueStore Store => Services.GetRequiredService>(); + + public async Task InitializeAsync() + { + await mongoDb.StartAsync(); + + Services = + new ServiceCollection() + .AddSingleton(_ => new MongoClient(mongoDb.GetConnectionString())) + .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) + .AddMongoAssetKeyValueStore() + .BuildServiceProvider(); + + foreach (var service in Services.GetRequiredService>()) + { + await service.InitializeAsync(default); + } + } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mongoDb.StopAsync(); + } +} diff --git a/assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreTests.cs b/assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreTests.cs new file mode 100644 index 0000000..2e9ad20 --- /dev/null +++ b/assets/Squidex.Assets.Tests/KeyValueStore/MongoKeyValueStoreTests.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Assets.KeyValueStore; + +public class MongoKeyValueStoreTests(MongoKeyValueStoreFixture fixture) + : KeyValueStoreTests, IClassFixture +{ + protected override Task> CreateSutAsync() + { + return Task.FromResult>(fixture.Store); + } +} diff --git a/assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreFixture.cs b/assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreFixture.cs new file mode 100644 index 0000000..5dc3a85 --- /dev/null +++ b/assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreFixture.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Squidex.Assets.EntityFramework; +using Squidex.Hosting; +using Testcontainers.PostgreSql; +using Xunit; + +namespace Squidex.Assets.KeyValueStore; + +public class PostgresKeyValueStoreFixture : IAsyncLifetime +{ + private readonly PostgreSqlContainer postgresSql = + new PostgreSqlBuilder() + .WithReuse(true) + .WithLabel("reuse-id", "assets-kvp-postgres") + .Build(); + + public class TestContext(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.AddAssetKeyValueStore(); + base.OnModelCreating(modelBuilder); + } + } + + public IServiceProvider Services { get; private set; } + + public EFAssetKeyValueStore Store => Services.GetRequiredService>(); + + public async Task InitializeAsync() + { + await postgresSql.StartAsync(); + + Services = new ServiceCollection() + .AddDbContext(b => + { + b.UseNpgsql(postgresSql.GetConnectionString()); + }) + .AddEntityFrameworkAssetKeyValueStore() + .BuildServiceProvider(); + + var factory = Services.GetRequiredService>(); + var context = await factory.CreateDbContextAsync(); + var creator = (RelationalDatabaseCreator)context.Database.GetService(); + + await creator.EnsureCreatedAsync(); + + foreach (var service in Services.GetRequiredService>()) + { + await service.InitializeAsync(default); + } + } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await postgresSql.StopAsync(); + } +} diff --git a/assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreTests.cs b/assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreTests.cs new file mode 100644 index 0000000..c5eec2f --- /dev/null +++ b/assets/Squidex.Assets.Tests/KeyValueStore/PostgresKeyValueStoreTests.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +namespace Squidex.Assets.KeyValueStore; + +public class PostgresKeyValueStoreTests(PostgresKeyValueStoreFixture fixture) + : KeyValueStoreTests, IClassFixture +{ + protected override Task> CreateSutAsync() + { + return Task.FromResult>(fixture.Store); + } +} diff --git a/assets/Squidex.Assets.Tests/KeyValueStore/TestValue.cs b/assets/Squidex.Assets.Tests/KeyValueStore/TestValue.cs new file mode 100644 index 0000000..b5792c5 --- /dev/null +++ b/assets/Squidex.Assets.Tests/KeyValueStore/TestValue.cs @@ -0,0 +1,13 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Assets.KeyValueStore; + +public sealed class TestValue +{ + public string Value { get; set; } +} diff --git a/assets/Squidex.Assets.Tests/MongoGridFSAssetStoreFixture.cs b/assets/Squidex.Assets.Tests/MongoGridFSAssetStoreFixture.cs index 51b4d56..adebba2 100644 --- a/assets/Squidex.Assets.Tests/MongoGridFSAssetStoreFixture.cs +++ b/assets/Squidex.Assets.Tests/MongoGridFSAssetStoreFixture.cs @@ -20,28 +20,18 @@ public sealed class MongoGridFSAssetStoreFixture : IAsyncLifetime private readonly MongoDbContainer mongoDb = new MongoDbBuilder() .WithReuse(Debugger.IsAttached) - .WithLabel("reuse-id", "asset-postgres") + .WithLabel("reuse-id", "asset-mongodb") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public MongoGridFsAssetStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mongoDb.StopAsync(); - } + public MongoGridFsAssetStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mongoDb.StartAsync(); - services = + Services = new ServiceCollection() .AddSingleton(_ => new MongoClient(mongoDb.GetConnectionString())) .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) @@ -56,9 +46,19 @@ public async Task InitializeAsync() }) .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mongoDb.StopAsync(); + } } diff --git a/assets/Squidex.Assets.Tests/MongoGridFsAssetStoreTests.cs b/assets/Squidex.Assets.Tests/MongoGridFsAssetStoreTests.cs index e6258f7..015f122 100644 --- a/assets/Squidex.Assets.Tests/MongoGridFsAssetStoreTests.cs +++ b/assets/Squidex.Assets.Tests/MongoGridFsAssetStoreTests.cs @@ -9,7 +9,6 @@ namespace Squidex.Assets; -[Trait("Category", "Dependencies")] public class MongoGridFsAssetStoreTests(MongoGridFSAssetStoreFixture fixture) : AssetStoreTests, IClassFixture { diff --git a/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj b/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj index 9bdaefc..7487467 100644 --- a/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj +++ b/assets/Squidex.Assets.Tests/Squidex.Assets.Tests.csproj @@ -37,9 +37,11 @@ + + all @@ -73,6 +75,7 @@ + diff --git a/assets/Squidex.Assets/IAssetKeyValueStore.cs b/assets/Squidex.Assets/IAssetKeyValueStore.cs index 776419d..c8bc992 100644 --- a/assets/Squidex.Assets/IAssetKeyValueStore.cs +++ b/assets/Squidex.Assets/IAssetKeyValueStore.cs @@ -9,7 +9,7 @@ namespace Squidex.Assets; public interface IAssetKeyValueStore { - Task GetAsync(string key, + Task GetAsync(string key, CancellationToken ct = default); Task SetAsync(string key, T value, DateTimeOffset expiration, diff --git a/events/Squidex.Events.EntityFramework/EFEventStore_Writer.cs b/events/Squidex.Events.EntityFramework/EFEventStore_Writer.cs index d438b89..19ccaaf 100644 --- a/events/Squidex.Events.EntityFramework/EFEventStore_Writer.cs +++ b/events/Squidex.Events.EntityFramework/EFEventStore_Writer.cs @@ -103,9 +103,8 @@ public async Task DeleteAsync(StreamFilter filter, { await using var context = await dbContextFactory.CreateDbContextAsync(ct); - var query = context.Set().ByStream(filter); - - await query.ExecuteDeleteAsync(ct); + await context.Set().ByStream(filter) + .ExecuteDeleteAsync(ct); } private static async Task GetEventStreamOffsetAsync(DbSet commitSet, string streamName) diff --git a/events/Squidex.Events.Tests/GetEventStoreFixture.cs b/events/Squidex.Events.Tests/GetEventStoreFixture.cs index 21a8db3..d05693f 100644 --- a/events/Squidex.Events.Tests/GetEventStoreFixture.cs +++ b/events/Squidex.Events.Tests/GetEventStoreFixture.cs @@ -24,34 +24,34 @@ public sealed class GetEventStoreFixture : IAsyncLifetime .WithEnvironment("EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP", "true") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await eventStore.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await eventStore.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddSingleton(_ => EventStoreClientSettings.Create(eventStore.GetConnectionString())) .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) .AddGetEventStore(TestHelpers.Configuration) .Services .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await eventStore.StopAsync(); + } } diff --git a/events/Squidex.Events.Tests/MariaDbEventStoreFixture.cs b/events/Squidex.Events.Tests/MariaDbEventStoreFixture.cs index 191652a..87eb58e 100644 --- a/events/Squidex.Events.Tests/MariaDbEventStoreFixture.cs +++ b/events/Squidex.Events.Tests/MariaDbEventStoreFixture.cs @@ -24,27 +24,15 @@ public sealed class MariaDbEventStoreFixture : IAsyncLifetime .WithUsername("root") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public IServiceProvider Services => services; - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mariaDb.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mariaDb.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddDbContext(b => { b.UseMySql(mariaDb.GetConnectionString(), ServerVersion.AutoDetect(mariaDb.GetConnectionString())); @@ -57,15 +45,25 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - var factory = services.GetRequiredService>(); + var factory = Services.GetRequiredService>(); var context = await factory.CreateDbContextAsync(); var creator = (RelationalDatabaseCreator)context.Database.GetService(); await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mariaDb.StopAsync(); + } } diff --git a/events/Squidex.Events.Tests/MongoEventStoreReplicaFixture.cs b/events/Squidex.Events.Tests/MongoEventStoreReplicaFixture.cs index 850549f..3198eba 100644 --- a/events/Squidex.Events.Tests/MongoEventStoreReplicaFixture.cs +++ b/events/Squidex.Events.Tests/MongoEventStoreReplicaFixture.cs @@ -22,25 +22,15 @@ public sealed class MongoEventStoreReplicaFixture : IAsyncLifetime .WithReplicaSet() .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mongoDb.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mongoDb.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddSingleton(_ => new MongoClient(mongoDb.GetConnectionString())) .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) .AddMongoEventStore(TestHelpers.Configuration, options => @@ -50,9 +40,19 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mongoDb.StopAsync(); + } } diff --git a/events/Squidex.Events.Tests/MongoEventStoreStandaloneFixture.cs b/events/Squidex.Events.Tests/MongoEventStoreStandaloneFixture.cs index 79ec99d..001f99c 100644 --- a/events/Squidex.Events.Tests/MongoEventStoreStandaloneFixture.cs +++ b/events/Squidex.Events.Tests/MongoEventStoreStandaloneFixture.cs @@ -21,25 +21,15 @@ public sealed class MongoEventStoreStandaloneFixture : IAsyncLifetime .WithLabel("reuse-id", "eventstore-mongo-standalone") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mongoDb.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mongoDb.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddSingleton(_ => new MongoClient(mongoDb.GetConnectionString())) .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) .AddMongoEventStore(TestHelpers.Configuration, options => @@ -49,9 +39,19 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mongoDb.StopAsync(); + } } diff --git a/events/Squidex.Events.Tests/MysqlEventStoreFixture.cs b/events/Squidex.Events.Tests/MysqlEventStoreFixture.cs index 80b42f8..1ff9921 100644 --- a/events/Squidex.Events.Tests/MysqlEventStoreFixture.cs +++ b/events/Squidex.Events.Tests/MysqlEventStoreFixture.cs @@ -24,27 +24,15 @@ public sealed class MySqlEventStoreFixture : IAsyncLifetime .WithUsername("root") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public IServiceProvider Services => services; - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mysql.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mysql.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddDbContext(b => { b.UseMySql(mysql.GetConnectionString(), ServerVersion.AutoDetect(mysql.GetConnectionString())); @@ -57,15 +45,25 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - var factory = services.GetRequiredService>(); + var factory = Services.GetRequiredService>(); var context = await factory.CreateDbContextAsync(); var creator = (RelationalDatabaseCreator)context.Database.GetService(); await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mysql.StopAsync(); + } } diff --git a/events/Squidex.Events.Tests/PostgresEventStoreFixture.cs b/events/Squidex.Events.Tests/PostgresEventStoreFixture.cs index 4ec5906..d70d088 100644 --- a/events/Squidex.Events.Tests/PostgresEventStoreFixture.cs +++ b/events/Squidex.Events.Tests/PostgresEventStoreFixture.cs @@ -23,27 +23,15 @@ public sealed class PostgresEventStoreFixture : IAsyncLifetime .WithLabel("reuse-id", "eventstore-postgres") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public IServiceProvider Services => services; - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await postgresSql.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await postgresSql.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddDbContext(b => { b.UseNpgsql(postgresSql.GetConnectionString()); @@ -56,15 +44,25 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - var factory = services.GetRequiredService>(); + var factory = Services.GetRequiredService>(); var context = await factory.CreateDbContextAsync(); var creator = (RelationalDatabaseCreator)context.Database.GetService(); await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await postgresSql.StopAsync(); + } } diff --git a/events/Squidex.Events.Tests/SqlServerEventStoreFixture.cs b/events/Squidex.Events.Tests/SqlServerEventStoreFixture.cs index 3237744..df09bdf 100644 --- a/events/Squidex.Events.Tests/SqlServerEventStoreFixture.cs +++ b/events/Squidex.Events.Tests/SqlServerEventStoreFixture.cs @@ -22,27 +22,15 @@ public sealed class SqlServerEventStoreFixture : IAsyncLifetime .WithReuse(true) .WithLabel("reuse-id", "eventstore-sqlserver").Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IEventStore Store => services.GetRequiredService(); - - public IServiceProvider Services => services; - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await msSql.StopAsync(); - } + public IEventStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await msSql.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddDbContext(b => { b.UseSqlServer(msSql.GetConnectionString()); @@ -55,15 +43,25 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - var factory = services.GetRequiredService>(); + var factory = Services.GetRequiredService>(); var context = await factory.CreateDbContextAsync(); var creator = (RelationalDatabaseCreator)context.Database.GetService(); await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await msSql.StopAsync(); + } } diff --git a/messaging/Squidex.Messaging.Tests/EFMessagingDataStoreFixture.cs b/messaging/Squidex.Messaging.Tests/EFMessagingDataStoreFixture.cs index caba08c..dff93cd 100644 --- a/messaging/Squidex.Messaging.Tests/EFMessagingDataStoreFixture.cs +++ b/messaging/Squidex.Messaging.Tests/EFMessagingDataStoreFixture.cs @@ -23,9 +23,9 @@ public class EFMessagingDataStoreFixture : IAsyncLifetime .WithLabel("reuse-id", "messagingstore-kafka") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IMessagingDataStore Store => services.GetRequiredService(); + public IMessagingDataStore Store => Services.GetRequiredService(); public sealed class AppDbContext(DbContextOptions options) : DbContext(options) { @@ -38,7 +38,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public async Task DisposeAsync() { - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.ReleaseAsync(default); } @@ -50,7 +50,7 @@ public async Task InitializeAsync() { await postgresSql.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddDbContext(b => { b.UseNpgsql(postgresSql.GetConnectionString()); @@ -61,13 +61,13 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - var factory = services.GetRequiredService>(); + var factory = Services.GetRequiredService>(); var context = await factory.CreateDbContextAsync(); var creator = (RelationalDatabaseCreator)context.Database.GetService(); await creator.EnsureCreatedAsync(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } diff --git a/messaging/Squidex.Messaging.Tests/EFMessagingFixture.cs b/messaging/Squidex.Messaging.Tests/EFMessagingFixture.cs index b243fd1..7ea1c27 100644 --- a/messaging/Squidex.Messaging.Tests/EFMessagingFixture.cs +++ b/messaging/Squidex.Messaging.Tests/EFMessagingFixture.cs @@ -32,11 +32,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } - public async Task DisposeAsync() - { - await PostgresSql.StopAsync(); - } - public async Task InitializeAsync() { await PostgresSql.StartAsync(); @@ -54,4 +49,9 @@ public async Task InitializeAsync() await creator.EnsureCreatedAsync(); } + + public async Task DisposeAsync() + { + await PostgresSql.StopAsync(); + } } diff --git a/messaging/Squidex.Messaging.Tests/KafkaFixture.cs b/messaging/Squidex.Messaging.Tests/KafkaFixture.cs index 71ced47..31ccafb 100644 --- a/messaging/Squidex.Messaging.Tests/KafkaFixture.cs +++ b/messaging/Squidex.Messaging.Tests/KafkaFixture.cs @@ -21,11 +21,6 @@ public class KafkaFixture : IAsyncLifetime .WithLabel("reuse-id", "messaging-kafka") .Build(); - public async Task DisposeAsync() - { - await Kafka.StopAsync(); - } - public async Task InitializeAsync() { await Kafka.StartAsync(); @@ -39,4 +34,9 @@ await adminClient.CreateTopicsAsync([ new TopicSpecification { Name = "dev" } ]); } + + public async Task DisposeAsync() + { + await Kafka.StopAsync(); + } } diff --git a/messaging/Squidex.Messaging.Tests/MongoMessagingDataStoreFixture.cs b/messaging/Squidex.Messaging.Tests/MongoMessagingDataStoreFixture.cs index ec10e0a..bb3442f 100644 --- a/messaging/Squidex.Messaging.Tests/MongoMessagingDataStoreFixture.cs +++ b/messaging/Squidex.Messaging.Tests/MongoMessagingDataStoreFixture.cs @@ -21,25 +21,15 @@ public class MongoMessagingDataStoreFixture : IAsyncLifetime .WithLabel("reuse-id", "messagingstore-mongo") .Build(); - private IServiceProvider services; + public IServiceProvider Services { get; private set; } - public IMessagingDataStore Store => services.GetRequiredService(); - - public async Task DisposeAsync() - { - foreach (var service in services.GetRequiredService>()) - { - await service.ReleaseAsync(default); - } - - await mongoDb.StopAsync(); - } + public IMessagingDataStore Store => Services.GetRequiredService(); public async Task InitializeAsync() { await mongoDb.StartAsync(); - services = new ServiceCollection() + Services = new ServiceCollection() .AddSingleton(_ => new MongoClient(mongoDb.GetConnectionString())) .AddSingleton(c => c.GetRequiredService().GetDatabase("Test")) .AddMessaging() @@ -47,9 +37,19 @@ public async Task InitializeAsync() .Services .BuildServiceProvider(); - foreach (var service in services.GetRequiredService>()) + foreach (var service in Services.GetRequiredService>()) { await service.InitializeAsync(default); } } + + public async Task DisposeAsync() + { + foreach (var service in Services.GetRequiredService>()) + { + await service.ReleaseAsync(default); + } + + await mongoDb.StopAsync(); + } } diff --git a/messaging/Squidex.Messaging.Tests/RabbitMqFixture.cs b/messaging/Squidex.Messaging.Tests/RabbitMqFixture.cs index 0719e9a..e917137 100644 --- a/messaging/Squidex.Messaging.Tests/RabbitMqFixture.cs +++ b/messaging/Squidex.Messaging.Tests/RabbitMqFixture.cs @@ -19,13 +19,13 @@ public class RabbitMqFixture : IAsyncLifetime .WithLabel("reuse-id", "messaging-rabbit") .Build(); - public async Task DisposeAsync() + public async Task InitializeAsync() { - await RabbitMq.StopAsync(); + await RabbitMq.StartAsync(); } - public async Task InitializeAsync() + public async Task DisposeAsync() { - await RabbitMq.StartAsync(); + await RabbitMq.StopAsync(); } } diff --git a/messaging/Squidex.Messaging.Tests/RedisFixture.cs b/messaging/Squidex.Messaging.Tests/RedisFixture.cs index 15a6e78..34d9736 100644 --- a/messaging/Squidex.Messaging.Tests/RedisFixture.cs +++ b/messaging/Squidex.Messaging.Tests/RedisFixture.cs @@ -24,15 +24,15 @@ public class RedisFixture : IAsyncLifetime public ConnectionMultiplexer Connection { get; set; } - public async Task DisposeAsync() - { - await redis.StopAsync(); - } - public async Task InitializeAsync() { await redis.StartAsync(); Connection = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString()); } + + public async Task DisposeAsync() + { + await redis.StopAsync(); + } }