Browse Source

Optimize duplicate finder.

pull/596/head
Sebastian 5 years ago
parent
commit
a6714e03c6
  1. 14
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 16
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  3. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  4. 27
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  5. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  8. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  9. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  10. 4
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  11. 36
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  12. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs
  13. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  14. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

14
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -60,7 +60,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
Index Index
.Ascending(x => x.IndexedAppId) .Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted) .Ascending(x => x.IsDeleted)
.Ascending(x => x.FileHash)) .Ascending(x => x.FileHash)
.Ascending(x => x.FileName)
.Ascending(x => x.FileSize))
}, ct); }, ct);
} }
@ -129,15 +131,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
} }
} }
public async Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(DomainId appId, string hash) public async Task<IAssetEntity?> FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize)
{ {
using (Profiler.TraceMethod<MongoAssetRepository>()) using (Profiler.TraceMethod<MongoAssetRepository>())
{ {
var assetEntities = var assetEntity =
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash) await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.FileHash == hash && x.FileName == fileName && x.FileSize == fileSize)
.ToListAsync(); .FirstOrDefaultAsync();
return assetEntities.OfType<IAssetEntity>().ToList(); return assetEntity;
} }
} }

16
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -62,11 +62,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
if (!createAsset.Duplicate) if (!createAsset.Duplicate)
{ {
var existings = await assetQuery.QueryByHashAsync(ctx, createAsset.AppId.Id, createAsset.FileHash); var existing =
await assetQuery.FindByHashAsync(ctx,
createAsset.FileHash,
createAsset.File.FileName,
createAsset.File.FileSize);
foreach (var existing in existings) if (existing != null)
{
if (IsDuplicate(existing, createAsset.File))
{ {
var result = new AssetCreatedResult(existing, true); var result = new AssetCreatedResult(existing, true);
@ -76,7 +78,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
return; return;
} }
} }
}
await UploadAsync(context, tempFile, createAsset, createAsset.Tags, true, next); await UploadAsync(context, tempFile, createAsset, createAsset.Tags, true, next);
} }
@ -149,11 +150,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
return null; return null;
} }
private static bool IsDuplicate(IAssetEntity asset, AssetFile file)
{
return asset?.FileName == file.FileName && asset.FileSize == file.FileSize;
}
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile)
{ {
using (var uploadStream = command.File.OpenRead()) using (var uploadStream = command.File.OpenRead())

6
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -13,14 +13,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetQueryService public interface IAssetQueryService
{ {
Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Context context, DomainId appId, string hash);
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q query); Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q query);
Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId); Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, DomainId parentId);
Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id); Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(DomainId appId, DomainId id);
Task<IEnrichedAssetEntity?> FindAssetAsync(Context context, DomainId id); Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize);
Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id);
} }
} }

27
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -37,8 +37,24 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
this.queryParser = queryParser; this.queryParser = queryParser;
} }
public async Task<IEnrichedAssetEntity?> FindAssetAsync(Context context, DomainId id) public async Task<IEnrichedAssetEntity?> FindByHashAsync(Context context, string hash, string fileName, long fileSize)
{ {
Guard.NotNull(context, nameof(context));
var asset = await assetRepository.FindAssetAsync(context.App.Id, hash, fileName, fileSize);
if (asset != null)
{
return await assetEnricher.EnrichAsync(asset, context);
}
return null;
}
public async Task<IEnrichedAssetEntity?> FindAsync(Context context, DomainId id)
{
Guard.NotNull(context, nameof(context));
var asset = await assetRepository.FindAssetAsync(context.App.Id, id); var asset = await assetRepository.FindAssetAsync(context.App.Id, id);
if (asset != null) if (asset != null)
@ -78,15 +94,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return assetFolders; return assetFolders;
} }
public async Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Context context, DomainId appId, string hash)
{
Guard.NotNull(hash, nameof(hash));
var assets = await assetRepository.QueryByHashAsync(appId, hash);
return await assetEnricher.EnrichAsync(assets, context);
}
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q query) public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, DomainId? parentId, Q query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));

4
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -14,8 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
public interface IAssetRepository public interface IAssetRepository
{ {
Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(DomainId appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query); Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, DomainId? parentId, ClrQuery query);
Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, HashSet<DomainId> ids); Task<IResultList<IAssetEntity>> QueryAsync(DomainId appId, HashSet<DomainId> ids);
@ -24,6 +22,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId); Task<IReadOnlyList<DomainId>> QueryChildIdsAsync(DomainId appId, DomainId parentId);
Task<IAssetEntity?> FindAssetAsync(DomainId appId, string hash, string fileName, long fileSize);
Task<IAssetEntity?> FindAssetAsync(DomainId appId); Task<IAssetEntity?> FindAssetAsync(DomainId appId);
Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id); Task<IAssetEntity?> FindAssetAsync(DomainId appId, DomainId id);

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query); Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query);
Task<IEnrichedContentEntity> FindContentAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any); Task<IEnrichedContentEntity> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any);
Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName); Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName);
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.queryParser = queryParser; this.queryParser = queryParser;
} }
public async Task<IEnrichedContentEntity> FindContentAsync(Context context, string schemaIdOrName, DomainId id, long version = -1) public async Task<IEnrichedContentEntity> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(); await maxRequests.WaitAsync();
try try
{ {
asset = await assetQuery.FindAssetAsync(context, id); asset = await assetQuery.FindAsync(context, id);
} }
finally finally
{ {
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync(); await maxRequests.WaitAsync();
try try
{ {
content = await contentQuery.FindContentAsync(context, schemaId.ToString(), id); content = await contentQuery.FindAsync(context, schemaId.ToString(), id);
} }
finally finally
{ {

2
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -152,7 +152,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAsset(string app, DomainId id) public async Task<IActionResult> GetAsset(string app, DomainId id)
{ {
var asset = await assetQuery.FindAssetAsync(Context, id); var asset = await assetQuery.FindAsync(Context, id);
if (asset == null) if (asset == null)
{ {

4
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -278,7 +278,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, DomainId id) public async Task<IActionResult> GetContent(string app, string name, DomainId id)
{ {
var content = await contentQuery.FindContentAsync(Context, name, id); var content = await contentQuery.FindAsync(Context, name, id);
var response = ContentDto.FromContent(Context, content, Resources); var response = ContentDto.FromContent(Context, content, Resources);
@ -305,7 +305,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string app, string name, DomainId id, int version) public async Task<IActionResult> GetContentVersion(string app, string name, DomainId id, int version)
{ {
var content = await contentQuery.FindContentAsync(Context, name, id, version); var content = await contentQuery.FindAsync(Context, name, id, version);
var response = ContentDto.FromContent(Context, content, Resources); var response = ContentDto.FromContent(Context, content, Resources);

36
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -70,8 +70,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>._, requestContext)) A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>._, requestContext))
.ReturnsLazily(() => SimpleMapper.Map(asset.Snapshot, new AssetEntity())); .ReturnsLazily(() => SimpleMapper.Map(asset.Snapshot, new AssetEntity()));
A.CallTo(() => assetQuery.QueryByHashAsync(A<Context>.That.Matches(x => x.ShouldEnrichAsset()), AppId, A<string>._)) A.CallTo(() => assetQuery.FindByHashAsync(A<Context>._, A<string>._, A<string>._, A<long>._))
.Returns(new List<IEnrichedAssetEntity>()); .Returns(Task.FromResult<IEnrichedAssetEntity?>(null));
A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id.ToString(), null)) A.CallTo(() => grainFactory.GetGrain<IAssetGrain>(Id.ToString(), null))
.Returns(asset); .Returns(asset);
@ -192,21 +192,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.False(result.IsDuplicate); Assert.False(result.IsDuplicate);
} }
[Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_name_found()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupSameHashAsset("other-name", file.FileSize, out _);
await sut.HandleAsync(context);
var result = context.Result<AssetCreatedResult>();
Assert.False(result.IsDuplicate);
}
[Fact] [Fact]
public async Task Create_should_pass_through_duplicate() public async Task Create_should_pass_through_duplicate()
{ {
@ -224,19 +209,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
result.Should().BeEquivalentTo(duplicate, x => x.ExcludingMissingMembers()); result.Should().BeEquivalentTo(duplicate, x => x.ExcludingMissingMembers());
} }
[Fact]
public async Task Create_should_not_return_duplicate_result_if_file_with_same_hash_but_other_size_found()
{
var command = CreateCommand(new CreateAsset { AssetId = assetId, File = file });
var context = CreateContextForCommand(command);
SetupSameHashAsset(file.FileName, 12345, out _);
await sut.HandleAsync(context);
Assert.False(context.Result<AssetCreatedResult>().IsDuplicate);
}
[Fact] [Fact]
public async Task Update_should_update_domain_object() public async Task Update_should_update_domain_object()
{ {
@ -319,8 +291,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
FileSize = fileSize FileSize = fileSize
}; };
A.CallTo(() => assetQuery.QueryByHashAsync(A<Context>.That.Matches(x => !x.ShouldEnrichAsset()), A<DomainId>._, A<string>._)) A.CallTo(() => assetQuery.FindByHashAsync(A<Context>.That.Matches(x => x.ShouldEnrichAsset()), A<string>._, A<string>._, A<long>._))
.Returns(new List<IEnrichedAssetEntity> { duplicate }); .Returns(duplicate);
} }
private void AssertMetadataEnriched() private void AssertMetadataEnriched()

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryTests.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
[Fact] [Fact]
public async Task Should_query_asset_by_hash() public async Task Should_query_asset_by_hash()
{ {
var assets = await _.AssetRepository.QueryByHashAsync(_.RandomAppId(), _.RandomValue()); var assets = await _.AssetRepository.FindAssetAsync(_.RandomAppId(), _.RandomValue(), _.RandomValue(), 123);
Assert.NotNull(assets); Assert.NotNull(assets);
} }

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext)) A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext))
.Returns(enriched); .Returns(enriched);
var result = await sut.FindAssetAsync(requestContext, found.Id); var result = await sut.FindAsync(requestContext, found.Id);
Assert.Same(enriched, result); Assert.Same(enriched, result);
} }
@ -62,15 +62,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var enriched = new AssetEntity(); var enriched = new AssetEntity();
A.CallTo(() => assetRepository.QueryByHashAsync(appId.Id, "hash")) A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, "hash", "name", 123))
.Returns(new List<IAssetEntity> { found }); .Returns(found);
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found), requestContext)) A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext))
.Returns(new List<IEnrichedAssetEntity> { enriched }); .Returns(enriched);
var result = await sut.QueryByHashAsync(requestContext, appId.Id, "hash"); var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123);
Assert.Same(enriched, result.Single()); Assert.Same(enriched, result);
} }
[Fact] [Fact]

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._)) A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._))
.Returns(CreateContent(contentId)); .Returns(CreateContent(contentId));
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindContentAsync(ctx, schemaId.Name, contentId)); await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindAsync(ctx, schemaId.Name, contentId));
} }
[Fact] [Fact]
@ -126,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._)) A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._))
.Returns<IContentEntity?>(null); .Returns<IContentEntity?>(null);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindContentAsync(ctx, schemaId.Name, contentId)); await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindAsync(ctx, schemaId.Name, contentId));
} }
[Theory] [Theory]
@ -145,7 +145,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, scope)) A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, scope))
.Returns(content); .Returns(content);
var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId); var result = await sut.FindAsync(ctx, schemaId.Name, contentId);
Assert.Equal(contentTransformed, result!.Data); Assert.Equal(contentTransformed, result!.Data);
Assert.Equal(content.Id, result.Id); Assert.Equal(content.Id, result.Id);
@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, contentId, 13)) A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, contentId, 13))
.Returns(content); .Returns(content);
var result = await sut.FindContentAsync(ctx, schemaId.Name, contentId, 13); var result = await sut.FindAsync(ctx, schemaId.Name, contentId, 13);
Assert.Equal(contentTransformed, result!.Data); Assert.Equal(contentTransformed, result!.Data);
Assert.Equal(content.Id, result.Id); Assert.Equal(content.Id, result.Id);

Loading…
Cancel
Save