From 349a0e722ec7ca0b5ce9ce4f64f7bc16e6bb6812 Mon Sep 17 00:00:00 2001 From: Charlie Kendall Date: Tue, 3 Mar 2020 22:02:02 +0000 Subject: [PATCH] Add InMemory Snoozle implementation and cleanup - The HttpVerbs parameter for the custom computation is no longer optional - PropertyConfigurationsForWrite has been split into Create/Update --- src/.editorconfig | 9 ++ .../Configuration/InMemoryConfiguration.cs | 6 + .../IInMemoryModelConfiguration.cs | 9 ++ .../IInMemoryPropertyConfiguration.cs | 9 ++ .../IInMemoryResourceConfiguration.cs | 8 ++ .../IInMemoryRuntimeConfiguration.cs | 19 +++ .../IInMemoryRuntimeConfigurationProvider.cs | 8 ++ .../Implementation/InMemoryDataProvider.cs | 62 +++++++++ .../InMemoryModelConfiguration.cs | 10 ++ .../InMemoryModelConfigurationBuilder.cs | 12 ++ .../InMemoryPropertyConfiguration.cs | 15 +++ .../InMemoryPropertyConfigurationBuilder.cs | 13 ++ .../InMemoryResourceConfiguration.cs | 16 +++ .../InMemoryRuntimeConfiguration.cs | 69 ++++++++++ .../InMemoryRuntimeConfigurationProvider.cs | 14 ++ .../InMemoryResourceConfigurationBuilder.cs | 51 ++++++++ .../ResourceConfigurationBuilderExtensions.cs | 122 ++++++++++++++++++ .../ServiceCollectionExtensions.cs | 62 +++++++++ src/Snoozle.InMemory/Snoozle.InMemory.csproj | 24 ++++ .../Controllers/ValuesController.cs | 45 +++++++ src/Snoozle.TestHarness/Program.cs | 24 ++++ .../Properties/launchSettings.json | 30 +++++ src/Snoozle.TestHarness/RestResources/Cat.cs | 22 ++++ .../Snoozle.TestHarness.csproj | 17 +++ src/Snoozle.TestHarness/Startup.cs | 52 ++++++++ .../appsettings.Development.json | 9 ++ src/Snoozle.TestHarness/appsettings.json | 8 ++ src/Snoozle.sln | 12 ++ .../Abstractions/BasePropertyConfiguration.cs | 4 + .../BasePropertyConfigurationBuilder.cs | 31 ++--- .../Abstractions/BaseResourceConfiguration.cs | 11 +- .../BaseResourceConfigurationBuilder.cs | 10 +- .../Abstractions/IPropertyConfiguration.cs | 4 + .../IPropertyConfigurationBuilder.cs | 22 +++- .../Abstractions/IResourceConfiguration.cs | 4 +- .../Models/ValueComputationFuncModel.cs | 8 +- src/Snoozle/Core/RestResourceController.cs | 6 +- src/Snoozle/Expressions/ExpressionBuilder.cs | 2 +- .../PropertyConfigurationBuilderExtensions.cs | 29 ++--- 39 files changed, 837 insertions(+), 51 deletions(-) create mode 100644 src/Snoozle.InMemory/Configuration/InMemoryConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/IInMemoryModelConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/IInMemoryPropertyConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/IInMemoryResourceConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfigurationProvider.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryDataProvider.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryModelConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryModelConfigurationBuilder.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryPropertyConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryPropertyConfigurationBuilder.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryResourceConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfiguration.cs create mode 100644 src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfigurationProvider.cs create mode 100644 src/Snoozle.InMemory/InMemoryResourceConfigurationBuilder.cs create mode 100644 src/Snoozle.InMemory/ResourceConfigurationBuilderExtensions.cs create mode 100644 src/Snoozle.InMemory/ServiceCollectionExtensions.cs create mode 100644 src/Snoozle.InMemory/Snoozle.InMemory.csproj create mode 100644 src/Snoozle.TestHarness/Controllers/ValuesController.cs create mode 100644 src/Snoozle.TestHarness/Program.cs create mode 100644 src/Snoozle.TestHarness/Properties/launchSettings.json create mode 100644 src/Snoozle.TestHarness/RestResources/Cat.cs create mode 100644 src/Snoozle.TestHarness/Snoozle.TestHarness.csproj create mode 100644 src/Snoozle.TestHarness/Startup.cs create mode 100644 src/Snoozle.TestHarness/appsettings.Development.json create mode 100644 src/Snoozle.TestHarness/appsettings.json diff --git a/src/.editorconfig b/src/.editorconfig index f1d70de..d1023f1 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -23,3 +23,12 @@ dotnet_diagnostic.CA1308.severity = none # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = none + +# CA1812: Internal class that is apparently never instantiated +dotnet_diagnostic.CA1812.severity = none + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = none + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = none diff --git a/src/Snoozle.InMemory/Configuration/InMemoryConfiguration.cs b/src/Snoozle.InMemory/Configuration/InMemoryConfiguration.cs new file mode 100644 index 0000000..1b32863 --- /dev/null +++ b/src/Snoozle.InMemory/Configuration/InMemoryConfiguration.cs @@ -0,0 +1,6 @@ +namespace Snoozle.InMemory.Configuration +{ + public class InMemoryConfiguration + { + } +} diff --git a/src/Snoozle.InMemory/Implementation/IInMemoryModelConfiguration.cs b/src/Snoozle.InMemory/Implementation/IInMemoryModelConfiguration.cs new file mode 100644 index 0000000..cdbdc60 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/IInMemoryModelConfiguration.cs @@ -0,0 +1,9 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public interface IInMemoryModelConfiguration : IModelConfiguration + { + string JsonFilePath { get; set; } + } +} \ No newline at end of file diff --git a/src/Snoozle.InMemory/Implementation/IInMemoryPropertyConfiguration.cs b/src/Snoozle.InMemory/Implementation/IInMemoryPropertyConfiguration.cs new file mode 100644 index 0000000..92e9e59 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/IInMemoryPropertyConfiguration.cs @@ -0,0 +1,9 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public interface IInMemoryPropertyConfiguration : IPropertyConfiguration + { + int GetNextPrimaryKeyValue(); + } +} diff --git a/src/Snoozle.InMemory/Implementation/IInMemoryResourceConfiguration.cs b/src/Snoozle.InMemory/Implementation/IInMemoryResourceConfiguration.cs new file mode 100644 index 0000000..1575fc6 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/IInMemoryResourceConfiguration.cs @@ -0,0 +1,8 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public interface IInMemoryResourceConfiguration : IResourceConfiguration + { + } +} diff --git a/src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfiguration.cs b/src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfiguration.cs new file mode 100644 index 0000000..0a8421b --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfiguration.cs @@ -0,0 +1,19 @@ +using Snoozle.Abstractions; +using System.Collections.Generic; + +namespace Snoozle.InMemory.Implementation +{ + public interface IInMemoryRuntimeConfiguration : IRuntimeConfiguration + where TResource : class, IRestResource + { + TResource GetEntryByPrimaryKey(string primaryKey); + + IEnumerable GetAllEntries(); + + TResource InsertEntry(object resource); + + TResource UpdateEntry(object resource, string primaryKey); + + bool DeleteEntry(string primaryKey); + } +} \ No newline at end of file diff --git a/src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfigurationProvider.cs b/src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfigurationProvider.cs new file mode 100644 index 0000000..1a5a4d9 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/IInMemoryRuntimeConfigurationProvider.cs @@ -0,0 +1,8 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public interface IInMemoryRuntimeConfigurationProvider : IRuntimeConfigurationProvider> + { + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryDataProvider.cs b/src/Snoozle.InMemory/Implementation/InMemoryDataProvider.cs new file mode 100644 index 0000000..cd30ca4 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryDataProvider.cs @@ -0,0 +1,62 @@ +using Snoozle.Abstractions; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Snoozle.InMemory.Implementation +{ + internal class InMemoryDataProvider : IDataProvider + { + private readonly IInMemoryRuntimeConfigurationProvider _inMemoryRuntimeConfigurationProvider; + + public InMemoryDataProvider(IInMemoryRuntimeConfigurationProvider inMemoryRuntimeConfigurationProvider) + { + _inMemoryRuntimeConfigurationProvider = inMemoryRuntimeConfigurationProvider; + } + + public Task DeleteByIdAsync(object primaryKey) + where TResource : class, IRestResource + { + IInMemoryRuntimeConfiguration config = GetConfig(); + + return Task.FromResult(config.DeleteEntry(primaryKey.ToString())); + } + + public Task InsertAsync(TResource resourceToCreate) + where TResource : class, IRestResource + { + IInMemoryRuntimeConfiguration config = GetConfig(); + + return Task.FromResult(config.InsertEntry(resourceToCreate)); + } + + public Task> SelectAllAsync() + where TResource : class, IRestResource + { + IInMemoryRuntimeConfiguration config = GetConfig(); + + return Task.FromResult(config.GetAllEntries()); + } + + public Task SelectByIdAsync(object primaryKey) + where TResource : class, IRestResource + { + IInMemoryRuntimeConfiguration config = GetConfig(); + + return Task.FromResult(config.GetEntryByPrimaryKey(primaryKey.ToString())); + } + + public Task UpdateAsync(TResource resourceToUpdate, object primaryKey) + where TResource : class, IRestResource + { + IInMemoryRuntimeConfiguration config = GetConfig(); + + return Task.FromResult(config.UpdateEntry(resourceToUpdate, primaryKey.ToString())); + } + + private IInMemoryRuntimeConfiguration GetConfig() + where TResource : class, IRestResource + { + return (IInMemoryRuntimeConfiguration)_inMemoryRuntimeConfigurationProvider.GetRuntimeConfigurationForType(typeof(TResource)); + } + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryModelConfiguration.cs b/src/Snoozle.InMemory/Implementation/InMemoryModelConfiguration.cs new file mode 100644 index 0000000..8027b92 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryModelConfiguration.cs @@ -0,0 +1,10 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public class InMemoryModelConfiguration : BaseModelConfiguration, IInMemoryModelConfiguration + where TResource : class, IRestResource + { + public string JsonFilePath { get; set; } + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryModelConfigurationBuilder.cs b/src/Snoozle.InMemory/Implementation/InMemoryModelConfigurationBuilder.cs new file mode 100644 index 0000000..d75f7b1 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryModelConfigurationBuilder.cs @@ -0,0 +1,12 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public class InMemoryModelConfigurationBuilder : BaseModelConfigurationBuilder, IModelConfigurationBuilder + { + public InMemoryModelConfigurationBuilder(IInMemoryModelConfiguration modelConfiguration) + : base(modelConfiguration) + { + } + } +} \ No newline at end of file diff --git a/src/Snoozle.InMemory/Implementation/InMemoryPropertyConfiguration.cs b/src/Snoozle.InMemory/Implementation/InMemoryPropertyConfiguration.cs new file mode 100644 index 0000000..503288b --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryPropertyConfiguration.cs @@ -0,0 +1,15 @@ +using Snoozle.Abstractions; +using System.Threading; + +namespace Snoozle.InMemory.Implementation +{ + public class InMemoryPropertyConfiguration : BasePropertyConfiguration, IInMemoryPropertyConfiguration + { + private int _currentPrimaryKeyValue; + + public int GetNextPrimaryKeyValue() + { + return Interlocked.Increment(ref _currentPrimaryKeyValue); + } + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryPropertyConfigurationBuilder.cs b/src/Snoozle.InMemory/Implementation/InMemoryPropertyConfigurationBuilder.cs new file mode 100644 index 0000000..d3122d5 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryPropertyConfigurationBuilder.cs @@ -0,0 +1,13 @@ +using Snoozle.Abstractions; + +namespace Snoozle.InMemory.Implementation +{ + public class InMemoryPropertyConfigurationBuilder : BasePropertyConfigurationBuilder, IPropertyConfigurationBuilder + where TResource : class, IRestResource + { + public InMemoryPropertyConfigurationBuilder(IInMemoryPropertyConfiguration propertyConfiguration) + : base(propertyConfiguration) + { + } + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryResourceConfiguration.cs b/src/Snoozle.InMemory/Implementation/InMemoryResourceConfiguration.cs new file mode 100644 index 0000000..93bf896 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryResourceConfiguration.cs @@ -0,0 +1,16 @@ +using Snoozle.Abstractions; +using System.Collections.Generic; + +namespace Snoozle.InMemory.Implementation +{ + public class InMemoryResourceConfiguration : BaseResourceConfiguration, IInMemoryResourceConfiguration + where TResource : class, IRestResource + { + public InMemoryResourceConfiguration( + IInMemoryModelConfiguration modelConfiguration, + IEnumerable propertyConfigurations) + : base(modelConfiguration, propertyConfigurations) + { + } + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfiguration.cs b/src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfiguration.cs new file mode 100644 index 0000000..6b8ca64 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfiguration.cs @@ -0,0 +1,69 @@ +using Snoozle.Abstractions; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Snoozle.InMemory.Implementation +{ + internal class InMemoryRuntimeConfiguration : BaseRuntimeConfiguration, IInMemoryRuntimeConfiguration + where TResource : class, IRestResource + { + private readonly ConcurrentDictionary _data; + + // This is created by reflection, so be careful when changing/adding parameters + public InMemoryRuntimeConfiguration(IInMemoryResourceConfiguration resourceConfiguration, List initialData) + : base(resourceConfiguration) + { + initialData = initialData ?? new List(); + _data = new ConcurrentDictionary(initialData?.ToDictionary(x => GetPrimaryKeyValue(x).ToString())); + PropertyConfigurationBuilderExtensions.SetGlobalPrimaryKeyInteger(GetMaxPrimaryKeyIntegerValueOrDefault()); + } + + private int GetMaxPrimaryKeyIntegerValueOrDefault() + { + try + { + return _data.Keys.Select(x => int.Parse(x)).Max(); + } + catch + { + return default; + } + } + + public bool DeleteEntry(string primaryKey) + { + _data.TryRemove(primaryKey, out TResource resourceRemoved); + + return resourceRemoved != default; + } + + public IEnumerable GetAllEntries() + { + return _data.Values.AsEnumerable(); + } + + public TResource GetEntryByPrimaryKey(string primaryKey) + { + return _data.GetValueOrDefault(primaryKey); + } + + public TResource InsertEntry(object resource) + { + TResource typedResource = (TResource)resource; + object primaryKey = GetPrimaryKeyValue(typedResource); + + return _data.TryAdd(primaryKey.ToString(), typedResource) ? typedResource : default; + } + + public TResource UpdateEntry(object resource, string primaryKey) + { + TResource typedResource = (TResource)resource; + + // If we are not able to retrieve the current value and update it to the new value, return the default value + return _data.TryGetValue(primaryKey, out TResource existingResource) && _data.TryUpdate(primaryKey, typedResource, existingResource) + ? typedResource + : default; + } + } +} diff --git a/src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfigurationProvider.cs b/src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfigurationProvider.cs new file mode 100644 index 0000000..caa9700 --- /dev/null +++ b/src/Snoozle.InMemory/Implementation/InMemoryRuntimeConfigurationProvider.cs @@ -0,0 +1,14 @@ +using Snoozle.Abstractions; +using System; +using System.Collections.Generic; + +namespace Snoozle.InMemory.Implementation +{ + internal class InMemoryRuntimeConfigurationProvider : BaseRuntimeConfigurationProvider>, IInMemoryRuntimeConfigurationProvider + { + public InMemoryRuntimeConfigurationProvider(Dictionary> runtimeConfigurations) + : base(runtimeConfigurations) + { + } + } +} diff --git a/src/Snoozle.InMemory/InMemoryResourceConfigurationBuilder.cs b/src/Snoozle.InMemory/InMemoryResourceConfigurationBuilder.cs new file mode 100644 index 0000000..dd56068 --- /dev/null +++ b/src/Snoozle.InMemory/InMemoryResourceConfigurationBuilder.cs @@ -0,0 +1,51 @@ +using Snoozle.Abstractions; +using Snoozle.Exceptions; +using Snoozle.InMemory.Implementation; +using System.Linq; + +namespace Snoozle.InMemory +{ + public abstract class InMemoryResourceConfigurationBuilder : BaseResourceConfigurationBuilder + where TResource : class, IRestResource + { + protected override IPropertyConfigurationBuilder CreatePropertyConfigurationBuilder( + IInMemoryPropertyConfiguration propertyConfiguration) + { + return new InMemoryPropertyConfigurationBuilder(propertyConfiguration); + } + + protected override IInMemoryResourceConfiguration CreateResourceConfiguration() + { + return new InMemoryResourceConfiguration(ModelConfiguration, PropertyConfigurations); + } + + protected override IInMemoryModelConfiguration CreateModelConfiguration() + { + return new InMemoryModelConfiguration(); + } + + protected override IInMemoryPropertyConfiguration CreatePropertyConfiguration() + { + return new InMemoryPropertyConfiguration(); + } + + protected override IModelConfigurationBuilder CreateModelConfigurationBuilder() + { + return new InMemoryModelConfigurationBuilder(ModelConfiguration); + } + + protected override void SetPropertyConfigurationDefaults() + { + base.SetPropertyConfigurationDefaults(); + } + + protected override void ValidateFinal() + { + base.ValidateFinal(); + + ExceptionHelper.InvalidOperation.ThrowIfTrue( + PropertyConfigurations.Single(prop => prop.IsPrimaryResourceIdentifier).ValueComputationFunc == null, + $"The primary identifier for {typeof(TResource).Name} must have a unique value computation function defined for it (e.g. () => Guid.NewGuid())."); + } + } +} diff --git a/src/Snoozle.InMemory/ResourceConfigurationBuilderExtensions.cs b/src/Snoozle.InMemory/ResourceConfigurationBuilderExtensions.cs new file mode 100644 index 0000000..b928fae --- /dev/null +++ b/src/Snoozle.InMemory/ResourceConfigurationBuilderExtensions.cs @@ -0,0 +1,122 @@ +using Snoozle.Abstractions; +using Snoozle.Exceptions; +using Snoozle.InMemory.Implementation; +using System; +using System.Threading; + +namespace Snoozle.InMemory +{ + public static class ModelConfigurationBuilderExtensions + { + /// + /// Path to the JSON file that contains the initial data for the REST API. + /// + public static IModelConfigurationBuilder HasJsonFilePath( + this IModelConfigurationBuilder @this, + string jsonFilePath) + { + ExceptionHelper.ArgumentNull.ThrowIfNecessary(jsonFilePath, nameof(jsonFilePath)); + + @this.ModelConfiguration.JsonFilePath = jsonFilePath; + + return @this; + } + } + + public static class PropertyConfigurationBuilderExtensions + { + private static int _globalPrimaryKey = 0; + private static readonly object _lock = new object(); + + /// + /// Sets the minimum value of the auto-incremented primary key to be used globally by all resources if the provided value is larger than the + /// existing minimum value. This should only be called during app startup. + /// + public static void SetGlobalPrimaryKeyInteger(int value) + { + lock (_lock) + { + if (value > _globalPrimaryKey) + { + _globalPrimaryKey = value; + } + } + } + + /// + /// Set the value of the property to a global auto-incrementing integer. This is shared accross all properties that use this method. + /// + /// The type of the property configuration. + /// The property configuration builder instance. + /// The property configuration builder instance. + public static IPropertyConfigurationBuilder AutoIncrementingInteger( + this IComputedValueBuilder builder) + where TPropertyConfiguration : IInMemoryPropertyConfiguration + { + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = () => Interlocked.Increment(ref _globalPrimaryKey); + + // Any property that has a computed value is by definition not read-only, so explicitly enforce this + var propertyBuilder = builder as IPropertyConfigurationBuilder; + propertyBuilder.IsReadOnly(false); + + return propertyBuilder; + } + + /// + /// Set the value of the property to a global auto-incrementing integer. This is shared accross all properties that use this method. + /// + /// The type of the property configuration. + /// The property configuration builder instance. + /// The property configuration builder instance. + public static IPropertyConfigurationBuilder AutoIncrementingInteger( + this IComputedValueBuilder builder) + where TPropertyConfiguration : IInMemoryPropertyConfiguration + { + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = () => Interlocked.Increment(ref _globalPrimaryKey); + + // Any property that has a computed value is by definition not read-only, so explicitly enforce this + var propertyBuilder = builder as IPropertyConfigurationBuilder; + propertyBuilder.IsReadOnly(false); + + return propertyBuilder; + } + + /// + /// Set the value of the property to a randomly generated GUID. + /// + /// The type of the property configuration. + /// The property configuration builder instance. + /// The property configuration builder instance. + public static IPropertyConfigurationBuilder RandomlyGeneratedGuid( + this IComputedValueBuilder builder) + where TPropertyConfiguration : IInMemoryPropertyConfiguration + { + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = () => Guid.NewGuid(); + + // Any property that has a computed value is by definition not read-only, so explicitly enforce this + var propertyBuilder = builder as IPropertyConfigurationBuilder; + propertyBuilder.IsReadOnly(false); + + return propertyBuilder; + } + + /// + /// Set the value of the property to a randomly generated GUID. + /// + /// The type of the property configuration. + /// The property configuration builder instance. + /// The property configuration builder instance. + public static IPropertyConfigurationBuilder RandomlyGeneratedGuid( + this IComputedValueBuilder builder) + where TPropertyConfiguration : IInMemoryPropertyConfiguration + { + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = () => Guid.NewGuid(); + + // Any property that has a computed value is by definition not read-only, so explicitly enforce this + var propertyBuilder = builder as IPropertyConfigurationBuilder; + propertyBuilder.IsReadOnly(false); + + return propertyBuilder; + } + } +} diff --git a/src/Snoozle.InMemory/ServiceCollectionExtensions.cs b/src/Snoozle.InMemory/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a8f528c --- /dev/null +++ b/src/Snoozle.InMemory/ServiceCollectionExtensions.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Snoozle.Abstractions; +using Snoozle.InMemory.Implementation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Snoozle.InMemory +{ + public static class ServiceCollectionExtensions + { + public static IMvcBuilder AddSnoozleInMemory(this IMvcBuilder @this) + { + IServiceCollection serviceCollection = @this.Services; + IInMemoryRuntimeConfigurationProvider runtimeConfigurationProvider = BuildRuntimeConfigurationProvider(); + + serviceCollection.AddScoped(); + serviceCollection.AddSingleton(runtimeConfigurationProvider); + + @this.AddSnoozleCore(runtimeConfigurationProvider); + + return @this; + } + + private static IInMemoryRuntimeConfigurationProvider BuildRuntimeConfigurationProvider() + { + IEnumerable resourceConfigurations = + ResourceConfigurationBuilder.Build(typeof(InMemoryResourceConfigurationBuilder<>)); + + var runtimeConfigurations = new Dictionary>(); + + foreach (IInMemoryResourceConfiguration configuration in resourceConfigurations) + { + object resourceList = null; + + // Read any initial data from the JSON file specified if possible + if (configuration.ModelConfiguration.JsonFilePath != null) + { + string json = File.ReadAllText(configuration.ModelConfiguration.JsonFilePath); + Type genericListType = typeof(List<>).MakeGenericType(configuration.ResourceType); + MethodInfo genericMethod = typeof(JsonConvert) + .GetMethod(nameof(JsonConvert.DeserializeObject), 1, new Type[] { typeof(string) }) + .MakeGenericMethod(genericListType); + + // Create the generic list of resource objects + resourceList = genericMethod.Invoke(null, new object[] { json }); + } + + var runtimeConfiguration = Activator.CreateInstance( + typeof(InMemoryRuntimeConfiguration<>).MakeGenericType(configuration.ResourceType), + configuration, + resourceList) as IInMemoryRuntimeConfiguration; + + runtimeConfigurations.Add(configuration.ResourceType, runtimeConfiguration); + } + + return new InMemoryRuntimeConfigurationProvider(runtimeConfigurations); + } + } +} \ No newline at end of file diff --git a/src/Snoozle.InMemory/Snoozle.InMemory.csproj b/src/Snoozle.InMemory/Snoozle.InMemory.csproj new file mode 100644 index 0000000..cb2d909 --- /dev/null +++ b/src/Snoozle.InMemory/Snoozle.InMemory.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp2.1;netcoreapp2.2; + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/Snoozle.TestHarness/Controllers/ValuesController.cs b/src/Snoozle.TestHarness/Controllers/ValuesController.cs new file mode 100644 index 0000000..5fd4168 --- /dev/null +++ b/src/Snoozle.TestHarness/Controllers/ValuesController.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Snoozle.TestHarness.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ValuesController : ControllerBase + { + // GET api/values + [HttpGet] + public ActionResult> Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public ActionResult Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/src/Snoozle.TestHarness/Program.cs b/src/Snoozle.TestHarness/Program.cs new file mode 100644 index 0000000..be07341 --- /dev/null +++ b/src/Snoozle.TestHarness/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Snoozle.TestHarness +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/src/Snoozle.TestHarness/Properties/launchSettings.json b/src/Snoozle.TestHarness/Properties/launchSettings.json new file mode 100644 index 0000000..f5bab80 --- /dev/null +++ b/src/Snoozle.TestHarness/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60622", + "sslPort": 44358 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Snoozle.TestHarness": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/values", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/Snoozle.TestHarness/RestResources/Cat.cs b/src/Snoozle.TestHarness/RestResources/Cat.cs new file mode 100644 index 0000000..41deabd --- /dev/null +++ b/src/Snoozle.TestHarness/RestResources/Cat.cs @@ -0,0 +1,22 @@ +using System; + +namespace Snoozle.InMemory.TestHarness.RestResources +{ + public class Cat : IRestResource + { + public Guid? Id { get; set; } + + public int? HairLength { get; set; } + + public string Name { get; set; } + } + + public class CatResourceConfiguration : InMemoryResourceConfigurationBuilder + { + public override void Configure() + { + ConfigurationForModel().HasJsonFilePath("C:/temp/cats.json"); + ConfigurationForProperty(x => x.Id).HasComputedValue(HttpVerbs.POST).RandomlyGeneratedGuid(); + } + } +} diff --git a/src/Snoozle.TestHarness/Snoozle.TestHarness.csproj b/src/Snoozle.TestHarness/Snoozle.TestHarness.csproj new file mode 100644 index 0000000..247cfce --- /dev/null +++ b/src/Snoozle.TestHarness/Snoozle.TestHarness.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.2 + InProcess + + + + + + + + + + + + diff --git a/src/Snoozle.TestHarness/Startup.cs b/src/Snoozle.TestHarness/Startup.cs new file mode 100644 index 0000000..1c2ef4f --- /dev/null +++ b/src/Snoozle.TestHarness/Startup.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Snoozle.InMemory; + +namespace Snoozle.TestHarness +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services + .AddMvc() + .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) + .AddSnoozleInMemory(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseMvc(); + } + } +} diff --git a/src/Snoozle.TestHarness/appsettings.Development.json b/src/Snoozle.TestHarness/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/src/Snoozle.TestHarness/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Snoozle.TestHarness/appsettings.json b/src/Snoozle.TestHarness/appsettings.json new file mode 100644 index 0000000..def9159 --- /dev/null +++ b/src/Snoozle.TestHarness/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Snoozle.sln b/src/Snoozle.sln index 2ccf8c6..f6d38e7 100644 --- a/src/Snoozle.sln +++ b/src/Snoozle.sln @@ -12,6 +12,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snoozle.InMemory", "Snoozle.InMemory\Snoozle.InMemory.csproj", "{FEDD6BE5-DD8B-4F44-A11E-C4772711F08F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snoozle.TestHarness", "Snoozle.TestHarness\Snoozle.TestHarness.csproj", "{EBE8103F-AF97-401B-BF87-557E542FD4B7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +30,14 @@ Global {CCBD46EC-BBB7-46B3-A1D7-D666D55FD897}.Debug|Any CPU.Build.0 = Debug|Any CPU {CCBD46EC-BBB7-46B3-A1D7-D666D55FD897}.Release|Any CPU.ActiveCfg = Release|Any CPU {CCBD46EC-BBB7-46B3-A1D7-D666D55FD897}.Release|Any CPU.Build.0 = Release|Any CPU + {FEDD6BE5-DD8B-4F44-A11E-C4772711F08F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEDD6BE5-DD8B-4F44-A11E-C4772711F08F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEDD6BE5-DD8B-4F44-A11E-C4772711F08F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEDD6BE5-DD8B-4F44-A11E-C4772711F08F}.Release|Any CPU.Build.0 = Release|Any CPU + {EBE8103F-AF97-401B-BF87-557E542FD4B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBE8103F-AF97-401B-BF87-557E542FD4B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBE8103F-AF97-401B-BF87-557E542FD4B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBE8103F-AF97-401B-BF87-557E542FD4B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Snoozle/Abstractions/BasePropertyConfiguration.cs b/src/Snoozle/Abstractions/BasePropertyConfiguration.cs index e5e8b2f..dafb6a4 100644 --- a/src/Snoozle/Abstractions/BasePropertyConfiguration.cs +++ b/src/Snoozle/Abstractions/BasePropertyConfiguration.cs @@ -40,5 +40,9 @@ public abstract class BasePropertyConfiguration : IPropertyConfiguration /// A Func to be applied to the property during write operations. /// public ValueComputationFuncModel ValueComputationFunc { get; set; } = null; + + public bool HasComputedValue => ValueComputationFunc != null; + + public bool HasComputationEndpointTrigger(HttpVerbs endpoint) => (ValueComputationFunc?.EndpointTriggers & endpoint) == endpoint; } } diff --git a/src/Snoozle/Abstractions/BasePropertyConfigurationBuilder.cs b/src/Snoozle/Abstractions/BasePropertyConfigurationBuilder.cs index 8c081e7..37d796d 100644 --- a/src/Snoozle/Abstractions/BasePropertyConfigurationBuilder.cs +++ b/src/Snoozle/Abstractions/BasePropertyConfigurationBuilder.cs @@ -1,4 +1,6 @@ -namespace Snoozle.Abstractions +using Snoozle.Abstractions.Models; + +namespace Snoozle.Abstractions { /// /// Provides methods for setting the core property configuration values. @@ -17,32 +19,31 @@ protected BasePropertyConfigurationBuilder(TPropertyConfiguration propertyConfig PropertyConfiguration = propertyConfiguration; } + /// public TPropertyConfiguration PropertyConfiguration { get; } - /// - /// Sets the property to read-only. - /// - public IPropertyConfigurationBuilder IsReadOnlyColumn() + /// + public IPropertyConfigurationBuilder IsReadOnly(bool isReadOnly = true) { - PropertyConfiguration.IsReadOnly = true; + PropertyConfiguration.IsReadOnly = isReadOnly; + return this; } - /// - /// Sets the property as the primary key/identifier for the resource. Only one primary identifier can be set per resource. - /// - public IPropertyConfigurationBuilder IsPrimaryIdentifier() + /// + public IPropertyConfigurationBuilder IsPrimaryIdentifier(bool isValueAutoGeneratedByDataStore = true) { PropertyConfiguration.IsPrimaryResourceIdentifier = true; - IsReadOnlyColumn(); + IsReadOnly(isValueAutoGeneratedByDataStore); + return this; } - /// - /// Gets a that sets the value of the property automatically on write operations. - /// - public IComputedValueBuilder HasComputedValue() + /// + public IComputedValueBuilder HasComputedValue(HttpVerbs endpointTriggers) { + PropertyConfiguration.ValueComputationFunc = new ValueComputationFuncModel(endpointTriggers); + return this; } } diff --git a/src/Snoozle/Abstractions/BaseResourceConfiguration.cs b/src/Snoozle/Abstractions/BaseResourceConfiguration.cs index 1fc9d6e..f324709 100644 --- a/src/Snoozle/Abstractions/BaseResourceConfiguration.cs +++ b/src/Snoozle/Abstractions/BaseResourceConfiguration.cs @@ -32,9 +32,16 @@ protected BaseResourceConfiguration(TModelConfiguration modelConfiguration, IEnu public TPropertyConfiguration PrimaryIdentifier => AllPropertyConfigurations.Single(prop => prop.IsPrimaryResourceIdentifier); /// - /// All property configurations for values to be updated during read operations. + /// All property configurations for values to be set during creation operations. /// - public IEnumerable PropertyConfigurationsForWrite => AllPropertyConfigurations.OrderBy(prop => prop.Index).Where(prop => !prop.IsReadOnly); + public IEnumerable PropertyConfigurationsForCreate => AllPropertyConfigurations.OrderBy(prop => prop.Index).Where(prop => !prop.IsReadOnly); + + + /// + /// All property configurations for values to be set during update operations. + /// + public IEnumerable PropertyConfigurationsForUpdate => + PropertyConfigurationsForCreate.Where(x => (!x.HasComputedValue || x.ValueComputationFunc.HasEndpointTrigger(HttpVerbs.PUT)) && !x.IsPrimaryResourceIdentifier); /// /// Allowed HTTP method verbs for the resource. This overrides the globally configured verbs. diff --git a/src/Snoozle/Abstractions/BaseResourceConfigurationBuilder.cs b/src/Snoozle/Abstractions/BaseResourceConfigurationBuilder.cs index 9a7003d..038bec2 100644 --- a/src/Snoozle/Abstractions/BaseResourceConfigurationBuilder.cs +++ b/src/Snoozle/Abstractions/BaseResourceConfigurationBuilder.cs @@ -172,11 +172,15 @@ protected virtual void SetConventionsForModel() /// protected virtual void ValidateFinal() { - IEnumerable primaryIdentifiers = _propertyConfigurations.Values.Where(prop => prop.IsPrimaryResourceIdentifier); + IEnumerable primaryIdentifiers = PropertyConfigurations.Where(prop => prop.IsPrimaryResourceIdentifier); ExceptionHelper.InvalidOperation.ThrowIfTrue( - primaryIdentifiers.Count() > 1, - $"There must be a maximum of 1 property marked as the primary identifier for {nameof(TResource)}. " + + primaryIdentifiers.Count() != 1, + $"There must be exactly of 1 property marked as the primary identifier for {typeof(TResource).Name}. " + $"Currently: {string.Join(", ", primaryIdentifiers.Select(x => x.PropertyName))}"); + + ExceptionHelper.InvalidOperation.ThrowIfTrue( + primaryIdentifiers.First().HasComputedValue && primaryIdentifiers.First().ValueComputationFunc.EndpointTriggers.HasFlag(HttpVerbs.PUT), + $"The primary identifier for {typeof(TResource).Name} must not be updatable via PUT."); } } } diff --git a/src/Snoozle/Abstractions/IPropertyConfiguration.cs b/src/Snoozle/Abstractions/IPropertyConfiguration.cs index a45f9f9..8f9bf95 100644 --- a/src/Snoozle/Abstractions/IPropertyConfiguration.cs +++ b/src/Snoozle/Abstractions/IPropertyConfiguration.cs @@ -17,5 +17,9 @@ public interface IPropertyConfiguration bool IsPrimaryResourceIdentifier { get; set; } ValueComputationFuncModel ValueComputationFunc { get; set; } + + bool HasComputedValue { get; } + + bool HasComputationEndpointTrigger(HttpVerbs endpoint); } } diff --git a/src/Snoozle/Abstractions/IPropertyConfigurationBuilder.cs b/src/Snoozle/Abstractions/IPropertyConfigurationBuilder.cs index 91efe6c..905fc4f 100644 --- a/src/Snoozle/Abstractions/IPropertyConfigurationBuilder.cs +++ b/src/Snoozle/Abstractions/IPropertyConfigurationBuilder.cs @@ -2,16 +2,32 @@ { public interface IPropertyConfigurationBuilderCore { + /// + /// The property configuration this builder is for. + /// TPropertyConfiguration PropertyConfiguration { get; } } public interface IPropertyConfigurationBuilder : IPropertyConfigurationBuilderCore { - IPropertyConfigurationBuilder IsReadOnlyColumn(); + /// + /// Sets the property to read-only. + /// + IPropertyConfigurationBuilder IsReadOnly(bool isReadOnly = true); - IPropertyConfigurationBuilder IsPrimaryIdentifier(); + /// + /// Sets the property as the primary key/identifier for the resource. Only one primary identifier can be set per resource. + /// + /// + /// True if the primary identifier value is determined automatically by the data persistence layer (e.g. auto-incrementing PK). + /// + IPropertyConfigurationBuilder IsPrimaryIdentifier(bool isValueAutoGeneratedByDataStore = true); - IComputedValueBuilder HasComputedValue(); + /// + /// Gets a that sets the value of the property automatically on write operations. + /// + /// The endpoints that will trigger this computed value. Values can be one or more of:, + IComputedValueBuilder HasComputedValue(HttpVerbs endpointTriggers); } public interface IComputedValueBuilder : IPropertyConfigurationBuilderCore diff --git a/src/Snoozle/Abstractions/IResourceConfiguration.cs b/src/Snoozle/Abstractions/IResourceConfiguration.cs index 98062db..a23be13 100644 --- a/src/Snoozle/Abstractions/IResourceConfiguration.cs +++ b/src/Snoozle/Abstractions/IResourceConfiguration.cs @@ -17,7 +17,9 @@ public interface IResourceConfiguration PropertyConfigurationsForWrite { get; } + IEnumerable PropertyConfigurationsForCreate { get; } + + IEnumerable PropertyConfigurationsForUpdate { get; } string Route { get; } diff --git a/src/Snoozle/Abstractions/Models/ValueComputationFuncModel.cs b/src/Snoozle/Abstractions/Models/ValueComputationFuncModel.cs index 47b8700..ac4375e 100644 --- a/src/Snoozle/Abstractions/Models/ValueComputationFuncModel.cs +++ b/src/Snoozle/Abstractions/Models/ValueComputationFuncModel.cs @@ -11,22 +11,22 @@ public class ValueComputationFuncModel /// /// Initialises a new instance of the class. /// - /// The func used to generate the value. /// The HTTP method verbs that will trigger the computation. - public ValueComputationFuncModel(Expression> valueComputationFunc, HttpVerbs endpointTriggers) + public ValueComputationFuncModel(HttpVerbs endpointTriggers) { - ValueComputationFunc = valueComputationFunc; EndpointTriggers = endpointTriggers; } /// /// The func to generate the property value. /// - public Expression> ValueComputationFunc { get; set; } = null; + public Expression> ValueComputationFunc { get; set; } /// /// The HTTP method verbs that will trigger the computation. /// public HttpVerbs EndpointTriggers { get; set; } = HttpVerbs.POST | HttpVerbs.PUT; + + public bool HasEndpointTrigger(HttpVerbs flag) => EndpointTriggers.HasFlag(flag); } } diff --git a/src/Snoozle/Core/RestResourceController.cs b/src/Snoozle/Core/RestResourceController.cs index b297f22..8808d36 100644 --- a/src/Snoozle/Core/RestResourceController.cs +++ b/src/Snoozle/Core/RestResourceController.cs @@ -178,8 +178,8 @@ private ActionResult MethodNotAllowed() private bool MethodIsDisallowed(HttpVerbs httpVerb) { - var disallowedGlobally = (_options.AllowedVerbs & httpVerb) != httpVerb; - var disallowedOnResource = (_runtimeConfiguration.AllowedVerbsFlags & httpVerb) != httpVerb; + var disallowedGlobally = !_options.AllowedVerbs.HasFlag(httpVerb); + var disallowedOnResource = !_runtimeConfiguration.AllowedVerbsFlags.HasFlag(httpVerb); return disallowedGlobally || disallowedOnResource; } @@ -190,7 +190,7 @@ private void ApplyComputedValues(TResource resource, HttpVerbs httpVerb) { // Only apply the computed value if the user has specified that it is to be applied for the given HTTP verb // i.e. some values are only updated for POST (e.g. DateCreated) - if ((action.EndpointTriggers & httpVerb) == httpVerb) + if (action.EndpointTriggers.HasFlag(httpVerb)) { action.ValueComputationAction(resource); } diff --git a/src/Snoozle/Expressions/ExpressionBuilder.cs b/src/Snoozle/Expressions/ExpressionBuilder.cs index a1c902a..e32a933 100644 --- a/src/Snoozle/Expressions/ExpressionBuilder.cs +++ b/src/Snoozle/Expressions/ExpressionBuilder.cs @@ -39,7 +39,7 @@ internal static class ExpressionBuilder where TPropertyConfiguration : class, IPropertyConfiguration where TModelConfiguration : class, IModelConfiguration { - TPropertyConfiguration[] configs = resourceConfiguration.PropertyConfigurationsForWrite.Where(c => c.ValueComputationFunc != null).ToArray(); + TPropertyConfiguration[] configs = resourceConfiguration.AllPropertyConfigurations.Where(c => c.ValueComputationFunc != null).ToArray(); var paramResource = Expression.Parameter(typeof(object), "resourceObject"); var typedResource = Expression.Variable(typeof(TResource), "typedResource"); diff --git a/src/Snoozle/PropertyConfigurationBuilderExtensions.cs b/src/Snoozle/PropertyConfigurationBuilderExtensions.cs index e9e988f..d6e3138 100644 --- a/src/Snoozle/PropertyConfigurationBuilderExtensions.cs +++ b/src/Snoozle/PropertyConfigurationBuilderExtensions.cs @@ -1,6 +1,4 @@ using Snoozle.Abstractions; -using Snoozle.Abstractions.Models; -using Snoozle.Exceptions; using System; using System.Linq.Expressions; @@ -13,14 +11,12 @@ public static class PropertyConfigurationBuilderExtensions /// /// The type of the property configuration. /// The property configuration builder instance. - /// The endpoints that will trigger this computed value. /// The property configuration builder instance. public static IPropertyConfigurationBuilder DateTimeNow( - this IComputedValueBuilder builder, - HttpVerbs endpointTriggers = HttpVerbs.POST | HttpVerbs.PUT) + this IComputedValueBuilder builder) where TPropertyConfiguration : IPropertyConfiguration { - builder.PropertyConfiguration.ValueComputationFunc = new ValueComputationFuncModel(() => DateTime.Now, endpointTriggers); + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = () => DateTime.Now; return builder as IPropertyConfigurationBuilder; } @@ -30,15 +26,13 @@ public static class PropertyConfigurationBuilderExtensions /// /// The type of the property configuration. /// The property configuration builder instance. - /// The endpoints that will trigger this computed value. /// The property configuration builder instance. public static IPropertyConfigurationBuilder DateTimeUtcNow( this IComputedValueBuilder builder, - HttpVerbs endpointTriggers = HttpVerbs.POST | HttpVerbs.PUT) + TPropertyConfiguration> builder) where TPropertyConfiguration : IPropertyConfiguration { - builder.PropertyConfiguration.ValueComputationFunc = new ValueComputationFuncModel(() => DateTime.UtcNow, endpointTriggers); + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = () => DateTime.UtcNow; return builder as IPropertyConfigurationBuilder; } @@ -50,22 +44,19 @@ public static class PropertyConfigurationBuilderExtensions /// The type of the property configuration. /// The property configuration builder instance. /// The func to apply to the property to generate the value that will be persisted. - /// The endpoints that will trigger this computed value. /// The property configuration builder instance. public static IPropertyConfigurationBuilder Custom( this IComputedValueBuilder builder, - Expression> computationFunc, - HttpVerbs endpointTriggers = HttpVerbs.POST | HttpVerbs.PUT) + Expression> computationFunc) where TPropertyConfiguration : IPropertyConfiguration { - ExceptionHelper.Argument.ThrowIfTrue( - computationFunc.Body.Type != typeof(TProperty), - $"Computation Func return type must match property type ({typeof(TProperty).Name}).", - nameof(computationFunc)); + builder.PropertyConfiguration.ValueComputationFunc.ValueComputationFunc = Expression.Lambda>(Expression.Convert(computationFunc.Body, typeof(object))); - builder.PropertyConfiguration.ValueComputationFunc = new ValueComputationFuncModel(computationFunc, endpointTriggers); ; + // Any property that has a computed value is by definition not read-only, so explicitly enforce this + var propertyBuilder = builder as IPropertyConfigurationBuilder; + propertyBuilder.IsReadOnly(false); - return builder as IPropertyConfigurationBuilder; + return propertyBuilder; } } }