Skip to content

Commit 04aff35

Browse files
author
Bart Koelman
committed
Added dynamic query building for resources with constructor parameters
1 parent 7cf2b9a commit 04aff35

File tree

13 files changed

+310
-50
lines changed

13 files changed

+310
-50
lines changed

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
7171
.HasForeignKey<Person>(p => p.PassportId)
7272
.OnDelete(DeleteBehavior.SetNull);
7373

74+
modelBuilder.Entity<Passport>()
75+
.HasMany(passport => passport.GrantedVisas)
76+
.WithOne()
77+
.OnDelete(DeleteBehavior.Cascade);
78+
7479
modelBuilder.Entity<TodoItem>()
7580
.HasOne(p => p.OneToOnePerson)
7681
.WithOne(p => p.OneToOneTodoItem)

src/JsonApiDotNetCore/Data/DefaultResourceRepository.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Threading.Tasks;
66
using JsonApiDotNetCore.Extensions;
7+
using JsonApiDotNetCore.Internal;
78
using JsonApiDotNetCore.Internal.Contracts;
89
using JsonApiDotNetCore.Internal.Generics;
910
using JsonApiDotNetCore.Internal.Query;
@@ -27,18 +28,21 @@ public class DefaultResourceRepository<TResource, TId> : IResourceRepository<TRe
2728
private readonly DbSet<TResource> _dbSet;
2829
private readonly IResourceGraph _resourceGraph;
2930
private readonly IGenericServiceFactory _genericServiceFactory;
31+
private readonly IResourceFactory _resourceFactory;
3032
private readonly ILogger<DefaultResourceRepository<TResource, TId>> _logger;
3133

3234
public DefaultResourceRepository(
3335
ITargetedFields targetedFields,
3436
IDbContextResolver contextResolver,
3537
IResourceGraph resourceGraph,
3638
IGenericServiceFactory genericServiceFactory,
39+
IResourceFactory resourceFactory,
3740
ILoggerFactory loggerFactory)
3841
{
3942
_targetedFields = targetedFields;
4043
_resourceGraph = resourceGraph;
4144
_genericServiceFactory = genericServiceFactory;
45+
_resourceFactory = resourceFactory;
4246
_context = contextResolver.GetContext();
4347
_dbSet = _context.Set<TResource>();
4448
_logger = loggerFactory.CreateLogger<DefaultResourceRepository<TResource, TId>>();
@@ -66,7 +70,7 @@ public virtual IQueryable<TResource> Select(IQueryable<TResource> entities, IEnu
6670
{
6771
_logger.LogTrace($"Entering {nameof(Select)}({nameof(entities)}, {nameof(propertyNames)}).");
6872

69-
return entities.Select(propertyNames);
73+
return entities.Select(propertyNames, _resourceFactory);
7074
}
7175

7276
/// <inheritdoc />
@@ -468,8 +472,9 @@ public DefaultResourceRepository(
468472
IDbContextResolver contextResolver,
469473
IResourceGraph resourceGraph,
470474
IGenericServiceFactory genericServiceFactory,
475+
IResourceFactory resourceFactory,
471476
ILoggerFactory loggerFactory)
472-
: base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, loggerFactory)
477+
: base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory)
473478
{ }
474479
}
475480
}

src/JsonApiDotNetCore/Extensions/QueryableExtensions.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
6363
return CallGenericWhereMethod(source, filterQuery);
6464
}
6565

66-
public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, IEnumerable<string> columns)
66+
public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, IEnumerable<string> columns, IResourceFactory resourceFactory)
6767
{
68-
return columns == null || !columns.Any() ? source : CallGenericSelectMethod(source, columns);
68+
return columns == null || !columns.Any() ? source : CallGenericSelectMethod(source, columns, resourceFactory);
6969
}
7070

7171
public static IOrderedQueryable<TSource> Sort<TSource>(this IQueryable<TSource> source, SortQueryContext sortQuery)
@@ -342,7 +342,7 @@ private static Expression CreateTupleAccessForConstantExpression(object value, T
342342
return Expression.Property(tupleCreateCall, "Item1");
343343
}
344344

345-
private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<TSource> source, IEnumerable<string> columns)
345+
private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<TSource> source, IEnumerable<string> columns, IResourceFactory resourceFactory)
346346
{
347347
var sourceType = typeof(TSource);
348348
var parameter = Expression.Parameter(source.ElementType, "x");
@@ -386,7 +386,7 @@ private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<T
386386
collectionElementType.GetProperty(prop), Expression.PropertyOrField(nestedParameter, prop))).ToList();
387387

388388
// { new Item() }
389-
var newNestedExp = Expression.New(collectionElementType);
389+
var newNestedExp = resourceFactory.CreateNewExpression(collectionElementType);
390390
var initNestedExp = Expression.MemberInit(newNestedExp, nestedBindings);
391391
// { y => new Item() {Id = y.Id, Name = y.Name}}
392392
var body = Expression.Lambda(initNestedExp, nestedParameter);
@@ -419,7 +419,7 @@ private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<T
419419
nestedBindings.Add(Expression.Bind(propInfo, nestedBody));
420420
}
421421
// { new Owner() }
422-
var newExp = Expression.New(nestedPropertyType);
422+
var newExp = resourceFactory.CreateNewExpression(nestedPropertyType);
423423
// { new Owner() { Id = x.Owner.Id, Name = x.Owner.Name }}
424424
var newInit = Expression.MemberInit(newExp, nestedBindings);
425425

@@ -436,7 +436,8 @@ private static IQueryable<TSource> CallGenericSelectMethod<TSource>(IQueryable<T
436436
nestedBindings.Clear();
437437
}
438438

439-
var sourceInit = Expression.MemberInit(Expression.New(sourceType), sourceBindings);
439+
var newExpression = resourceFactory.CreateNewExpression(sourceType);
440+
var sourceInit = Expression.MemberInit(newExpression, sourceBindings);
440441
var finalBody = Expression.Lambda(sourceInit, parameter);
441442

442443
return source.Provider.CreateQuery<TSource>(Expression.Call(

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,30 @@ public static bool IsOrImplementsInterface(this Type source, Type interfaceType)
7171

7272
public static bool HasSingleConstructorWithoutParameters(this Type type)
7373
{
74-
ConstructorInfo[] constructors = type.GetConstructors();
74+
ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray();
7575

7676
return constructors.Length == 1 && constructors[0].GetParameters().Length == 0;
7777
}
78+
79+
public static ConstructorInfo GetLongestConstructor(this Type type)
80+
{
81+
ConstructorInfo[] constructors = type.GetConstructors().Where(c => !c.IsStatic).ToArray();
82+
83+
ConstructorInfo bestMatch = constructors[0];
84+
int maxParameterLength = constructors[0].GetParameters().Length;
85+
86+
for (int index = 1; index < constructors.Length; index++)
87+
{
88+
var constructor = constructors[index];
89+
int length = constructor.GetParameters().Length;
90+
if (length > maxParameterLength)
91+
{
92+
bestMatch = constructor;
93+
maxParameterLength = length;
94+
}
95+
}
96+
97+
return bestMatch;
98+
}
7899
}
79100
}

src/JsonApiDotNetCore/Internal/IResourceFactory.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
25
using JsonApiDotNetCore.Extensions;
36
using JsonApiDotNetCore.Models;
47
using Microsoft.Extensions.DependencyInjection;
@@ -9,6 +12,7 @@ public interface IResourceFactory
912
{
1013
public IIdentifiable CreateInstance(Type resourceType);
1114
public TResource CreateInstance<TResource>() where TResource : IIdentifiable;
15+
public NewExpression CreateNewExpression(Type resourceType);
1216
}
1317

1418
internal sealed class DefaultResourceFactory : IResourceFactory
@@ -53,5 +57,25 @@ private static object InnerCreateInstance(Type type, IServiceProvider servicePro
5357
exception);
5458
}
5559
}
60+
61+
public NewExpression CreateNewExpression(Type resourceType)
62+
{
63+
if (resourceType.HasSingleConstructorWithoutParameters())
64+
{
65+
return Expression.New(resourceType);
66+
}
67+
68+
List<Expression> constructorArguments = new List<Expression>();
69+
70+
var longestConstructor = resourceType.GetLongestConstructor();
71+
foreach (ParameterInfo constructorParameter in longestConstructor.GetParameters())
72+
{
73+
var constructorArgument =
74+
ActivatorUtilities.CreateInstance(_serviceProvider, constructorParameter.ParameterType);
75+
constructorArguments.Add(Expression.Constant(constructorArgument));
76+
}
77+
78+
return Expression.New(longestConstructor, constructorArguments);
79+
}
5680
}
5781
}

test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using System.Collections.Generic;
32
using GettingStarted.Models;
43
using GettingStarted.ResourceDefinitionExample;
@@ -41,14 +40,14 @@ public ServiceDiscoveryFacadeTests()
4140

4241
_services.AddSingleton<IJsonApiOptions>(options);
4342
_services.AddSingleton<ILoggerFactory>(new LoggerFactory());
44-
_services.AddScoped((_) => new Mock<ILinkBuilder>().Object);
45-
_services.AddScoped((_) => new Mock<ICurrentRequest>().Object);
46-
_services.AddScoped((_) => new Mock<ITargetedFields>().Object);
47-
_services.AddScoped((_) => new Mock<IResourceGraph>().Object);
48-
_services.AddScoped((_) => new Mock<IGenericServiceFactory>().Object);
49-
_services.AddScoped((_) => new Mock<IResourceContextProvider>().Object);
43+
_services.AddScoped(_ => new Mock<ILinkBuilder>().Object);
44+
_services.AddScoped(_ => new Mock<ICurrentRequest>().Object);
45+
_services.AddScoped(_ => new Mock<ITargetedFields>().Object);
46+
_services.AddScoped(_ => new Mock<IResourceGraph>().Object);
47+
_services.AddScoped(_ => new Mock<IGenericServiceFactory>().Object);
48+
_services.AddScoped(_ => new Mock<IResourceContextProvider>().Object);
5049
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(DefaultResourceChangeTracker<>));
51-
_services.AddScoped<IResourceFactory, FakeResourceFactory>();
50+
_services.AddScoped(_ => new Mock<IResourceFactory>().Object);
5251

5352
_resourceGraphBuilder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
5453
}
@@ -132,22 +131,10 @@ public TestModelRepository(
132131
ITargetedFields targetedFields,
133132
IResourceGraph resourceGraph,
134133
IGenericServiceFactory genericServiceFactory,
134+
IResourceFactory resourceFactory,
135135
ILoggerFactory loggerFactory)
136-
: base(targetedFields, _dbContextResolver, resourceGraph, genericServiceFactory, loggerFactory)
136+
: base(targetedFields, _dbContextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory)
137137
{ }
138138
}
139-
140-
public class FakeResourceFactory : IResourceFactory
141-
{
142-
public IIdentifiable CreateInstance(Type resourceType)
143-
{
144-
throw new NotImplementedException();
145-
}
146-
147-
public TResource CreateInstance<TResource>() where TResource : IIdentifiable
148-
{
149-
throw new NotImplementedException();
150-
}
151-
}
152139
}
153140
}

test/IntegrationTests/Data/EntityRepositoryTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using System.Threading.Tasks;
1313
using IntegrationTests;
1414
using JsonApiDotNetCore.Configuration;
15+
using JsonApiDotNetCore.Internal;
16+
using Microsoft.EntityFrameworkCore.Infrastructure;
1517
using Microsoft.Extensions.Logging.Abstractions;
1618
using Xunit;
1719

@@ -161,11 +163,13 @@ public async Task Paging_PageNumberIsNegative_GiveBackReverseAmountOfIds(int pag
161163

162164
private (DefaultResourceRepository<TodoItem> Repository, Mock<ITargetedFields> TargetedFields) Setup(AppDbContext context)
163165
{
166+
var serviceProvider = ((IInfrastructure<IServiceProvider>) context).Instance;
167+
var resourceFactory = new DefaultResourceFactory(serviceProvider);
164168
var contextResolverMock = new Mock<IDbContextResolver>();
165169
contextResolverMock.Setup(m => m.GetContext()).Returns(context);
166170
var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource<TodoItem>().Build();
167171
var targetedFields = new Mock<ITargetedFields>();
168-
var repository = new DefaultResourceRepository<TodoItem>(targetedFields.Object, contextResolverMock.Object, resourceGraph, null, NullLoggerFactory.Instance);
172+
var repository = new DefaultResourceRepository<TodoItem>(targetedFields.Object, contextResolverMock.Object, resourceGraph, null, resourceFactory, NullLoggerFactory.Instance);
169173
return (repository, targetedFields);
170174
}
171175

0 commit comments

Comments
 (0)