Skip to content

Commit

Permalink
Modular mapping configuration (#162)
Browse files Browse the repository at this point in the history
* Modular configuration for property mappings

* Update Readme, unit tests and sample

Co-authored-by: Steven Decoodt <steven.decoodt@vinci-energies.net>
  • Loading branch information
sdecoodt and Steven Decoodt committed Jan 12, 2022
1 parent 820358e commit 863d75b
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 13 deletions.
67 changes: 66 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,78 @@ public class ApplicationSieveProcessor : SieveProcessor
}
```



Now you should inject the new class instead:
```C#
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
```

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<Post>(p => p.Title)
.CanFilter()
.HasName("a_different_query_name_here");

mapper.Property<Post>(p => p.CommentCount)
.CanSort();

mapper.Property<Post>(p => p.DateCreated)
.CanSort()
.CanFilter()
.HasName("created_on");

return mapper;
}
}
```
With the processor simplified to:
```C#
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}

protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper
.ApplyConfiguration<SieveConfigurationForPost>()
.ApplyConfiguration<SieveConfigurationForComment>();
}
}
```
There is also the option to scan and add all configurations for a given assembly
```C#
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(
IOptions<SieveOptions> 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.
Expand Down
15 changes: 15 additions & 0 deletions Sieve.Sample/Entities/SieveConfigurationForPost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Sieve.Services;

namespace Sieve.Sample.Entities
{
public class SieveConfigurationForPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.Title)
.CanSort()
.CanFilter()
.HasName("CustomTitleName");
}
}
}
7 changes: 7 additions & 0 deletions Sieve.Sample/Services/ApplicationSieveProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ public ApplicationSieveProcessor(IOptions<SieveOptions> options, ISieveCustomSor

protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
// Option 1: Map all properties centrally
mapper.Property<Post>(p => p.Title)
.CanSort()
.CanFilter()
.HasName("CustomTitleName");

// Option 2: Manually apply functionally grouped mapping configurations
//mapper.ApplyConfiguration<SieveConfigurationForPost>();

// Option 3: Scan and apply all configurations
//mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly);

return mapper;
}
}
Expand Down
70 changes: 70 additions & 0 deletions Sieve/Services/ISieveConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#nullable enable
using System;
using System.Linq;
using System.Reflection;

namespace Sieve.Services
{
/// <summary>
/// Use this interface to create SieveConfiguration (just like EntityTypeConfigurations are defined for EF)
/// </summary>
public interface ISieveConfiguration
{
/// <summary>
/// Configures sieve property mappings.
/// </summary>
/// <param name="mapper"> The mapper used to configure the sieve properties on. </param>
void Configure(SievePropertyMapper mapper);
}

/// <summary>
/// Configuration extensions to the <see cref="SievePropertyMapper" />
/// </summary>
public static class SieveConfigurationExtensions
{
/// <summary>
/// Applies configuration that is defined in an <see cref="ISieveConfiguration" /> instance.
/// </summary>
/// <param name="mapper"> The mapper to apply the configuration on. </param>
/// <typeparam name="T">The configuration to be applied. </typeparam>
/// <returns>
/// The same <see cref="SievePropertyMapper" /> instance so that additional configuration calls can be chained.
/// </returns>
public static SievePropertyMapper ApplyConfiguration<T>(this SievePropertyMapper mapper) where T : ISieveConfiguration, new()
{
var configuration = new T();
configuration.Configure(mapper);
return mapper;
}

/// <summary>
/// Applies configuration from all <see cref="ISieveConfiguration" />
/// instances that are defined in provided assembly.
/// </summary>
/// <param name="mapper"> The mapper to apply the configuration on. </param>
/// <param name="assembly"> The assembly to scan. </param>
/// <returns>
/// The same <see cref="SievePropertyMapper" /> instance so that additional configuration calls can be chained.
/// </returns>
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;
}
}
}
37 changes: 37 additions & 0 deletions SieveUnitTests/Abstractions/Entity/SieveConfigurationForIPost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Sieve.Services;

namespace SieveUnitTests.Abstractions.Entity
{
public class SieveConfigurationForIPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<IPost>(p => p.ThisHasNoAttributeButIsAccessible)
.CanSort()
.CanFilter()
.HasName("shortname");

mapper.Property<IPost>(p => p.TopComment.Text)
.CanFilter();

mapper.Property<IPost>(p => p.TopComment.Id)
.CanSort();

mapper.Property<IPost>(p => p.OnlySortableViaFluentApi)
.CanSort();

mapper.Property<IPost>(p => p.TopComment.Text)
.CanFilter()
.HasName("topc");

mapper.Property<IPost>(p => p.FeaturedComment.Text)
.CanFilter()
.HasName("featc");

mapper
.Property<IPost>(p => p.DateCreated)
.CanSort()
.HasName("CreateDate");
}
}
}
37 changes: 37 additions & 0 deletions SieveUnitTests/Entities/SieveConfigurationForPost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Sieve.Services;

namespace SieveUnitTests.Entities
{
public class SieveConfigurationForPost : ISieveConfiguration
{
public void Configure(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.ThisHasNoAttributeButIsAccessible)
.CanSort()
.CanFilter()
.HasName("shortname");

mapper.Property<Post>(p => p.TopComment.Text)
.CanFilter();

mapper.Property<Post>(p => p.TopComment.Id)
.CanSort();

mapper.Property<Post>(p => p.OnlySortableViaFluentApi)
.CanSort();

mapper.Property<Post>(p => p.TopComment.Text)
.CanFilter()
.HasName("topc");

mapper.Property<Post>(p => p.FeaturedComment.Text)
.CanFilter()
.HasName("featc");

mapper
.Property<Post>(p => p.DateCreated)
.CanSort()
.HasName("CreateDate");
}
}
}
46 changes: 34 additions & 12 deletions SieveUnitTests/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;
using Sieve.Exceptions;
using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Entities;
using SieveUnitTests.Services;
using Xunit;
Expand All @@ -10,15 +11,10 @@ namespace SieveUnitTests
{
public class Mapper
{
private readonly ApplicationSieveProcessor _processor;
private readonly IQueryable<Post> _posts;

public Mapper()
{
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());

_posts = new List<Post>
{
new Post
Expand All @@ -45,33 +41,59 @@ public Mapper()
}.AsQueryable();
}

[Fact]
public void MapperWorks()
/// <summary>
/// Processors with the same mappings but configured via a different method.
/// </summary>
/// <returns></returns>
public static IEnumerable<object[]> 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
{
Filters = "OnlySortableViaFluentApi@=50",
Sorts = "OnlySortableViaFluentApi"
};

var result = _processor.Apply(model, _posts, applyFiltering: false, applyPagination: false);
var result = processor.Apply(model, _posts, applyFiltering: false, applyPagination: false);

Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
Assert.Throws<SieveMethodNotFoundException>(() => processor.Apply(model, _posts));

Assert.Equal(3, result.First().Id);

Expand Down
26 changes: 26 additions & 0 deletions SieveUnitTests/Services/ModularConfigurationSieveProcessor.cs
Original file line number Diff line number Diff line change
@@ -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<SieveOptions> options,
ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods)
: base(options, customSortMethods, customFilterMethods)
{
}

protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper
.ApplyConfiguration<SieveConfigurationForPost>()
.ApplyConfiguration<SieveConfigurationForIPost>();
}
}
}

0 comments on commit 863d75b

Please sign in to comment.