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
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ public StarshipDocumentMaterializer(
TestDbContext dbContext,
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
IBaseUrlService baseUrlService, ISingleResourceDocumentBuilder singleResourceDocumentBuilder,
ISortExpressionExtractor sortExpressionExtractor,
IQueryableEnumerationTransformer queryableEnumerationTransformer, IResourceTypeRegistry resourceTypeRegistry)
: base(
queryableResourceCollectionDocumentBuilder, baseUrlService, singleResourceDocumentBuilder,
queryableEnumerationTransformer, resourceTypeRegistry)
queryableEnumerationTransformer, sortExpressionExtractor, resourceTypeRegistry)
{
_dbContext = dbContext;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using JSONAPI.Core;
using JSONAPI.Documents.Builders;
using JSONAPI.EntityFramework.Http;
using JSONAPI.Http;

namespace JSONAPI.AcceptanceTests.EntityFrameworkTestWebApp.DocumentMaterializers
{
Expand All @@ -15,8 +16,9 @@ public class StarshipOfficersRelatedResourceMaterializer : EntityFrameworkToMany

public StarshipOfficersRelatedResourceMaterializer(ResourceTypeRelationship relationship, DbContext dbContext,
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
ISortExpressionExtractor sortExpressionExtractor,
IResourceTypeRegistration primaryTypeRegistration)
: base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, primaryTypeRegistration)
: base(relationship, dbContext, queryableResourceCollectionDocumentBuilder, sortExpressionExtractor, primaryTypeRegistration)
{
_dbContext = dbContext;
}
Expand Down
2 changes: 2 additions & 0 deletions JSONAPI.Autofac/JsonApiAutofacModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType<JsonApiExceptionFilterAttribute>().SingleInstance();
builder.RegisterType<DefaultQueryableResourceCollectionDocumentBuilder>().As<IQueryableResourceCollectionDocumentBuilder>();

// Misc
builder.RegisterType<DefaultSortExpressionExtractor>().As<ISortExpressionExtractor>().SingleInstance();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class EntityFrameworkDocumentMaterializer<T> : IDocumentMaterializer wher
private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder;
private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder;
private readonly IEntityFrameworkResourceObjectMaterializer _entityFrameworkResourceObjectMaterializer;
private readonly ISortExpressionExtractor _sortExpressionExtractor;
private readonly IBaseUrlService _baseUrlService;

/// <summary>
Expand All @@ -34,20 +35,23 @@ public EntityFrameworkDocumentMaterializer(
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
ISingleResourceDocumentBuilder singleResourceDocumentBuilder,
IEntityFrameworkResourceObjectMaterializer entityFrameworkResourceObjectMaterializer,
ISortExpressionExtractor sortExpressionExtractor,
IBaseUrlService baseUrlService)
{
_dbContext = dbContext;
_resourceTypeRegistration = resourceTypeRegistration;
_queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder;
_singleResourceDocumentBuilder = singleResourceDocumentBuilder;
_entityFrameworkResourceObjectMaterializer = entityFrameworkResourceObjectMaterializer;
_sortExpressionExtractor = sortExpressionExtractor;
_baseUrlService = baseUrlService;
}

public virtual Task<IResourceCollectionDocument> GetRecords(HttpRequestMessage request, CancellationToken cancellationToken)
{
var query = _dbContext.Set<T>().AsQueryable();
return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken);
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);
return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, sortExpressions, cancellationToken);
}

public virtual async Task<ISingleResourceDocument> GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken)
Expand Down Expand Up @@ -127,8 +131,9 @@ protected async Task<IResourceCollectionDocument> GetRelatedToMany<TRelated>(str
_resourceTypeRegistration.ResourceTypeName, id));

var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda);
var sortExpressions = _sortExpressionExtractor.ExtractSortExpressions(request);

return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, cancellationToken);
return await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, sortExpressions, cancellationToken);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ public EntityFrameworkToManyRelatedResourceDocumentMaterializer(
ResourceTypeRelationship relationship,
DbContext dbContext,
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
ISortExpressionExtractor sortExpressionExtractor,
IResourceTypeRegistration primaryTypeRegistration)
: base(queryableResourceCollectionDocumentBuilder)
: base(queryableResourceCollectionDocumentBuilder, sortExpressionExtractor)
{
_relationship = relationship;
_dbContext = dbContext;
Expand Down
51 changes: 27 additions & 24 deletions JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using JSONAPI.Core;
using JSONAPI.Documents.Builders;
Expand Down Expand Up @@ -73,111 +72,115 @@ private DefaultSortingTransformer GetTransformer()
return new DefaultSortingTransformer(registry);
}

private TFixture[] GetArray<TFixture>(string uri, IQueryable<TFixture> fixturesQuery)
private TFixture[] GetArray<TFixture>(string[] sortExpressions, IQueryable<TFixture> fixturesQuery)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
return GetTransformer().Sort(fixturesQuery, request).ToArray();
return GetTransformer().Sort(fixturesQuery, sortExpressions).ToArray();
}

private Dummy[] GetDummyArray(string uri)
private Dummy[] GetDummyArray(string[] sortExpressions)
{
return GetArray<Dummy>(uri, _fixturesQuery);
return GetArray(sortExpressions, _fixturesQuery);
}

private Dummy2[] GetDummy2Array(string uri)
private Dummy2[] GetDummy2Array(string[] sortExpressions)
{
return GetArray<Dummy2>(uri, _fixtures2Query);
return GetArray(sortExpressions, _fixtures2Query);
}

private void RunTransformAndExpectFailure(string uri, string expectedMessage)
private void RunTransformAndExpectFailure(string[] sortExpressions, string expectedMessage)
{
Action action = () =>
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// ReSharper disable once UnusedVariable
var result = GetTransformer().Sort(_fixturesQuery, request).ToArray();
var result = GetTransformer().Sort(_fixturesQuery, sortExpressions).ToArray();
};
action.ShouldThrow<JsonApiException>().Which.Error.Detail.Should().Be(expectedMessage);
}

[TestMethod]
public void Sorts_by_attribute_ascending()
{
var array = GetDummyArray("http://api.example.com/dummies?sort=first-name");
var array = GetDummyArray(new [] { "first-name" });
array.Should().BeInAscendingOrder(d => d.FirstName);
}

[TestMethod]
public void Sorts_by_attribute_descending()
{
var array = GetDummyArray("http://api.example.com/dummies?sort=-first-name");
var array = GetDummyArray(new [] { "-first-name" });
array.Should().BeInDescendingOrder(d => d.FirstName);
}

[TestMethod]
public void Sorts_by_two_ascending_attributes()
{
var array = GetDummyArray("http://api.example.com/dummies?sort=last-name,first-name");
var array = GetDummyArray(new [] { "last-name", "first-name" });
array.Should().ContainInOrder(_fixtures.OrderBy(d => d.LastName + d.FirstName));
}

[TestMethod]
public void Sorts_by_two_descending_attributes()
{
var array = GetDummyArray("http://api.example.com/dummies?sort=-last-name,-first-name");
var array = GetDummyArray(new [] { "-last-name", "-first-name" });
array.Should().ContainInOrder(_fixtures.OrderByDescending(d => d.LastName + d.FirstName));
}

[TestMethod]
public void Sorts_by_id_when_expressions_are_empty()
{
var array = GetDummyArray(new string[] { });
array.Should().ContainInOrder(_fixtures.OrderBy(d => d.Id));
}

[TestMethod]
public void Returns_400_if_sort_argument_is_empty()
{
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=", "One of the sort expressions is empty.");
RunTransformAndExpectFailure(new[] { "" }, "One of the sort expressions is empty.");
}

[TestMethod]
public void Returns_400_if_sort_argument_is_whitespace()
{
RunTransformAndExpectFailure("http://api.example.com/dummies?sort= ", "One of the sort expressions is empty.");
RunTransformAndExpectFailure(new [] { " " }, "One of the sort expressions is empty.");
}

[TestMethod]
public void Returns_400_if_sort_argument_is_empty_descending()
{
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=-", "One of the sort expressions is empty.");
RunTransformAndExpectFailure(new [] { "-" }, "One of the sort expressions is empty.");
}

[TestMethod]
public void Returns_400_if_sort_argument_is_whitespace_descending()
{
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=- ", "One of the sort expressions is empty.");
RunTransformAndExpectFailure(new[] { "- " }, "One of the sort expressions is empty.");
}

[TestMethod]
public void Returns_400_if_no_property_exists()
{
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar",
RunTransformAndExpectFailure(new[] { "foobar" },
"The attribute \"foobar\" does not exist on type \"dummies\".");
}

[TestMethod]
public void Returns_400_if_the_same_property_is_specified_more_than_once()
{
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name",
RunTransformAndExpectFailure(new[] { "last-name", "last-name" },
"The attribute \"last-name\" was specified more than once.");
}

[TestMethod]
public void Can_sort_by_DateTimeOffset()
{
var array = GetDummyArray("http://api.example.com/dummies?sort=birth-date");
var array = GetDummyArray(new [] { "birth-date" });
array.Should().BeInAscendingOrder(d => d.BirthDate);
}

[TestMethod]
public void Can_sort_by_resource_with_integer_key()
{
var array = GetDummy2Array("http://api.example.com/dummy2s?sort=name");
var array = GetDummy2Array(new [] { "name" });
array.Should().BeInAscendingOrder(d => d.Name);
}
}
Expand Down
19 changes: 15 additions & 4 deletions JSONAPI.Tests/Documents/Builders/FallbackDocumentBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ public async Task Creates_single_resource_document_for_registered_non_collection
var mockBaseUrlService = new Mock<IBaseUrlService>(MockBehavior.Strict);
mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com");

var mockSortExpressionExtractor = new Mock<ISortExpressionExtractor>(MockBehavior.Strict);
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new [] { "id "});

// Act
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object);
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
var resultDocument = await fallbackDocumentBuilder.BuildDocument(objectContent, request, cancellationTokenSource.Token);

// Assert
Expand All @@ -70,19 +73,24 @@ public async Task Creates_resource_collection_document_for_queryables()

var mockBaseUrlService = new Mock<IBaseUrlService>(MockBehavior.Strict);
mockBaseUrlService.Setup(s => s.GetBaseUrl(request)).Returns("https://www.example.com/");

var sortExpressions = new[] { "id" };

var cancellationTokenSource = new CancellationTokenSource();

var mockQueryableDocumentBuilder = new Mock<IQueryableResourceCollectionDocumentBuilder>(MockBehavior.Strict);
mockQueryableDocumentBuilder
.Setup(b => b.BuildDocument(items, request, cancellationTokenSource.Token, null))
.Setup(b => b.BuildDocument(items, request, sortExpressions, cancellationTokenSource.Token, null))
.Returns(Task.FromResult(mockDocument.Object));

var mockResourceCollectionDocumentBuilder = new Mock<IResourceCollectionDocumentBuilder>(MockBehavior.Strict);

var mockSortExpressionExtractor = new Mock<ISortExpressionExtractor>(MockBehavior.Strict);
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(sortExpressions);

// Act
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object);
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token);

// Assert
Expand Down Expand Up @@ -116,9 +124,12 @@ public async Task Creates_resource_collection_document_for_non_queryable_enumera
.Setup(b => b.BuildDocument(items, "https://www.example.com/", It.IsAny<string[]>(), It.IsAny<IMetadata>(), null))
.Returns(() => (mockDocument.Object));

var mockSortExpressionExtractor = new Mock<ISortExpressionExtractor>(MockBehavior.Strict);
mockSortExpressionExtractor.Setup(e => e.ExtractSortExpressions(request)).Returns(new[] { "id " });

// Act
var fallbackDocumentBuilder = new FallbackDocumentBuilder(singleResourceDocumentBuilder.Object,
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockBaseUrlService.Object);
mockQueryableDocumentBuilder.Object, mockResourceCollectionDocumentBuilder.Object, mockSortExpressionExtractor.Object, mockBaseUrlService.Object);
var resultDocument = await fallbackDocumentBuilder.BuildDocument(items, request, cancellationTokenSource.Token);

// Assert
Expand Down
86 changes: 86 additions & 0 deletions JSONAPI.Tests/Http/DefaultSortExpressionExtractorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Net.Http;
using FluentAssertions;
using JSONAPI.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace JSONAPI.Tests.Http
{
[TestClass]
public class DefaultSortExpressionExtractorTests
{
[TestMethod]
public void ExtractsSingleSortExpressionFromUri()
{
// Arrange
const string uri = "http://api.example.com/dummies?sort=first-name";
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// Act
var extractor = new DefaultSortExpressionExtractor();
var sortExpressions = extractor.ExtractSortExpressions(request);

// Assert
sortExpressions.Should().BeEquivalentTo("first-name");
}

[TestMethod]
public void ExtractsSingleDescendingSortExpressionFromUri()
{
// Arrange
const string uri = "http://api.example.com/dummies?sort=-first-name";
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// Act
var extractor = new DefaultSortExpressionExtractor();
var sortExpressions = extractor.ExtractSortExpressions(request);

// Assert
sortExpressions.Should().BeEquivalentTo("-first-name");
}

[TestMethod]
public void ExtractsMultipleSortExpressionsFromUri()
{
// Arrange
const string uri = "http://api.example.com/dummies?sort=last-name,first-name";
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// Act
var extractor = new DefaultSortExpressionExtractor();
var sortExpressions = extractor.ExtractSortExpressions(request);

// Assert
sortExpressions.Should().BeEquivalentTo("last-name", "first-name");
}

[TestMethod]
public void ExtractsMultipleSortExpressionsFromUriWithDifferentDirections()
{
// Arrange
const string uri = "http://api.example.com/dummies?sort=last-name,-first-name";
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// Act
var extractor = new DefaultSortExpressionExtractor();
var sortExpressions = extractor.ExtractSortExpressions(request);

// Assert
sortExpressions.Should().BeEquivalentTo("last-name", "-first-name");
}

[TestMethod]
public void ExtractsNothingWhenThereIsNoSortParam()
{
// Arrange
const string uri = "http://api.example.com/dummies";
var request = new HttpRequestMessage(HttpMethod.Get, uri);

// Act
var extractor = new DefaultSortExpressionExtractor();
var sortExpressions = extractor.ExtractSortExpressions(request);

// Assert
sortExpressions.Length.Should().Be(0);
}
}
}
1 change: 1 addition & 0 deletions JSONAPI.Tests/JSONAPI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<Compile Include="Core\ResourceTypeRegistrarTests.cs" />
<Compile Include="Core\ResourceTypeRegistryTests.cs" />
<Compile Include="Extensions\TypeExtensionsTests.cs" />
<Compile Include="Http\DefaultSortExpressionExtractorTests.cs" />
<Compile Include="Json\JsonApiFormatterTests.cs" />
<Compile Include="Models\Author.cs" />
<Compile Include="Models\Comment.cs" />
Expand Down
Loading