Browse Source
* Added possibility to chain includes. * Removed interface. * Removed the need for generating GUIDs as ids and added tests.main
committed by
Eric Fleming
14 changed files with 478 additions and 2 deletions
@ -0,0 +1,26 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq.Expressions; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ApplicationCore.Helpers.Query |
||||
|
{ |
||||
|
public class IncludeAggregator<TEntity> |
||||
|
{ |
||||
|
public IncludeQuery<TEntity, TProperty> Include<TProperty>(Expression<Func<TEntity, TProperty>> selector) |
||||
|
{ |
||||
|
var visitor = new IncludeVisitor(); |
||||
|
visitor.Visit(selector); |
||||
|
|
||||
|
var pathMap = new Dictionary<IIncludeQuery, string>(); |
||||
|
var query = new IncludeQuery<TEntity, TProperty>(pathMap); |
||||
|
|
||||
|
if (!string.IsNullOrEmpty(visitor.Path)) |
||||
|
{ |
||||
|
pathMap[query] = visitor.Path; |
||||
|
} |
||||
|
|
||||
|
return query; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ApplicationCore.Helpers.Query |
||||
|
{ |
||||
|
public class IncludeQuery<TEntity, TPreviousProperty> : IIncludeQuery<TEntity, TPreviousProperty> |
||||
|
{ |
||||
|
public Dictionary<IIncludeQuery, string> PathMap { get; } = new Dictionary<IIncludeQuery, string>(); |
||||
|
public IncludeVisitor Visitor { get; } = new IncludeVisitor(); |
||||
|
|
||||
|
public IncludeQuery(Dictionary<IIncludeQuery, string> pathMap) |
||||
|
{ |
||||
|
PathMap = pathMap; |
||||
|
} |
||||
|
|
||||
|
public HashSet<string> Paths => PathMap.Select(x => x.Value).ToHashSet(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq.Expressions; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ApplicationCore.Helpers.Query |
||||
|
{ |
||||
|
public static class IncludeQueryExtensions |
||||
|
{ |
||||
|
public static IIncludeQuery<TEntity, TNewProperty> Include<TEntity, TPreviousProperty, TNewProperty>( |
||||
|
this IIncludeQuery<TEntity, TPreviousProperty> query, |
||||
|
Expression<Func<TEntity, TNewProperty>> selector) |
||||
|
{ |
||||
|
query.Visitor.Visit(selector); |
||||
|
|
||||
|
var includeQuery = new IncludeQuery<TEntity, TNewProperty>(query.PathMap); |
||||
|
query.PathMap[includeQuery] = query.Visitor.Path; |
||||
|
|
||||
|
return includeQuery; |
||||
|
} |
||||
|
|
||||
|
public static IIncludeQuery<TEntity, TNewProperty> ThenInclude<TEntity, TPreviousProperty, TNewProperty>( |
||||
|
this IIncludeQuery<TEntity, TPreviousProperty> query, |
||||
|
Expression<Func<TPreviousProperty, TNewProperty>> selector) |
||||
|
{ |
||||
|
query.Visitor.Visit(selector); |
||||
|
|
||||
|
// If the visitor did not generated a path, return a new IncludeQuery with an unmodified PathMap.
|
||||
|
if (string.IsNullOrEmpty(query.Visitor.Path)) |
||||
|
{ |
||||
|
return new IncludeQuery<TEntity, TNewProperty>(query.PathMap); |
||||
|
} |
||||
|
|
||||
|
var pathMap = query.PathMap; |
||||
|
var existingPath = pathMap[query]; |
||||
|
pathMap.Remove(query); |
||||
|
|
||||
|
var includeQuery = new IncludeQuery<TEntity, TNewProperty>(query.PathMap); |
||||
|
pathMap[includeQuery] = $"{existingPath}.{query.Visitor.Path}"; |
||||
|
|
||||
|
return includeQuery; |
||||
|
} |
||||
|
|
||||
|
public static IIncludeQuery<TEntity, TNewProperty> ThenInclude<TEntity, TPreviousProperty, TNewProperty>( |
||||
|
this IIncludeQuery<TEntity, IEnumerable<TPreviousProperty>> query, |
||||
|
Expression<Func<TPreviousProperty, TNewProperty>> selector) |
||||
|
{ |
||||
|
query.Visitor.Visit(selector); |
||||
|
|
||||
|
// If the visitor did not generated a path, return a new IncludeQuery with an unmodified PathMap.
|
||||
|
if (string.IsNullOrEmpty(query.Visitor.Path)) |
||||
|
{ |
||||
|
return new IncludeQuery<TEntity, TNewProperty>(query.PathMap); |
||||
|
} |
||||
|
|
||||
|
var pathMap = query.PathMap; |
||||
|
var existingPath = pathMap[query]; |
||||
|
pathMap.Remove(query); |
||||
|
|
||||
|
var includeQuery = new IncludeQuery<TEntity, TNewProperty>(query.PathMap); |
||||
|
pathMap[includeQuery] = $"{existingPath}.{query.Visitor.Path}"; |
||||
|
|
||||
|
return includeQuery; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System.Linq.Expressions; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ApplicationCore.Helpers.Query |
||||
|
{ |
||||
|
public class IncludeVisitor : ExpressionVisitor |
||||
|
{ |
||||
|
public string Path { get; private set; } = string.Empty; |
||||
|
|
||||
|
protected override Expression VisitMember(MemberExpression node) |
||||
|
{ |
||||
|
Path = string.IsNullOrEmpty(Path) ? node.Member.Name : $"{node.Member.Name}.{Path}"; |
||||
|
|
||||
|
return base.VisitMember(node); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces |
||||
|
{ |
||||
|
public interface IIncludeQuery |
||||
|
{ |
||||
|
Dictionary<IIncludeQuery, string> PathMap { get; } |
||||
|
IncludeVisitor Visitor { get; } |
||||
|
HashSet<string> Paths { get; } |
||||
|
} |
||||
|
|
||||
|
public interface IIncludeQuery<TEntity, out TPreviousProperty> : IIncludeQuery |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query |
||||
|
{ |
||||
|
public class Book |
||||
|
{ |
||||
|
public string Title { get; set; } |
||||
|
public DateTime PublishingDate { get; set; } |
||||
|
public Person Author { get; set; } |
||||
|
|
||||
|
public int GetNumberOfSales() |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query.IncludeAggregatorTests |
||||
|
{ |
||||
|
public class Include |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeSimpleType() |
||||
|
{ |
||||
|
var includeAggregator = new IncludeAggregator<Person>(); |
||||
|
|
||||
|
// There may be ORM libraries where including a simple type makes sense.
|
||||
|
var includeQuery = includeAggregator.Include(p => p.Age); |
||||
|
|
||||
|
Assert.Contains(includeQuery.Paths, path => path == nameof(Person.Age)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeFunction() |
||||
|
{ |
||||
|
var includeAggregator = new IncludeAggregator<Person>(); |
||||
|
|
||||
|
// This include does not make much sense, but it should at least do not modify the paths.
|
||||
|
var includeQuery = includeAggregator.Include(p => p.FavouriteBook.GetNumberOfSales()); |
||||
|
|
||||
|
Assert.Contains(includeQuery.Paths, path => path == nameof(Person.FavouriteBook)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeObject() |
||||
|
{ |
||||
|
var includeAggregator = new IncludeAggregator<Person>(); |
||||
|
var includeQuery = includeAggregator.Include(p => p.FavouriteBook.Author); |
||||
|
|
||||
|
Assert.Contains(includeQuery.Paths, path => path == $"{nameof(Person.FavouriteBook)}.{nameof(Book.Author)}"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeCollection() |
||||
|
{ |
||||
|
var includeAggregator = new IncludeAggregator<Book>(); |
||||
|
var includeQuery = includeAggregator.Include(o => o.Author.Friends); |
||||
|
|
||||
|
Assert.Contains(includeQuery.Paths, path => path == $"{nameof(Book.Author)}.{nameof(Person.Friends)}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query; |
||||
|
using Microsoft.eShopWeb.UnitTests.Builders; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query.IncludeQueryTests |
||||
|
{ |
||||
|
public class Include |
||||
|
{ |
||||
|
private IncludeQueryBuilder _includeQueryBuilder = new IncludeQueryBuilder(); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeSimpleType() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
|
||||
|
// There may be ORM libraries where including a simple type makes sense.
|
||||
|
var newIncludeQuery = includeQuery.Include(b => b.Title); |
||||
|
|
||||
|
Assert.Contains(newIncludeQuery.Paths, path => path == nameof(Book.Title)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeFunction() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
|
||||
|
// This include does not make much sense, but it should at least do not modify paths.
|
||||
|
var newIncludeQuery = includeQuery.Include(b => b.GetNumberOfSales()); |
||||
|
|
||||
|
// The resulting paths should not include number of sales.
|
||||
|
Assert.DoesNotContain(newIncludeQuery.Paths, path => path == nameof(Book.GetNumberOfSales)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeObject() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var newIncludeQuery = includeQuery.Include(b => b.Author); |
||||
|
|
||||
|
Assert.Contains(newIncludeQuery.Paths, path => path == nameof(Book.Author)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeCollection() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
|
||||
|
var newIncludeQuery = includeQuery.Include(b => b.Author.Friends); |
||||
|
var expectedPath = $"{nameof(Book.Author)}.{nameof(Person.Friends)}"; |
||||
|
|
||||
|
Assert.Contains(newIncludeQuery.Paths, path => path == expectedPath); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_IncreaseNumberOfPathsByOne() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var numberOfPathsBeforeInclude = includeQuery.Paths.Count; |
||||
|
|
||||
|
var newIncludeQuery = includeQuery.Include(b => b.Author.Friends); |
||||
|
var numberOfPathsAferInclude = newIncludeQuery.Paths.Count; |
||||
|
|
||||
|
var expectedNumerOfPaths = numberOfPathsBeforeInclude + 1; |
||||
|
|
||||
|
Assert.Equal(expectedNumerOfPaths, numberOfPathsAferInclude); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_NotModifyAnotherPath() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var pathsBeforeInclude = includeQuery.Paths; |
||||
|
|
||||
|
var newIncludeQuery = includeQuery.Include(b => b.Author.Friends); |
||||
|
var pathsAfterInclude = newIncludeQuery.Paths; |
||||
|
|
||||
|
Assert.Subset(pathsAfterInclude, pathsBeforeInclude); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query; |
||||
|
using Microsoft.eShopWeb.UnitTests.Builders; |
||||
|
using System.Linq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query.IncludeQueryTests |
||||
|
{ |
||||
|
public class ThenInclude |
||||
|
{ |
||||
|
private IncludeQueryBuilder _includeQueryBuilder = new IncludeQueryBuilder(); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeSimpleType() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var pathBeforeInclude = includeQuery.Paths.First(); |
||||
|
|
||||
|
// There may be ORM libraries where including a simple type makes sense.
|
||||
|
var newIncludeQuery = includeQuery.ThenInclude(p => p.Age); |
||||
|
var pathAfterInclude = newIncludeQuery.Paths.First(); |
||||
|
var expectedPath = $"{pathBeforeInclude}.{nameof(Person.Age)}"; |
||||
|
|
||||
|
Assert.Equal(expectedPath, pathAfterInclude); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeFunction() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var pathBeforeInclude = includeQuery.Paths.First(); |
||||
|
|
||||
|
// This include does not make much sense, but it should at least not modify the paths.
|
||||
|
var newIncludeQuery = includeQuery.ThenInclude(p => p.GetQuote()); |
||||
|
var pathAfterInclude = newIncludeQuery.Paths.First(); |
||||
|
|
||||
|
Assert.Equal(pathBeforeInclude, pathAfterInclude); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeObject() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var pathBeforeInclude = includeQuery.Paths.First(); |
||||
|
|
||||
|
var newIncludeQuery = includeQuery.ThenInclude(p => p.FavouriteBook); |
||||
|
var pathAfterInclude = newIncludeQuery.Paths.First(); |
||||
|
var expectedPath = $"{pathBeforeInclude}.{nameof(Person.FavouriteBook)}"; |
||||
|
|
||||
|
Assert.Equal(expectedPath, pathAfterInclude); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludeCollection() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithObjectAsPreviousProperty(); |
||||
|
var pathBeforeInclude = includeQuery.Paths.First(); |
||||
|
|
||||
|
var newIncludeQuery = includeQuery.ThenInclude(p => p.Friends); |
||||
|
var pathAfterInclude = newIncludeQuery.Paths.First(); |
||||
|
var expectedPath = $"{pathBeforeInclude}.{nameof(Person.Friends)}"; |
||||
|
|
||||
|
Assert.Equal(expectedPath, pathAfterInclude); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_ReturnIncludeQueryWithCorrectPath_IfIncludePropertyOverCollection() |
||||
|
{ |
||||
|
var includeQuery = _includeQueryBuilder.WithCollectionAsPreviousProperty(); |
||||
|
var pathBeforeInclude = includeQuery.Paths.First(); |
||||
|
|
||||
|
var newIncludeQuery = includeQuery.ThenInclude(p => p.FavouriteBook); |
||||
|
var pathAfterInclude = newIncludeQuery.Paths.First(); |
||||
|
var expectedPath = $"{pathBeforeInclude}.{nameof(Person.FavouriteBook)}"; |
||||
|
|
||||
|
Assert.Equal(expectedPath, pathAfterInclude); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq.Expressions; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query.IncludeVisitorTests |
||||
|
{ |
||||
|
public class Visit |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_SetPath_IfPassedExpressionWithSimpleType() |
||||
|
{ |
||||
|
var visitor = new IncludeVisitor(); |
||||
|
Expression<Func<Book, string>> expression = (book) => book.Author.FirstName; |
||||
|
visitor.Visit(expression); |
||||
|
|
||||
|
var expectedPath = $"{nameof(Book.Author)}.{nameof(Person.FirstName)}"; |
||||
|
Assert.Equal(expectedPath, visitor.Path); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_SetPath_IfPassedExpressionWithObject() |
||||
|
{ |
||||
|
var visitor = new IncludeVisitor(); |
||||
|
Expression<Func<Book, Book>> expression = (book) => book.Author.FavouriteBook; |
||||
|
visitor.Visit(expression); |
||||
|
|
||||
|
var expectedPath = $"{nameof(Book.Author)}.{nameof(Person.FavouriteBook)}"; |
||||
|
Assert.Equal(expectedPath, visitor.Path); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_SetPath_IfPassedExpressionWithCollection() |
||||
|
{ |
||||
|
var visitor = new IncludeVisitor(); |
||||
|
Expression<Func<Book, List<Person>>> expression = (book) => book.Author.Friends; |
||||
|
visitor.Visit(expression); |
||||
|
|
||||
|
var expectedPath = $"{nameof(Book.Author)}.{nameof(Person.Friends)}"; |
||||
|
Assert.Equal(expectedPath, visitor.Path); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_SetPath_IfPassedExpressionWithFunction() |
||||
|
{ |
||||
|
var visitor = new IncludeVisitor(); |
||||
|
Expression<Func<Book, string>> expression = (book) => book.Author.GetQuote(); |
||||
|
visitor.Visit(expression); |
||||
|
|
||||
|
var expectedPath = nameof(Book.Author); |
||||
|
Assert.Equal(expectedPath, visitor.Path); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query |
||||
|
{ |
||||
|
public class Person |
||||
|
{ |
||||
|
public int Age { get; set; } |
||||
|
public string FirstName { get; set; } |
||||
|
public string LastName { get; set; } |
||||
|
|
||||
|
public Book FavouriteBook { get; set; } |
||||
|
public List<Person> Friends { get; set; } |
||||
|
|
||||
|
public string GetQuote() |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query; |
||||
|
using Microsoft.eShopWeb.ApplicationCore.Interfaces; |
||||
|
using Microsoft.eShopWeb.UnitTests.ApplicationCore.Helpers.Query; |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Microsoft.eShopWeb.UnitTests.Builders |
||||
|
{ |
||||
|
public class IncludeQueryBuilder |
||||
|
{ |
||||
|
public IncludeQuery<Person, List<Person>> WithCollectionAsPreviousProperty() |
||||
|
{ |
||||
|
var pathMap = new Dictionary<IIncludeQuery, string>(); |
||||
|
var query = new IncludeQuery<Person, List<Person>>(pathMap); |
||||
|
pathMap[query] = nameof(Person.Friends); |
||||
|
|
||||
|
return query; |
||||
|
} |
||||
|
|
||||
|
public IncludeQuery<Book, Person> WithObjectAsPreviousProperty() |
||||
|
{ |
||||
|
var pathMap = new Dictionary<IIncludeQuery, string>(); |
||||
|
var query = new IncludeQuery<Book, Person>(pathMap); |
||||
|
pathMap[query] = nameof(Book.Author); |
||||
|
|
||||
|
return query; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue