From 1c182ae47f76c38b7e555c5f709656764bd7052c Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 3 Mar 2017 23:57:22 +0100 Subject: [PATCH] Performance improvements --- src/Squidex.Core/Schemas/Field.cs | 2 +- src/Squidex.Core/Schemas/StringFieldEditor.cs | 2 +- .../Json/ConverterContractResolver.cs | 44 ++++++++++ .../Contents/MongoContentRepository.cs | 35 ++++---- .../MongoContentRepository_EventHandling.cs | 8 +- .../Contents/Visitors/EdmModelExtensions.cs | 27 ++++++ .../Contents/Builders/EdmModelBuilder.cs} | 47 +++++++---- src/Squidex.Read/Squidex.Read.csproj | 5 ++ src/Squidex/Config/Domain/ReadModule.cs | 5 ++ src/Squidex/Config/Domain/Serializers.cs | 20 ++--- .../Pipeline/ApiExceptionFilterAttribute.cs | 6 ++ .../Schemas/DateTimeFieldPropertiesTests.cs | 2 +- .../Schemas/DateTimeFieldTests.cs | 14 +++- .../Schemas/JsonFieldTests.cs | 6 +- .../Squidex.Core.Tests/Schemas/SchemaTests.cs | 19 +++-- .../Schemas/SchemaValidationTests.cs | 10 +-- .../CQRS/Commands/AggregateHandlerTests.cs | 12 +++ .../Json/ConverterContractResolverTests.cs | 84 +++++++++++++++++++ .../Json/InstantConverterTests.cs | 26 ++++++ .../TestHelpers/JsonHelper.cs | 21 ++++- .../Apps/CachingAppProviderTests.cs | 26 ++++-- .../MongoDb/Contents/ODataQueryTests.cs | 34 ++++++-- .../Schemas/CachingSchemaProviderTests.cs | 28 +++++-- .../Contents/ContentCommandHandlerTests.cs | 2 - .../Schemas/SchemaCommandHandlerTests.cs | 5 +- .../Squidex.Write.Tests.csproj | 4 - 26 files changed, 391 insertions(+), 103 deletions(-) create mode 100644 src/Squidex.Infrastructure/Json/ConverterContractResolver.cs create mode 100644 src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs rename src/{Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs => Squidex.Read/Contents/Builders/EdmModelBuilder.cs} (57%) create mode 100644 tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 967d79848..3a48de351 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -86,7 +86,7 @@ namespace Squidex.Core.Schemas var rawErrors = new List(); try { - var typedValue = value.Type == JTokenType.Null ? null : ConvertValue(value); + var typedValue = value.Type == JTokenType.Null ? null : ConvertValue(value); foreach (var validator in validators.Value) { diff --git a/src/Squidex.Core/Schemas/StringFieldEditor.cs b/src/Squidex.Core/Schemas/StringFieldEditor.cs index fb92c7f71..6a222e401 100644 --- a/src/Squidex.Core/Schemas/StringFieldEditor.cs +++ b/src/Squidex.Core/Schemas/StringFieldEditor.cs @@ -10,9 +10,9 @@ namespace Squidex.Core.Schemas { public enum StringFieldEditor { - Dropdown, Input, Markdown, + Dropdown, Radio, RichText, TextArea diff --git a/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs b/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs new file mode 100644 index 000000000..625d36fa1 --- /dev/null +++ b/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// ConverterContractResolver.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Squidex.Infrastructure.Json +{ + public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver + { + private readonly JsonConverter[] converters; + + public ConverterContractResolver(params JsonConverter[] converters) + { + this.converters = converters; + } + + protected override JsonConverter ResolveContractConverter(Type objectType) + { + var result = base.ResolveContractConverter(objectType); + + if (result != null) + { + return result; + } + + foreach (var converter in converters) + { + if (converter.CanConvert(objectType)) + { + return converter; + } + } + + return null; + } + } +} diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index a29daffa5..2791c07ba 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -12,12 +12,13 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.OData.Core; using MongoDB.Driver; -using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.Contents; +using Squidex.Read.Contents.Builders; using Squidex.Read.Contents.Repositories; using Squidex.Read.MongoDb.Contents.Visitors; +using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; namespace Squidex.Read.MongoDb.Contents @@ -27,6 +28,7 @@ namespace Squidex.Read.MongoDb.Contents private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; private readonly ISchemaProvider schemaProvider; + private readonly EdmModelBuilder modelBuilder; protected static IndexKeysDefinitionBuilder IndexKeys { @@ -36,13 +38,14 @@ namespace Squidex.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider) + public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider, EdmModelBuilder modelBuilder) { Guard.NotNull(database, nameof(database)); + Guard.NotNull(modelBuilder, nameof(modelBuilder)); Guard.NotNull(schemaProvider, nameof(schemaProvider)); this.database = database; - + this.modelBuilder = modelBuilder; this.schemaProvider = schemaProvider; } @@ -50,14 +53,16 @@ namespace Squidex.Read.MongoDb.Contents { List result = null; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { IFindFluent cursor; try { - var parser = schema.ParseQuery(languages, odataQuery); + var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + + var parser = model.ParseQuery(odataQuery); - cursor = collection.Find(parser, schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schema); + cursor = collection.Find(parser, schemaEntity.Schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schemaEntity.Schema); } catch (NotSupportedException) { @@ -76,7 +81,7 @@ namespace Squidex.Read.MongoDb.Contents foreach (var entity in entities) { - entity.ParseData(schema); + entity.ParseData(schemaEntity.Schema); } result = entities.OfType().ToList(); @@ -89,14 +94,16 @@ namespace Squidex.Read.MongoDb.Contents { var result = 0L; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { IFindFluent cursor; try { - var parser = schema.ParseQuery(languages, odataQuery); + var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + + var parser = model.ParseQuery(odataQuery); - cursor = collection.Find(parser, schema, nonPublished); + cursor = collection.Find(parser, schemaEntity.Schema, nonPublished); } catch (NotSupportedException) { @@ -121,17 +128,17 @@ namespace Squidex.Read.MongoDb.Contents { MongoContentEntity result = null; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { result = await collection.Find(x => x.Id == id).FirstOrDefaultAsync(); - result?.ParseData(schema); + result?.ParseData(schemaEntity.Schema); }); return result; } - private async Task ForSchemaAsync(Guid schemaId, Func, Schema, Task> action) + private async Task ForSchemaAsync(Guid schemaId, Func, ISchemaEntityWithSchema, Task> action) { var collection = GetCollection(schemaId); @@ -142,7 +149,7 @@ namespace Squidex.Read.MongoDb.Contents return; } - await action(collection, schemaEntity.Schema); + await action(collection, schemaEntity); } } } diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 8574dd475..e20978138 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -68,24 +68,24 @@ namespace Squidex.Read.MongoDb.Contents protected Task On(ContentCreated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.SchemaId.Id, (collection, schemaEntity) => { return collection.CreateAsync(@event, headers, x => { SimpleMapper.Map(@event, x); - x.SetData(schema, @event.Data); + x.SetData(schemaEntity.Schema, @event.Data); }); }); } protected Task On(ContentUpdated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.SchemaId.Id, (collection, schemaEntity) => { return collection.UpdateAsync(@event, headers, x => { - x.SetData(schema, @event.Data); + x.SetData(schemaEntity.Schema, @event.Data); }); }); } diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs b/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs new file mode 100644 index 000000000..d3b992635 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// SchemaExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Core.UriParser; +using Microsoft.OData.Edm; + +namespace Squidex.Read.MongoDb.Contents.Visitors +{ + public static class EdmModelExtensions + { + public static ODataUriParser ParseQuery(this IEdmModel model, string query) + { + var path = model.EntityContainer.EntitySets().First().Path.Path.Last().Split('.').Last(); + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + } +} diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs similarity index 57% rename from src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs rename to src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index 7a9cf44ca..3dda0acb2 100644 --- a/src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -1,5 +1,5 @@ // ========================================================================== -// SchemaExtensions.cs +// EdmModelBuilder.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,19 +8,45 @@ using System; using System.Collections.Generic; -using Microsoft.OData.Core.UriParser; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Read.Schemas; +using Squidex.Read.Utils; -namespace Squidex.Read.MongoDb.Contents.Visitors +namespace Squidex.Read.Contents.Builders { - public static class SchemaExtensions + public sealed class EdmModelBuilder : CachingProvider { - public static EdmModel BuildEdmModel(this Schema schema, HashSet languages) + public EdmModelBuilder(IMemoryCache cache) + : base(cache) + { + } + + public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, HashSet languages) + { + Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(schemaEntity, nameof(schemaEntity)); + + var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{string.Join(",", languages.Select(x => x.Iso2Code).OrderBy(x => x))}"; + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); + + return BuildEdmModel(schemaEntity.Schema, languages); + }); + + return result; + } + + private static EdmModel BuildEdmModel(Schema schema, HashSet languages) { var model = new EdmModel(); + var container = new EdmEntityContainer("Squidex", "Container"); var schemaType = schema.BuildEdmType(languages, x => @@ -41,18 +67,9 @@ namespace Squidex.Read.MongoDb.Contents.Visitors model.AddElement(schemaType); model.AddElement(entityType); - container.AddEntitySet($"{schema.Name}_Set", entityType); + container.AddEntitySet("ContentSet", entityType); return model; } - - public static ODataUriParser ParseQuery(this Schema schema, HashSet languages, string query) - { - var model = schema.BuildEdmModel(languages); - - var parser = new ODataUriParser(model, new Uri($"{schema.Name}_Set?{query}", UriKind.Relative)); - - return parser; - } } } diff --git a/src/Squidex.Read/Squidex.Read.csproj b/src/Squidex.Read/Squidex.Read.csproj index 911f23a17..af62c3bc9 100644 --- a/src/Squidex.Read/Squidex.Read.csproj +++ b/src/Squidex.Read/Squidex.Read.csproj @@ -16,4 +16,9 @@ + + + C:\Users\mail2\.nuget\packages\identityserver4\1.1.1\lib\netstandard1.4\IdentityServer4.dll + + diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 980b63975..cc7613ed3 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -12,6 +12,7 @@ using Squidex.Read.Apps; using Squidex.Read.Apps.Services; using Squidex.Read.Apps.Services.Implementations; using Squidex.Read.Contents; +using Squidex.Read.Contents.Builders; using Squidex.Read.History; using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; @@ -49,6 +50,10 @@ namespace Squidex.Config.Domain builder.RegisterType() .As() .SingleInstance(); + + builder.RegisterType() + .AsSelf() + .SingleInstance(); } } } diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 3f23d2323..507af12f8 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -10,7 +10,6 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; using NodaTime; using NodaTime.Serialization.JsonNet; using Squidex.Events; @@ -27,16 +26,15 @@ namespace Squidex.Config.Domain { settings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - - settings.Converters.Add(new InstantConverter()); - settings.Converters.Add(new LanguageConverter()); - settings.Converters.Add(new NamedGuidIdConverter()); - settings.Converters.Add(new NamedLongIdConverter()); - settings.Converters.Add(new NamedStringIdConverter()); - settings.Converters.Add(new PropertiesBagConverter()); - settings.Converters.Add(new RefTokenConverter()); - settings.Converters.Add(new StringEnumConverter()); + settings.ContractResolver = new ConverterContractResolver( + new InstantConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertiesBagConverter(), + new RefTokenConverter(), + new StringEnumConverter()); settings.NullValueHandling = NullValueHandling.Ignore; diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index 5a9b7db4c..2283c2c7f 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -35,6 +35,7 @@ namespace Squidex.Pipeline static ApiExceptionFilterAttribute() { AddHandler(OnDomainObjectNotFoundException); + AddHandler(OnDomainObjectVersionException); AddHandler(OnDomainException); AddHandler(OnValidationException); } @@ -44,6 +45,11 @@ namespace Squidex.Pipeline return new NotFoundResult(); } + private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) + { + return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 409 }; + } + private static IActionResult OnDomainException(DomainException ex) { return new BadRequestObjectResult(new ErrorDto { Message = ex.Message }); diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs index 067b08a35..365b65bb5 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs @@ -136,7 +136,7 @@ namespace Squidex.Core.Schemas private static Instant FutureDays(int days) { - return SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(days)); + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); } } } \ No newline at end of file diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs index 9a251e461..117a40be7 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -89,9 +90,20 @@ namespace Squidex.Core.Schemas new[] { "My-DateTime is not a valid value" }); } + [Fact] + public async Task Should_add_errors_if_value_is_another_type() + { + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime" }); + + await sut.ValidateAsync(CreateValue(123), errors); + + errors.ShouldBeEquivalentTo( + new[] { "My-DateTime is not a valid value" }); + } + private static Instant FutureDays(int days) { - return SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(days)); + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); } private static JValue CreateValue(object v) diff --git a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs index 0076e4a8a..8815bfc76 100644 --- a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs @@ -39,17 +39,17 @@ namespace Squidex.Core.Schemas { var sut = new JsonField(1, "my-json", new JsonFieldProperties { Label = "My-Json" }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(new JValue(1)), errors); Assert.Empty(errors); } [Fact] - public async Task Should_add_errors_if_datetime_is_required() + public async Task Should_add_errors_if_json_is_required() { var sut = new JsonField(1, "my-json", new JsonFieldProperties { Label = "My-Json", IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); errors.ShouldBeEquivalentTo( new[] { "My-Json is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs index c12608625..cdb9fc223 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using Newtonsoft.Json.Linq; +using NJsonSchema; using Squidex.Infrastructure; using Xunit; @@ -255,9 +256,9 @@ namespace Squidex.Core.Schemas { var languages = new HashSet(new[] { Language.DE, Language.EN }); - var json = BuildMixedSchema().BuildSchema(languages, (n, s) => s).ToJson(); + var jsonSchema = BuildMixedSchema().BuildSchema(languages, (n, s) => new JsonSchema4 { SchemaReference = s }); - Assert.NotNull(json); + Assert.NotNull(jsonSchema); } [Fact] @@ -276,19 +277,19 @@ namespace Squidex.Core.Schemas var schema = Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddOrUpdateField(new JsonField(0, "my-json", + .AddOrUpdateField(new JsonField(1, "my-json", new JsonFieldProperties())) - .AddOrUpdateField(new StringField(1, "my-string1", + .AddOrUpdateField(new StringField(2, "my-string1", new StringFieldProperties { Label = "My String1", IsLocalizable = true, IsRequired = true, AllowedValues = allowedValues })) - .AddOrUpdateField(new StringField(2, "my-string2", + .AddOrUpdateField(new StringField(3, "my-string2", new StringFieldProperties { Hints = "My String1" })) - .AddOrUpdateField(new NumberField(3, "my-number", + .AddOrUpdateField(new NumberField(4, "my-number", new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddOrUpdateField(new BooleanField(4, "my-boolean", + .AddOrUpdateField(new BooleanField(5, "my-boolean", new BooleanFieldProperties())) - .AddOrUpdateField(new DateTimeField(5, "my-datetime", + .AddOrUpdateField(new DateTimeField(6, "my-datetime", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddOrUpdateField(new DateTimeField(6, "my-date", + .AddOrUpdateField(new DateTimeField(7, "my-date", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })); return schema; diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs index 8999e19a2..b94c2c3d0 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs @@ -293,15 +293,15 @@ namespace Squidex.Core.Schemas var schema = Schema.Create("my-schema", new SchemaProperties()) - .AddOrUpdateField(new JsonField(0, "my-json", + .AddOrUpdateField(new JsonField(1, "my-json", new JsonFieldProperties())) - .AddOrUpdateField(new StringField(1, "my-string", + .AddOrUpdateField(new StringField(2, "my-string", new StringFieldProperties { DefaultValue = "EN-String", IsLocalizable = true })) - .AddOrUpdateField(new NumberField(2, "my-number", + .AddOrUpdateField(new NumberField(3, "my-number", new NumberFieldProperties { DefaultValue = 123 })) - .AddOrUpdateField(new BooleanField(3, "my-boolean", + .AddOrUpdateField(new BooleanField(4, "my-boolean", new BooleanFieldProperties { DefaultValue = true })) - .AddOrUpdateField(new DateTimeField(4, "my-datetime", + .AddOrUpdateField(new DateTimeField(5, "my-datetime", new DateTimeFieldProperties { DefaultValue = now })); var data = diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs index 9c4b627af..a1278a0b7 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs @@ -82,6 +82,12 @@ namespace Squidex.Infrastructure.CQRS.Commands Assert.Equal(repository.Object, sut.Repository); } + [Fact] + public Task Create_async_should_throw_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(new Mock().Object), x => TaskHelper.False)); + } + [Fact] public async Task Create_async_should_create_domain_object_and_save() { @@ -132,6 +138,12 @@ namespace Squidex.Infrastructure.CQRS.Commands repository.VerifyAll(); } + [Fact] + public Task Update_async_should_throw_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(new Mock().Object), x => TaskHelper.False)); + } + [Fact] public async Task Update_async_should_create_domain_object_and_save() { diff --git a/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs b/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs new file mode 100644 index 000000000..b19bae215 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// ConverterContractResolverTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public class ConverterContractResolverTests + { + public class MyClass + { + [JsonConverter(typeof(TodayConverter))] + public Instant MyProperty { get; set; } + } + + public sealed class TodayConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue("TODAY"); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Instant); + } + } + + [Fact] + public void Should_respect_property_converter() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new ConverterContractResolver(new InstantConverter()) + }; + + var json = JsonConvert.SerializeObject(new MyClass { MyProperty = value }, serializerSettings); + + Assert.Equal(@"{ ""MyProperty"": ""TODAY"" }", json); + } + + [Fact] + public void Should_ignore_other_converters() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new ConverterContractResolver(new InstantConverter()) + }; + + serializerSettings.Converters.Add(new TodayConverter()); + + var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); + var output = JsonConvert.DeserializeObject>(result, serializerSettings); + + Assert.Equal(value, output.Item1); + } + + [Fact] + public void Should_serialize_and_deserialize_instant() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new ConverterContractResolver(new InstantConverter())); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs new file mode 100644 index 000000000..59d9492be --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// InstantConverterTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public sealed class InstantConverterTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new InstantConverter()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs index a1f0799be..d1585a0dc 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -8,13 +8,28 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using Xunit; namespace Squidex.Infrastructure.TestHelpers { public static class JsonHelper { - public static void SerializeAndDeserialize(this T value, JsonConverter converter) where T : class + public static void SerializeAndDeserialize(this T value, IContractResolver contractResolver) + { + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = contractResolver, + NullValueHandling = NullValueHandling.Include + }; + + var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); + var output = JsonConvert.DeserializeObject>(result, serializerSettings); + + Assert.Equal(value, output.Item1); + } + + public static void SerializeAndDeserialize(this T value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); @@ -27,14 +42,14 @@ namespace Squidex.Infrastructure.TestHelpers Assert.Equal(value, output.Item1); } - public static void DoesNotDeserialize(string value, JsonConverter converter) where T : class + public static void DoesNotDeserialize(string value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); serializerSettings.Converters.Add(converter); serializerSettings.NullValueHandling = NullValueHandling.Include; - Assert.Throws(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}")); + Assert.ThrowsAny(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}", serializerSettings)); } } } diff --git a/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs b/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs index 5f4429110..a3f80b572 100644 --- a/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs +++ b/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs @@ -14,7 +14,6 @@ using Moq; using Squidex.Infrastructure; using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Services.Implementations; -using Squidex.Read.MongoDb.Apps; using Xunit; // ReSharper disable ConvertToConstant.Local @@ -27,14 +26,23 @@ namespace Squidex.Read.Apps private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Mock repository = new Mock(); private readonly CachingAppProvider sut; - private readonly MongoAppEntity appV1; - private readonly MongoAppEntity appV2; + private readonly IAppEntity appV1; + private readonly IAppEntity appV2; private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); public CachingAppProviderTests() { - appV1 = new MongoAppEntity { Name = appId.Name, Id = appId.Id }; - appV2 = new MongoAppEntity { Name = appId.Name, Id = appId.Id }; + var appV1Mock = new Mock(); + var appV2Mock = new Mock(); + + appV1Mock.Setup(x => x.Id).Returns(appId.Id); + appV1Mock.Setup(x => x.Name).Returns(appId.Name); + + appV2Mock.Setup(x => x.Id).Returns(appId.Id); + appV2Mock.Setup(x => x.Name).Returns(appId.Name); + + appV1 = appV1Mock.Object; + appV2 = appV2Mock.Object; sut = new CachingAppProvider(cache, repository.Object); } @@ -42,7 +50,7 @@ namespace Squidex.Read.Apps [Fact] public async Task Should_also_retrieve_app_by_name_if_retrieved_by_id_before() { - repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(Task.FromResult(appV1)); + repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(Task.FromResult(appV1)); await ProvideAppById(appV1); await ProvideAppByName(appV1); @@ -54,7 +62,7 @@ namespace Squidex.Read.Apps [Fact] public async Task Should_also_retrieve_app_by_id_if_retrieved_by_name_before() { - repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(Task.FromResult(appV1)); + repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(Task.FromResult(appV1)); await ProvideAppByName(appV1); await ProvideAppById(appV1); @@ -68,7 +76,7 @@ namespace Squidex.Read.Apps { var apps = ProviderResults(appV1, appV2); - repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(() => Task.FromResult(apps())); + repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(() => Task.FromResult(apps())); await ProvideAppById(appV1); @@ -84,7 +92,7 @@ namespace Squidex.Read.Apps { var apps = ProviderResults(appV1, appV2); - repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(() => Task.FromResult(apps())); + repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(() => Task.FromResult(apps())); await ProvideAppByName(appV1); diff --git a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs index 882ea67b9..dca834f67 100644 --- a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs @@ -9,13 +9,18 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OData.Edm; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Moq; using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; +using Squidex.Read.Contents.Builders; using Squidex.Read.MongoDb.Contents.Visitors; +using Squidex.Read.Schemas; using Xunit; // ReSharper disable SpecifyACultureInStringConversionExplicitly @@ -39,6 +44,7 @@ namespace Squidex.Read.MongoDb.Contents private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly IEdmModel edmModel; private readonly HashSet languages = new HashSet { Language.EN, @@ -50,10 +56,22 @@ namespace Squidex.Read.MongoDb.Contents InstantSerializer.Register(); } + public ODataQueryTests() + { + var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); + + var schemaEntity = new Mock(); + schemaEntity.Setup(x => x.Id).Returns(Guid.NewGuid()); + schemaEntity.Setup(x => x.Version).Returns(3); + schemaEntity.Setup(x => x.Schema).Returns(schema); + + edmModel = builder.BuildEdmModel(schemaEntity.Object, languages); + } + [Fact] public void Should_parse_query() { - var parser = schema.ParseQuery(languages, "$filter=data/firstName/de eq 'Sebastian'"); + var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); Assert.NotNull(parser); } @@ -223,7 +241,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_top() { - var parser = schema.ParseQuery(languages, "$top=3"); + var parser = edmModel.ParseQuery("$top=3"); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -234,7 +252,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_max_top_if_larger() { - var parser = schema.ParseQuery(languages, "$top=300"); + var parser = edmModel.ParseQuery("$top=300"); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -245,7 +263,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_default_top() { - var parser = schema.ParseQuery(languages, ""); + var parser = edmModel.ParseQuery(""); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -256,7 +274,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_skip() { - var parser = schema.ParseQuery(languages, "$skip=3"); + var parser = edmModel.ParseQuery("$skip=3"); var cursor = new Mock>(); cursor.Object.Skip(parser); @@ -267,7 +285,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_not_set_skip() { - var parser = schema.ParseQuery(languages, ""); + var parser = edmModel.ParseQuery(""); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -282,7 +300,7 @@ namespace Squidex.Read.MongoDb.Contents private string S(string value) { - var parser = schema.ParseQuery(languages, value); + var parser = edmModel.ParseQuery(value); var cursor = new Mock>(); var i = string.Empty; @@ -299,7 +317,7 @@ namespace Squidex.Read.MongoDb.Contents private string F(string value) { - var parser = schema.ParseQuery(languages, value); + var parser = edmModel.ParseQuery(value); var query = FilterBuilder.Build(parser, schema).Render(serializer, registry).ToString(); diff --git a/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs b/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs index 8c1ddb489..a86cb4db7 100644 --- a/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs +++ b/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs @@ -14,7 +14,6 @@ using Moq; using Squidex.Infrastructure; using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Services.Implementations; -using Squidex.Read.MongoDb.Schemas; using Xunit; // ReSharper disable ConvertToConstant.Local @@ -27,15 +26,26 @@ namespace Squidex.Read.Schemas private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Mock repository = new Mock(); private readonly CachingSchemaProvider sut; - private readonly MongoSchemaEntity schemaV1; - private readonly MongoSchemaEntity schemaV2; + private readonly ISchemaEntityWithSchema schemaV1; + private readonly ISchemaEntityWithSchema schemaV2; private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); public CachingSchemaProviderTests() { - schemaV1 = new MongoSchemaEntity { Name = schemaId.Name, Id = schemaId.Id, AppId = appId.Id }; - schemaV2 = new MongoSchemaEntity { Name = schemaId.Name, Id = schemaId.Id, AppId = appId.Id }; + var schemaV1Mock = new Mock(); + var schemaV2Mock = new Mock(); + + schemaV1Mock.Setup(x => x.Id).Returns(schemaId.Id); + schemaV1Mock.Setup(x => x.Name).Returns(schemaId.Name); + schemaV1Mock.Setup(x => x.AppId).Returns(appId.Id); + + schemaV2Mock.Setup(x => x.Id).Returns(schemaId.Id); + schemaV2Mock.Setup(x => x.Name).Returns(schemaId.Name); + schemaV2Mock.Setup(x => x.AppId).Returns(appId.Id); + + schemaV1 = schemaV1Mock.Object; + schemaV2 = schemaV2Mock.Object; sut = new CachingSchemaProvider(cache, repository.Object); } @@ -43,7 +53,7 @@ namespace Squidex.Read.Schemas [Fact] public async Task Should_also_retrieve_schema_by_name_if_retrieved_by_id_before() { - repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(Task.FromResult(schemaV1)); + repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(Task.FromResult(schemaV1)); await ProvideSchemaById(schemaV1); await ProvideSchemaByName(schemaV1); @@ -55,7 +65,7 @@ namespace Squidex.Read.Schemas [Fact] public async Task Should_also_retrieve_schema_by_id_if_retrieved_by_name_before() { - repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(Task.FromResult(schemaV1)); + repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(Task.FromResult(schemaV1)); await ProvideSchemaByName(schemaV1); await ProvideSchemaById(schemaV1); @@ -69,7 +79,7 @@ namespace Squidex.Read.Schemas { var schemas = ProviderResults(schemaV1, schemaV2); - repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(() => Task.FromResult(schemas())); + repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(() => Task.FromResult(schemas())); await ProvideSchemaById(schemaV1); @@ -85,7 +95,7 @@ namespace Squidex.Read.Schemas { var schemas = ProviderResults(schemaV1, schemaV2); - repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(() => Task.FromResult(schemas())); + repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(() => Task.FromResult(schemas())); await ProvideSchemaByName(schemaV1); diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs index 9f329362f..681471797 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -73,8 +73,6 @@ namespace Squidex.Write.Contents { await sut.HandleAsync(context); }); - - Assert.Equal(contentId, context.Result()); } [Fact] diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index 68ebd1251..bc9a93699 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Moq; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; using Squidex.Write.Schemas.Commands; @@ -65,8 +66,6 @@ namespace Squidex.Write.Schemas { await sut.HandleAsync(context); }); - - Assert.Equal(SchemaName, context.Result()); } [Fact] @@ -134,7 +133,7 @@ namespace Squidex.Write.Schemas await sut.HandleAsync(context); }); - Assert.Equal(1, context.Result()); + Assert.Equal(1, context.Result>().IdOrValue); } [Fact] diff --git a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj index b9ef1380c..acad1fcf1 100644 --- a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj +++ b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj @@ -5,10 +5,6 @@ $(PackageTargetFallback);dnxcore50 Squidex.Write - - full - True -