diff --git a/src/PinkParrot/Configurations/InfrastructureDependencies.cs b/src/PinkParrot/Configurations/InfrastructureModule.cs similarity index 87% rename from src/PinkParrot/Configurations/InfrastructureDependencies.cs rename to src/PinkParrot/Configurations/InfrastructureModule.cs index 63a847407..a0dde6e87 100644 --- a/src/PinkParrot/Configurations/InfrastructureDependencies.cs +++ b/src/PinkParrot/Configurations/InfrastructureModule.cs @@ -11,6 +11,8 @@ using Autofac; using EventStore.ClientAPI; using EventStore.ClientAPI.SystemData; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; using MongoDB.Driver; using PinkParrot.Infrastructure.CQRS.Autofac; using PinkParrot.Infrastructure.CQRS.Commands; @@ -19,7 +21,7 @@ using PinkParrot.Read.Services.Implementations; namespace PinkParrot.Configurations { - public class InfrastructureDependencies : Module + public class InfrastructureModule : Module { protected override void Load(ContainerBuilder builder) { @@ -36,11 +38,19 @@ namespace PinkParrot.Configurations var mongoDatabase = mongoDbClient.GetDatabase("PinkParrot"); eventStore.ConnectAsync().Wait(); - + builder.RegisterInstance(new UserCredentials("admin", "changeit")) .AsSelf() .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterInstance(mongoDatabase) .As() .SingleInstance(); diff --git a/src/PinkParrot/Configurations/ReadDependencies.cs b/src/PinkParrot/Configurations/ReadModule.cs similarity index 90% rename from src/PinkParrot/Configurations/ReadDependencies.cs rename to src/PinkParrot/Configurations/ReadModule.cs index 00af51b62..08d793128 100644 --- a/src/PinkParrot/Configurations/ReadDependencies.cs +++ b/src/PinkParrot/Configurations/ReadModule.cs @@ -15,7 +15,7 @@ using PinkParrot.Read.Services.Implementations; namespace PinkParrot.Configurations { - public sealed class ReadDependencies : Module + public sealed class ReadModule : Module { protected override void Load(ContainerBuilder builder) { @@ -27,7 +27,7 @@ namespace PinkParrot.Configurations .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .As() .SingleInstance(); diff --git a/src/PinkParrot/Configurations/WriteDependencies.cs b/src/PinkParrot/Configurations/WriteModule.cs similarity index 96% rename from src/PinkParrot/Configurations/WriteDependencies.cs rename to src/PinkParrot/Configurations/WriteModule.cs index 3ed7f7bde..8d52481c5 100644 --- a/src/PinkParrot/Configurations/WriteDependencies.cs +++ b/src/PinkParrot/Configurations/WriteModule.cs @@ -13,7 +13,7 @@ using PinkParrot.Write.Schema; namespace PinkParrot.Configurations { - public class WriteDependencies : Module + public class WriteModule : Module { protected override void Load(ContainerBuilder builder) { diff --git a/src/PinkParrot/Modules/Api/BaseController.cs b/src/PinkParrot/Modules/Api/BaseController.cs new file mode 100644 index 000000000..cd7e35351 --- /dev/null +++ b/src/PinkParrot/Modules/Api/BaseController.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// BaseController.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; +using PinkParrot.Infrastructure.CQRS.Commands; + +namespace PinkParrot.Modules.Api +{ + public abstract class BaseController : Controller + { + public ICommandBus CommandBus { get; } + + protected BaseController(ICommandBus commandBus) + { + CommandBus = commandBus; + } + } +} diff --git a/src/PinkParrot/Modules/Api/Schemas/SchemaFieldsController.cs b/src/PinkParrot/Modules/Api/Schemas/SchemaFieldsController.cs index faf786452..f8dc11541 100644 --- a/src/PinkParrot/Modules/Api/Schemas/SchemaFieldsController.cs +++ b/src/PinkParrot/Modules/Api/Schemas/SchemaFieldsController.cs @@ -17,13 +17,11 @@ using Swashbuckle.SwaggerGen.Annotations; namespace PinkParrot.Modules.Api.Schemas { - public class SchemasFieldsController : Controller + public class SchemasFieldsController : BaseController { - private readonly ICommandBus commandBus; - public SchemasFieldsController(ICommandBus commandBus) + : base(commandBus) { - this.commandBus = commandBus; } /// @@ -38,7 +36,7 @@ namespace PinkParrot.Modules.Api.Schemas { var command = new AddModelField { Properties = field }; - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } /// @@ -52,7 +50,7 @@ namespace PinkParrot.Modules.Api.Schemas [SwaggerOperation(Tags = new[] { "Schemas" })] public Task Update(string name, long fieldId, [FromBody] UpdateModelField command) { - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } /// @@ -65,7 +63,7 @@ namespace PinkParrot.Modules.Api.Schemas [SwaggerOperation(Tags = new[] { "Schemas" })] public Task Hide(string name, long fieldId, HideModelField command) { - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } /// @@ -78,7 +76,7 @@ namespace PinkParrot.Modules.Api.Schemas [SwaggerOperation(Tags = new[] { "Schemas" })] public Task Show(string name, long fieldId, ShowModelField command) { - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } /// @@ -91,7 +89,7 @@ namespace PinkParrot.Modules.Api.Schemas [SwaggerOperation(Tags = new[] { "Schemas" })] public Task Enable(string name, long fieldId, EnableModelField command) { - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } /// @@ -104,7 +102,7 @@ namespace PinkParrot.Modules.Api.Schemas [SwaggerOperation(Tags = new[] { "Schemas" })] public Task Disable(string name, long fieldId, DisableModelField command) { - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } @@ -118,7 +116,7 @@ namespace PinkParrot.Modules.Api.Schemas [SwaggerOperation(Tags = new[] { "Schemas" })] public Task Delete(string name, long fieldId, DeleteModelField command) { - return commandBus.PublishAsync(command); + return CommandBus.PublishAsync(command); } } } \ No newline at end of file diff --git a/src/PinkParrot/Modules/Api/Schemas/SchemaListModel.cs b/src/PinkParrot/Modules/Api/Schemas/SchemaListModel.cs new file mode 100644 index 000000000..b1ca9bd0a --- /dev/null +++ b/src/PinkParrot/Modules/Api/Schemas/SchemaListModel.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// SchemaListModel.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; + +namespace PinkParrot.Modules.Api.Schemas +{ + public class SchemaListModel + { + [Required] + public Guid Id { get; set; } + + [Required] + public string Name { get; set; } + + [Required] + public DateTime Created { get; set; } + + [Required] + public DateTime LastModified { get; set; } + } +} diff --git a/src/PinkParrot/Modules/Api/Schemas/SchemasController.cs b/src/PinkParrot/Modules/Api/Schemas/SchemasController.cs index 03cee6941..f53113173 100644 --- a/src/PinkParrot/Modules/Api/Schemas/SchemasController.cs +++ b/src/PinkParrot/Modules/Api/Schemas/SchemasController.cs @@ -8,11 +8,14 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using PinkParrot.Core.Schema; using PinkParrot.Infrastructure.CQRS.Commands; -using PinkParrot.Read.Models; +using PinkParrot.Infrastructure.Reflection; +using PinkParrot.Read.Repositories; +using PinkParrot.Read.Services; using PinkParrot.Write.Schema.Commands; using Swashbuckle.SwaggerGen.Annotations; @@ -20,13 +23,17 @@ using Swashbuckle.SwaggerGen.Annotations; namespace PinkParrot.Modules.Api.Schemas { - public class SchemasController : Controller + public class SchemasController : BaseController { - private readonly ICommandBus commandBus; + private readonly IModelSchemaRepository modelSchemaRepository; + private readonly ITenantProvider tenantProvider; - public SchemasController(ICommandBus commandBus) + public SchemasController(ICommandBus commandBus, ITenantProvider tenantProvider, IModelSchemaRepository modelSchemaRepository) + : base(commandBus) { - this.commandBus = commandBus; + this.modelSchemaRepository = modelSchemaRepository; + + this.tenantProvider = tenantProvider; } /// @@ -35,9 +42,12 @@ namespace PinkParrot.Modules.Api.Schemas [HttpGet] [Route("schemas/")] [SwaggerOperation(Tags = new[] { "Schemas" })] - public Task> Query() + public async Task> Query() { - return null; + var tenantId = await tenantProvider.ProvideTenantIdByDomainAsync(Request.Host.ToString()); + var schemes = await modelSchemaRepository.QueryAllAsync(tenantId); + + return schemes.Select(s => SimpleMapper.Map(s, new SchemaListModel())).ToList(); } /// @@ -52,12 +62,12 @@ namespace PinkParrot.Modules.Api.Schemas [HttpPost] [Route("schemas/")] [SwaggerOperation(Tags = new[] { "Schemas" })] - [ProducesResponseType(typeof(EntityCreated), 204)] + [ProducesResponseType(typeof(EntityCreated), 201)] public async Task Create([FromBody] ModelSchemaProperties schema) { var command = new CreateModelSchema { AggregateId = Guid.NewGuid(), Properties = schema }; - await commandBus.PublishAsync(command); + await CommandBus.PublishAsync(command); return CreatedAtAction("Query", new EntityCreated { Id = command.AggregateId }); } @@ -73,11 +83,12 @@ namespace PinkParrot.Modules.Api.Schemas [HttpPut] [Route("schemas/{name}/")] [SwaggerOperation(Tags = new[] { "Schemas" })] + [ProducesResponseType(typeof(void), 204)] public async Task Update(string name, [FromBody] ModelSchemaProperties schema) { var command = new UpdateModelSchema { Properties = schema }; - await commandBus.PublishAsync(command); + await CommandBus.PublishAsync(command); return NoContent(); } @@ -92,9 +103,10 @@ namespace PinkParrot.Modules.Api.Schemas [HttpDelete] [Route("schemas/{name}/")] [SwaggerOperation(Tags = new[] { "Schemas" })] + [ProducesResponseType(typeof(void), 204)] public async Task Delete(string name) { - await commandBus.PublishAsync(new DeleteModelSchema()); + await CommandBus.PublishAsync(new DeleteModelSchema()); return NoContent(); } diff --git a/src/PinkParrot/Startup.cs b/src/PinkParrot/Startup.cs index 07c535691..c8da389eb 100644 --- a/src/PinkParrot/Startup.cs +++ b/src/PinkParrot/Startup.cs @@ -30,9 +30,9 @@ namespace PinkParrot var builder = new ContainerBuilder(); builder.Populate(services); - builder.RegisterModule(); - builder.RegisterModule(); - builder.RegisterModule(); + builder.RegisterModule(); + builder.RegisterModule(); + builder.RegisterModule(); return new AutofacServiceProvider(builder.Build()); } diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs index 9cde1fd76..cab396210 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs @@ -22,7 +22,7 @@ namespace PinkParrot.Infrastructure.CQRS foreach (var property in bag.Properties) { - Set(property.Key, property.Value); + Set(property.Key, property.Value.RawValue); } } diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs index 14b14752b..75677f981 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs @@ -10,6 +10,7 @@ using System; using System.Text; using EventStore.ClientAPI; using Newtonsoft.Json; +using NodaTime; using PinkParrot.Infrastructure.CQRS.Events; // ReSharper disable InconsistentNaming @@ -27,14 +28,14 @@ namespace PinkParrot.Infrastructure.CQRS.EventStore public Envelope Parse(ResolvedEvent @event) { - var headers = ReadJson(@event.Event.Metadata); + var headers = ReadJson(@event.Event.Metadata); var eventType = Type.GetType(headers.Properties[CommonHeaders.EventType].ToString()); var eventData = ReadJson(@event.Event.Data, eventType); var envelope = new Envelope(eventData, headers); - envelope.Headers.Set(CommonHeaders.Timestamp, DateTime.SpecifyKind(@event.Event.Created, DateTimeKind.Utc)); + envelope.Headers.Set(CommonHeaders.Timestamp, Instant.FromDateTimeUtc(DateTime.SpecifyKind(@event.Event.Created, DateTimeKind.Utc))); envelope.Headers.Set(CommonHeaders.EventNumber, @event.OriginalEventNumber); return envelope; diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/MongoExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/MongoExtensions.cs new file mode 100644 index 000000000..06fc0806a --- /dev/null +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/MongoExtensions.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// MongoExtensions.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using MongoDB.Driver; + +namespace PinkParrot.Infrastructure.MongoDb +{ + public static class MongoExtensions + { + public static async Task InsertOneIfExistsAsync(this IMongoCollection collection, T document) + { + try + { + await collection.InsertOneAsync(document); + } + catch (MongoWriteException ex) + { + if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + return false; + } + + throw; + } + + return true; + } + } +} diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/BaseMongoDbRepository.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/MongoRepositoryBase.cs similarity index 95% rename from src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/BaseMongoDbRepository.cs rename to src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/MongoRepositoryBase.cs index c2e984ab3..f208a2844 100644 --- a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/BaseMongoDbRepository.cs +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/MongoDb/MongoRepositoryBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// BaseMongoDbRepository.cs +// MongoRepositoryBase.cs // PinkParrot Headless CMS // ========================================================================== // Copyright (c) PinkParrot Group @@ -12,7 +12,7 @@ using MongoDB.Driver; namespace PinkParrot.Infrastructure.MongoDb { - public abstract class BaseMongoDbRepository + public abstract class MongoRepositoryBase { private const string CollectionFormat = "{0}Set"; private readonly IMongoCollection mongoCollection; @@ -83,7 +83,7 @@ namespace PinkParrot.Infrastructure.MongoDb } } - protected BaseMongoDbRepository(IMongoDatabase database) + protected MongoRepositoryBase(IMongoDatabase database) { Guard.NotNull(database, nameof(database)); diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/IPropertyAccessor.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/IPropertyAccessor.cs new file mode 100644 index 000000000..e40249982 --- /dev/null +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/IPropertyAccessor.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// IPropertyAccessor.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== +namespace PinkParrot.Infrastructure.Reflection +{ + public interface IPropertyAccessor + { + object Get(object target); + + void Set(object target, object value); + } +} diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/PropertiesTypeAccessor.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/PropertiesTypeAccessor.cs new file mode 100644 index 000000000..5e8ccb10d --- /dev/null +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/PropertiesTypeAccessor.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// PropertiesTypeAccessor.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace PinkParrot.Infrastructure.Reflection +{ + public sealed class PropertiesTypeAccessor + { + private static readonly ConcurrentDictionary AccessorCache = new ConcurrentDictionary(); + private readonly Dictionary accessors = new Dictionary(); + private readonly List properties = new List(); + + public IEnumerable Properties + { + get + { + return properties; + } + } + + private PropertiesTypeAccessor(Type type) + { + var allProperties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + + foreach (var property in allProperties.Where(property => property.CanRead && property.CanWrite)) + { + accessors[property.Name] = new PropertyAccessor(type, property); + + properties.Add(property); + } + } + + public static PropertiesTypeAccessor Create(Type targetType) + { + Guard.NotNull(targetType, nameof(targetType)); + + return AccessorCache.GetOrAdd(targetType, x => new PropertiesTypeAccessor(x)); + } + + public void SetValue(object target, string propertyName, object value) + { + Guard.NotNull(target, "target"); + + var accessor = FindAccessor(propertyName); + + accessor.Set(target, value); + } + + public object GetValue(object target, string propertyName) + { + Guard.NotNull(target, nameof(target)); + + var accessor = FindAccessor(propertyName); + + return accessor.Get(target); + } + + private IPropertyAccessor FindAccessor(string propertyName) + { + Guard.NotNullOrEmpty(propertyName, nameof(propertyName)); + + IPropertyAccessor accessor; + + if (!accessors.TryGetValue(propertyName, out accessor)) + { + throw new ArgumentException("Property does not exist.", nameof(propertyName)); + } + + return accessor; + } + } +} diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/PropertyAccessor.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/PropertyAccessor.cs new file mode 100644 index 000000000..52f21f56d --- /dev/null +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/PropertyAccessor.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// PropertyAccessor.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Reflection; + +namespace PinkParrot.Infrastructure.Reflection +{ + public sealed class PropertyAccessor : IPropertyAccessor + { + private sealed class PropertyWrapper : IPropertyAccessor + { + private readonly Func getMethod; + private readonly Action setMethod; + + public PropertyWrapper(PropertyInfo propertyInfo) + { + if (propertyInfo.CanRead) + { + getMethod = (Func)propertyInfo.GetGetMethod(true).CreateDelegate(typeof(Func)); + } + else + { + getMethod = x => { throw new NotSupportedException(); }; + } + + if (propertyInfo.CanWrite) + { + setMethod = (Action)propertyInfo.GetSetMethod(true).CreateDelegate(typeof(Action)); + } + else + { + setMethod = (x, y) => { throw new NotSupportedException(); }; + } + } + + public object Get(object source) + { + return getMethod((TObject)source); + } + + public void Set(object source, object value) + { + setMethod((TObject)source, (TValue)value); + } + } + + private readonly IPropertyAccessor internalAccessor; + + public PropertyAccessor(Type targetType, PropertyInfo propertyInfo) + { + Guard.NotNull(targetType, nameof(targetType)); + Guard.NotNull(propertyInfo, nameof(propertyInfo)); + + internalAccessor = (IPropertyAccessor)Activator.CreateInstance(typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType), propertyInfo); + } + + public object Get(object target) + { + Guard.NotNull(target, nameof(target)); + + return internalAccessor.Get(target); + } + + public void Set(object target, object value) + { + Guard.NotNull(target, nameof(target)); + + internalAccessor.Set(target, value); + } + } +} diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/SimpleMapper.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/SimpleMapper.cs new file mode 100644 index 000000000..8ead4fe25 --- /dev/null +++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Reflection/SimpleMapper.cs @@ -0,0 +1,175 @@ +// ========================================================================== +// SimpleMapper.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; + +// ReSharper disable StaticMemberInGenericType + +namespace PinkParrot.Infrastructure.Reflection +{ + public static class SimpleMapper + { + private sealed class ConversionPropertyMapper : PropertyMapper + { + private readonly Type targetType; + + public ConversionPropertyMapper(IPropertyAccessor srcAccessor, IPropertyAccessor dstAccessor, Type targetType) + : base(srcAccessor, dstAccessor) + { + this.targetType = targetType; + } + + public override void Map(object source, object destination, CultureInfo culture) + { + var value = GetValue(source); + + if (value == null) + { + return; + } + + object converted; + try + { + converted = Convert.ChangeType(value, targetType, culture); + + SetValue(destination, converted); + } + catch (InvalidCastException) + { + if (targetType == typeof(string)) + { + converted = value.ToString(); + + SetValue(destination, converted); + } + } + } + } + + private class PropertyMapper + { + private readonly IPropertyAccessor srcAccessor; + private readonly IPropertyAccessor dstAccessor; + + public PropertyMapper(IPropertyAccessor srcAccessor, IPropertyAccessor dstAccessor) + { + this.srcAccessor = srcAccessor; + this.dstAccessor = dstAccessor; + } + + public virtual void Map(object source, object destination, CultureInfo culture) + { + var value = GetValue(source); + + SetValue(destination, value); + } + + protected void SetValue(object destination, object value) + { + dstAccessor.Set(destination, value); + } + + protected object GetValue(object source) + { + return srcAccessor.Get(source); + } + } + + private static class ClassMapper + where TSource : class + where TDestination : class + { + private static readonly PropertyMapper[] Mappers; + + private static readonly Type[] Convertibles = + { + typeof(bool), + typeof(byte), + typeof(char), + typeof(decimal), + typeof(float), + typeof(double), + typeof(short), + typeof(int), + typeof(long), + typeof(string), + typeof(DateTime) + }; + + static ClassMapper() + { + var dstType = typeof(TDestination); + var srcType = typeof(TSource); + + var destinationProperties = dstType.GetProperties(); + + var newMappers = new List(); + + foreach (var srcProperty in srcType.GetProperties().Where(x => x.CanRead)) + { + var dstProperty = destinationProperties.FirstOrDefault(x => x.Name == srcProperty.Name); + + if (dstProperty == null || !dstProperty.CanWrite) + { + continue; + } + + var srcPropertyType = srcProperty.PropertyType; + var dstPropertyType = dstProperty.PropertyType; + + if (srcPropertyType == dstPropertyType) + { + newMappers.Add(new PropertyMapper(new PropertyAccessor(srcType, srcProperty), new PropertyAccessor(dstType, dstProperty))); + } + else + { + if (Convertibles.Contains(dstPropertyType)) + { + newMappers.Add(new ConversionPropertyMapper(new PropertyAccessor(srcType, srcProperty), new PropertyAccessor(dstType, dstProperty), dstPropertyType)); + } + } + } + + Mappers = newMappers.ToArray(); + } + + public static TDestination Map(TSource source, TDestination destination, CultureInfo culture) + { + foreach (var mapper in Mappers) + { + mapper.Map(source, destination, culture); + } + + return destination; + } + } + + public static TDestination Map(TSource source, TDestination destination) + where TSource : class + where TDestination : class + { + return Map(source, destination, CultureInfo.CurrentCulture); + } + + public static TDestination Map(TSource source, TDestination destination, CultureInfo culture) + where TSource : class + where TDestination : class + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(culture, nameof(culture)); + Guard.NotNull(destination, nameof(destination)); + + return ClassMapper.Map(source, destination, culture); + } + } +} diff --git a/src/pinkparrot_read/PinkParrot.Read/IEntity.cs b/src/pinkparrot_read/PinkParrot.Read/IEntity.cs new file mode 100644 index 000000000..855af63b5 --- /dev/null +++ b/src/pinkparrot_read/PinkParrot.Read/IEntity.cs @@ -0,0 +1,14 @@ +using System; + +namespace PinkParrot.Read.Models +{ + public interface IModelSchemaRM1 + { + DateTime Created { get; set; } + string Hints { get; set; } + string Label { get; set; } + DateTime Modified { get; set; } + string Name { get; set; } + Guid SchemaId { get; set; } + } +} \ No newline at end of file diff --git a/src/pinkparrot_read/PinkParrot.Read/IModelSchemaRM.cs b/src/pinkparrot_read/PinkParrot.Read/IModelSchemaRM.cs new file mode 100644 index 000000000..981e836c3 --- /dev/null +++ b/src/pinkparrot_read/PinkParrot.Read/IModelSchemaRM.cs @@ -0,0 +1,11 @@ +using System; + +namespace PinkParrot.Read.Models +{ + public interface IModelSchemaRM + { + DateTime Created { get; set; } + DateTime Modified { get; set; } + Guid SchemaId { get; set; } + } +} \ No newline at end of file diff --git a/src/pinkparrot_read/PinkParrot.Read/Models/IEntity.cs b/src/pinkparrot_read/PinkParrot.Read/Models/IEntity.cs new file mode 100644 index 000000000..ad074dc65 --- /dev/null +++ b/src/pinkparrot_read/PinkParrot.Read/Models/IEntity.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IEntity.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace PinkParrot.Read.Models +{ + public interface IEntity + { + Guid Id { get; set; } + + DateTime Created { get; set; } + + DateTime LastModified { get; set; } + } +} \ No newline at end of file diff --git a/src/pinkparrot_read/PinkParrot.Read/Models/ITenantEntity.cs b/src/pinkparrot_read/PinkParrot.Read/Models/ITenantEntity.cs new file mode 100644 index 000000000..b55127e6d --- /dev/null +++ b/src/pinkparrot_read/PinkParrot.Read/Models/ITenantEntity.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ITenantEntity.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace PinkParrot.Read.Models +{ + public interface ITenantEntity + { + Guid TenantId { get; set; } + } +} \ No newline at end of file diff --git a/src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaRM.cs b/src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaListRM.cs similarity index 62% rename from src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaRM.cs rename to src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaListRM.cs index 04d9e3266..ccae19ee2 100644 --- a/src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaRM.cs +++ b/src/pinkparrot_read/PinkParrot.Read/Models/ModelSchemaListRM.cs @@ -7,38 +7,37 @@ // ========================================================================== using System; -using System.ComponentModel.DataAnnotations; +using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using PinkParrot.Infrastructure; namespace PinkParrot.Read.Models { - public sealed class ModelSchemaRM + public sealed class ModelSchemaListRM : IEntity, ITenantEntity { - [Hide] [BsonId] - public string Id { get; set; } - - [Required] [BsonElement] - public Guid SchemaId { get; set; } + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } - [Required] + [BsonRequired] [BsonElement] public string Name { get; set; } - [Required] + [BsonRequired] [BsonElement] public DateTime Created { get; set; } - [Required] + [BsonRequired] [BsonElement] - public DateTime Modified { get; set; } + public DateTime LastModified { get; set; } + [BsonRequired] [BsonElement] - public string Label { get; set; } - + public Guid TenantId { get; set; } + + [BsonRequired] [BsonElement] - public string Hints { get; set; } + public bool IsDeleted { get; set; } } } diff --git a/src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaRepository.cs b/src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaRepository.cs index 73aa42957..32165aa4c 100644 --- a/src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaRepository.cs +++ b/src/pinkparrot_read/PinkParrot.Read/Repositories/IModelSchemaRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using PinkParrot.Read.Models; @@ -14,6 +15,6 @@ namespace PinkParrot.Read.Repositories { public interface IModelSchemaRepository { - Task> QueryAllAsync(); + Task> QueryAllAsync(Guid tenantId); } } diff --git a/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/EntityMapper.cs b/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/EntityMapper.cs new file mode 100644 index 000000000..d0a162c2b --- /dev/null +++ b/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/EntityMapper.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// EntityMapper.cs +// PinkParrot Headless CMS +// ========================================================================== +// Copyright (c) PinkParrot Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using PinkParrot.Infrastructure.CQRS; +using PinkParrot.Infrastructure.MongoDb; +using PinkParrot.Read.Models; + +namespace PinkParrot.Read.Repositories.Implementations +{ + public static class EntityMapper + { + public static T Create(EnvelopeHeaders headers) where T : IEntity, new() + { + var timestamp = headers.Timestamp().ToDateTimeUtc(); + + var entity = new T { Id = headers.AggregateId(), Created = timestamp }; + + var tenantEntity = entity as ITenantEntity; + + if (tenantEntity != null) + { + tenantEntity.TenantId = headers.TenantId(); + } + + return Update(entity, headers); + } + + public static T Update(T entity, EnvelopeHeaders headers) where T : IEntity + { + var timestamp = headers.Timestamp().ToDateTimeUtc(); + + entity.LastModified = timestamp; + + return entity; + } + + public static Task CreateAsync(this IMongoCollection collection, EnvelopeHeaders headers, Action updater) where T : class, IEntity, new() + { + var entity = Create(headers); + + updater(entity); + + return collection.InsertOneIfExistsAsync(entity); + } + + public static async Task UpdateAsync(this IMongoCollection collection, EnvelopeHeaders headers, Action updater) where T : class, IEntity + { + var entity = await collection.Find(t => t.Id == headers.AggregateId()).FirstOrDefaultAsync(); + + if (entity == null) + { + return; + } + + Update(entity, headers); + + updater(entity); + + await collection.ReplaceOneAsync(t => t.Id == entity.Id, entity); + } + } +} diff --git a/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaRepository.cs b/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaListRepository.cs similarity index 50% rename from src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaRepository.cs rename to src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaListRepository.cs index 7b7e89876..dcb3ddf1f 100644 --- a/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaRepository.cs +++ b/src/pinkparrot_read/PinkParrot.Read/Repositories/Implementations/MongoModelSchemaListRepository.cs @@ -16,47 +16,46 @@ using PinkParrot.Infrastructure.CQRS; using PinkParrot.Infrastructure.CQRS.Events; using PinkParrot.Infrastructure.Dispatching; using PinkParrot.Infrastructure.MongoDb; +using PinkParrot.Infrastructure.Tasks; using PinkParrot.Read.Models; namespace PinkParrot.Read.Repositories.Implementations { - public sealed class MongoModelSchemaRepository : BaseMongoDbRepository, IModelSchemaRepository, ICatchEventConsumer + public sealed class MongoModelSchemaListRepository : MongoRepositoryBase, IModelSchemaRepository, ICatchEventConsumer { - public MongoModelSchemaRepository(IMongoDatabase database) + public MongoModelSchemaListRepository(IMongoDatabase database) : base(database) { } - protected override Task SetupCollectionAsync(IMongoCollection collection) + protected override Task SetupCollectionAsync(IMongoCollection collection) { - return Collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.SchemaId)); + return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.Id)); } - public IQueryable QuerySchemas() + public IQueryable QuerySchemas() { return Collection.AsQueryable(); } - public Task> QueryAllAsync() + public Task> QueryAllAsync(Guid tenantId) { - return Collection.Find(s => true).ToListAsync(); + return Collection.Find(s => s.TenantId == tenantId && s.IsDeleted == false).ToListAsync(); } - public async void On(ModelSchemaCreated @event, EnvelopeHeaders headers) + public void On(ModelSchemaUpdated @event, EnvelopeHeaders headers) { - var now = DateTime.UtcNow; + Collection.UpdateAsync(headers, e => e.Name = @event.Properties.Name).Forget(); + } - var entity = new ModelSchemaRM - { - SchemaId = headers.AggregateId(), - Created = now, - Modified = now, - Name = @event.Properties.Name, - Hints = @event.Properties.Hints, - Label = @event.Properties.Label, - }; + public void On(ModelSchemaDeleted @event, EnvelopeHeaders headers) + { + Collection.UpdateAsync(headers, e => e.IsDeleted = true).Forget(); + } - await Collection.InsertOneAsync(entity); + public void On(ModelSchemaCreated @event, EnvelopeHeaders headers) + { + Collection.CreateAsync(headers, e => e.Name = @event.Properties.Name); } public void On(Envelope @event) diff --git a/src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoStreamPositionsStorage.cs b/src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoStreamPositionsStorage.cs index 84ede0443..b927345d7 100644 --- a/src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoStreamPositionsStorage.cs +++ b/src/pinkparrot_read/PinkParrot.Read/Services/Implementations/MongoStreamPositionsStorage.cs @@ -8,16 +8,15 @@ using EventStore.ClientAPI; using MongoDB.Bson; +using MongoDB.Driver; using PinkParrot.Infrastructure.CQRS.EventStore; using PinkParrot.Infrastructure.MongoDb; -using IFindFluentExtensions = MongoDB.Driver.IFindFluentExtensions; -using IMongoDatabase = MongoDB.Driver.IMongoDatabase; //// ReSharper disable once ConvertIfStatementToNullCoalescingExpression namespace PinkParrot.Read.Services.Implementations { - public sealed class MongoStreamPositionsStorage : BaseMongoDbRepository, IStreamPositionStorage + public sealed class MongoStreamPositionsStorage : MongoRepositoryBase, IStreamPositionStorage { private static readonly ObjectId Id = new ObjectId("507f1f77bcf86cd799439011"); @@ -28,14 +27,14 @@ namespace PinkParrot.Read.Services.Implementations public Position? ReadPosition() { - var document = IFindFluentExtensions.FirstOrDefault(Collection.Find(t => t.Id == Id)); + var document = Collection.Find(t => t.Id == Id).FirstOrDefault(); return document != null ? new Position(document.CommitPosition, document.PreparePosition) : Position.Start; } public void WritePosition(Position position) { - var document = IFindFluentExtensions.FirstOrDefault(Collection.Find(t => t.Id == Id)); + var document = Collection.Find(t => t.Id == Id).FirstOrDefault(); var isFound = document != null;