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"?> |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<Freezable /> |
|||
|
|||
<Equals /> |
|||
</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
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
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