diff --git a/README.md b/README.md index aa78c72..8049fad 100644 --- a/README.md +++ b/README.md @@ -263,13 +263,78 @@ public class ApplicationSieveProcessor : SieveProcessor } ``` + + Now you should inject the new class instead: ```C# services.AddScoped(); ``` - Find More on Sieve's Fluent API [here](https://github.com/Biarity/Sieve/issues/4#issuecomment-364629048). +### Modular Fluent API configuration +Adding all fluent mappings directly in the processor can become unwieldy on larger projects. +It can also clash with vertical architectures. +To enable functional grouping of mappings the `ISieveConfiguration` interface was created together with extensions to the default mapper. +```C# +public class SieveConfigurationForPost : ISieveConfiguration +{ + protected override SievePropertyMapper Configure(SievePropertyMapper mapper) + { + mapper.Property(p => p.Title) + .CanFilter() + .HasName("a_different_query_name_here"); + + mapper.Property(p => p.CommentCount) + .CanSort(); + + mapper.Property(p => p.DateCreated) + .CanSort() + .CanFilter() + .HasName("created_on"); + + return mapper; + } +} +``` +With the processor simplified to: +```C# +public class ApplicationSieveProcessor : SieveProcessor +{ + public ApplicationSieveProcessor( + IOptions options, + ISieveCustomSortMethods customSortMethods, + ISieveCustomFilterMethods customFilterMethods) + : base(options, customSortMethods, customFilterMethods) + { + } + + protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) + { + return mapper + .ApplyConfiguration() + .ApplyConfiguration(); + } +} +``` +There is also the option to scan and add all configurations for a given assembly +```C# +public class ApplicationSieveProcessor : SieveProcessor +{ + public ApplicationSieveProcessor( + IOptions options, + ISieveCustomSortMethods customSortMethods, + ISieveCustomFilterMethods customFilterMethods) + : base(options, customSortMethods, customFilterMethods) + { + } + + protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) + { + return mapper.ApplyConfigurationForAssembly(typeof(ApplicationSieveProcessor).Assembly); + } +} +``` + ## Upgrading to v2.2.0 2.2.0 introduced OR logic for filter values. This means your custom filters will need to accept multiple values rather than just the one. diff --git a/Sieve.Sample/Entities/SieveConfigurationForPost.cs b/Sieve.Sample/Entities/SieveConfigurationForPost.cs new file mode 100644 index 0000000..0455322 --- /dev/null +++ b/Sieve.Sample/Entities/SieveConfigurationForPost.cs @@ -0,0 +1,15 @@ +using Sieve.Services; + +namespace Sieve.Sample.Entities +{ + public class SieveConfigurationForPost : ISieveConfiguration + { + public void Configure(SievePropertyMapper mapper) + { + mapper.Property(p => p.Title) + .CanSort() + .CanFilter() + .HasName("CustomTitleName"); + } + } +} diff --git a/Sieve.Sample/Services/ApplicationSieveProcessor.cs b/Sieve.Sample/Services/ApplicationSieveProcessor.cs index 29157f5..eddbea6 100644 --- a/Sieve.Sample/Services/ApplicationSieveProcessor.cs +++ b/Sieve.Sample/Services/ApplicationSieveProcessor.cs @@ -13,11 +13,18 @@ namespace Sieve.Sample.Services protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) { + // Option 1: Map all properties centrally mapper.Property(p => p.Title) .CanSort() .CanFilter() .HasName("CustomTitleName"); + // Option 2: Manually apply functionally grouped mapping configurations + //mapper.ApplyConfiguration(); + + // Option 3: Scan and apply all configurations + //mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly); + return mapper; } } diff --git a/Sieve/Services/ISieveConfiguration.cs b/Sieve/Services/ISieveConfiguration.cs new file mode 100644 index 0000000..22f3402 --- /dev/null +++ b/Sieve/Services/ISieveConfiguration.cs @@ -0,0 +1,70 @@ +#nullable enable +using System; +using System.Linq; +using System.Reflection; + +namespace Sieve.Services +{ + /// + /// Use this interface to create SieveConfiguration (just like EntityTypeConfigurations are defined for EF) + /// + public interface ISieveConfiguration + { + /// + /// Configures sieve property mappings. + /// + /// The mapper used to configure the sieve properties on. + void Configure(SievePropertyMapper mapper); + } + + /// + /// Configuration extensions to the + /// + public static class SieveConfigurationExtensions + { + /// + /// Applies configuration that is defined in an instance. + /// + /// The mapper to apply the configuration on. + /// The configuration to be applied. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public static SievePropertyMapper ApplyConfiguration(this SievePropertyMapper mapper) where T : ISieveConfiguration, new() + { + var configuration = new T(); + configuration.Configure(mapper); + return mapper; + } + + /// + /// Applies configuration from all + /// instances that are defined in provided assembly. + /// + /// The mapper to apply the configuration on. + /// The assembly to scan. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public static SievePropertyMapper ApplyConfigurationsFromAssembly(this SievePropertyMapper mapper, Assembly assembly) + { + foreach (var type in assembly.GetTypes().Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition)) + { + // Only accept types that contain a parameterless constructor, are not abstract. + var noArgConstructor = type.GetConstructor(Type.EmptyTypes); + if (noArgConstructor is null) + { + continue; + } + + if (type.GetInterfaces().Any(t => t == typeof(ISieveConfiguration))) + { + var configuration = (ISieveConfiguration)noArgConstructor.Invoke(new object?[] { }); + configuration.Configure(mapper); + } + } + + return mapper; + } + } +} diff --git a/SieveUnitTests/Abstractions/Entity/SieveConfigurationForIPost.cs b/SieveUnitTests/Abstractions/Entity/SieveConfigurationForIPost.cs new file mode 100644 index 0000000..f6270ed --- /dev/null +++ b/SieveUnitTests/Abstractions/Entity/SieveConfigurationForIPost.cs @@ -0,0 +1,37 @@ +using Sieve.Services; + +namespace SieveUnitTests.Abstractions.Entity +{ + public class SieveConfigurationForIPost : ISieveConfiguration + { + public void Configure(SievePropertyMapper mapper) + { + mapper.Property(p => p.ThisHasNoAttributeButIsAccessible) + .CanSort() + .CanFilter() + .HasName("shortname"); + + mapper.Property(p => p.TopComment.Text) + .CanFilter(); + + mapper.Property(p => p.TopComment.Id) + .CanSort(); + + mapper.Property(p => p.OnlySortableViaFluentApi) + .CanSort(); + + mapper.Property(p => p.TopComment.Text) + .CanFilter() + .HasName("topc"); + + mapper.Property(p => p.FeaturedComment.Text) + .CanFilter() + .HasName("featc"); + + mapper + .Property(p => p.DateCreated) + .CanSort() + .HasName("CreateDate"); + } + } +} diff --git a/SieveUnitTests/Entities/SieveConfigurationForPost.cs b/SieveUnitTests/Entities/SieveConfigurationForPost.cs new file mode 100644 index 0000000..8a02178 --- /dev/null +++ b/SieveUnitTests/Entities/SieveConfigurationForPost.cs @@ -0,0 +1,37 @@ +using Sieve.Services; + +namespace SieveUnitTests.Entities +{ + public class SieveConfigurationForPost : ISieveConfiguration + { + public void Configure(SievePropertyMapper mapper) + { + mapper.Property(p => p.ThisHasNoAttributeButIsAccessible) + .CanSort() + .CanFilter() + .HasName("shortname"); + + mapper.Property(p => p.TopComment.Text) + .CanFilter(); + + mapper.Property(p => p.TopComment.Id) + .CanSort(); + + mapper.Property(p => p.OnlySortableViaFluentApi) + .CanSort(); + + mapper.Property(p => p.TopComment.Text) + .CanFilter() + .HasName("topc"); + + mapper.Property(p => p.FeaturedComment.Text) + .CanFilter() + .HasName("featc"); + + mapper + .Property(p => p.DateCreated) + .CanSort() + .HasName("CreateDate"); + } + } +} diff --git a/SieveUnitTests/Mapper.cs b/SieveUnitTests/Mapper.cs index f8a126b..32861cc 100644 --- a/SieveUnitTests/Mapper.cs +++ b/SieveUnitTests/Mapper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Sieve.Exceptions; using Sieve.Models; +using Sieve.Services; using SieveUnitTests.Entities; using SieveUnitTests.Services; using Xunit; @@ -10,15 +11,10 @@ namespace SieveUnitTests { public class Mapper { - private readonly ApplicationSieveProcessor _processor; private readonly IQueryable _posts; public Mapper() { - _processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(), - new SieveCustomSortMethods(), - new SieveCustomFilterMethods()); - _posts = new List { new Post @@ -45,23 +41,49 @@ namespace SieveUnitTests }.AsQueryable(); } - [Fact] - public void MapperWorks() + /// + /// Processors with the same mappings but configured via a different method. + /// + /// + public static IEnumerable GetProcessors() + { + yield return new object[] { + new ApplicationSieveProcessor( + new SieveOptionsAccessor(), + new SieveCustomSortMethods(), + new SieveCustomFilterMethods())}; + yield return new object[] { + new ModularConfigurationSieveProcessor( + new SieveOptionsAccessor(), + new SieveCustomSortMethods(), + new SieveCustomFilterMethods())}; + yield return new object[] { + new ModularConfigurationWithScanSieveProcessor( + new SieveOptionsAccessor(), + new SieveCustomSortMethods(), + new SieveCustomFilterMethods())}; + } + + + [Theory] + [MemberData(nameof(GetProcessors))] + public void MapperWorks(ISieveProcessor processor) { var model = new SieveModel { Filters = "shortname@=A", }; - var result = _processor.Apply(model, _posts); + var result = processor.Apply(model, _posts); Assert.Equal("A", result.First().ThisHasNoAttributeButIsAccessible); Assert.True(result.Count() == 1); } - [Fact] - public void MapperSortOnlyWorks() + [Theory] + [MemberData(nameof(GetProcessors))] + public void MapperSortOnlyWorks(ISieveProcessor processor) { var model = new SieveModel { @@ -69,9 +91,9 @@ namespace SieveUnitTests Sorts = "OnlySortableViaFluentApi" }; - var result = _processor.Apply(model, _posts, applyFiltering: false, applyPagination: false); + var result = processor.Apply(model, _posts, applyFiltering: false, applyPagination: false); - Assert.Throws(() => _processor.Apply(model, _posts)); + Assert.Throws(() => processor.Apply(model, _posts)); Assert.Equal(3, result.First().Id); diff --git a/SieveUnitTests/Services/ModularConfigurationSieveProcessor.cs b/SieveUnitTests/Services/ModularConfigurationSieveProcessor.cs new file mode 100644 index 0000000..df78f66 --- /dev/null +++ b/SieveUnitTests/Services/ModularConfigurationSieveProcessor.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Options; +using Sieve.Models; +using Sieve.Services; +using SieveUnitTests.Abstractions.Entity; +using SieveUnitTests.Entities; + +namespace SieveUnitTests.Services +{ + public class ModularConfigurationSieveProcessor : SieveProcessor + { + public ModularConfigurationSieveProcessor( + IOptions options, + ISieveCustomSortMethods customSortMethods, + ISieveCustomFilterMethods customFilterMethods) + : base(options, customSortMethods, customFilterMethods) + { + } + + protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) + { + return mapper + .ApplyConfiguration() + .ApplyConfiguration(); + } + } +} diff --git a/SieveUnitTests/Services/ModularConfigurationWithScanSieveProcessor.cs b/SieveUnitTests/Services/ModularConfigurationWithScanSieveProcessor.cs new file mode 100644 index 0000000..07b3030 --- /dev/null +++ b/SieveUnitTests/Services/ModularConfigurationWithScanSieveProcessor.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Options; +using Sieve.Models; +using Sieve.Services; + +namespace SieveUnitTests.Services +{ + public class ModularConfigurationWithScanSieveProcessor : SieveProcessor + { + public ModularConfigurationWithScanSieveProcessor( + IOptions options, + ISieveCustomSortMethods customSortMethods, + ISieveCustomFilterMethods customFilterMethods) + : base(options, customSortMethods, customFilterMethods) + { + } + + protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) => + mapper.ApplyConfigurationsFromAssembly(typeof(ModularConfigurationWithScanSieveProcessor).Assembly); + } +}