mirror of https://github.com/Squidex/squidex.git
Browse Source
* Simplify the query code. * Custom deep equals. * Cleanup. * Json fix. * Equal fixed for custom equality comparer. * Better equals solution. * Fixespull/479/head
committed by
GitHub
107 changed files with 1761 additions and 1056 deletions
@ -1,4 +1,6 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
<Freezable /> |
<Freezable /> |
||||
|
|
||||
|
<Equals /> |
||||
</Weavers> |
</Weavers> |
||||
@ -1,270 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using MongoDB.Bson; |
|
||||
using MongoDB.Driver; |
|
||||
using NodaTime; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.Apps; |
|
||||
using Squidex.Domain.Apps.Entities.Contents; |
|
||||
using Squidex.Domain.Apps.Entities.Contents.State; |
|
||||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
|
||||
using Squidex.Domain.Apps.Entities.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Json; |
|
||||
using Squidex.Infrastructure.MongoDb; |
|
||||
using Squidex.Infrastructure.Queries; |
|
||||
using Squidex.Infrastructure.Reflection; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
|
||||
{ |
|
||||
internal class MongoContentCollection : MongoRepositoryBase<MongoContentEntity> |
|
||||
{ |
|
||||
private readonly IAppProvider appProvider; |
|
||||
private readonly IJsonSerializer serializer; |
|
||||
|
|
||||
public MongoContentCollection(IMongoDatabase database, IJsonSerializer serializer, IAppProvider appProvider) |
|
||||
: base(database) |
|
||||
{ |
|
||||
this.appProvider = appProvider; |
|
||||
|
|
||||
this.serializer = serializer; |
|
||||
} |
|
||||
|
|
||||
protected override Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default) |
|
||||
{ |
|
||||
return collection.Indexes.CreateManyAsync(new[] |
|
||||
{ |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.IndexedAppId) |
|
||||
.Ascending(x => x.IsDeleted) |
|
||||
.Ascending(x => x.Status) |
|
||||
.Descending(x => x.LastModified)), |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.IndexedAppId) |
|
||||
.Ascending(x => x.IsDeleted) |
|
||||
.Ascending(x => x.Status) |
|
||||
.Ascending(x => x.Id)), |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.IndexedSchemaId) |
|
||||
.Ascending(x => x.IsDeleted) |
|
||||
.Ascending(x => x.Status) |
|
||||
.Descending(x => x.LastModified)), |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.IndexedSchemaId) |
|
||||
.Ascending(x => x.IsDeleted) |
|
||||
.Ascending(x => x.Status) |
|
||||
.Ascending(x => x.Id)), |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.IndexedAppId) |
|
||||
.Ascending(x => x.Id)), |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.ScheduledAt) |
|
||||
.Ascending(x => x.IsDeleted)), |
|
||||
new CreateIndexModel<MongoContentEntity>(Index |
|
||||
.Ascending(x => x.ReferencedIds)) |
|
||||
}, ct); |
|
||||
} |
|
||||
|
|
||||
protected override string CollectionName() |
|
||||
{ |
|
||||
return "State_Contents"; |
|
||||
} |
|
||||
|
|
||||
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, ClrQuery query, List<Guid>? ids, Status[]? status, bool inDraft, bool includeDraft = true) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
query = query.AdjustToModel(schema.SchemaDef, inDraft); |
|
||||
|
|
||||
var filter = query.ToFilter(schema.Id, ids, status); |
|
||||
|
|
||||
var contentCount = Collection.Find(filter).CountDocumentsAsync(); |
|
||||
var contentItems = |
|
||||
Collection.Find(filter) |
|
||||
.WithoutDraft(includeDraft) |
|
||||
.ContentTake(query) |
|
||||
.ContentSkip(query) |
|
||||
.ContentSort(query) |
|
||||
.ToListAsync(); |
|
||||
|
|
||||
await Task.WhenAll(contentItems, contentCount); |
|
||||
|
|
||||
foreach (var entity in contentItems.Result) |
|
||||
{ |
|
||||
entity.ParseData(schema.SchemaDef, serializer); |
|
||||
} |
|
||||
|
|
||||
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result); |
|
||||
} |
|
||||
catch (MongoQueryException ex) |
|
||||
{ |
|
||||
if (ex.Message.Contains("17406")) |
|
||||
{ |
|
||||
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
throw; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, HashSet<Guid> ids, Status[]? status, bool includeDraft) |
|
||||
{ |
|
||||
var find = Collection.Find(FilterFactory.IdsByApp(app.Id, ids, status)); |
|
||||
|
|
||||
var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); |
|
||||
|
|
||||
var schemaIds = contentItems.Select(x => x.IndexedSchemaId).ToList(); |
|
||||
var schemas = await Task.WhenAll(schemaIds.Select(x => appProvider.GetSchemaAsync(app.Id, x))); |
|
||||
|
|
||||
var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); |
|
||||
|
|
||||
foreach (var entity in contentItems) |
|
||||
{ |
|
||||
var schema = schemas.FirstOrDefault(x => x?.Id == entity.IndexedSchemaId); |
|
||||
|
|
||||
if (schema != null) |
|
||||
{ |
|
||||
entity.ParseData(schema.SchemaDef, serializer); |
|
||||
|
|
||||
result.Add((entity, schema)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
public async Task<IResultList<IContentEntity>> QueryAsync(ISchemaEntity schema, HashSet<Guid> ids, Status[]? status, bool includeDraft) |
|
||||
{ |
|
||||
var find = Collection.Find(FilterFactory.IdsBySchema(schema.Id, ids, status)); |
|
||||
|
|
||||
var contentItems = await find.WithoutDraft(includeDraft).ToListAsync(); |
|
||||
|
|
||||
foreach (var entity in contentItems) |
|
||||
{ |
|
||||
entity.ParseData(schema.SchemaDef, serializer); |
|
||||
} |
|
||||
|
|
||||
return ResultList.Create<IContentEntity>(contentItems.Count, contentItems); |
|
||||
} |
|
||||
|
|
||||
public async Task<IContentEntity?> FindContentAsync(ISchemaEntity schema, Guid id, Status[]? status, bool includeDraft) |
|
||||
{ |
|
||||
var find = Collection.Find(x => x.Id == id); |
|
||||
|
|
||||
var contentEntity = await find.WithoutDraft(includeDraft).FirstOrDefaultAsync(); |
|
||||
|
|
||||
if (contentEntity != null) |
|
||||
{ |
|
||||
if (contentEntity.IndexedSchemaId != schema.Id || status?.Contains(contentEntity.Status) == false) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
contentEntity?.ParseData(schema.SchemaDef, serializer); |
|
||||
} |
|
||||
|
|
||||
return contentEntity; |
|
||||
} |
|
||||
|
|
||||
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback) |
|
||||
{ |
|
||||
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) |
|
||||
.Not(x => x.DataByIds) |
|
||||
.Not(x => x.DataDraftByIds) |
|
||||
.ForEachAsync(c => |
|
||||
{ |
|
||||
callback(c); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> filterNode) |
|
||||
{ |
|
||||
var filter = filterNode.AdjustToModel(schema.SchemaDef, true)?.ToFilter(schema.Id); |
|
||||
|
|
||||
var contentEntities = |
|
||||
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) |
|
||||
.ToListAsync(); |
|
||||
|
|
||||
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); |
|
||||
} |
|
||||
|
|
||||
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids) |
|
||||
{ |
|
||||
var contentEntities = |
|
||||
await Collection.Find(Filter.And(Filter.Eq(x => x.IndexedAppId, appId), Filter.In(x => x.Id, ids))).Only(x => x.Id, x => x.IndexedSchemaId) |
|
||||
.ToListAsync(); |
|
||||
|
|
||||
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); |
|
||||
} |
|
||||
|
|
||||
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId) |
|
||||
{ |
|
||||
var contentEntities = |
|
||||
await Collection.Find(x => x.IndexedAppId == appId).Only(x => x.Id) |
|
||||
.ToListAsync(); |
|
||||
|
|
||||
return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); |
|
||||
} |
|
||||
|
|
||||
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key, Func<Guid, Guid, Task<ISchemaEntity>> getSchema) |
|
||||
{ |
|
||||
var contentEntity = |
|
||||
await Collection.Find(x => x.Id == key) |
|
||||
.FirstOrDefaultAsync(); |
|
||||
|
|
||||
if (contentEntity != null) |
|
||||
{ |
|
||||
var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); |
|
||||
|
|
||||
contentEntity.ParseData(schema.SchemaDef, serializer); |
|
||||
|
|
||||
return (SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); |
|
||||
} |
|
||||
|
|
||||
return (null!, EtagVersion.NotFound); |
|
||||
} |
|
||||
|
|
||||
public Task ReadAllAsync(Func<ContentState, long, Task> callback, Func<Guid, Guid, Task<ISchemaEntity>> getSchema, CancellationToken ct = default) |
|
||||
{ |
|
||||
return Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(async contentEntity => |
|
||||
{ |
|
||||
var schema = await getSchema(contentEntity.IndexedAppId, contentEntity.IndexedSchemaId); |
|
||||
|
|
||||
contentEntity.ParseData(schema.SchemaDef, serializer); |
|
||||
|
|
||||
await callback(SimpleMapper.Map(contentEntity, new ContentState()), contentEntity.Version); |
|
||||
}, ct); |
|
||||
} |
|
||||
|
|
||||
public Task CleanupAsync(Guid id) |
|
||||
{ |
|
||||
return Collection.UpdateManyAsync( |
|
||||
Filter.And( |
|
||||
Filter.AnyEq(x => x.ReferencedIds, id), |
|
||||
Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), |
|
||||
Update.AddToSet(x => x.ReferencedIdsDeleted, id)); |
|
||||
} |
|
||||
|
|
||||
public Task RemoveAsync(Guid id) |
|
||||
{ |
|
||||
return Collection.DeleteOneAsync(x => x.Id == id); |
|
||||
} |
|
||||
|
|
||||
public Task UpsertAsync(MongoContentEntity content, long oldVersion) |
|
||||
{ |
|
||||
return Collection.UpsertVersionedAsync(content.Id, oldVersion, content); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
internal sealed class CleanupReferences : OperationBase |
||||
|
{ |
||||
|
protected override Task PrepareAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
var index = |
||||
|
new CreateIndexModel<MongoContentEntity>( |
||||
|
Index.Ascending(x => x.ReferencedIds)); |
||||
|
|
||||
|
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); |
||||
|
} |
||||
|
|
||||
|
public Task DoAsync(Guid id) |
||||
|
{ |
||||
|
return Collection.UpdateManyAsync( |
||||
|
Filter.And( |
||||
|
Filter.AnyEq(x => x.ReferencedIds, id), |
||||
|
Filter.AnyNe(x => x.ReferencedIdsDeleted, id)), |
||||
|
Update.AddToSet(x => x.ReferencedIdsDeleted, id)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
public abstract class OperationBase |
||||
|
{ |
||||
|
protected static readonly SortDefinitionBuilder<MongoContentEntity> Sort = Builders<MongoContentEntity>.Sort; |
||||
|
protected static readonly UpdateDefinitionBuilder<MongoContentEntity> Update = Builders<MongoContentEntity>.Update; |
||||
|
protected static readonly FieldDefinitionBuilder<MongoContentEntity> Fields = FieldDefinitionBuilder<MongoContentEntity>.Instance; |
||||
|
protected static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter; |
||||
|
protected static readonly IndexKeysDefinitionBuilder<MongoContentEntity> Index = Builders<MongoContentEntity>.IndexKeys; |
||||
|
protected static readonly ProjectionDefinitionBuilder<MongoContentEntity> Projection = Builders<MongoContentEntity>.Projection; |
||||
|
|
||||
|
public IMongoCollection<MongoContentEntity> Collection { get; private set; } |
||||
|
|
||||
|
public Task PrepareAsync(IMongoCollection<MongoContentEntity> collection, CancellationToken ct = default) |
||||
|
{ |
||||
|
Collection = collection; |
||||
|
|
||||
|
return PrepareAsync(ct); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task PrepareAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
return TaskHelper.Done; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
internal sealed class QueryContent : OperationBase |
||||
|
{ |
||||
|
private readonly IJsonSerializer serializer; |
||||
|
|
||||
|
public QueryContent(IJsonSerializer serializer) |
||||
|
{ |
||||
|
this.serializer = serializer; |
||||
|
} |
||||
|
|
||||
|
public async Task<IContentEntity?> DoAsync(ISchemaEntity schema, Guid id, Status[]? status, bool includeDraft) |
||||
|
{ |
||||
|
Guard.NotNull(schema); |
||||
|
|
||||
|
var find = Collection.Find(x => x.Id == id).WithoutDraft(includeDraft); |
||||
|
|
||||
|
var contentEntity = await find.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (contentEntity != null) |
||||
|
{ |
||||
|
if (contentEntity.IndexedSchemaId != schema.Id || !contentEntity.HasStatus(status)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
contentEntity?.ParseData(schema.SchemaDef, serializer); |
||||
|
} |
||||
|
|
||||
|
return contentEntity; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,128 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
internal sealed class QueryContentsByIds : OperationBase |
||||
|
{ |
||||
|
private readonly IJsonSerializer serializer; |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
public QueryContentsByIds(IJsonSerializer serializer, IAppProvider appProvider) |
||||
|
{ |
||||
|
this.serializer = serializer; |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
} |
||||
|
|
||||
|
protected override Task PrepareAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
var index = |
||||
|
new CreateIndexModel<MongoContentEntity>(Index |
||||
|
.Ascending(x => x.IndexedAppId) |
||||
|
.Ascending(x => x.IsDeleted) |
||||
|
.Ascending(x => x.Status) |
||||
|
.Descending(x => x.LastModified)); |
||||
|
|
||||
|
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> DoAsync(Guid appId, ISchemaEntity? schema, HashSet<Guid> ids, Status[]? status, bool includeDraft) |
||||
|
{ |
||||
|
Guard.NotNull(ids); |
||||
|
|
||||
|
var find = Collection.Find(CreateFilter(appId, ids, status)).WithoutDraft(includeDraft); |
||||
|
|
||||
|
var contentItems = await find.ToListAsync(); |
||||
|
var contentSchemas = await GetSchemasAsync(appId, schema, contentItems); |
||||
|
|
||||
|
var result = new List<(IContentEntity Content, ISchemaEntity Schema)>(); |
||||
|
|
||||
|
foreach (var contentEntity in contentItems) |
||||
|
{ |
||||
|
if (contentEntity.HasStatus(status) && contentSchemas.TryGetValue(contentEntity.IndexedSchemaId, out var contentSchema)) |
||||
|
{ |
||||
|
contentEntity.ParseData(contentSchema.SchemaDef, serializer); |
||||
|
|
||||
|
result.Add((contentEntity, contentSchema)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private async Task<IDictionary<Guid, ISchemaEntity>> GetSchemasAsync(Guid appId, ISchemaEntity? schema, List<MongoContentEntity> contentItems) |
||||
|
{ |
||||
|
var schemas = new Dictionary<Guid, ISchemaEntity>(); |
||||
|
|
||||
|
if (schema != null) |
||||
|
{ |
||||
|
schemas[schema.Id] = schema; |
||||
|
} |
||||
|
|
||||
|
var misingSchemaIds = contentItems.Select(x => x.IndexedSchemaId).Distinct().Where(x => !schemas.ContainsKey(x)); |
||||
|
var missingSchemas = await Task.WhenAll(misingSchemaIds.Select(x => appProvider.GetSchemaAsync(appId, x))); |
||||
|
|
||||
|
foreach (var missingSchema in missingSchemas) |
||||
|
{ |
||||
|
schemas[missingSchema.Id] = missingSchema; |
||||
|
} |
||||
|
|
||||
|
return schemas; |
||||
|
} |
||||
|
|
||||
|
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid appId, ICollection<Guid> ids, Status[]? status) |
||||
|
{ |
||||
|
var filters = new List<FilterDefinition<MongoContentEntity>> |
||||
|
{ |
||||
|
Filter.Eq(x => x.IndexedAppId, appId), |
||||
|
Filter.Ne(x => x.IsDeleted, true) |
||||
|
}; |
||||
|
|
||||
|
if (status != null) |
||||
|
{ |
||||
|
filters.Add(Filter.In(x => x.Status, status)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
filters.Add(Filter.Exists(x => x.Status)); |
||||
|
} |
||||
|
|
||||
|
if (ids != null && ids.Count > 0) |
||||
|
{ |
||||
|
if (ids.Count > 1) |
||||
|
{ |
||||
|
filters.Add( |
||||
|
Filter.Or( |
||||
|
Filter.In(x => x.Id, ids))); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var first = ids.First(); |
||||
|
|
||||
|
filters.Add( |
||||
|
Filter.Or( |
||||
|
Filter.Eq(x => x.Id, first))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Filter.And(filters); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,159 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Apps; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Text; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
using Squidex.Infrastructure.MongoDb.Queries; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
internal sealed class QueryContentsByQuery : OperationBase |
||||
|
{ |
||||
|
private readonly IJsonSerializer serializer; |
||||
|
private readonly ITextIndexer indexer; |
||||
|
|
||||
|
public QueryContentsByQuery(IJsonSerializer serializer, ITextIndexer indexer) |
||||
|
{ |
||||
|
this.serializer = serializer; |
||||
|
|
||||
|
this.indexer = indexer; |
||||
|
} |
||||
|
|
||||
|
protected override Task PrepareAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
var index1 = |
||||
|
new CreateIndexModel<MongoContentEntity>(Index |
||||
|
.Ascending(x => x.IndexedSchemaId) |
||||
|
.Ascending(x => x.IsDeleted) |
||||
|
.Ascending(x => x.Status) |
||||
|
.Ascending(x => x.Id) |
||||
|
.Ascending(x => x.ReferencedIds) |
||||
|
.Descending(x => x.LastModified)); |
||||
|
|
||||
|
var index2 = |
||||
|
new CreateIndexModel<MongoContentEntity>(Index |
||||
|
.Ascending(x => x.IndexedSchemaId) |
||||
|
.Ascending(x => x.IsDeleted) |
||||
|
.Ascending(x => x.Status) |
||||
|
.Descending(x => x.LastModified)); |
||||
|
|
||||
|
return Collection.Indexes.CreateManyAsync(new[] { index1, index2, }, ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<IResultList<IContentEntity>> DoAsync(IAppEntity app, ISchemaEntity schema, ClrQuery query, Status[]? status, bool inDraft, bool includeDraft = true) |
||||
|
{ |
||||
|
Guard.NotNull(app); |
||||
|
Guard.NotNull(schema); |
||||
|
Guard.NotNull(query); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
query = query.AdjustToModel(schema.SchemaDef, inDraft); |
||||
|
|
||||
|
List<Guid>? fullTextIds = null; |
||||
|
|
||||
|
if (!string.IsNullOrWhiteSpace(query.FullText)) |
||||
|
{ |
||||
|
fullTextIds = await indexer.SearchAsync(query.FullText, app, schema.Id, inDraft ? Scope.Draft : Scope.Published); |
||||
|
|
||||
|
if (fullTextIds?.Count == 0) |
||||
|
{ |
||||
|
return ResultList.CreateFrom<IContentEntity>(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var filter = CreateFilter(schema.Id, fullTextIds, status, query); |
||||
|
|
||||
|
var contentCount = Collection.Find(filter).CountDocumentsAsync(); |
||||
|
var contentItems = |
||||
|
Collection.Find(filter) |
||||
|
.WithoutDraft(includeDraft) |
||||
|
.Take(query) |
||||
|
.Skip(query) |
||||
|
.Sort(query) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
await Task.WhenAll(contentItems, contentCount); |
||||
|
|
||||
|
foreach (var entity in contentItems.Result) |
||||
|
{ |
||||
|
entity.ParseData(schema.SchemaDef, serializer); |
||||
|
} |
||||
|
|
||||
|
return ResultList.Create<IContentEntity>(contentCount.Result, contentItems.Result); |
||||
|
} |
||||
|
catch (MongoQueryException ex) |
||||
|
{ |
||||
|
if (ex.Message.Contains("17406")) |
||||
|
{ |
||||
|
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items."); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid schemaId, ICollection<Guid>? ids, Status[]? status, ClrQuery? query) |
||||
|
{ |
||||
|
var filters = new List<FilterDefinition<MongoContentEntity>> |
||||
|
{ |
||||
|
Filter.Eq(x => x.IndexedSchemaId, schemaId), |
||||
|
Filter.Ne(x => x.IsDeleted, true) |
||||
|
}; |
||||
|
|
||||
|
if (status != null) |
||||
|
{ |
||||
|
filters.Add(Filter.In(x => x.Status, status)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
filters.Add(Filter.Exists(x => x.Status)); |
||||
|
} |
||||
|
|
||||
|
if (ids != null && ids.Count > 0) |
||||
|
{ |
||||
|
if (ids.Count > 1) |
||||
|
{ |
||||
|
filters.Add( |
||||
|
Filter.Or( |
||||
|
Filter.In(x => x.Id, ids), |
||||
|
Filter.AnyIn(x => x.ReferencedIds, ids))); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var first = ids.First(); |
||||
|
|
||||
|
filters.Add( |
||||
|
Filter.Or( |
||||
|
Filter.Eq(x => x.Id, first), |
||||
|
Filter.AnyEq(x => x.ReferencedIds, first))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (query?.Filter != null) |
||||
|
{ |
||||
|
filters.Add(query.Filter.BuildFilter<MongoContentEntity>()); |
||||
|
} |
||||
|
|
||||
|
return Filter.And(filters); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
using Squidex.Infrastructure.MongoDb.Queries; |
||||
|
using Squidex.Infrastructure.Queries; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
internal sealed class QueryIdsAsync : OperationBase |
||||
|
{ |
||||
|
private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>(); |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
public QueryIdsAsync(IAppProvider appProvider) |
||||
|
{ |
||||
|
this.appProvider = appProvider; |
||||
|
} |
||||
|
|
||||
|
protected override Task PrepareAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
var index1 = |
||||
|
new CreateIndexModel<MongoContentEntity>(Index |
||||
|
.Ascending(x => x.IndexedAppId) |
||||
|
.Ascending(x => x.Id) |
||||
|
.Ascending(x => x.ReferencedIds)); |
||||
|
|
||||
|
var index2 = |
||||
|
new CreateIndexModel<MongoContentEntity>(Index |
||||
|
.Ascending(x => x.IndexedSchemaId)); |
||||
|
|
||||
|
return Collection.Indexes.CreateManyAsync(new[] { index1, index2, }, ct); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> DoAsync(Guid appId, HashSet<Guid> ids) |
||||
|
{ |
||||
|
var contentEntities = |
||||
|
await Collection.Find(Filter.And(Filter.Eq(x => x.IndexedAppId, appId), Filter.In(x => x.Id, ids))).Only(x => x.Id, x => x.IndexedSchemaId) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> DoAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode) |
||||
|
{ |
||||
|
var schema = await appProvider.GetSchemaAsync(appId, schemaId); |
||||
|
|
||||
|
if (schema == null) |
||||
|
{ |
||||
|
return EmptyIds; |
||||
|
} |
||||
|
|
||||
|
var filter = BuildFilter(filterNode.AdjustToModel(schema.SchemaDef, true), schemaId); |
||||
|
|
||||
|
var contentEntities = |
||||
|
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) |
||||
|
.ToListAsync(); |
||||
|
|
||||
|
return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); |
||||
|
} |
||||
|
|
||||
|
public static FilterDefinition<MongoContentEntity> BuildFilter(FilterNode<ClrValue>? filterNode, Guid schemaId) |
||||
|
{ |
||||
|
var filters = new List<FilterDefinition<MongoContentEntity>> |
||||
|
{ |
||||
|
Filter.Eq(x => x.IndexedSchemaId, schemaId), |
||||
|
Filter.Ne(x => x.IsDeleted, true), |
||||
|
}; |
||||
|
|
||||
|
if (filterNode != null) |
||||
|
{ |
||||
|
filters.Add(filterNode.BuildFilter<MongoContentEntity>()); |
||||
|
} |
||||
|
|
||||
|
return Filter.And(filters); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using NodaTime; |
||||
|
using Squidex.Domain.Apps.Entities.Contents; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations |
||||
|
{ |
||||
|
internal sealed class QueryScheduledContents : OperationBase |
||||
|
{ |
||||
|
protected override Task PrepareAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
var index = |
||||
|
new CreateIndexModel<MongoContentEntity>(Index |
||||
|
.Ascending(x => x.ScheduledAt) |
||||
|
.Ascending(x => x.IsDeleted)); |
||||
|
|
||||
|
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); |
||||
|
} |
||||
|
|
||||
|
public Task DoAsync(Instant now, Func<IContentEntity, Task> callback) |
||||
|
{ |
||||
|
Guard.NotNull(callback); |
||||
|
|
||||
|
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true) |
||||
|
.Not(x => x.DataByIds) |
||||
|
.Not(x => x.DataDraftByIds) |
||||
|
.ForEachAsync(c => |
||||
|
{ |
||||
|
callback(c); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,134 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using MongoDB.Driver; |
|
||||
using Squidex.Domain.Apps.Core.Contents; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Infrastructure.MongoDb; |
|
||||
using Squidex.Infrastructure.MongoDb.Queries; |
|
||||
using Squidex.Infrastructure.Queries; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors |
|
||||
{ |
|
||||
public static class FilterFactory |
|
||||
{ |
|
||||
private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter; |
|
||||
|
|
||||
public static ClrQuery AdjustToModel(this ClrQuery query, Schema schema, bool useDraft) |
|
||||
{ |
|
||||
var pathConverter = Adapt.Path(schema, useDraft); |
|
||||
|
|
||||
if (query.Filter != null) |
|
||||
{ |
|
||||
query.Filter = query.Filter.Accept(new AdaptionVisitor(pathConverter)); |
|
||||
} |
|
||||
|
|
||||
query.Sort = query.Sort.Select(x => new SortNode(pathConverter(x.Path), x.Order)).ToList(); |
|
||||
|
|
||||
return query; |
|
||||
} |
|
||||
|
|
||||
public static FilterNode<ClrValue>? AdjustToModel(this FilterNode<ClrValue> filterNode, Schema schema, bool useDraft) |
|
||||
{ |
|
||||
var pathConverter = Adapt.Path(schema, useDraft); |
|
||||
|
|
||||
return filterNode.Accept(new AdaptionVisitor(pathConverter)); |
|
||||
} |
|
||||
|
|
||||
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query) |
|
||||
{ |
|
||||
return cursor.Sort(query.BuildSort<MongoContentEntity>()); |
|
||||
} |
|
||||
|
|
||||
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query) |
|
||||
{ |
|
||||
return cursor.Take(query); |
|
||||
} |
|
||||
|
|
||||
public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ClrQuery query) |
|
||||
{ |
|
||||
return cursor.Skip(query); |
|
||||
} |
|
||||
|
|
||||
public static IFindFluent<MongoContentEntity, MongoContentEntity> WithoutDraft(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, bool includeDraft) |
|
||||
{ |
|
||||
return !includeDraft ? cursor.Not(x => x.DataDraftByIds, x => x.IsDeleted) : cursor; |
|
||||
} |
|
||||
|
|
||||
public static FilterDefinition<MongoContentEntity> IdsByApp(Guid appId, ICollection<Guid> ids, Status[]? status) |
|
||||
{ |
|
||||
return CreateFilter(appId, null, ids, status, null); |
|
||||
} |
|
||||
|
|
||||
public static FilterDefinition<MongoContentEntity> IdsBySchema(Guid schemaId, ICollection<Guid> ids, Status[]? status) |
|
||||
{ |
|
||||
return CreateFilter(null, schemaId, ids, status, null); |
|
||||
} |
|
||||
|
|
||||
public static FilterDefinition<MongoContentEntity> ToFilter(this ClrQuery query, Guid schemaId, ICollection<Guid>? ids, Status[]? status) |
|
||||
{ |
|
||||
return CreateFilter(null, schemaId, ids, status, query); |
|
||||
} |
|
||||
|
|
||||
private static FilterDefinition<MongoContentEntity> CreateFilter(Guid? appId, Guid? schemaId, ICollection<Guid>? ids, Status[]? status, |
|
||||
ClrQuery? query) |
|
||||
{ |
|
||||
var filters = new List<FilterDefinition<MongoContentEntity>>(); |
|
||||
|
|
||||
if (appId.HasValue) |
|
||||
{ |
|
||||
filters.Add(Filter.Eq(x => x.IndexedAppId, appId.Value)); |
|
||||
} |
|
||||
|
|
||||
if (schemaId.HasValue) |
|
||||
{ |
|
||||
filters.Add(Filter.Eq(x => x.IndexedSchemaId, schemaId.Value)); |
|
||||
} |
|
||||
|
|
||||
filters.Add(Filter.Ne(x => x.IsDeleted, true)); |
|
||||
|
|
||||
if (status != null) |
|
||||
{ |
|
||||
filters.Add(Filter.In(x => x.Status, status)); |
|
||||
} |
|
||||
|
|
||||
if (ids != null && ids.Count > 0) |
|
||||
{ |
|
||||
if (ids.Count > 1) |
|
||||
{ |
|
||||
filters.Add(Filter.In(x => x.Id, ids)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
filters.Add(Filter.Eq(x => x.Id, ids.First())); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
if (query?.Filter != null) |
|
||||
{ |
|
||||
filters.Add(query.Filter.BuildFilter<MongoContentEntity>()); |
|
||||
} |
|
||||
|
|
||||
return Filter.And(filters); |
|
||||
} |
|
||||
|
|
||||
public static FilterDefinition<MongoContentEntity> ToFilter(this FilterNode<ClrValue> filterNode, Guid schemaId) |
|
||||
{ |
|
||||
var filters = new List<FilterDefinition<MongoContentEntity>> |
|
||||
{ |
|
||||
Filter.Eq(x => x.IndexedSchemaId, schemaId), |
|
||||
Filter.Ne(x => x.IsDeleted, true), |
|
||||
filterNode.BuildFilter<MongoContentEntity>() |
|
||||
}; |
|
||||
|
|
||||
return Filter.And(filters); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,3 @@ |
|||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
||||
|
<Equals /> |
||||
|
</Weavers> |
||||
@ -0,0 +1,26 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
||||
|
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
||||
|
<xs:element name="Weavers"> |
||||
|
<xs:complexType> |
||||
|
<xs:all> |
||||
|
<xs:element name="Equals" minOccurs="0" maxOccurs="1" type="xs:anyType" /> |
||||
|
</xs:all> |
||||
|
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
||||
|
<xs:annotation> |
||||
|
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
||||
|
</xs:annotation> |
||||
|
</xs:attribute> |
||||
|
</xs:complexType> |
||||
|
</xs:element> |
||||
|
</xs:schema> |
||||
@ -0,0 +1,40 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
internal sealed class ArrayComparer<T> : IDeepComparer |
||||
|
{ |
||||
|
private readonly IDeepComparer itemComparer; |
||||
|
|
||||
|
public ArrayComparer(IDeepComparer itemComparer) |
||||
|
{ |
||||
|
this.itemComparer = itemComparer; |
||||
|
} |
||||
|
|
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
var lhs = (T[])x!; |
||||
|
var rhs = (T[])y!; |
||||
|
|
||||
|
if (lhs.Length != rhs.Length) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < lhs.Length; i++) |
||||
|
{ |
||||
|
if (!itemComparer.IsEquals(lhs[i], rhs[i])) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections; |
||||
|
using Squidex.Infrastructure.Reflection.Internal; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
internal sealed class CollectionComparer : IDeepComparer |
||||
|
{ |
||||
|
private readonly IDeepComparer itemComparer; |
||||
|
private readonly PropertyAccessor? sizeProperty; |
||||
|
|
||||
|
public CollectionComparer(IDeepComparer itemComparer, PropertyAccessor? sizeProperty) |
||||
|
{ |
||||
|
this.itemComparer = itemComparer; |
||||
|
this.sizeProperty = sizeProperty; |
||||
|
} |
||||
|
|
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
var lhs = (IEnumerable)x!; |
||||
|
var rhs = (IEnumerable)y!; |
||||
|
|
||||
|
if (sizeProperty != null) |
||||
|
{ |
||||
|
var sizeLhs = sizeProperty.Get(lhs); |
||||
|
var sizeRhs = sizeProperty.Get(rhs); |
||||
|
|
||||
|
if (!Equals(sizeLhs, sizeRhs)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var enumeratorLhs = lhs.GetEnumerator(); |
||||
|
var enumeratorRhs = rhs.GetEnumerator(); |
||||
|
|
||||
|
while (true) |
||||
|
{ |
||||
|
var movedLhs = enumeratorLhs.MoveNext(); |
||||
|
var movedRhs = enumeratorRhs.MoveNext(); |
||||
|
|
||||
|
if (movedRhs != movedLhs) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (movedRhs) |
||||
|
{ |
||||
|
if (!itemComparer.IsEquals(enumeratorLhs.Current, enumeratorRhs.Current)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
internal sealed class DefaultComparer : IDeepComparer |
||||
|
{ |
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
if (Equals(x, y)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (x == null || y == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var type = x.GetType(); |
||||
|
|
||||
|
if (type != y.GetType()) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var inner = SimpleEquals.BuildInner(type); |
||||
|
|
||||
|
return inner.IsEquals(x, y); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
internal sealed class DictionaryComparer<TKey, TValue> : IDeepComparer where TKey : notnull |
||||
|
{ |
||||
|
private readonly IEqualityComparer<KeyValuePair<TKey, TValue>> comparer; |
||||
|
|
||||
|
public DictionaryComparer(IDeepComparer comparer) |
||||
|
{ |
||||
|
this.comparer = new CollectionExtensions.KeyValuePairComparer<TKey, TValue>( |
||||
|
new DeepEqualityComparer<TKey>(comparer), |
||||
|
new DeepEqualityComparer<TValue>(comparer)); |
||||
|
} |
||||
|
|
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
var lhs = (IReadOnlyDictionary<TKey, TValue>)x!; |
||||
|
var rhs = (IReadOnlyDictionary<TKey, TValue>)y!; |
||||
|
|
||||
|
if (lhs.Count != rhs.Count) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return !lhs.Except(rhs, comparer).Any(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
public interface IDeepComparer |
||||
|
{ |
||||
|
bool IsEquals(object? x, object? y); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
internal sealed class NoopComparer : IDeepComparer |
||||
|
{ |
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using Squidex.Infrastructure.Reflection.Internal; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
public sealed class ObjectComparer : IDeepComparer |
||||
|
{ |
||||
|
private readonly PropertyAccessor[] propertyAccessors; |
||||
|
private readonly IDeepComparer valueComparer; |
||||
|
|
||||
|
public ObjectComparer(IDeepComparer valueComparer, Type type) |
||||
|
{ |
||||
|
propertyAccessors = |
||||
|
type.GetPublicProperties() |
||||
|
.Where(x => x.CanRead) |
||||
|
.Where(x => x.GetCustomAttribute<IgnoreEqualsAttribute>() == null) |
||||
|
.Select(x => new PropertyAccessor(x.DeclaringType!, x)) |
||||
|
.ToArray(); |
||||
|
|
||||
|
this.valueComparer = valueComparer; |
||||
|
} |
||||
|
|
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
for (var i = 0; i < propertyAccessors.Length; i++) |
||||
|
{ |
||||
|
var property = propertyAccessors[i]; |
||||
|
|
||||
|
var lhs = property.Get(x!); |
||||
|
var rhs = property.Get(y!); |
||||
|
|
||||
|
if (!valueComparer.IsEquals(lhs, rhs)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection.Equality |
||||
|
{ |
||||
|
internal sealed class SetComparer<T> : IDeepComparer |
||||
|
{ |
||||
|
private readonly IEqualityComparer<T> equalityComparer; |
||||
|
|
||||
|
public SetComparer(IDeepComparer comparer) |
||||
|
{ |
||||
|
equalityComparer = new DeepEqualityComparer<T>(comparer); |
||||
|
} |
||||
|
|
||||
|
public bool IsEquals(object? x, object? y) |
||||
|
{ |
||||
|
var lhs = (ISet<T>)x!; |
||||
|
var rhs = (ISet<T>)y!; |
||||
|
|
||||
|
if (lhs.Count != rhs.Count) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return lhs.Intersect(rhs, equalityComparer).Count() == rhs.Count; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,16 +1,16 @@ |
|||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Squidex Headless CMS
|
// Squidex Headless CMS
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
// All rights reserved. Licensed under the MIT license.
|
// All rights reserved. Licensed under the MIT license.
|
||||
// ==========================================================================
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
namespace Squidex.Infrastructure.Reflection |
namespace Squidex.Infrastructure.Reflection |
||||
{ |
{ |
||||
public interface IPropertyAccessor |
[AttributeUsage(AttributeTargets.Property)] |
||||
|
public sealed class IgnoreEqualsAttribute : Attribute |
||||
{ |
{ |
||||
object? Get(object target); |
|
||||
|
|
||||
void Set(object target, object? value); |
|
||||
} |
} |
||||
} |
} |
||||
@ -1,78 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Concurrent; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Reflection; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Reflection |
|
||||
{ |
|
||||
public sealed class PropertiesTypeAccessor |
|
||||
{ |
|
||||
private static readonly ConcurrentDictionary<Type, PropertiesTypeAccessor> AccessorCache = new ConcurrentDictionary<Type, PropertiesTypeAccessor>(); |
|
||||
private readonly Dictionary<string, IPropertyAccessor> accessors = new Dictionary<string, IPropertyAccessor>(); |
|
||||
private readonly List<PropertyInfo> properties = new List<PropertyInfo>(); |
|
||||
|
|
||||
public IEnumerable<PropertyInfo> Properties |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
return properties; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private PropertiesTypeAccessor(Type type) |
|
||||
{ |
|
||||
var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); |
|
||||
|
|
||||
foreach (var property in allProperties) |
|
||||
{ |
|
||||
accessors[property.Name] = new PropertyAccessor(type, property); |
|
||||
|
|
||||
properties.Add(property); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static PropertiesTypeAccessor Create(Type targetType) |
|
||||
{ |
|
||||
Guard.NotNull(targetType); |
|
||||
|
|
||||
return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x)); |
|
||||
} |
|
||||
|
|
||||
public void SetValue(object target, string propertyName, object? value) |
|
||||
{ |
|
||||
Guard.NotNull(target); |
|
||||
|
|
||||
var accessor = FindAccessor(propertyName); |
|
||||
|
|
||||
accessor.Set(target, value); |
|
||||
} |
|
||||
|
|
||||
public object? GetValue(object target, string propertyName) |
|
||||
{ |
|
||||
Guard.NotNull(target); |
|
||||
|
|
||||
var accessor = FindAccessor(propertyName); |
|
||||
|
|
||||
return accessor.Get(target); |
|
||||
} |
|
||||
|
|
||||
private IPropertyAccessor FindAccessor(string propertyName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(propertyName); |
|
||||
|
|
||||
if (!accessors.TryGetValue(propertyName, out var accessor)) |
|
||||
{ |
|
||||
throw new ArgumentException("Property does not exist.", nameof(propertyName)); |
|
||||
} |
|
||||
|
|
||||
return accessor; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,84 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
|
|
||||
#pragma warning disable RECS0108 // Warns about static fields in generic types
|
|
||||
|
|
||||
namespace Squidex.Infrastructure.Reflection |
|
||||
{ |
|
||||
public static class SimpleCopier |
|
||||
{ |
|
||||
private struct PropertyMapper |
|
||||
{ |
|
||||
private readonly IPropertyAccessor accessor; |
|
||||
private readonly Func<object?, object?> converter; |
|
||||
|
|
||||
public PropertyMapper(IPropertyAccessor accessor, Func<object?, object?> converter) |
|
||||
{ |
|
||||
this.accessor = accessor; |
|
||||
this.converter = converter; |
|
||||
} |
|
||||
|
|
||||
public void MapProperty(object source, object target) |
|
||||
{ |
|
||||
var value = converter(accessor.Get(source)); |
|
||||
|
|
||||
accessor.Set(target, value); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static class ClassCopier<T> where T : class, new() |
|
||||
{ |
|
||||
private static readonly List<PropertyMapper> Mappers = new List<PropertyMapper>(); |
|
||||
|
|
||||
static ClassCopier() |
|
||||
{ |
|
||||
var type = typeof(T); |
|
||||
|
|
||||
foreach (var property in type.GetPublicProperties()) |
|
||||
{ |
|
||||
if (!property.CanWrite || !property.CanRead) |
|
||||
{ |
|
||||
continue; |
|
||||
} |
|
||||
|
|
||||
var accessor = new PropertyAccessor(type, property); |
|
||||
|
|
||||
if (property.PropertyType.Implements<ICloneable>()) |
|
||||
{ |
|
||||
Mappers.Add(new PropertyMapper(accessor, x => ((ICloneable)x!)?.Clone())); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
Mappers.Add(new PropertyMapper(accessor, x => x)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static T CopyThis(T source) |
|
||||
{ |
|
||||
var destination = new T(); |
|
||||
|
|
||||
foreach (var mapper in Mappers) |
|
||||
{ |
|
||||
mapper.MapProperty(source, destination); |
|
||||
} |
|
||||
|
|
||||
return destination; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public static T Copy<T>(this T source) where T : class, new() |
|
||||
{ |
|
||||
Guard.NotNull(source); |
|
||||
|
|
||||
return ClassCopier<T>.CopyThis(source); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,121 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Squidex.Infrastructure.Reflection.Equality; |
||||
|
using Squidex.Infrastructure.Reflection.Internal; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Reflection |
||||
|
{ |
||||
|
public static class SimpleEquals |
||||
|
{ |
||||
|
private static readonly ConcurrentDictionary<Type, IDeepComparer> Comparers = new ConcurrentDictionary<Type, IDeepComparer>(); |
||||
|
private static readonly DefaultComparer DefaultComparer = new DefaultComparer(); |
||||
|
private static readonly NoopComparer NoopComparer = new NoopComparer(); |
||||
|
|
||||
|
internal static IDeepComparer Build(Type type) |
||||
|
{ |
||||
|
return BuildCore(type) ?? DefaultComparer; |
||||
|
} |
||||
|
|
||||
|
internal static IDeepComparer BuildInner(Type type) |
||||
|
{ |
||||
|
return BuildCore(type) ?? NoopComparer; |
||||
|
} |
||||
|
|
||||
|
private static IDeepComparer BuildCore(Type t) |
||||
|
{ |
||||
|
return Comparers.GetOrAdd(t, type => |
||||
|
{ |
||||
|
if (IsSimpleType(type) || IsEquatable(type)) |
||||
|
{ |
||||
|
return NoopComparer; |
||||
|
} |
||||
|
|
||||
|
if (IsArray(type)) |
||||
|
{ |
||||
|
var comparerType = typeof(ArrayComparer<>).MakeGenericType(type.GetElementType()!); |
||||
|
|
||||
|
return (IDeepComparer)Activator.CreateInstance(comparerType, DefaultComparer)!; |
||||
|
} |
||||
|
|
||||
|
if (IsSet(type)) |
||||
|
{ |
||||
|
var comparerType = typeof(SetComparer<>).MakeGenericType(type.GetGenericArguments()); |
||||
|
|
||||
|
return (IDeepComparer)Activator.CreateInstance(comparerType, DefaultComparer)!; |
||||
|
} |
||||
|
|
||||
|
if (IsDictionary(type)) |
||||
|
{ |
||||
|
var comparerType = typeof(DictionaryComparer<,>).MakeGenericType(type.GetGenericArguments()); |
||||
|
|
||||
|
return (IDeepComparer)Activator.CreateInstance(comparerType, DefaultComparer)!; |
||||
|
} |
||||
|
|
||||
|
if (IsCollection(type)) |
||||
|
{ |
||||
|
PropertyAccessor? count = null; |
||||
|
|
||||
|
var countProperty = type.GetProperty("Count"); |
||||
|
|
||||
|
if (countProperty != null && countProperty.PropertyType == typeof(int)) |
||||
|
{ |
||||
|
count = new PropertyAccessor(type, countProperty); |
||||
|
} |
||||
|
|
||||
|
return (IDeepComparer)Activator.CreateInstance(typeof(CollectionComparer), DefaultComparer, count)!; |
||||
|
} |
||||
|
|
||||
|
return new ObjectComparer(DefaultComparer, type); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private static bool IsArray(Type type) |
||||
|
{ |
||||
|
return type.IsArray; |
||||
|
} |
||||
|
|
||||
|
private static bool IsCollection(Type type) |
||||
|
{ |
||||
|
return type.GetInterfaces().Contains(typeof(IEnumerable)); |
||||
|
} |
||||
|
|
||||
|
private static bool IsSimpleType(Type type) |
||||
|
{ |
||||
|
return type.IsValueType || type == typeof(string); |
||||
|
} |
||||
|
|
||||
|
private static bool IsEquatable(Type type) |
||||
|
{ |
||||
|
return type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEquatable<>)); |
||||
|
} |
||||
|
|
||||
|
private static bool IsSet(Type type) |
||||
|
{ |
||||
|
return |
||||
|
type.GetGenericArguments().Length == 1 && |
||||
|
type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ISet<>)); |
||||
|
} |
||||
|
|
||||
|
private static bool IsDictionary(Type type) |
||||
|
{ |
||||
|
return |
||||
|
type.GetGenericArguments().Length == 2 && |
||||
|
type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)); |
||||
|
} |
||||
|
|
||||
|
public static bool IsEquals<T>(T x, T y) |
||||
|
{ |
||||
|
return DefaultComparer.IsEquals(x, y); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue