From 6addf0f974a8cd26d9d7b4b9b832a5c0f309dd73 Mon Sep 17 00:00:00 2001 From: James Doran Date: Sat, 21 Dec 2019 08:54:32 +1030 Subject: [PATCH 1/3] YesSql now stores schema version (#213) --- .../YesSqlServiceCollectionExtensions.cs | 2 + .../Schema/ISchemaVersionStore.cs | 10 ++ .../Schema/SchemaVersionDocument.cs | 7 ++ .../Schema/SchemaVersionStore.cs | 37 +++++++ .../StartupTasks/InitializeStoreTask.cs | 103 ++++++++++++------ 5 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 src/persistence/Elsa.Persistence.YesSql/Schema/ISchemaVersionStore.cs create mode 100644 src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionDocument.cs create mode 100644 src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionStore.cs diff --git a/src/persistence/Elsa.Persistence.YesSql/Extensions/YesSqlServiceCollectionExtensions.cs b/src/persistence/Elsa.Persistence.YesSql/Extensions/YesSqlServiceCollectionExtensions.cs index 4ce5bdeeb..a8feca36f 100644 --- a/src/persistence/Elsa.Persistence.YesSql/Extensions/YesSqlServiceCollectionExtensions.cs +++ b/src/persistence/Elsa.Persistence.YesSql/Extensions/YesSqlServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Elsa.AutoMapper.Extensions; using Elsa.AutoMapper.Extensions.NodaTime; using Elsa.Persistence.YesSql.Indexes; using Elsa.Persistence.YesSql.Mapping; +using Elsa.Persistence.YesSql.Schema; using Elsa.Persistence.YesSql.Services; using Elsa.Persistence.YesSql.StartupTasks; using Elsa.Runtime; @@ -22,6 +23,7 @@ namespace Elsa.Persistence.YesSql.Extensions .AddSingleton(sp => StoreFactory.CreateStore(sp, configure)) .AddSingleton() .AddSingleton() + .AddTransient() .AddScoped(CreateSession) .AddAutoMapperProfile(ServiceLifetime.Singleton) .AddAutoMapperProfile(ServiceLifetime.Singleton) diff --git a/src/persistence/Elsa.Persistence.YesSql/Schema/ISchemaVersionStore.cs b/src/persistence/Elsa.Persistence.YesSql/Schema/ISchemaVersionStore.cs new file mode 100644 index 000000000..74e7872d8 --- /dev/null +++ b/src/persistence/Elsa.Persistence.YesSql/Schema/ISchemaVersionStore.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Elsa.Persistence.YesSql.Schema +{ + public interface ISchemaVersionStore + { + Task GetVersionAsync(); + Task SaveVersionAsync(int version); + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionDocument.cs b/src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionDocument.cs new file mode 100644 index 000000000..725a68ead --- /dev/null +++ b/src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionDocument.cs @@ -0,0 +1,7 @@ +namespace Elsa.Persistence.YesSql.Schema +{ + public class SchemaVersionDocument + { + public int Version { get; set; } + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionStore.cs b/src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionStore.cs new file mode 100644 index 000000000..793f42a99 --- /dev/null +++ b/src/persistence/Elsa.Persistence.YesSql/Schema/SchemaVersionStore.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using YesSql; + +namespace Elsa.Persistence.YesSql.Schema +{ + public class SchemaVersionStore : ISchemaVersionStore + { + private readonly ISession session; + + public SchemaVersionStore(ISession session) + { + this.session = session; + } + + public async Task GetVersionAsync() + { + var schemaVersion = await session.Query().FirstOrDefaultAsync(); + + if (schemaVersion == null) + { + schemaVersion = new SchemaVersionDocument {Version = 0}; + session.Save(schemaVersion); + await session.CommitAsync(); + } + + return schemaVersion.Version; + } + + public async Task SaveVersionAsync(int version) + { + var schemaVersion = await session.Query().FirstOrDefaultAsync(); + schemaVersion.Version = version; + session.Save(schemaVersion); + await session.CommitAsync(); + } + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.YesSql/StartupTasks/InitializeStoreTask.cs b/src/persistence/Elsa.Persistence.YesSql/StartupTasks/InitializeStoreTask.cs index 441221caa..21d8956d9 100644 --- a/src/persistence/Elsa.Persistence.YesSql/StartupTasks/InitializeStoreTask.cs +++ b/src/persistence/Elsa.Persistence.YesSql/StartupTasks/InitializeStoreTask.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Elsa.Persistence.YesSql.Indexes; +using Elsa.Persistence.YesSql.Schema; using Elsa.Runtime; using YesSql; using YesSql.Sql; @@ -11,19 +14,67 @@ namespace Elsa.Persistence.YesSql.StartupTasks public class InitializeStoreTask : IStartupTask { private readonly IStore store; + private readonly ISchemaVersionStore schemaVersionStore; + private readonly SchemaUpdate[] schemaVersionUpdates; - public InitializeStoreTask(IStore store) + public InitializeStoreTask(IStore store, + ISchemaVersionStore schemaVersionStore) { this.store = store; - } - - public Task ExecuteAsync(CancellationToken cancellationToken = default) - { - CreateTables(); - return Task.CompletedTask; + this.schemaVersionStore = schemaVersionStore; + + schemaVersionUpdates = new[] + { + new SchemaUpdate {Version = 1, Update = UpdateToVersion1} + }; } - private void CreateTables() + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + int currentVersion = await schemaVersionStore.GetVersionAsync(); + + foreach (var schemaUpdate in schemaVersionUpdates.Where(x => x.Version > currentVersion)) + { + schemaUpdate.Update(); + await schemaVersionStore.SaveVersionAsync(schemaUpdate.Version); + } + } + + private void UpdateToVersion1() + { + PerformUpdates(builder => + { + builder + .CreateMapIndexTable(nameof(WorkflowDefinitionIndex), table => table + .Column("WorkflowDefinitionId") + .Column("Version") + .Column("IsPublished") + .Column("IsLatest") + .Column("IsDisabled") + ) + .CreateMapIndexTable(nameof(WorkflowDefinitionStartActivitiesIndex), table => table + .Column("StartActivityId") + .Column("StartActivityType") + .Column("IsDisabled") + ) + .CreateMapIndexTable(nameof(WorkflowInstanceIndex), table => table + .Column("WorkflowInstanceId") + .Column("WorkflowDefinitionId") + .Column("CorrelationId") + .Column("WorkflowStatus") + .Column("CreatedAt") + ) + .CreateMapIndexTable(nameof(WorkflowInstanceBlockingActivitiesIndex), table => table + .Column("ActivityId") + .Column("ActivityType") + .Column("CorrelationId") + .Column("WorkflowStatus") + .Column("CreatedAt") + ); + }); + } + + private void PerformUpdates(Action builder) { using (var connection = store.Configuration.ConnectionFactory.CreateConnection()) { @@ -31,37 +82,19 @@ namespace Elsa.Persistence.YesSql.StartupTasks using (var transaction = connection.BeginTransaction(store.Configuration.IsolationLevel)) { - new SchemaBuilder(store.Configuration, transaction, false) - .CreateMapIndexTable(nameof(WorkflowDefinitionIndex), table => table - .Column("WorkflowDefinitionId") - .Column("Version") - .Column("IsPublished") - .Column("IsLatest") - .Column("IsDisabled") - ) - .CreateMapIndexTable(nameof(WorkflowDefinitionStartActivitiesIndex), table => table - .Column("StartActivityId") - .Column("StartActivityType") - .Column("IsDisabled") - ) - .CreateMapIndexTable(nameof(WorkflowInstanceIndex), table => table - .Column("WorkflowInstanceId") - .Column("WorkflowDefinitionId") - .Column("CorrelationId") - .Column("WorkflowStatus") - .Column("CreatedAt") - ) - .CreateMapIndexTable(nameof(WorkflowInstanceBlockingActivitiesIndex), table => table - .Column("ActivityId") - .Column("ActivityType") - .Column("CorrelationId") - .Column("WorkflowStatus") - .Column("CreatedAt") - ); + var schemaBuilder = new SchemaBuilder(store.Configuration, transaction, false); + + builder(schemaBuilder); transaction.Commit(); } } } + + private class SchemaUpdate + { + internal int Version { get; set; } + internal Action Update { get; set; } + } } } \ No newline at end of file From 80ad40824eac09b489e0248eaf9861cc0690cab6 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 21 Dec 2019 10:05:10 +0100 Subject: [PATCH 2/3] Fix broken date/time/nodatime serialization (#214) Added extensible support for specialized primitive handling such as DateTime and NodaTime primitives. This fixes the issue where a Variable is serialized using a primitive's string representation and losing the target type, which is needed for reconstructing the type upon deserialization. --- .../Converters/TypeNameHandlingConverter.cs | 84 +++++++++--------- .../Handlers/AnnualDateHandler.cs | 11 +++ .../Serialization/Handlers/DateTimeHandler.cs | 10 +++ .../Handlers/DefaultValueHandler.cs | 15 ++++ .../Serialization/Handlers/DurationHandler.cs | 11 +++ .../Serialization/Handlers/IValueHandler.cs | 15 ++++ .../Serialization/Handlers/InstantHandler.cs | 11 +++ .../Handlers/LocalDateHandler.cs | 11 +++ .../Handlers/LocalDateTimeHandler.cs | 11 +++ .../Handlers/LocalTimeHandler.cs | 11 +++ .../Serialization/Handlers/ObjectHandler.cs | 36 ++++++++ .../Handlers/OffsetDateHandler.cs | 11 +++ .../Serialization/Handlers/OffsetHandler.cs | 11 +++ .../Handlers/OffsetTimeHandler.cs | 11 +++ .../Handlers/PrimitiveValueHandler.cs | 33 +++++++ .../Handlers/YearMonthHandler.cs | 11 +++ .../Handlers/ZonedDateTimeHandler.cs | 11 +++ .../CustomSchemaDbContextBuilderExtensions.cs | 2 +- .../CustomSchemaModelCacheKeyFactory.cs | 10 ++- .../CustomSchemaOptionsExtension.cs | 30 +++++++ .../CustomSchemaOptionsExtensionInfo.cs | 52 +++++++++++ .../DbContextCustomSchema.cs | 6 +- .../IDbContextCustomSchema.cs | 6 +- .../SchemaEntityTypeConfiguration.cs | 4 +- .../CustomSchemaOptionsExtension.cs | 86 ------------------- .../DbContexts/ElsaContext.cs | 14 +-- 26 files changed, 373 insertions(+), 151 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/AnnualDateHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/DateTimeHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/DefaultValueHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/DurationHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/IValueHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/InstantHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateTimeHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/LocalTimeHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/ObjectHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/OffsetDateHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/OffsetHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/OffsetTimeHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/PrimitiveValueHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/YearMonthHandler.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Handlers/ZonedDateTimeHandler.cs rename src/persistence/Elsa.Persistence.EntityFrameworkCore/{Extensions => CustomSchema}/CustomSchemaDbContextBuilderExtensions.cs (99%) rename src/persistence/Elsa.Persistence.EntityFrameworkCore/{DbContexts => CustomSchema}/CustomSchemaModelCacheKeyFactory.cs (80%) create mode 100644 src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtension.cs create mode 100644 src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtensionInfo.cs rename src/persistence/Elsa.Persistence.EntityFrameworkCore/{DbContexts => CustomSchema}/DbContextCustomSchema.cs (89%) rename src/persistence/Elsa.Persistence.EntityFrameworkCore/{DbContexts => CustomSchema}/IDbContextCustomSchema.cs (60%) rename src/persistence/Elsa.Persistence.EntityFrameworkCore/{DbContexts => CustomSchema}/SchemaEntityTypeConfiguration.cs (93%) delete mode 100644 src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaOptionsExtension.cs diff --git a/src/core/Elsa.Abstractions/Converters/TypeNameHandlingConverter.cs b/src/core/Elsa.Abstractions/Converters/TypeNameHandlingConverter.cs index 655d41af1..435c5e0fb 100644 --- a/src/core/Elsa.Abstractions/Converters/TypeNameHandlingConverter.cs +++ b/src/core/Elsa.Abstractions/Converters/TypeNameHandlingConverter.cs @@ -1,70 +1,70 @@ using System; +using System.Collections.Generic; using System.Linq; +using Elsa.Serialization.Handlers; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Serialization.JsonNet; +using NodaTime.Text; namespace Elsa.Converters { public class TypeNameHandlingConverter : JsonConverter { - private const string TypeFieldName = "TypeName"; + private static readonly IDictionary ValueHandlers = new Dictionary(); + + public static void RegisterTypeHandler() where T : IValueHandler + { + var handler = Activator.CreateInstance(); + RegisterTypeHandler(handler); + } + + public static void RegisterTypeHandler(IValueHandler handler) + { + ValueHandlers[handler.GetType()] = handler; + } + + static TypeNameHandlingConverter() + { + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + RegisterTypeHandler(); + } + public override bool CanRead => true; public override bool CanWrite => true; public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { + var valueType = value.GetType(); var token = JToken.FromObject(value); + var handler = GetHandler(x => x.CanSerialize(token, valueType)); - switch (token.Type) - { - case JTokenType.Object: - token[TypeFieldName] = GetAssemblyQualifiedTypeName(value.GetType()); - token.WriteTo(writer, serializer.Converters.ToArray()); - break; - case JTokenType.Date: // Taking over DateTime serialization because NodaTime disabled date handling. - var dateToken = new JObject(); - dateToken[TypeFieldName] = "DateTime"; - dateToken["Value"] = token; - dateToken.WriteTo(writer, serializer.Converters.ToArray()); - break; - default: - token.WriteTo(writer); - break; - } + handler.Serialize(writer, serializer, token); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var token = JToken.ReadFrom(reader); + var handler = GetHandler(x => x.CanDeserialize(token, objectType)); - switch (token.Type) - { - case JTokenType.Object: - var typeName = token[TypeFieldName].Value(); - - if (typeName == "DateTime") - { - var dateTime = token["Value"].ToObject(); - return dateTime; - } - else - { - var type = Type.GetType(typeName); - return token.ToObject(type, serializer); - } - default: - return token.ToObject(objectType); - } + return handler.Deserialize(reader, serializer, objectType, token); } public override bool CanConvert(Type objectType) => true; - private string GetAssemblyQualifiedTypeName(Type type) - { - var typeName = type.FullName; - var assemblyName = type.Assembly.GetName().Name; - - return $"{typeName}, {assemblyName}"; - } + private IValueHandler GetHandler(Func predicate) => + ValueHandlers.Values.OrderByDescending(x => x.Priority).FirstOrDefault(predicate) ?? new DefaultValueHandler(); } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/AnnualDateHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/AnnualDateHandler.cs new file mode 100644 index 000000000..5f7aa966e --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/AnnualDateHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class AnnualDateHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => AnnualDatePattern.Iso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/DateTimeHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/DateTimeHandler.cs new file mode 100644 index 000000000..319fa7b5f --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/DateTimeHandler.cs @@ -0,0 +1,10 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace Elsa.Serialization.Handlers +{ + public sealed class DateTimeHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => value.Value(); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/DefaultValueHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/DefaultValueHandler.cs new file mode 100644 index 000000000..91150e3a6 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/DefaultValueHandler.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Elsa.Serialization.Handlers +{ + public class DefaultValueHandler : IValueHandler + { + public int Priority => -9000; + public bool CanSerialize(JToken value, Type type) => true; + public bool CanDeserialize(JToken value, Type type) => true; + public object Deserialize(JsonReader reader, JsonSerializer serializer, Type type, JToken value) => serializer.Deserialize(value.CreateReader(), type); + public void Serialize(JsonWriter writer, JsonSerializer serializer, JToken value) => serializer.Serialize(writer, value); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/DurationHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/DurationHandler.cs new file mode 100644 index 000000000..8fbe56d50 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/DurationHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class DurationHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => DurationPattern.Roundtrip.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/IValueHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/IValueHandler.cs new file mode 100644 index 000000000..4ba69e2e3 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/IValueHandler.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Elsa.Serialization.Handlers +{ + public interface IValueHandler + { + int Priority { get; } + bool CanSerialize(JToken value, Type type); + bool CanDeserialize(JToken value, Type type); + object Deserialize(JsonReader reader, JsonSerializer serializer, Type type, JToken value); + void Serialize(JsonWriter writer, JsonSerializer serializer, JToken value); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/InstantHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/InstantHandler.cs new file mode 100644 index 000000000..1694ab3f0 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/InstantHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class InstantHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => InstantPattern.ExtendedIso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateHandler.cs new file mode 100644 index 000000000..412dab1ba --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class LocalDateHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => LocalDatePattern.Iso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateTimeHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateTimeHandler.cs new file mode 100644 index 000000000..c14ad3d58 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/LocalDateTimeHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class LocalDateTimeHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => LocalDateTimePattern.ExtendedIso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/LocalTimeHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/LocalTimeHandler.cs new file mode 100644 index 000000000..ac21f2874 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/LocalTimeHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class LocalTimeHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => LocalTimePattern.LongExtendedIso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/ObjectHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/ObjectHandler.cs new file mode 100644 index 000000000..2045f2e17 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/ObjectHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Elsa.Serialization.Handlers +{ + public class ObjectHandler : IValueHandler + { + private const string TypeFieldName = "TypeName"; + public int Priority => -8999; + public bool CanSerialize(JToken value, Type type) => value.Type == JTokenType.Object; + public bool CanDeserialize(JToken value, Type type) => value.Type == JTokenType.Object; + + public object Deserialize(JsonReader reader, JsonSerializer serializer, Type type, JToken value) + { + var typeName = value[TypeFieldName].Value(); + var objectType = Type.GetType(typeName); + return value.ToObject(objectType, serializer); + } + + public void Serialize(JsonWriter writer, JsonSerializer serializer, JToken value) + { + value[TypeFieldName] = GetAssemblyQualifiedTypeName(value.GetType()); + value.WriteTo(writer, serializer.Converters.ToArray()); + } + + private string GetAssemblyQualifiedTypeName(Type type) + { + var typeName = type.FullName; + var assemblyName = type.Assembly.GetName().Name; + + return $"{typeName}, {assemblyName}"; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetDateHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetDateHandler.cs new file mode 100644 index 000000000..cce81c57b --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetDateHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class OffsetDateHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => OffsetDatePattern.GeneralIso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetHandler.cs new file mode 100644 index 000000000..fc1dea4cf --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class OffsetHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => OffsetPattern.GeneralInvariant.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetTimeHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetTimeHandler.cs new file mode 100644 index 000000000..0c33a0708 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/OffsetTimeHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class OffsetTimeHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => OffsetTimePattern.GeneralIso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/PrimitiveValueHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/PrimitiveValueHandler.cs new file mode 100644 index 000000000..151209aff --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/PrimitiveValueHandler.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Elsa.Serialization.Handlers +{ + public abstract class PrimitiveValueHandler : IValueHandler + { + public virtual int Priority => 0; + public bool CanSerialize(JToken value, Type type) => type == typeof(T); + public bool CanDeserialize(JToken value, Type type) => value.Type == JTokenType.Object && value["Type"]?.Value() == TypeName; + protected virtual string TypeName => typeof(T).Name; + + public virtual object Deserialize(JsonReader reader, JsonSerializer serializer, Type type, JToken value) + { + var valueToken = value["Value"]; + return ParseValue(valueToken); + } + + public virtual void Serialize(JsonWriter writer, JsonSerializer serializer, JToken value) + { + var token = new JObject + { + ["Type"] = TypeName, + ["Value"] = value + }; + token.WriteTo(writer, serializer.Converters.ToArray()); + } + + protected abstract object ParseValue(JToken value); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/YearMonthHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/YearMonthHandler.cs new file mode 100644 index 000000000..ae7bec3e4 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/YearMonthHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class YearMonthHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => YearMonthPattern.Iso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Handlers/ZonedDateTimeHandler.cs b/src/core/Elsa.Abstractions/Serialization/Handlers/ZonedDateTimeHandler.cs new file mode 100644 index 000000000..f551dc652 --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Handlers/ZonedDateTimeHandler.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using NodaTime; +using NodaTime.Text; + +namespace Elsa.Serialization.Handlers +{ + public sealed class ZonedDateTimeHandler : PrimitiveValueHandler + { + protected override object ParseValue(JToken value) => ZonedDateTimePattern.ExtendedFormatOnlyIso.Parse(value.ToString()).Value; + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Extensions/CustomSchemaDbContextBuilderExtensions.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaDbContextBuilderExtensions.cs similarity index 99% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore/Extensions/CustomSchemaDbContextBuilderExtensions.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaDbContextBuilderExtensions.cs index 4e1f48fb1..fb7ea6601 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/Extensions/CustomSchemaDbContextBuilderExtensions.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaDbContextBuilderExtensions.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -namespace Elsa.Persistence.EntityFrameworkCore.Extensions +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema { public static class CustomSchemaDbContextBuilderExtensions { diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaModelCacheKeyFactory.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaModelCacheKeyFactory.cs similarity index 80% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaModelCacheKeyFactory.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaModelCacheKeyFactory.cs index 299287adf..778c8b77c 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaModelCacheKeyFactory.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaModelCacheKeyFactory.cs @@ -1,12 +1,15 @@ +using Elsa.Persistence.EntityFrameworkCore.DbContexts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; -namespace Elsa.Persistence.EntityFrameworkCore.DbContexts +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema { public class CustomSchemaModelCacheKeyFactory : IModelCacheKeyFactory { - public CustomSchemaModelCacheKeyFactory() { } + public CustomSchemaModelCacheKeyFactory() + { + } + public object Create(DbContext context) { string schema = null; @@ -18,6 +21,7 @@ namespace Elsa.Persistence.EntityFrameworkCore.DbContexts schema = dbContextCustomSchema.Schema; } } + return new { Type = context.GetType(), diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtension.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtension.cs new file mode 100644 index 000000000..ef6fbed45 --- /dev/null +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtension.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema +{ + public class CustomSchemaOptionsExtension : IDbContextOptionsExtension + { + public DbContextOptionsExtensionInfo Info => new CustomSchemaOptionsExtensionInfo(this); + + public IDbContextCustomSchema ContextCustomSchema { get; protected set; } + + public CustomSchemaOptionsExtension(IDbContextCustomSchema customSchema) : base() + { + ContextCustomSchema = customSchema; + } + + protected CustomSchemaOptionsExtension(CustomSchemaOptionsExtension copyFrom) + { + copyFrom.ContextCustomSchema = ContextCustomSchema; + } + + public void ApplyServices(IServiceCollection services) + { + } + + public void Validate(IDbContextOptions options) + { + } + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtensionInfo.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtensionInfo.cs new file mode 100644 index 000000000..cd77771a7 --- /dev/null +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/CustomSchemaOptionsExtensionInfo.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema +{ + public sealed class CustomSchemaOptionsExtensionInfo : DbContextOptionsExtensionInfo + { + private string logFragment; + + public CustomSchemaOptionsExtensionInfo(IDbContextOptionsExtension dbContextOptionsExtension) : base(dbContextOptionsExtension) + { + } + + /// + /// The extension for which this instance contains metadata. + /// + private new CustomSchemaOptionsExtension Extension => (CustomSchemaOptionsExtension)base.Extension; + + public override bool IsDatabaseProvider => false; + + public override string LogFragment + { + get + { + if (!string.IsNullOrWhiteSpace(LogFragment)) + return logFragment; + + if (Extension.ContextCustomSchema != null && Extension.ContextCustomSchema.UseCustomSchema) + { + var builder = new StringBuilder(); + + builder.Append($"Use Custom Schema: {Extension.ContextCustomSchema.UseCustomSchema}"); + builder.Append($"Custom Schema: {Extension.ContextCustomSchema.Schema}"); + builder.Append($"Migrations History Table Name: {Extension.ContextCustomSchema.MigrationsHistoryTableName}"); + + logFragment = builder.ToString(); + } + + return logFragment; + } + } + + public override long GetServiceProviderHashCode() => 0; + + public override void PopulateDebugInfo([NotNull] IDictionary debugInfo) + { + debugInfo["CustomSchemaExtensionInfo"] = true.ToString(); + } + } +} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/DbContextCustomSchema.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/DbContextCustomSchema.cs similarity index 89% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/DbContextCustomSchema.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/DbContextCustomSchema.cs index a6ee71365..0719a5608 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/DbContextCustomSchema.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/DbContextCustomSchema.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Elsa.Persistence.EntityFrameworkCore.DbContexts +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema { public class DbContextCustomSchema : IDbContextCustomSchema { diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/IDbContextCustomSchema.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/IDbContextCustomSchema.cs similarity index 60% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/IDbContextCustomSchema.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/IDbContextCustomSchema.cs index c24cd75b2..906e6d053 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/IDbContextCustomSchema.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/IDbContextCustomSchema.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Elsa.Persistence.EntityFrameworkCore.DbContexts +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema { public interface IDbContextCustomSchema { diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/SchemaEntityTypeConfiguration.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/SchemaEntityTypeConfiguration.cs similarity index 93% rename from src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/SchemaEntityTypeConfiguration.cs rename to src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/SchemaEntityTypeConfiguration.cs index bbe34aee9..bd29f6918 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/SchemaEntityTypeConfiguration.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/CustomSchema/SchemaEntityTypeConfiguration.cs @@ -1,8 +1,8 @@ +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using System; -namespace Elsa.Persistence.EntityFrameworkCore.DbContexts +namespace Elsa.Persistence.EntityFrameworkCore.CustomSchema { public class SchemaEntityTypeConfiguration : IEntityTypeConfiguration where TEntity : class diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaOptionsExtension.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaOptionsExtension.cs deleted file mode 100644 index 4f372d172..000000000 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/CustomSchemaOptionsExtension.cs +++ /dev/null @@ -1,86 +0,0 @@ -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace Elsa.Persistence.EntityFrameworkCore.DbContexts -{ - public class CustomSchemaOptionsExtension : IDbContextOptionsExtension - { - public DbContextOptionsExtensionInfo Info => new CustomSchemaExtensionInfo(this); - - public IDbContextCustomSchema ContextCustomSchema { get; protected set; } - public CustomSchemaOptionsExtension(IDbContextCustomSchema customSchema) : base() - { - ContextCustomSchema = customSchema; - } - protected CustomSchemaOptionsExtension(CustomSchemaOptionsExtension copyFrom) - { - copyFrom.ContextCustomSchema = ContextCustomSchema; - } - - public void ApplyServices(IServiceCollection services) - { - - } - - public void Validate(IDbContextOptions options) - { - } - - protected CustomSchemaOptionsExtension Clone() - { - return new CustomSchemaOptionsExtension(this); - } - } - - public class CustomSchemaExtensionInfo : DbContextOptionsExtensionInfo - { - string logFragment = null; - public CustomSchemaExtensionInfo(IDbContextOptionsExtension dbContextOptionsExtension) : base(dbContextOptionsExtension) - { - } - - /// - /// The extension for which this instance contains metadata. - /// - public new virtual CustomSchemaOptionsExtension Extension - => (CustomSchemaOptionsExtension)base.Extension; - - public override bool IsDatabaseProvider => false; - - public override string LogFragment - { - get - { - if (!string.IsNullOrWhiteSpace(LogFragment)) return logFragment; - - if(Extension.ContextCustomSchema != null && Extension.ContextCustomSchema.UseCustomSchema) - { - var builder = new StringBuilder(); - - builder.Append($"Use Custom Schema: {Extension.ContextCustomSchema.UseCustomSchema}"); - builder.Append($"Custom Schema: {Extension.ContextCustomSchema.Schema}"); - builder.Append($"Migrations History Table Name: {Extension.ContextCustomSchema.MigrationsHistoryTableName}"); - - logFragment = builder.ToString(); - } - - return logFragment; - } - } - - public override long GetServiceProviderHashCode() - { - return 0; - } - - public override void PopulateDebugInfo([NotNullAttribute] IDictionary debugInfo) - { - debugInfo["CustomSchemaExtensionInfo"] = true.ToString(); - } - } -} \ No newline at end of file diff --git a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/ElsaContext.cs b/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/ElsaContext.cs index 4ed74a2dd..6927f85bc 100644 --- a/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/ElsaContext.cs +++ b/src/persistence/Elsa.Persistence.EntityFrameworkCore/DbContexts/ElsaContext.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using Elsa.Models; +using Elsa.Persistence.EntityFrameworkCore.CustomSchema; using Elsa.Persistence.EntityFrameworkCore.Entities; using Elsa.Persistence.EntityFrameworkCore.Extensions; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NodaTime; @@ -15,17 +14,18 @@ namespace Elsa.Persistence.EntityFrameworkCore.DbContexts public class ElsaContext : DbContext { private readonly JsonSerializerSettings serializerSettings; - /// - /// The CustomSchemaModelCacheKeyFactory will not resolve services from the DI container for constructor injection - /// so this is necessary in order to set the custom schema for the Model Cache. - /// - internal IDbContextCustomSchema DbContextCustomSchema { get; set; } public ElsaContext(DbContextOptions options) : base(options) { serializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); DbContextCustomSchema = options.GetDbContextCustomSchema(); } + + /// + /// The CustomSchemaModelCacheKeyFactory will not resolve services from the DI container for constructor injection + /// so this is necessary in order to set the custom schema for the Model Cache. + /// + internal IDbContextCustomSchema DbContextCustomSchema { get; } public DbSet WorkflowDefinitionVersions { get; set; } public DbSet WorkflowInstances { get; set; } From 0fbb47ff8fa232322238f6f18f86f372f8e3a496 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 21 Dec 2019 10:45:16 +0100 Subject: [PATCH 3/3] Fix broken Sample23 --- src/samples/Sample23/ElsaContextFactory.cs | 1 + src/samples/Sample23/Program.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/samples/Sample23/ElsaContextFactory.cs b/src/samples/Sample23/ElsaContextFactory.cs index d49e8b1e4..520651f97 100644 --- a/src/samples/Sample23/ElsaContextFactory.cs +++ b/src/samples/Sample23/ElsaContextFactory.cs @@ -1,4 +1,5 @@ using Elsa; +using Elsa.Persistence.EntityFrameworkCore.CustomSchema; using Elsa.Persistence.EntityFrameworkCore.DbContexts; using Elsa.Persistence.EntityFrameworkCore.Extensions; using Microsoft.EntityFrameworkCore; diff --git a/src/samples/Sample23/Program.cs b/src/samples/Sample23/Program.cs index 05ef67381..583b03b08 100644 --- a/src/samples/Sample23/Program.cs +++ b/src/samples/Sample23/Program.cs @@ -5,6 +5,7 @@ using Elsa.Extensions; using Elsa.Models; using Elsa.Persistence; using Elsa.Persistence.EntityFrameworkCore; +using Elsa.Persistence.EntityFrameworkCore.CustomSchema; using Elsa.Persistence.EntityFrameworkCore.DbContexts; using Elsa.Persistence.EntityFrameworkCore.Extensions; using Elsa.Runtime;