-
-
Notifications
You must be signed in to change notification settings - Fork 159
Description
DESCRIPTION
I am having issues with JADNC in conjunction with EF Core constructor binding. To demonstrate that issue, I have created the following small test class TestResource
, and played with multiple levers:
- with and without constructor;
- with and without
nullable
enabled; and - different types for
Id
property, i.e.,int
(shown below),Guid
, andstring
.
public class TestResource : Identifiable
{
public TestResource(string name)
{
Name = name;
}
[Attr]
public string Name { get; set; }
public int? ParentId { get; set; }
[HasOne]
public TestResource Parent { get; set; }
[HasMany]
public ICollection<TestResource> Children { get; set; } = new HashSet<TestResource>();
}
The migration generates the following code for the above TestResource
class (with the DbSet<TestResource> TestResources
property and the foreign key relationship with .OnDelete(DeleteBehavior.Restrict)
specified in the DbContext
):
migrationBuilder.CreateTable(
name: "TestResources",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
ParentId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TestResources", x => x.Id);
table.ForeignKey(
name: "FK_TestResources_TestResources_ParentId",
column: x => x.ParentId,
principalTable: "TestResources",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
The controller is as simple as can be:
public class TestResourcesController : JsonApiController<TestResource, int>
{
/// <inheritdoc />
public TestResourcesController(
IJsonApiOptions options,
ILoggerFactory loggerFactory,
IResourceService<TestResource, int> resourceService) : base(options, loggerFactory, resourceService)
{
}
}
With the simple request GET https://localhost:5001/TestResources
, everything works just fine. The test instances are returned as expected. However, a request like:
GET https://localhost:5001/TestResources?include=children
orGET https://localhost:5001/TestResources?include=parent
only works when the TestResource
class does not have a constructor.
With the constructor, which is required when nullable
is enabled, the following error is returned regardless of the options described under (2) and (3) above:
{
"errors": [
{
"id": "a66ad743-9f38-45cf-b97e-26e7fcb01ea1",
"status": "500",
"title": "An unhandled error occurred while processing this request.",
"detail": "Failed to create an instance of 'ServiceCatalog.Models.TestResource': Parameter 'name' could not be resolved."
}
]
}
Here's the corresponding exception stack trace:
[10:27:12 INF] Entity Framework Core 5.0.10 initialized 'RelationalDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
[10:27:12 INF] Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT COUNT(*)
FROM [TestResources] AS [t]
[10:27:12 ERR] Failed to create an instance of 'ServiceCatalog.Models.TestResource': Parameter 'name' could not be resolved.
System.InvalidOperationException: Failed to create an instance of 'ServiceCatalog.Models.TestResource': Parameter 'name' could not be resolved.
---> System.InvalidOperationException: Unable to resolve service for type 'System.Char[]' while attempting to activate 'System.String'.
at object Microsoft.Extensions.DependencyInjection.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider)
at object Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, params object[] parameters)
at NewExpression JsonApiDotNetCore.Resources.ResourceFactory.CreateNewExpression(Type resourceType) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Resources/ResourceFactory.cs:line 67
--- End of inner exception stack trace ---
at NewExpression JsonApiDotNetCore.Resources.ResourceFactory.CreateNewExpression(Type resourceType) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Resources/ResourceFactory.cs:line 92
at Expression JsonApiDotNetCore.Queries.Internal.QueryableBuilding.SelectClauseBuilder.CreateLambdaBodyInitializer(IDictionary<ResourceFieldAttribute, QueryLayer> selectors, ResourceContext resourceContext, LambdaScope lambdaScope, bool lambdaAccessorRequiresTestForNull) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs:line 77
at Expression JsonApiDotNetCore.Queries.Internal.QueryableBuilding.SelectClauseBuilder.ApplySelect(IDictionary<ResourceFieldAttribute, QueryLayer> selectors, ResourceContext resourceContext) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs:line 62
at Expression JsonApiDotNetCore.Queries.Internal.QueryableBuilding.QueryableBuilder.ApplyProjection(Expression source, IDictionary<ResourceFieldAttribute, QueryLayer> projection, ResourceContext resourceContext) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs:line 122
at Expression JsonApiDotNetCore.Queries.Internal.QueryableBuilding.QueryableBuilder.ApplyQuery(QueryLayer layer) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs:line 78
at IQueryable<TResource> JsonApiDotNetCore.Repositories.EntityFrameworkCoreRepository<TResource, TId>.ApplyQueryLayer(QueryLayer layer) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs:line 140
at async Task<IReadOnlyCollection<TResource>> JsonApiDotNetCore.Repositories.EntityFrameworkCoreRepository<TResource, TId>.GetAsync(QueryLayer layer, CancellationToken cancellationToken) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs:line 75
at object CallSite.Target(Closure, CallSite, object)
at async Task<IReadOnlyCollection<TResource>> JsonApiDotNetCore.Repositories.ResourceRepositoryAccessor.GetAsync<TResource>(QueryLayer layer, CancellationToken cancellationToken) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs:line 40
at async Task<IReadOnlyCollection<TResource>> JsonApiDotNetCore.Services.JsonApiResourceService<TResource, TId>.GetAsync(CancellationToken cancellationToken) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs:line 85
at async Task<IActionResult> JsonApiDotNetCore.Controllers.BaseJsonApiController<TResource, TId>.GetAsync(CancellationToken cancellationToken) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs:line 97
at async Task<IActionResult> JsonApiDotNetCore.Controllers.JsonApiController<TResource, TId>.GetAsync(CancellationToken cancellationToken) in C:/projects/jsonapidotnetcore/src/JsonApiDotNetCore/Controllers/JsonApiController.cs:line 48
at async ValueTask<IActionResult> Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)
at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()+Awaited(?)
at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()+Awaited(?)
at void Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()+Awaited(?)
at async Task Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextExceptionFilterAsync()+Awaited(?)
[10:27:12 ERR] HTTP GET /TestResources?include=children responded 500 in 679.9574 ms
STEPS TO REPRODUCE
First, in addition to the TestResource
class defined above, create the RelationalDbContext
class below:
public class RelationalDbContext : DbContext
{
public RelationalDbContext()
{
}
public DbSet<TestResource> TestResources => Set<TestResource>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}
optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ServiceCatalogDb;Integrated Security=true");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TestResource>(entity =>
{
entity.HasOne(e => e.Parent)
.WithMany(e => e.Children)
.HasForeignKey(e => e.ParentId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
});
}
}
Second, add a migration (e.g., using Add-Migration
or the corresponding CLI command).
Third, using the following xUnit-based unit test, for example, create some test data. In this case, we have one "Root" with two children "First" and "Second". Note that constructor binding works when using EF Core directly.
public class RelationalDbIntegrationTests
{
[Fact]
public async Task TestRelationalDatabaseAsync()
{
await using (var context = new RelationalDbContext())
{
await context.Database.EnsureDeletedAsync().ConfigureAwait(false);
await context.Database.MigrateAsync().ConfigureAwait(false);
context.Add(new TestResource("Root")
{
Children = new[]
{
new TestResource("First"),
new TestResource("Second")
}
});
await context.SaveChangesAsync().ConfigureAwait(false);
}
await using (var context = new RelationalDbContext())
{
// Establish that we can read the resource using EF core.
TestResource resource = await context.TestResources
.Include(testResource => testResource.Children)
.SingleAsync(testResource => testResource.Name == "Root");
Assert.NotEmpty(resource.Children);
}
}
}
Fourth, issue the following requests (using the browser or Postman) and note that number (1) works as expected while (2) and (3) produce the error described above.
GET https://localhost:5001/TestResources
;GET https://localhost:5001/TestResources?include=children
; andGET https://localhost:5001/TestResources?include=parent
.
Fifth, remove the constructor from the TestResource
class and change the unit test above to construct the test instances as follows:
context.Add(new TestResource
{
Name = "Root",
Children = new[]
{
new TestResource { Name = "First" },
new TestResource { Name = "Second" }
}
});
Sixth, issue the same requests as above and note that now all requests work as expected.
EXPECTED BEHAVIOR
When using constructor binding, requests (2) and (3) successfully return a result (like request (1) in all cases).
ACTUAL BEHAVIOR
When using constructor binding, requests (2) and (3) return an error as described above.
VERSIONS USED
- JsonApiDotNetCore version: 4.2.0
- ASP.NET Core version: 5.0.10
- Entity Framework Core version: 5.0.10
- Database provider: SQL Server Express LocalDb