简体   繁体   中英

Polymorphic model binding for generic type parameter

I'm sending a partial object along with a file.

var formData = new FormData();
formData.append('model', JSON.stringify({ ... }));
formData.append('file', file, file.name)

// send formData to backend

My controller method is using Delta<T> from OData.

[HttpPatch("{id}")]
public async Task<IActionResult> Patch(
    [FromRoute] Guid id,
    [ModelBinder(BinderType = typeof(FormDataJsonBinder))] Delta<AbstractModel> model,
    IFormFile file = null
)

However I'm not sending Delta<AbstractModel> as AbstractModel is an abstract class. So I'm really sending Delta<DerivedModel> or Delta<AnotherDerivedModel> etc...

My problem is that ASP.NET keeps throwing saying that it cannot cast to Delta<DerivedModel>

System.InvalidCastException: Unable to cast object of type 'Microsoft.AspNet.OData.Delta`1[DerivedModel]' to type 'Microsoft.AspNet.OData.Delta`1[AbstractModel]'.
   at lambda_method308(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

Here's my FormDataJsonBinder

public class FormDataJsonBinder : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        // Fetch the value of the argument by name and set it to the model state
        string fieldName = bindingContext.FieldName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
        if (valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
        else bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);

        // Do nothing if the value is null or empty
        string value = valueProviderResult.FirstValue;
        if (string.IsNullOrEmpty(value)) return Task.CompletedTask;

        try
        {
            // Delta is a special case
            if (bindingContext.ModelType.IsAssignableTo(typeof(Delta)))
            {
                var jsonObject = JObject.Parse(value);
                // This extracts type T from Delta<T>
                var innerModelType = bindingContext.ModelType.GenericTypeArguments.First();
                if (jsonObject["@odata.type"] is not null)
                {
                    var odataType = jsonObject["@odata.type"].Value<string>();
                    innerModelType = Type.GetType(odataType);
                }
                var innerModel = JsonConvert.DeserializeObject(value, innerModelType);
                var deltaType = typeof(Delta<>).MakeGenericType(innerModelType);
                var delta = Activator.CreateInstance(deltaType) as IDelta;

                foreach (var property in jsonObject.Properties())
                {
                    delta.TrySetPropertyValue(property.Name, innerModel.GetType().GetProperty(property.Name)?.GetValue(innerModel));
                }

                bindingContext.Result = ModelBindingResult.Success(delta);
            }
            else
            {
                // Deserialize the provided value and set the binding result
                var result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
                bindingContext.Result = ModelBindingResult.Success(result);
            }
        }
        catch (JsonException)
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

I also tried using a IModelBinderProvider as recommended here , but that also just throws "cannot cast to Delta<DerivedModel> "

The solution was to create the Delta object the right way.

var delta = Activator.CreateInstance(bindingContext.ModelType, innerModelType) as IDelta;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM