dotnet / dotnet/core

ModelBinding behavior for class using a TypeConverter appears to be inconsistent between Controller and ControllerBase

Open
#5,621 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
PowerShell
Stars
22k
Forks
4.9k
Avg merge
5d 5h
Merged PRs (30d)
29

Description

# ModelBinding behavior for class using a TypeConverter appears to be inconsistent between Controller and ControllerBase

# General

## Version: .Net 5.0.0

I have 2 projects, one is a traditional MVC application and the other is a WebAPI project with an Angular frontend. Both projects have near identical packages, and both `startup.cs` and `program.cs` are more or less identical. For all intents and purposes, both of the projects are more or less mirror images of each other, with different UI implementations.

For one specific endpoint, both projects expect the same input model (`GetJobListQuery`) to be passed into the controller action. `GetJobListQuery` contains a property called `SortDirection` with a type of `SortDirection`. The implementations look something like:

```csharp
public class GetJobListQuery
{
public SortDirection SortDirection { get; set; } = SortDirection.Ascending;
// code omitted for brevity.
}

[TypeConverter(typeof(SortDirectionTypeConverter))]
public readonly struct SortDirection
{
public static SortDirection Ascending => new SortDirection("ASC");
public static SortDirection Descending => new SortDirection("DESC");

private SortDirection(string direction)
{
Direction = direction;
}

public string Direction { get; }
// code omitted for brevity.
}
```

And the `TypeConverter` referenced in `SortDirection` is defined as such:

```csharp
public class SortDirectionTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return typeof(string) == sourceType || base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var strValue = value as string;
if (string.IsNullOrWhiteSpace(strValue))
return SortDirection.Ascending;

strValue = strValue.ToUpperInvariant();

var sortDirections = new Dictionary
{
[SortDirection.Ascending.Direction] = SortDirection.Ascending,
[SortDirection.Descending.Direction] = SortDirection.Descending
};
return !sortDirections.ContainsKey(strValue) ?
SortDirection.Ascending :
sortDirections[strValue];
}
}
```

In my MVC project, my controller looks something like:
```csharp
public class JobsController : Controller
{
private readonly IMapper _mapper;
private readonly IJobFilterDataBuilder _filterBuilder;
private readonly IMediatr _mediatr;

public JobsController(
IMapper mapper,
IMediatr _mediatr;
IJobFilterDataBuilder filterBuilder)
{
_mapper = mapper;
_modelBuilder = modelBuilder;
_filterBuilder = filterBuilder;
}

public async Task Index(
GetJobListQuery? model,
CancellationToken cancellationToken)
{
model ??= new GetJobListQuery();
var result = await _mediatr.Send(model, cancellationToken);
var vm = _mapper.Map(result);

vm.JobTypesSet = await _filterBuilder.GetJobTypes(cancellationToken);
vm.Techs = await _filterBuilder.GetTechs(cancellationToken);
vm.JobStatesSet = await _filterBuilder.GetJobStates(cancellationToken);
vm.Managers = await _filterBuilder.GetManagers(cancellationToken);

return View(vm);
}
}
```

And the WebAPI implementation looks like the following:
```csharp
[ApiController]
[Route("Api/[controller]")]
public class JobsController : ControllerBase
{
private readonly IMediatr _mediatr;
public JobsController(IMediatr mediatr)
{
_mediatr = mediatr;
}

[HttpPost]
public async Task GetList(
[FromBody] GetJobListQuery? model,
CancellationToken cancellationToken)
{
model ??= new GetJobListQuery();
return Ok(await _mediatr.Send(model, cancellationToken));
}
}
```

However, with the default `System.Text.Json` library in use, the WebAPI version throws an exception as soon as the endpoint is called. The `TypeConverter` is never called by the modelbinder, and instead I receive a `400 Bad Request` error, and the body contains:

```json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-bea4fcf1bc2f25499e80d3e3b947237b-fd4f82e81e29f347-00",
"errors": {
"$.sortDirection": [
"The JSON value could not be converted to Application.Common.Models.SortDirection. Path: $.sortDirection | LineNumber: 0 | BytePositionInLine: 33."
]
}
}
```

If I replace the default `System.Text.Json` library with `Microsoft.AspNetCore.Mvc.NewtonsoftJson`, everything behaves as expected. Is there any explanation for this discrepancy?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.