SievePropertyMapper for #4

This commit is contained in:
Biarity 2018-02-10 15:37:04 +10:00
parent b52362e2bc
commit 0bd38b8348
10 changed files with 330 additions and 10 deletions

View File

@ -1,11 +1,12 @@
using System; using Sieve.Models;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
namespace Sieve.Attributes namespace Sieve.Attributes
{ {
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class SieveAttribute : Attribute public class SieveAttribute : Attribute, ISievePropertyMetadata
{ {
/// <summary> /// <summary>
/// Override name used /// Override name used

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace Sieve.Models
{
public interface ISievePropertyMetadata
{
string Name { get; set; }
bool CanFilter { get; set; }
bool CanSort { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace Sieve.Models
{
public class SievePropertyMetadata : ISievePropertyMetadata
{
public string Name { get; set; }
public bool CanFilter { get; set; }
public bool CanSort { get; set; }
}
}

View File

@ -19,12 +19,14 @@ namespace Sieve.Services
private IOptions<SieveOptions> _options; private IOptions<SieveOptions> _options;
private ISieveCustomSortMethods _customSortMethods; private ISieveCustomSortMethods _customSortMethods;
private ISieveCustomFilterMethods _customFilterMethods; private ISieveCustomFilterMethods _customFilterMethods;
private SievePropertyMapper mapper = new SievePropertyMapper();
public SieveProcessor(IOptions<SieveOptions> options, public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods, ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods) ISieveCustomFilterMethods customFilterMethods)
{ {
mapper = MapProperties(mapper);
_options = options; _options = options;
_customSortMethods = customSortMethods; _customSortMethods = customSortMethods;
_customFilterMethods = customFilterMethods; _customFilterMethods = customFilterMethods;
@ -33,6 +35,7 @@ namespace Sieve.Services
public SieveProcessor(IOptions<SieveOptions> options, public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods) ISieveCustomSortMethods customSortMethods)
{ {
mapper = MapProperties(mapper);
_options = options; _options = options;
_customSortMethods = customSortMethods; _customSortMethods = customSortMethods;
} }
@ -40,12 +43,14 @@ namespace Sieve.Services
public SieveProcessor(IOptions<SieveOptions> options, public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomFilterMethods customFilterMethods) ISieveCustomFilterMethods customFilterMethods)
{ {
mapper = MapProperties(mapper);
_options = options; _options = options;
_customFilterMethods = customFilterMethods; _customFilterMethods = customFilterMethods;
} }
public SieveProcessor(IOptions<SieveOptions> options) public SieveProcessor(IOptions<SieveOptions> options)
{ {
mapper = MapProperties(mapper);
_options = options; _options = options;
} }
@ -57,7 +62,10 @@ namespace Sieve.Services
/// <param name="source">Data source</param> /// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param> /// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns> /// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplyAll<TEntity>(ISieveModel<IFilterTerm, ISortTerm> model, IQueryable<TEntity> source, object[] dataForCustomMethods = null) public IQueryable<TEntity> ApplyAll<TEntity>(
ISieveModel<IFilterTerm, ISortTerm> model,
IQueryable<TEntity> source,
object[] dataForCustomMethods = null)
{ {
var result = source; var result = source;
@ -84,7 +92,10 @@ namespace Sieve.Services
/// <param name="source">Data source</param> /// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param> /// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns> /// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplyFiltering<TEntity>(ISieveModel<IFilterTerm, ISortTerm> model, IQueryable<TEntity> result, object[] dataForCustomMethods = null) public IQueryable<TEntity> ApplyFiltering<TEntity>(
ISieveModel<IFilterTerm, ISortTerm> model,
IQueryable<TEntity> result,
object[] dataForCustomMethods = null)
{ {
if (model?.FiltersParsed == null) if (model?.FiltersParsed == null)
return result; return result;
@ -181,7 +192,10 @@ namespace Sieve.Services
/// <param name="source">Data source</param> /// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param> /// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns> /// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplySorting<TEntity>(ISieveModel<IFilterTerm, ISortTerm> model, IQueryable<TEntity> result, object[] dataForCustomMethods = null) public IQueryable<TEntity> ApplySorting<TEntity>(
ISieveModel<IFilterTerm, ISortTerm> model,
IQueryable<TEntity> result,
object[] dataForCustomMethods = null)
{ {
if (model?.SortsParsed == null) if (model?.SortsParsed == null)
return result; return result;
@ -219,7 +233,9 @@ namespace Sieve.Services
/// <param name="source">Data source</param> /// <param name="source">Data source</param>
/// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param> /// <param name="dataForCustomMethods">Additional data that will be passed down to custom methods</param>
/// <returns>Returns a transformed version of `source`</returns> /// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> ApplyPagination<TEntity>(ISieveModel<IFilterTerm, ISortTerm> model, IQueryable<TEntity> result) public IQueryable<TEntity> ApplyPagination<TEntity>(
ISieveModel<IFilterTerm, ISortTerm> model,
IQueryable<TEntity> result)
{ {
var page = model?.Page ?? 1; var page = model?.Page ?? 1;
var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize; var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize;
@ -232,15 +248,34 @@ namespace Sieve.Services
return result; return result;
} }
private PropertyInfo GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired, string name)
protected virtual SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
return mapper;
}
private PropertyInfo GetSieveProperty<TEntity>(
bool canSortRequired,
bool canFilterRequired,
string name)
{
return mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive)
?? FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
}
private PropertyInfo FindPropertyBySieveAttribute<TEntity>(
bool canSortRequired,
bool canFilterRequired,
string name,
bool isCaseSensitive)
{ {
return typeof(TEntity).GetProperties().FirstOrDefault(p => return typeof(TEntity).GetProperties().FirstOrDefault(p =>
{ {
if (p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute) if (p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute)
if ((canSortRequired ? sieveAttribute.CanSort : true) && if ((canSortRequired ? sieveAttribute.CanSort : true) &&
(canFilterRequired ? sieveAttribute.CanFilter : true) && (canFilterRequired ? sieveAttribute.CanFilter : true) &&
((sieveAttribute.Name ?? p.Name).Equals(name, ((sieveAttribute.Name ?? p.Name).Equals(name,
_options.Value.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))) isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)))
return true; return true;
return false; return false;
}); });

View File

@ -0,0 +1,107 @@
using Sieve.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
namespace Sieve.Services
{
public class SievePropertyMapper
{
private Dictionary<Type, Dictionary<PropertyInfo, ISievePropertyMetadata>> _map
= new Dictionary<Type, Dictionary<PropertyInfo, ISievePropertyMetadata>>();
public PropertyFluentApi<TEntity> Property<TEntity>(Expression<Func<TEntity, object>> expression)
{
_map.TryAdd(typeof(TEntity), new Dictionary<PropertyInfo, ISievePropertyMetadata>());
return new PropertyFluentApi<TEntity>(this, expression);
}
public class PropertyFluentApi<TEntity>
{
private SievePropertyMapper _sievePropertyMapper;
private PropertyInfo _property;
public PropertyFluentApi(SievePropertyMapper sievePropertyMapper, Expression<Func<TEntity, object>> expression)
{
_sievePropertyMapper = sievePropertyMapper;
_property = GetPropertyInfo(expression);
_name = _property.Name;
_canFilter = false;
_canSort = false;
}
private string _name;
private bool _canFilter;
private bool _canSort;
public PropertyFluentApi<TEntity> CanFilter()
{
_canFilter = true;
UpdateMap();
return this;
}
public PropertyFluentApi<TEntity> CanSort()
{
_canSort = true;
UpdateMap();
return this;
}
public PropertyFluentApi<TEntity> HasName(string name)
{
_name = name;
UpdateMap();
return this;
}
private void UpdateMap()
{
_sievePropertyMapper._map[typeof(TEntity)][_property] = new SievePropertyMetadata()
{
Name = _name,
CanFilter = _canFilter,
CanSort = _canSort
};
}
private static PropertyInfo GetPropertyInfo(Expression<Func<TEntity, object>> exp)
{
if (!(exp.Body is MemberExpression body))
{
var ubody = (UnaryExpression)exp.Body;
body = ubody.Operand as MemberExpression;
}
return body?.Member as PropertyInfo;
}
}
public PropertyInfo FindProperty<TEntity>(
bool canSortRequired,
bool canFilterRequired,
string name,
bool isCaseSensitive)
{
try
{
var me = _map[typeof(TEntity)]
.FirstOrDefault(kv =>
kv.Value.Name.Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) &&
(canSortRequired ? kv.Value.CanSort : true) &&
(canFilterRequired ? kv.Value.CanFilter : true));
return me.Key;
}
catch (Exception ex) when (ex is KeyNotFoundException || ex is ArgumentNullException)
{
return null;
}
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.Extensions.Options;
using Sieve.Models;
using Sieve.Services;
using SieveTests.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SieveTests.Services
{
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.Title)
.CanSort()
.CanFilter()
.HasName("CustomTitleName");
return mapper;
}
}
}

View File

@ -37,7 +37,7 @@ namespace SieveTests
services.AddScoped<ISieveCustomSortMethods, SieveCustomSortMethods>(); services.AddScoped<ISieveCustomSortMethods, SieveCustomSortMethods>();
services.AddScoped<ISieveCustomFilterMethods, SieveCustomFilterMethods>(); services.AddScoped<ISieveCustomFilterMethods, SieveCustomFilterMethods>();
services.AddScoped<ISieveProcessor, SieveProcessor>(); services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -21,5 +21,11 @@ namespace SieveUnitTests.Entities
[Sieve(CanFilter = true, CanSort = true)] [Sieve(CanFilter = true, CanSort = true)]
public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;
public string ThisHasNoAttribute { get; set; }
public string ThisHasNoAttributeButIsAccessible { get; set; }
public int OnlySortableViaFluentApi { get; set; }
} }
} }

84
SieveUnitTests/Mapper.cs Normal file
View File

@ -0,0 +1,84 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Entities;
using SieveUnitTests.Services;
using System;
using System.Linq;
using System.Collections.Generic;
namespace SieveUnitTests
{
[TestClass]
public class Mapper
{
private ApplicationSieveProcessor _processor;
private IQueryable<Post> _posts;
public Mapper()
{
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post>
{
new Post() {
Id = 1,
ThisHasNoAttributeButIsAccessible = "A",
ThisHasNoAttribute = "A",
OnlySortableViaFluentApi = 100
},
new Post() {
Id = 2,
ThisHasNoAttributeButIsAccessible = "B",
ThisHasNoAttribute = "B",
OnlySortableViaFluentApi = 50
},
new Post() {
Id = 3,
ThisHasNoAttributeButIsAccessible = "C",
ThisHasNoAttribute = "C",
OnlySortableViaFluentApi = 0
},
}.AsQueryable();
}
[TestMethod]
public void MapperWorks()
{
var model = new SieveModel()
{
Filters = "shortname@=A",
};
var result = _processor.ApplyAll(model, _posts);
Assert.AreEqual(result.First().ThisHasNoAttributeButIsAccessible, "A");
Assert.IsTrue(result.Count() == 1);
}
[TestMethod]
public void MapperSortOnlyWorks()
{
var model = new SieveModel()
{
Filters = "OnlySortableViaFluentApi@=50",
Sorts = "OnlySortableViaFluentApi"
};
var result = _processor.ApplyAll(model, _posts);
Assert.AreEqual(result.First().Id, 3);
Assert.IsTrue(result.Count() == 3);
}
}
}
//
//Sorts = "LikeCount",
//Page = 1,
//PageSize = 10
//

View File

@ -0,0 +1,31 @@
using Microsoft.Extensions.Options;
using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SieveUnitTests.Services
{
public class ApplicationSieveProcessor : SieveProcessor
{
public ApplicationSieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
{
}
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{
mapper.Property<Post>(p => p.ThisHasNoAttributeButIsAccessible)
.CanSort()
.CanFilter()
.HasName("shortname");
mapper.Property<Post>(p => p.OnlySortableViaFluentApi)
.CanSort();
return mapper;
}
}
}