Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions FluentSortingDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ VisualStudioVersion = 17.12.35506.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentSortingDotNet", "src\FluentSortingDotNet\FluentSortingDotNet.csproj", "{F156F26F-6B62-44C3-A53F-7703A9C3BFED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentSortingDotNet.ConsoleTest", "tests\FluentSortingDotNet.ConsoleTest\FluentSortingDotNet.ConsoleTest.csproj", "{520AD80C-9C18-44CB-85AB-18A04C7D41D1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5CF6B4C7-248A-44EB-9B78-9DC5C7E004C4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D5F916DA-2A0B-4B72-854A-D19FDFF0C2EF}"
Expand All @@ -15,7 +13,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentSortingDotNet.Benchma
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentSortingDotNet.Testing", "tests\FluentSortingDotNet.Testing\FluentSortingDotNet.Testing.csproj", "{95003460-2CF1-4CC4-9089-194B8C570A34}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentSortingDotNet.UnitTests", "tests\FluentSortingDotNet.UnitTests\FluentSortingDotNet.UnitTests.csproj", "{3A17280E-7BA2-4219-8E76-F45CBAB12144}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentSortingDotNet.Tests", "tests\FluentSortingDotNet.Tests\FluentSortingDotNet.Tests.csproj", "{3A17280E-7BA2-4219-8E76-F45CBAB12144}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -27,10 +25,6 @@ Global
{F156F26F-6B62-44C3-A53F-7703A9C3BFED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F156F26F-6B62-44C3-A53F-7703A9C3BFED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F156F26F-6B62-44C3-A53F-7703A9C3BFED}.Release|Any CPU.Build.0 = Release|Any CPU
{520AD80C-9C18-44CB-85AB-18A04C7D41D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{520AD80C-9C18-44CB-85AB-18A04C7D41D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{520AD80C-9C18-44CB-85AB-18A04C7D41D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{520AD80C-9C18-44CB-85AB-18A04C7D41D1}.Release|Any CPU.Build.0 = Release|Any CPU
{6E024A6D-ED64-49C7-9F1C-54800B9292C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E024A6D-ED64-49C7-9F1C-54800B9292C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E024A6D-ED64-49C7-9F1C-54800B9292C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -49,9 +43,11 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F156F26F-6B62-44C3-A53F-7703A9C3BFED} = {D5F916DA-2A0B-4B72-854A-D19FDFF0C2EF}
{520AD80C-9C18-44CB-85AB-18A04C7D41D1} = {5CF6B4C7-248A-44EB-9B78-9DC5C7E004C4}
{6E024A6D-ED64-49C7-9F1C-54800B9292C0} = {5CF6B4C7-248A-44EB-9B78-9DC5C7E004C4}
{95003460-2CF1-4CC4-9089-194B8C570A34} = {5CF6B4C7-248A-44EB-9B78-9DC5C7E004C4}
{3A17280E-7BA2-4219-8E76-F45CBAB12144} = {5CF6B4C7-248A-44EB-9B78-9DC5C7E004C4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E0C5B042-0A90-4794-8105-91387DFBCB8D}
EndGlobalSection
EndGlobal
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,21 @@ public record Person(string Name, int Age);
```csharp
using FluentSortingDotNet;

// The Sorter class also have an empty constructor that uses the DefaultSortParameterParser
// The Sorter class also have a overload with an empty constructor that uses the DefaultSortParameterParser
public sealed class PersonSorter(ISortParameterParser parser) : Sorter<Person>(parser)
{
protected override void Configure(SortBuilder<Person> builder)
{
// when no parameters are provided, sort by name descending
builder.ForParameter(p => p.Name).WithName("name").IsDefault(direction: SortDirection.Descending);
// When no parameters are provided, sort by name descending
builder.ForParameter(p => p.Name).IsDefault(direction: SortDirection.Descending);

builder.ForParameter(p => p.DateOfBirth).WithName("age");

// ignore case when sorting by name
// Ignore case when sorting by name
builder.IgnoreParameterCase();

// Ignore invalid parameters instead of throwing an exception when not validated with PersonSorter.Validate(string)
builder.IgnoreInvalidParameters();
}
}
```
Expand All @@ -55,27 +58,26 @@ public sealed class PersonSorter(ISortParameterParser parser) : Sorter<Person>(p
```csharp
using FluentSortingDotNet;

var sorter = new PersonSorter(DefaultSortParameterParser.Instance);

IQueryable<Person> peopleQuery = ...;
PersonSorter sorter = new(DefaultSortParameterParser.Instance);

SortResult result = sorter.Sort(ref peopleQuery, "name,-age");
SortContext sortContext = sorter.Validate("name,-age");

if (result.IsSuccess)
{
var orderedPeople = peopleQuery.ToList();
}
else
if (!sortContext.IsValid)
{
Console.WriteLine($"Invalid sort parameters: {string.Join(", ", result.InvalidSortParameters)}");
Console.WriteLine($"Invalid sort parameters: {string.Join(", ", sortContext.InvalidParameters)}");
return;
}

IQueryable<Person> peopleQuery = ...;

IQueryable<Person> sortedQuery = sorter.Sort(peopleQuery, sortContext);
```

### Dependency Injection

```csharp
services.AddSingleton<ISortParameterParser>(DefaultSortParameterParser.Instance);
services.AddSingleton<PersonSorter>();
services.AddSingleton<ISorter<Person>, PersonSorter>();
```

## Extensibility
Expand Down Expand Up @@ -164,10 +166,10 @@ It has a slightly worse performance (when using a sort query string) than callin
The performance is slightly better when sorting on the default sort parameters since the query is precompiled.
Both of the benchmarked query builders allocate a bit less memory since the expressions are reused.

![Query building benchmark results](tests/FluentSortingDotNet.Benchmarks/query-builder-1.0.0-rc.3.png "Query building benchmark results")
**TODO: Insert benchmark results table**

#### Parsing

The parsing has no real-world impact on performance.

![Parsing benchmark results](tests/FluentSortingDotNet.Benchmarks/parser-1.0.0-rc.3.png "Parsing benchmark results")
**TODO: Insert benchmark results table**
6 changes: 5 additions & 1 deletion src/FluentSortingDotNet/FluentSortingDotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<Description>Apply sorting from a string (e.g. query string) with a FluentValidation-like API.</Description>
<PackageTags>sorting, fluent, query, string, linq, order, by</PackageTags>
</PropertyGroup>

<ItemGroup>
<None Include="..\..\LICENSE.txt">
<Pack>True</Pack>
Expand All @@ -34,4 +34,8 @@
<PackageReference Include="System.Memory" Version="4.6.0" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="FluentSortingDotNet.Tests" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions src/FluentSortingDotNet/ISorter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using FluentSortingDotNet.Queries;
using System;
using System.Linq;

namespace FluentSortingDotNet;

/// <summary>
/// Represents a sorter can create a sort query for a <see cref="IQueryable{T}"/>.
/// </summary>
/// <typeparam name="T">The type of items to sort.</typeparam>
public interface ISorter<T>
{
/// <summary>
/// Create a new instance of the <see cref="ISortQuery{T}"/> class with the specified <paramref name="sortContext"/>.
/// </summary>
/// <param name="sortContext">The <see cref="SortContext{T}"/> that contains the sort parameters.</param>
/// <returns>A new instance of the <see cref="ISortQuery{T}"/> class.</returns>
ISortQuery<T> CreateSortQuery(SortContext<T> sortContext);

/// <summary>
/// Validates the specified sort query and returns a <see cref="SortContext{T}"/> that can be used to sort a query.
/// </summary>
/// <param name="sortQuery">The sort query to validate.</param>
/// <returns>A new <see cref="SortContext{T}"/> that contains the valid and invalid parameters.</returns>
SortContext<T> Validate(ReadOnlySpan<char> sortQuery);
}
1 change: 1 addition & 0 deletions src/FluentSortingDotNet/Internal/SortableParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ internal sealed class SortableParameter(LambdaExpression expression, string name
public LambdaExpression Expression { get; } = expression;
public string Name { get; set; } = name;
public SortDirection? DefaultDirection { get; set; }
public bool ShouldReverseDirection { get; set; }
}
73 changes: 35 additions & 38 deletions src/FluentSortingDotNet/Queries/DefaultSortQueryBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
using System;
using FluentSortingDotNet.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using FluentSortingDotNet.Internal;

namespace FluentSortingDotNet.Queries;

/// <summary>
/// Represents a class that can build a sort query.
/// </summary>
/// <typeparam name="T">The type of items to sort.</typeparam>
public sealed class DefaultSortQueryBuilder<T> : ISortQueryBuilder<T>, ISortQuery<T>
public sealed class DefaultSortQueryBuilder<T> : ISortQueryBuilder<T>
{
private bool _built;
private readonly List<SortExpression> _sortExpressions = new();

/// <inheritdoc />
Expand All @@ -28,50 +27,48 @@ public ISortQueryBuilder<T> SortBy(LambdaExpression expression, SortDirection so
/// <inheritdoc />
public ISortQuery<T> Build()
{
if (IsEmpty)
throw new InvalidOperationException("No sorting expressions have been added.");

_built = true;
return this;
return IsEmpty
? throw new InvalidOperationException("No sorting expressions have been added.")
: new SortQuery(_sortExpressions);
}

/// <inheritdoc />
/// <exception cref="InvalidOperationException">Thrown when the query has not been built.</exception>
public IQueryable<T> Apply(IQueryable<T> query)
private sealed class SortExpression(LambdaExpression expression, SortDirection sortDirection)
{
if (!_built)
throw new InvalidOperationException("Cannot apply sorting to an unbuilt query.");
public LambdaExpression Expression { get; } = expression;
public SortDirection SortDirection { get; } = sortDirection;
}

IOrderedQueryable<T>? orderedQuery = null;
private sealed class SortQuery(List<SortExpression> sortExpressions) : ISortQuery<T>
{
private readonly List<SortExpression> _sortExpressions = sortExpressions;

foreach (SortExpression expression in _sortExpressions)
public IQueryable<T> Apply(IQueryable<T> query)
{
if (orderedQuery == null)
IOrderedQueryable<T>? orderedQuery = null;

foreach (SortExpression expression in _sortExpressions)
{
orderedQuery = expression.SortDirection switch
if (orderedQuery == null)
{
SortDirection.Ascending => query.OrderBy(expression.Expression),
SortDirection.Descending => query.OrderByDescending(expression.Expression),
_ => throw new ArgumentOutOfRangeException(nameof(expression.SortDirection))
};
}
else
{
orderedQuery = expression.SortDirection switch
orderedQuery = expression.SortDirection switch
{
SortDirection.Ascending => query.OrderBy(expression.Expression),
SortDirection.Descending => query.OrderByDescending(expression.Expression),
_ => throw new InvalidOperationException("Invalid sort direction.")
};
}
else
{
SortDirection.Ascending => orderedQuery.ThenBy(expression.Expression),
SortDirection.Descending => orderedQuery.ThenByDescending(expression.Expression),
_ => throw new ArgumentOutOfRangeException(nameof(expression.SortDirection))
};
orderedQuery = expression.SortDirection switch
{
SortDirection.Ascending => orderedQuery.ThenBy(expression.Expression),
SortDirection.Descending => orderedQuery.ThenByDescending(expression.Expression),
_ => throw new InvalidOperationException("Invalid sort direction.")
};
}
}
}

return orderedQuery ?? query;
}

private sealed class SortExpression(LambdaExpression expression, SortDirection sortDirection)
{
public LambdaExpression Expression { get; } = expression;
public SortDirection SortDirection { get; } = sortDirection;
return orderedQuery ?? query;
}
}
}
9 changes: 6 additions & 3 deletions src/FluentSortingDotNet/Queries/ExpressionSortQueryBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System;
using FluentSortingDotNet.Internal;
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using FluentSortingDotNet.Internal;

namespace FluentSortingDotNet.Queries;

Expand Down Expand Up @@ -39,9 +39,12 @@ Expression CreateMethodCallExpression(Func<SortDirection, MethodInfo> methodProv
public ISortQuery<T> Build()
{
if (_sortExpression == null)
{
throw new InvalidOperationException("No sorting expressions have been added.");
}

return new DelegateSortQuery(Expression.Lambda<Func<IQueryable<T>, IQueryable<T>>>(_sortExpression, QueryParameter).Compile());
var lambda = Expression.Lambda<Func<IQueryable<T>, IQueryable<T>>>(_sortExpression, QueryParameter).Compile();
return new DelegateSortQuery(lambda);
}

private static LambdaExpression ReplaceParameter(LambdaExpression original, ParameterExpression toReplace, ParameterExpression replacement)
Expand Down
17 changes: 14 additions & 3 deletions src/FluentSortingDotNet/SortBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ namespace FluentSortingDotNet;
public sealed class SortBuilder<T>
{
private readonly List<SortableParameter> _sortableParameters = new();
private SorterOptions? _options;

internal SorterOptions? Options { get; private set; }
internal SorterOptions Options
=> _options ??= SorterOptions.Default;

internal SortBuilder(SorterOptions? options)
{
Options = options;
_options = options;
}

/// <summary>
Expand All @@ -26,11 +28,20 @@ internal SortBuilder(SorterOptions? options)
/// <returns>The current builder instance.</returns>
public SortBuilder<T> IgnoreParameterCase()
{
Options ??= new();
Options.ParameterNameComparer = StringComparer.OrdinalIgnoreCase;
return this;
}

/// <summary>
/// Ignores all invalid parameters when sorting instead of throwing an exception.
/// </summary>
/// <returns>The current builder instance.</returns>
public SortBuilder<T> IgnoreInvalidParameters()
{
Options.IgnoreInvalidParameters = true;
return this;
}

/// <summary>
/// Creates a new <see cref="SortParameterBuilder"/> for the specified property.
/// </summary>
Expand Down
38 changes: 38 additions & 0 deletions src/FluentSortingDotNet/SortContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;

namespace FluentSortingDotNet;

/// <summary>
/// Represents a context for sorting that contains valid and invalid sort parameters.
/// </summary>
/// <typeparam name="T">The type of items to sort.</typeparam>
/// <param name="validParameters">The valid sort parameters.</param>
/// <param name="invalidParameters">The invalid sort parameters.</param>
public sealed class SortContext<T>(IReadOnlyList<SortParameter>? validParameters = null, IReadOnlyList<string>? invalidParameters = null)
{
/// <summary>
/// Represents an empty <see cref="SortContext{T}"/> with no valid or invalid parameters.
/// </summary>
public static readonly SortContext<T> Empty = new();

/// <summary>
/// Gets a value indicating whether the sort context is valid.
/// </summary>
public bool IsValid => InvalidParameters.Count == 0;

/// <summary>
/// Gets a value indicating whether the sort context is empty.
/// </summary>
public bool IsEmpty => ValidParameters.Count == 0;

/// <summary>
/// Gets the valid sort parameters.
/// </summary>
public IReadOnlyList<SortParameter> ValidParameters { get; } = validParameters ?? Array.Empty<SortParameter>();

/// <summary>
/// Gets the invalid sort parameters.
/// </summary>
public IReadOnlyList<string> InvalidParameters { get; } = invalidParameters ?? Array.Empty<string>();
}
Loading