diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
index 190df1f11..1631c233a 100644
--- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
+++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
@@ -18,7 +18,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Migrations/Migrations.csproj b/backend/src/Migrations/Migrations.csproj
index 5f7d52abb..e7aaf543b 100644
--- a/backend/src/Migrations/Migrations.csproj
+++ b/backend/src/Migrations/Migrations.csproj
@@ -6,7 +6,7 @@
enable
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
index 950043ca2..d23bbca87 100644
--- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
+++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
@@ -12,7 +12,7 @@
True
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
index 47d2fd8be..20e9a7e11 100644
--- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
+++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
@@ -21,7 +21,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
index d0933051a..eb77d918a 100644
--- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
+++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
@@ -19,7 +19,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
index 767a80845..c1fd1ebaa 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
@@ -58,16 +58,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = AppCacheKey(appId);
- if (localCache.TryGetValue(cacheKey, out var cached) && cached is IAppEntity found)
+ var app = await GetOrCreate(cacheKey, () =>
{
- return found;
- }
-
- var app = await indexForApps.GetAppAsync(appId, canCache, ct);
+ return indexForApps.GetAppAsync(appId, canCache, ct);
+ });
if (app != null)
{
- localCache.Add(cacheKey, app);
localCache.Add(AppCacheKey(app.Name), app);
}
@@ -79,16 +76,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = AppCacheKey(appName);
- if (localCache.TryGetValue(cacheKey, out var cached) && cached is IAppEntity found)
+ var app = await GetOrCreate(cacheKey, () =>
{
- return found;
- }
-
- var app = await indexForApps.GetAppAsync(appName, canCache, ct);
+ return indexForApps.GetAppAsync(appName, canCache, ct);
+ });
if (app != null)
{
- localCache.Add(cacheKey, app);
localCache.Add(AppCacheKey(app.Id), app);
}
@@ -100,16 +94,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = SchemaCacheKey(appId, name);
- if (localCache.TryGetValue(cacheKey, out var cached) && cached is ISchemaEntity found)
+ var schema = await GetOrCreate(cacheKey, () =>
{
- return found;
- }
-
- var schema = await indexForSchemas.GetSchemaAsync(appId, name, canCache, ct);
+ return indexForSchemas.GetSchemaAsync(appId, name, canCache, ct);
+ });
if (schema != null)
{
- localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
}
@@ -121,16 +112,13 @@ namespace Squidex.Domain.Apps.Entities
{
var cacheKey = SchemaCacheKey(appId, id);
- if (localCache.TryGetValue(cacheKey, out var cached) && cached is ISchemaEntity found)
+ var schema = await GetOrCreate(cacheKey, () =>
{
- return found;
- }
-
- var schema = await indexForSchemas.GetSchemaAsync(appId, id, canCache, ct);
+ return indexForSchemas.GetSchemaAsync(appId, id, canCache, ct);
+ });
if (schema != null)
{
- localCache.Add(cacheKey, schema);
localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
}
@@ -140,40 +128,43 @@ namespace Squidex.Domain.Apps.Entities
public async Task> GetUserAppsAsync(string userId, PermissionSet permissions,
CancellationToken ct = default)
{
- var apps = await localCache.GetOrCreateAsync($"GetUserApps({userId})", () =>
+ var apps = await GetOrCreate($"GetUserApps({userId})", () =>
{
- return indexForApps.GetAppsForUserAsync(userId, permissions, ct);
+ return indexForApps.GetAppsForUserAsync(userId, permissions, ct)!;
});
- return apps;
+ return apps?.ToList() ?? new List();
}
public async Task> GetSchemasAsync(DomainId appId,
CancellationToken ct = default)
{
- var schemas = await localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", () =>
+ var schemas = await GetOrCreate($"GetSchemasAsync({appId})", () =>
{
- return indexForSchemas.GetSchemasAsync(appId, ct);
+ return indexForSchemas.GetSchemasAsync(appId, ct)!;
});
- foreach (var schema in schemas)
+ if (schemas != null)
{
- localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
- localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
+ foreach (var schema in schemas)
+ {
+ localCache.Add(SchemaCacheKey(appId, schema.Id), schema);
+ localCache.Add(SchemaCacheKey(appId, schema.SchemaDef.Name), schema);
+ }
}
- return schemas;
+ return schemas?.ToList() ?? new List();
}
public async Task> GetRulesAsync(DomainId appId,
CancellationToken ct = default)
{
- var rules = await localCache.GetOrCreateAsync($"GetRulesAsync({appId})", () =>
+ var rules = await GetOrCreate($"GetRulesAsync({appId})", () =>
{
- return indexForRules.GetRulesAsync(appId, ct);
+ return indexForRules.GetRulesAsync(appId, ct)!;
});
- return rules.ToList();
+ return rules?.ToList() ?? new List();
}
public async Task GetRuleAsync(DomainId appId, DomainId id,
@@ -184,6 +175,28 @@ namespace Squidex.Domain.Apps.Entities
return rules.Find(x => x.Id == id);
}
+ public async Task GetOrCreate(object key, Func> creator) where T : class
+ {
+ if (localCache.TryGetValue(key, out var value))
+ {
+ switch (value)
+ {
+ case T typed:
+ return typed;
+ case Task typedTask:
+ return await typedTask;
+ default:
+ return null;
+ }
+ }
+
+ var result = creator();
+
+ localCache.Add(key, result);
+
+ return await result;
+ }
+
private static string AppCacheKey(DomainId appId)
{
return $"APPS_ID_{appId}";
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs
new file mode 100644
index 000000000..bb7d0dcf9
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs
@@ -0,0 +1,40 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Orleans;
+using Orleans.Runtime;
+using tusdotnet.Interfaces;
+
+namespace Squidex.Domain.Apps.Entities.Assets
+{
+ public sealed class AssetCleanupGrain : Grain, IRemindable, IAssetCleanupGrain
+ {
+ private readonly ITusExpirationStore expirationStore;
+
+ public AssetCleanupGrain(ITusExpirationStore expirationStore)
+ {
+ this.expirationStore = expirationStore;
+ }
+
+ public override Task OnActivateAsync()
+ {
+ RegisterOrUpdateReminder("Cleanup", TimeSpan.Zero, TimeSpan.FromMinutes(10));
+
+ return base.OnActivateAsync();
+ }
+
+ public Task ActivateAsync()
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task ReceiveReminder(string reminderName, TickStatus status)
+ {
+ return expirationStore.RemoveExpiredFilesAsync(default);
+ }
+ }
+}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs
new file mode 100644
index 000000000..e525b8890
--- /dev/null
+++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs
@@ -0,0 +1,15 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Infrastructure.Orleans;
+
+namespace Squidex.Domain.Apps.Entities.Assets
+{
+ public interface IAssetCleanupGrain : IBackgroundGrain
+ {
+ }
+}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs
index 38d0cd365..591e7a547 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLResolver.cs
@@ -30,6 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private readonly ISchemasHash schemasHash;
private readonly IServiceProvider serviceProvider;
private readonly GraphQLOptions options;
+ private readonly SharedTypes sharedTypes;
private sealed record CacheEntry(GraphQLSchema Model, string Hash, Instant Created);
@@ -45,6 +46,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.schemasHash = schemasHash;
this.serviceProvider = serviceProvider;
this.options = options.Value;
+
+ sharedTypes = serviceProvider.GetRequiredService();
}
public async Task ConfigureAsync(ExecutionOptions executionOptions)
@@ -88,10 +91,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var hash = await schemasHash.ComputeHashAsync(app, schemas);
- return new CacheEntry(
- new Builder(app, serviceProvider.GetRequiredService()).BuildSchema(schemas),
- hash,
- SystemClock.Instance.GetCurrentInstant());
+ return new CacheEntry(new Builder(app, sharedTypes).BuildSchema(schemas),
+ hash, SystemClock.Instance.GetCurrentInstant());
}
private static object CreateCacheKey(DomainId appId, string etag)
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
index 5b776dbfd..2a8c468f3 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
@@ -6,13 +6,10 @@
// ==========================================================================
using GraphQL.DataLoader;
-using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries;
using Squidex.Infrastructure;
-using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
-using Squidex.Log;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
@@ -22,37 +19,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static readonly List EmptyAssets = new List();
private static readonly List EmptyContents = new List();
private readonly IDataLoaderContextAccessor dataLoaders;
- private readonly IUserResolver userResolver;
-
- public IUrlGenerator UrlGenerator { get; }
-
- public ICommandBus CommandBus { get; }
-
- public ISemanticLog Log { get; }
public override Context Context { get; }
- public GraphQLExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery,
- Context context,
- IDataLoaderContextAccessor dataLoaders,
- ICommandBus commandBus,
- IUrlGenerator urlGenerator,
- IUserResolver userResolver,
- ISemanticLog log)
- : base(assetQuery, contentQuery)
+ public GraphQLExecutionContext(IServiceProvider serviceProvider, IDataLoaderContextAccessor dataLoaders, Context context)
+ : base(serviceProvider)
{
this.dataLoaders = dataLoaders;
- this.userResolver = userResolver;
-
- CommandBus = commandBus;
-
- UrlGenerator = urlGenerator;
Context = context.Clone(b => b
.WithoutCleanup()
.WithoutContentEnrichment());
-
- Log = log;
}
public async Task FindUserAsync(RefToken refToken,
@@ -93,34 +70,38 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return content;
}
- public Task> GetReferencedAssetsAsync(IJsonValue value,
+ public async Task> GetReferencedAssetsAsync(IJsonValue value,
CancellationToken ct)
{
var ids = ParseIds(value);
if (ids == null)
{
- return Task.FromResult>(EmptyAssets);
+ return EmptyAssets;
}
var dataLoader = GetAssetsLoader();
- return LoadManyAsync(dataLoader, ids, ct);
+ var result = await dataLoader.LoadAsync(ids).GetResultAsync(ct);
+
+ return result?.NotNull().ToList() ?? EmptyAssets;
}
- public Task> GetReferencedContentsAsync(IJsonValue value,
+ public async Task> GetReferencedContentsAsync(IJsonValue value,
CancellationToken ct)
{
var ids = ParseIds(value);
if (ids == null)
{
- return Task.FromResult>(EmptyContents);
+ return EmptyContents;
}
var dataLoader = GetContentsLoader();
- return LoadManyAsync(dataLoader, ids, ct);
+ var result = await dataLoader.LoadAsync(ids).GetResultAsync(ct);
+
+ return result?.NotNull().ToList() ?? EmptyContents;
}
private IDataLoader GetAssetsLoader()
@@ -150,20 +131,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoaders.Context.GetOrAddBatchLoader(nameof(GetUserLoader),
async (batch, ct) =>
{
- var result = await userResolver.QueryManyAsync(batch.ToArray(), ct);
+ var result = await Resolve().QueryManyAsync(batch.ToArray(), ct);
return result;
});
}
- private static async Task> LoadManyAsync(IDataLoader dataLoader, ICollection keys,
- CancellationToken ct) where T : class
- {
- var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x).GetResultAsync(ct)));
-
- return contents.NotNull().ToList();
- }
-
private static ICollection? ParseIds(IJsonValue value)
{
try
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
index 04e8703a5..88ce45063 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
@@ -251,17 +251,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
private static readonly IFieldResolver Url = Resolve((asset, _, context) =>
{
- return context.UrlGenerator.AssetContent(asset.AppId, asset.Id.ToString());
+ var urlGenerator = context.Resolve();
+
+ return urlGenerator.AssetContent(asset.AppId, asset.Id.ToString());
});
private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) =>
{
- return context.UrlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion);
+ var urlGenerator = context.Resolve();
+
+ return urlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion);
});
private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) =>
{
- return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type);
+ var urlGenerator = context.Resolve();
+
+ return urlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type);
});
private static IFieldResolver Resolve(Func resolver)
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
index f35334c94..7bc42043d 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
@@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Translations;
using Squidex.Shared;
@@ -443,7 +444,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
contentCommand.SchemaId = schemaId;
contentCommand.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any);
- var commandContext = await context.CommandBus.PublishAsync(contentCommand);
+ var commandContext = await context.Resolve().PublishAsync(contentCommand);
return commandContext.PlainResult!;
});
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs
index 75b77a60e..01c005ccc 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs
@@ -7,6 +7,7 @@
using GraphQL;
using GraphQL.Resolvers;
+using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Infrastructure;
@@ -24,9 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
public static readonly IFieldResolver Url = Resolve((content, _, context) =>
{
- var appId = content.AppId;
+ var urlGenerator = context.Resolve();
- return context.UrlGenerator.ContentUI(appId, content.SchemaId, content.Id);
+ return urlGenerator.ContentUI(content.AppId, content.SchemaId, content.Id);
});
public static readonly IFieldResolver FlatData = Resolve((content, c, context) =>
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
index f378c9e60..c80f48565 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
@@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
catch (Exception ex)
{
- executionContext.Log.LogWarning(ex, w => w
+ executionContext.Resolve().LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldDefinition.Name));
@@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
catch (Exception ex)
{
- executionContext.Log.LogWarning(ex, w => w
+ executionContext.Resolve().LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldDefinition.Name));
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
index e244f0e6a..1959e9695 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
+++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
@@ -6,6 +6,7 @@
// ==========================================================================
using System.Collections.Concurrent;
+using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
@@ -16,24 +17,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10);
private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary();
private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary();
- private readonly IContentQueryService contentQuery;
- private readonly IAssetQueryService assetQuery;
public abstract Context Context { get; }
- protected QueryExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery)
+ public IServiceProvider Services { get; }
+
+ protected QueryExecutionContext(IServiceProvider serviceProvider)
{
- Guard.NotNull(assetQuery);
- Guard.NotNull(contentQuery);
+ Guard.NotNull(serviceProvider);
- this.assetQuery = assetQuery;
- this.contentQuery = contentQuery;
+ Services = serviceProvider;
}
public virtual Task FindContentAsync(string schemaIdOrName, DomainId id, long version,
CancellationToken ct)
{
- return contentQuery.FindAsync(Context, schemaIdOrName, id, version, ct);
+ return Resolve().FindAsync(Context, schemaIdOrName, id, version, ct);
}
public virtual async Task> QueryAssetsAsync(Q q,
@@ -44,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
try
{
- assets = await assetQuery.QueryAsync(Context, null, q, ct);
+ assets = await Resolve().QueryAsync(Context, null, q, ct);
}
finally
{
@@ -67,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
try
{
- contents = await contentQuery.QueryAsync(Context, schemaIdOrName, q, ct);
+ contents = await Resolve().QueryAsync(Context, schemaIdOrName, q, ct);
}
finally
{
@@ -96,7 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
try
{
- assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets).WithoutTotal(), ct);
+ var q = Q.Empty.WithIds(notLoadedAssets).WithoutTotal();
+
+ assets = await Resolve().QueryAsync(Context, null, q, ct);
}
finally
{
@@ -126,7 +127,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(ct);
try
{
- contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents).WithoutTotal(), ct);
+ var q = Q.Empty.WithIds(notLoadedContents).WithoutTotal();
+
+ contents = await Resolve().QueryAsync(Context, q, ct);
}
finally
{
@@ -141,5 +144,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return ids.Select(cachedContents.GetOrDefault).NotNull().ToList();
}
+
+ public T Resolve() where T : class
+ {
+ var key = typeof(T).Name;
+
+ if (TryGetValue(key, out var stored) && stored is T typed)
+ {
+ return typed;
+ }
+
+ typed = Services.GetRequiredService();
+
+ this[key] = typed;
+
+ return typed;
+ }
}
}
diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
index 5d95d7de1..878f2b733 100644
--- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
+++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
@@ -22,7 +22,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -38,6 +38,7 @@
+
diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
index 2635111ac..f00d8eaa6 100644
--- a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
+++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
@@ -14,7 +14,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
index a535a50c8..2ba42f114 100644
--- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
+++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj
@@ -19,7 +19,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
index 85d79ac66..67e945cda 100644
--- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
+++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
@@ -18,7 +18,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
index d111ed3d7..3f0297a7a 100644
--- a/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
+++ b/backend/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj
@@ -15,7 +15,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs
similarity index 90%
rename from backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs
rename to backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs
index 1fa864178..43c9485f3 100644
--- a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs
+++ b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoHealthCheck.cs
@@ -10,11 +10,11 @@ using MongoDB.Driver;
namespace Squidex.Infrastructure.Diagnostics
{
- public sealed class MongoDBHealthCheck : IHealthCheck
+ public sealed class MongoHealthCheck : IHealthCheck
{
private readonly IMongoDatabase mongoDatabase;
- public MongoDBHealthCheck(IMongoDatabase mongoDatabase)
+ public MongoHealthCheck(IMongoDatabase mongoDatabase)
{
this.mongoDatabase = mongoDatabase;
}
diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
index 1754a5e93..760ec1614 100644
--- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
+++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj
@@ -14,7 +14,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
index 311343fab..24aeaaccb 100644
--- a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
+++ b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
@@ -11,7 +11,7 @@
True
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
index 294017a71..e6fb249fe 100644
--- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
+++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
@@ -14,7 +14,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -31,8 +31,8 @@
-
-
+
+
diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj
index ae360bdce..7f75888e8 100644
--- a/backend/src/Squidex.Shared/Squidex.Shared.csproj
+++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj
@@ -9,7 +9,7 @@
True
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj
index ade9a860b..1f4a8fff5 100644
--- a/backend/src/Squidex.Web/Squidex.Web.csproj
+++ b/backend/src/Squidex.Web/Squidex.Web.csproj
@@ -19,7 +19,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
index d3f374798..473b27318 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
@@ -36,18 +36,21 @@ namespace Squidex.Areas.Api.Controllers.Assets
private readonly IAssetUsageTracker assetStatsRepository;
private readonly IAppPlansProvider appPlansProvider;
private readonly ITagService tagService;
+ private readonly AssetTusRunner assetTusRunner;
public AssetsController(
ICommandBus commandBus,
IAssetQueryService assetQuery,
IAssetUsageTracker assetStatsRepository,
IAppPlansProvider appPlansProvider,
- ITagService tagService)
+ ITagService tagService,
+ AssetTusRunner assetTusRunner)
: base(commandBus)
{
+ this.appPlansProvider = appPlansProvider;
this.assetQuery = assetQuery;
this.assetStatsRepository = assetStatsRepository;
- this.appPlansProvider = appPlansProvider;
+ this.assetTusRunner = assetTusRunner;
this.tagService = tagService;
}
@@ -218,6 +221,43 @@ namespace Squidex.Areas.Api.Controllers.Assets
return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
}
+ ///
+ /// Upload a new asset using tus.io.
+ ///
+ /// The name of the app.
+ ///
+ /// 201 => Asset created.
+ /// 400 => Asset request not valid.
+ /// 413 => Asset exceeds the maximum upload size.
+ /// 404 => App not found.
+ ///
+ ///
+ /// Use the tus protocol to upload an asset.
+ ///
+ [OpenApiIgnore]
+ [Route("apps/{app}/assets/tus/{**fileId}")]
+ [ProducesResponseType(typeof(AssetDto), 201)]
+ [AssetRequestSizeLimit]
+ [ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)]
+ [ApiCosts(1)]
+ public async Task PostAssetTus(string app)
+ {
+ var url = Url.Action(null, new { app, fileId = (object?)null })!;
+
+ var (result, file) = await assetTusRunner.InvokeAsync(HttpContext, url);
+
+ if (file != null)
+ {
+ var command = CreateAssetDto.ToCommand(file);
+
+ var response = await InvokeCommandAsync(command);
+
+ return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response);
+ }
+
+ return result;
+ }
+
///
/// Bulk update assets.
///
@@ -305,6 +345,42 @@ namespace Squidex.Areas.Api.Controllers.Assets
return Ok(response);
}
+ ///
+ /// Replace asset content using tus.
+ ///
+ /// The name of the app.
+ /// The id of the asset.
+ ///
+ /// 200 => Asset updated.
+ /// 400 => Asset request not valid.
+ /// 413 => Asset exceeds the maximum upload size.
+ /// 404 => Asset or app not found.
+ ///
+ ///
+ /// Use the tus protocol to upload an asset.
+ ///
+ [OpenApiIgnore]
+ [Route("apps/{app}/assets/{id}/content/tus/{**fileId}")]
+ [ProducesResponseType(typeof(AssetDto), StatusCodes.Status200OK)]
+ [AssetRequestSizeLimit]
+ [ApiPermissionOrAnonymous(Permissions.AppAssetsUpload)]
+ [ApiCosts(1)]
+ public async Task PutAssetContentTus(string app, DomainId id)
+ {
+ var (result, file) = await assetTusRunner.InvokeAsync(HttpContext, Url.Action(null, new { app, id })!);
+
+ if (file != null)
+ {
+ var command = new UpdateAsset { File = file, AssetId = id };
+
+ var response = await InvokeCommandAsync(command);
+
+ return Ok(response);
+ }
+
+ return result;
+ }
+
///
/// Update an asset.
///
diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs
index 5bd199380..70b88c35f 100644
--- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs
+++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetDto.cs
@@ -38,6 +38,28 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[FromQuery]
public bool Duplicate { get; set; }
+ public static CreateAsset ToCommand(AssetTusFile file)
+ {
+ var command = new CreateAsset { File = file };
+
+ if (file.Metadata.TryGetValue("id", out var id) && !string.IsNullOrWhiteSpace(id))
+ {
+ command.AssetId = DomainId.Create(id);
+ }
+
+ if (file.Metadata.TryGetValue("parentId", out var parentId) && !string.IsNullOrWhiteSpace(parentId))
+ {
+ command.ParentId = DomainId.Create(parentId);
+ }
+
+ if (file.Metadata.TryGetValue("duplicate", out var duplicate) && bool.TryParse(duplicate, out var parsed))
+ {
+ command.Duplicate = parsed;
+ }
+
+ return command;
+ }
+
public CreateAsset ToCommand(AssetFile file)
{
var command = SimpleMapper.Map(this, new CreateAsset { File = file });
diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs
index 5d0789ecb..4727705ea 100644
--- a/backend/src/Squidex/Config/Domain/AssetServices.cs
+++ b/backend/src/Squidex/Config/Domain/AssetServices.cs
@@ -18,6 +18,8 @@ using Squidex.Domain.Apps.Entities.Search;
using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
+using Squidex.Infrastructure.Orleans;
+using tusdotnet.Interfaces;
namespace Squidex.Config.Domain
{
@@ -49,12 +51,18 @@ namespace Squidex.Config.Domain
services.AddSingletonAs()
.AsSelf();
- services.AddSingletonAs()
- .AsSelf();
-
services.AddTransientAs()
.As();
+ services.AddSingletonAs()
+ .AsSelf();
+
+ services.AddSingletonAs()
+ .As().As();
+
+ services.AddSingletonAs()
+ .AsSelf();
+
services.AddTransientAs()
.As();
@@ -87,6 +95,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs()
.As();
+
+ services.AddSingletonAs>()
+ .AsSelf();
}
public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config)
@@ -176,10 +187,12 @@ namespace Squidex.Config.Domain
}
});
- services.AddSingletonAs(c => new DelegateInitializer(
- c.GetRequiredService().GetType().Name,
- c.GetRequiredService().InitializeAsync))
- .As();
+ services.AddSingletonAs(c =>
+ {
+ var service = c.GetRequiredService();
+
+ return new DelegateInitializer(service.GetType().Name, service.InitializeAsync);
+ }).As();
}
}
}
diff --git a/backend/src/Squidex/Config/Domain/ResizeServices.cs b/backend/src/Squidex/Config/Domain/ResizeServices.cs
index dd456814e..55b81ad19 100644
--- a/backend/src/Squidex/Config/Domain/ResizeServices.cs
+++ b/backend/src/Squidex/Config/Domain/ResizeServices.cs
@@ -6,8 +6,6 @@
// ==========================================================================
using Squidex.Assets;
-using Squidex.Assets.ImageMagick;
-using Squidex.Assets.ImageSharp;
using Squidex.Assets.Remote;
namespace Squidex.Config.Domain
diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs
index 831a71d32..6ce280749 100644
--- a/backend/src/Squidex/Config/Domain/StoreServices.cs
+++ b/backend/src/Squidex/Config/Domain/StoreServices.cs
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Identity;
using Migrations.Migrations.MongoDb;
using MongoDB.Driver;
using Newtonsoft.Json;
+using Squidex.Assets;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.DomainObject;
using Squidex.Domain.Apps.Entities.Apps.Repositories;
@@ -34,6 +35,7 @@ using Squidex.Domain.Apps.Entities.Schemas.Repositories;
using Squidex.Domain.Users;
using Squidex.Domain.Users.InMemory;
using Squidex.Domain.Users.MongoDb;
+using Squidex.Hosting;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.EventSourcing;
@@ -96,7 +98,10 @@ namespace Squidex.Config.Domain
.As();
services.AddHealthChecks()
- .AddCheck("MongoDB", tags: new[] { "node" });
+ .AddCheck("MongoDB", tags: new[] { "node" });
+
+ services.AddSingletonAs>()
+ .As>();
services.AddSingletonAs()
.As();
@@ -175,6 +180,13 @@ namespace Squidex.Config.Domain
services.AddSingleton(typeof(IPersistenceFactory<>),
typeof(Store<>));
+
+ services.AddSingletonAs(c =>
+ {
+ var service = c.GetRequiredService>();
+
+ return new DelegateInitializer(service.GetType().Name, service.InitializeAsync);
+ }).As();
}
private static IMongoClient GetClient(string configuration)
@@ -182,9 +194,9 @@ namespace Squidex.Config.Domain
return Singletons.GetOrAdd(configuration, s => new MongoClient(s));
}
- private static IMongoDatabase GetDatabase(IServiceProvider service, string name)
+ private static IMongoDatabase GetDatabase(IServiceProvider serviceProvider, string name)
{
- return service.GetRequiredService().GetDatabase(name);
+ return serviceProvider.GetRequiredService().GetDatabase(name);
}
}
}
diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj
index 323d7d3fa..dea59fb86 100644
--- a/backend/src/Squidex/Squidex.csproj
+++ b/backend/src/Squidex/Squidex.csproj
@@ -40,7 +40,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -74,13 +74,16 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
index d5d7436fd..f8b6b895f 100644
--- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
+++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
@@ -15,7 +15,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs
new file mode 100644
index 000000000..2820db8e9
--- /dev/null
+++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs
@@ -0,0 +1,39 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using FakeItEasy;
+using tusdotnet.Interfaces;
+using Xunit;
+
+namespace Squidex.Domain.Apps.Entities.Assets
+{
+ public class AssetCleanupGrainTests
+ {
+ private readonly ITusExpirationStore expirationStore = A.Fake();
+ private readonly AssetCleanupGrain sut;
+
+ public AssetCleanupGrainTests()
+ {
+ sut = new AssetCleanupGrain(expirationStore);
+ }
+
+ [Fact]
+ public async Task Should_do_nothing_on_activate()
+ {
+ await sut.ActivateAsync();
+ }
+
+ [Fact]
+ public async Task Should_call_expiration_store_when_reminder_invoked()
+ {
+ await sut.ReceiveReminder("Reminder", default);
+
+ A.CallTo(() => expirationStore.RemoveExpiredFilesAsync(default))
+ .MustHaveHappened();
+ }
+ }
+}
diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
index 0acffa654..1a84d2c12 100644
--- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
+++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
@@ -25,7 +25,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -34,7 +34,7 @@
-
+
diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
index 77209db91..e3990a881 100644
--- a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
+++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
@@ -16,7 +16,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
index 572f42b79..8e33dc038 100644
--- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
+++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
@@ -17,7 +17,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
index 78c9e66a1..36f5942a3 100644
--- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
+++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
@@ -13,7 +13,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTest.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTest.cs
index 6db28bd92..fb98886f1 100644
--- a/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTest.cs
+++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTest.cs
@@ -9,6 +9,7 @@ using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using Xunit;
+#pragma warning disable MA0048 // File name must match type name
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
index 7b9d4f096..c827be9f6 100644
--- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
+++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
@@ -9,7 +9,6 @@ using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using Xunit;
-#pragma warning disable xUnit1004 // Test methods should not be skipped
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable CS0618 // Type or member is obsolete
diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
index c549e11ad..52963844e 100644
--- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
+++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
@@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using Squidex.Assets;
using Squidex.ClientLibrary.Management;
using TestSuite.Fixtures;
using Xunit;
@@ -39,6 +40,136 @@ namespace TestSuite.ApiTests
}
}
+ [Fact]
+ public async Task Should_upload_asset_using_tus()
+ {
+ // STEP 1: Create asset
+ var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
+
+ var reportedException = (Exception)null;
+ var reportedProgress = new List();
+ var reportedAsset = (AssetDto)null;
+
+ await using (fileParameter.Data)
+ {
+ await _.Assets.UploadNewAssetAsync(_.AppName, fileParameter, new AssetUploadOptions
+ {
+ ProgressHandler = new AssetDelegatingProgressHandler
+ {
+ OnProgressAsync = (@event, _) =>
+ {
+ reportedProgress.Add(@event.Progress);
+ return Task.CompletedTask;
+ },
+ OnCompletedAsync = (@event, _) =>
+ {
+ reportedAsset = @event.Asset;
+ return Task.CompletedTask;
+ },
+ OnFailedAsync = (@event, _) =>
+ {
+ reportedException = @event.Exception;
+ return Task.CompletedTask;
+ }
+ }
+ });
+ }
+
+ Assert.NotEmpty(reportedProgress);
+ Assert.NotNull(reportedAsset);
+ Assert.Null(reportedException);
+
+ await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
+ {
+ var downloaded = await _.DownloadAsync(reportedAsset);
+
+ // Should dowload with correct size.
+ Assert.Equal(stream.Length, downloaded.Length);
+ }
+ }
+
+ [Fact]
+ public async Task Should_upload_asset_using_tus_in_chunks()
+ {
+ // STEP 1: Create asset
+ var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
+
+ var pausingStream = new PauseStream(fileParameter.Data, 0.25);
+ var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType);
+
+ var numUploads = 0;
+ var reportedException = (Exception)null;
+ var reportedProgress = new List();
+ var reportedAsset = (AssetDto)null;
+ var fileId = (string)null;
+
+ await using (pausingFile.Data)
+ {
+ using var cts = new CancellationTokenSource(5000);
+
+ while (reportedAsset == null)
+ {
+ pausingStream.Reset();
+
+ if (pausingStream.Position == pausingStream.Length)
+ {
+ throw new InvalidOperationException("Stream end reached.");
+ }
+
+ await _.Assets.UploadNewAssetAsync(_.AppName, pausingFile, new AssetUploadOptions
+ {
+ ProgressHandler = new AssetDelegatingProgressHandler
+ {
+ OnCreatedAsync = (@event, _) =>
+ {
+ fileId = @event.FileId;
+ return Task.CompletedTask;
+ },
+ OnProgressAsync = (@event, _) =>
+ {
+ reportedProgress.Add(@event.Progress);
+ return Task.CompletedTask;
+ },
+ OnCompletedAsync = (@event, _) =>
+ {
+ reportedAsset = @event.Asset;
+ return Task.CompletedTask;
+ },
+ OnFailedAsync = (@event, _) =>
+ {
+ if (!@event.Exception.ToString().Contains("PAUSED", StringComparison.OrdinalIgnoreCase))
+ {
+ reportedException = @event.Exception;
+ }
+
+ return Task.CompletedTask;
+ }
+ },
+ FileId = fileId
+ }, cts.Token);
+
+ Assert.Null(reportedException);
+
+ await Task.Delay(50, cts.Token);
+
+ numUploads++;
+ }
+ }
+
+ Assert.NotEmpty(reportedProgress);
+ Assert.NotNull(reportedAsset);
+ Assert.Null(reportedException);
+ Assert.True(numUploads > 1);
+
+ await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
+ {
+ var downloaded = await _.DownloadAsync(reportedAsset);
+
+ // Should dowload with correct size.
+ Assert.Equal(stream.Length, downloaded.Length);
+ }
+ }
+
[Fact]
public async Task Should_upload_asset_with_custom_id()
{
@@ -98,6 +229,143 @@ namespace TestSuite.ApiTests
}
}
+ [Fact]
+ public async Task Should_replace_asset_using_tus()
+ {
+ // STEP 1: Create asset
+ var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png");
+
+
+ // STEP 2: Reupload asset
+ var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
+
+ var reportedException = (Exception)null;
+ var reportedProgress = new List();
+ var reportedAsset = (AssetDto)null;
+
+ await using (fileParameter.Data)
+ {
+ await _.Assets.UploadExistingAssetAsync(_.AppName, asset_1.Id, fileParameter, new AssetUploadOptions
+ {
+ ProgressHandler = new AssetDelegatingProgressHandler
+ {
+ OnProgressAsync = (@event, _) =>
+ {
+ reportedProgress.Add(@event.Progress);
+ return Task.CompletedTask;
+ },
+ OnCompletedAsync = (@event, _) =>
+ {
+ reportedAsset = @event.Asset;
+ return Task.CompletedTask;
+ },
+ OnFailedAsync = (@event, _) =>
+ {
+ reportedException = @event.Exception;
+ return Task.CompletedTask;
+ }
+ }
+ });
+ }
+
+ Assert.NotNull(reportedAsset);
+ Assert.Equal(Enumerable.Range(1, 100).ToArray(), reportedProgress.ToArray());
+
+ await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
+ {
+ var downloaded = await _.DownloadAsync(reportedAsset);
+
+ // Should dowload with correct size.
+ Assert.Equal(stream.Length, downloaded.Length);
+ }
+ }
+
+ [Fact]
+ public async Task Should_replace_asset_using_tus_in_chunks()
+ {
+ // STEP 1: Create asset
+ var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png");
+
+
+ // STEP 2: Reupload asset
+ var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4");
+
+ var pausingStream = new PauseStream(fileParameter.Data, 0.25);
+ var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType);
+
+ var numUploads = 0;
+ var reportedException = (Exception)null;
+ var reportedProgress = new List();
+ var reportedAsset = (AssetDto)null;
+ var fileId = (string)null;
+
+ await using (pausingFile.Data)
+ {
+ using var cts = new CancellationTokenSource(5000);
+
+ while (reportedAsset == null)
+ {
+ pausingStream.Reset();
+
+ if (pausingStream.Position == pausingStream.Length)
+ {
+ throw new InvalidOperationException("Stream end reached.");
+ }
+
+ await _.Assets.UploadExistingAssetAsync(_.AppName, asset_1.Id, pausingFile, new AssetUploadOptions
+ {
+ ProgressHandler = new AssetDelegatingProgressHandler
+ {
+ OnCreatedAsync = (@event, _) =>
+ {
+ fileId = @event.FileId;
+ return Task.CompletedTask;
+ },
+ OnProgressAsync = (@event, _) =>
+ {
+ reportedProgress.Add(@event.Progress);
+ return Task.CompletedTask;
+ },
+ OnCompletedAsync = (@event, _) =>
+ {
+ reportedAsset = @event.Asset;
+ return Task.CompletedTask;
+ },
+ OnFailedAsync = (@event, _) =>
+ {
+ if (!@event.Exception.ToString().Contains("PAUSED", StringComparison.OrdinalIgnoreCase))
+ {
+ reportedException = @event.Exception;
+ }
+
+ return Task.CompletedTask;
+ }
+ },
+ FileId = fileId
+ }, cts.Token);
+
+ Assert.Null(reportedException);
+
+ await Task.Delay(50, cts.Token);
+
+ numUploads++;
+ }
+ }
+
+ Assert.NotEmpty(reportedProgress);
+ Assert.NotNull(reportedAsset);
+ Assert.Null(reportedException);
+ Assert.True(numUploads > 1);
+
+ await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open))
+ {
+ var downloaded = await _.DownloadAsync(reportedAsset);
+
+ // Should dowload with correct size.
+ Assert.Equal(stream.Length, downloaded.Length);
+ }
+ }
+
[Fact]
public async Task Should_annote_asset()
{
@@ -356,5 +624,42 @@ namespace TestSuite.ApiTests
Assert.NotEqual(asset_1.FileSize, asset_2.FileSize);
}
+
+ public class PauseStream : DelegateStream
+ {
+ private readonly double pauseAfter = 1;
+ private int totalRead;
+
+ public PauseStream(Stream innerStream, double pauseAfter)
+ : base(innerStream)
+ {
+ this.pauseAfter = pauseAfter;
+ }
+
+ public void Reset()
+ {
+ totalRead = 0;
+ }
+
+ public override async ValueTask ReadAsync(Memory buffer,
+ CancellationToken cancellationToken = default)
+ {
+ if (Position >= Length)
+ {
+ return 0;
+ }
+
+ if (totalRead >= Length * pauseAfter)
+ {
+ throw new InvalidOperationException("PAUSED");
+ }
+
+ var bytesRead = await base.ReadAsync(buffer, cancellationToken);
+
+ totalRead += bytesRead;
+
+ return bytesRead;
+ }
+ }
}
}
diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
index 635981deb..bc32720dd 100644
--- a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
+++ b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
@@ -14,12 +14,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
index 155055ba4..0061253e3 100644
--- a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
+++ b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj
@@ -6,7 +6,7 @@
enable
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs
index 87c29c7c8..1054dce86 100644
--- a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs
+++ b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs
@@ -10,6 +10,8 @@ using Newtonsoft.Json.Linq;
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
+#pragma warning disable MA0048 // File name must match type name
+
namespace TestSuite.Model
{
public sealed class TestEntity : Content
diff --git a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs
index 5649986c9..41d0028c8 100644
--- a/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs
+++ b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs
@@ -9,6 +9,8 @@ using Newtonsoft.Json;
using Squidex.ClientLibrary;
using Squidex.ClientLibrary.Management;
+#pragma warning disable MA0048 // File name must match type name
+
namespace TestSuite.Model
{
public sealed class TestEntityWithReferences : Content
diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
index c0933f8ab..d84d660af 100644
--- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
+++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
@@ -11,7 +11,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -21,7 +21,7 @@
-
+
diff --git a/backend/tools/TestSuite/TestSuite.sln b/backend/tools/TestSuite/TestSuite.sln
index 8bc1f5817..5fcbe0f8f 100644
--- a/backend/tools/TestSuite/TestSuite.sln
+++ b/backend/tools/TestSuite/TestSuite.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29613.14
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.Shared", "TestSuite.Shared\TestSuite.Shared.csproj", "{37484845-5542-4E52-AB00-C4576B84FE75}"
EndProject