Skip to content

Commit

Permalink
Added possibility to chain includes. (dotnet-architecture#331)
Browse files Browse the repository at this point in the history
* Added possibility to chain includes.

* Removed interface.

* Removed the need for generating GUIDs as ids and added tests.
  • Loading branch information
mrukas authored and efleming18 committed Dec 16, 2019
1 parent 9695e9e commit 13fc6ea
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 2 deletions.
26 changes: 26 additions & 0 deletions src/ApplicationCore/Helpers/Query/IncludeAggregator.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
19 changes: 19 additions & 0 deletions src/ApplicationCore/Helpers/Query/IncludeQuery.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
66 changes: 66 additions & 0 deletions src/ApplicationCore/Helpers/Query/IncludeQueryExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
16 changes: 16 additions & 0 deletions src/ApplicationCore/Helpers/Query/IncludeVisitor.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
16 changes: 16 additions & 0 deletions src/ApplicationCore/Interfaces/IIncludeQuery.cs
Original file line number Diff line number Diff line change
@@ -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
{
}
}
8 changes: 8 additions & 0 deletions src/ApplicationCore/Specifications/BaseSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Linq.Expressions;
using System.Collections.Generic;
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query;

namespace Microsoft.eShopWeb.ApplicationCore.Specifications
{
Expand All @@ -26,6 +27,13 @@ protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}

protected virtual void AddIncludes<TProperty>(Func<IncludeAggregator<T>, IIncludeQuery<T, TProperty>> includeGenerator)
{
var includeQuery = includeGenerator(new IncludeAggregator<T>());
IncludeStrings.AddRange(includeQuery.Paths);
}

protected virtual void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
using Microsoft.eShopWeb.ApplicationCore.Helpers.Query;

namespace Microsoft.eShopWeb.ApplicationCore.Specifications
{
Expand All @@ -7,8 +8,7 @@ public class CustomerOrdersWithItemsSpecification : BaseSpecification<Order>
public CustomerOrdersWithItemsSpecification(string buyerId)
: base(o => o.BuyerId == buyerId)
{
AddInclude(o => o.OrderItems);
AddInclude($"{nameof(Order.OrderItems)}.{nameof(OrderItem.ItemOrdered)}");
AddIncludes(query => query.Include(o => o.OrderItems).ThenInclude(i => i.ItemOrdered));
}
}
}
16 changes: 16 additions & 0 deletions tests/UnitTests/ApplicationCore/Helpers/Query/Book.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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)}");
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading

0 comments on commit 13fc6ea

Please sign in to comment.