Use InterFace In action Parameter

Mrbanad 30 Reputation points
2023-05-23T13:10:06.1633333+00:00

hi

I Have Action Like

[httpPost]
Public ActionResult Create(IProductItem Item){

Bla Bla

}

My Classes Are...

public interface IProductItem : IBaseModel

{

public decimal Price {get; set:}

}

public class Good : IProductItem{

public string Id {get; set:}

public string count {get; set:}

public string BranchId {get; set:}

public decimal Price {get; set:}

}

public class Download: IProductItem{

public string Id {get; set:}

public string Path{get; set:}

public decimal Price {get; set:}

}

public class Service: IProductItem{

public string Id {get; set:}

public TimeSpan Time{get; set:}

public decimal Price {get; set:}

}

When I send Request With Swagger got Error Cannot Desentrilize Interface.
I Use Nowtonsoft As the default Json Converter And It cannot Find Any Property And Show Me an Error "Property 'Price' Cannot Be null ... "

ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,515 questions
ASP.NET API
ASP.NET API
ASP.NET: A set of technologies in the .NET Framework for building web applications and XML web services.API: A software intermediary that allows two applications to interact with each other.
331 questions
0 comments No comments
{count} votes

Accepted answer
  1. Zhi Lv - MSFT 32,146 Reputation points Microsoft Vendor
    2023-05-25T05:26:20.8833333+00:00

    Hi @Mrbanad

    Agree with Bruce, we cannot use an interface as a controller action parameter in Asp.net core Application.

    According to your code, I suggest you can try to change the interface to a class and use polymorphic model binding. Code like this:

        public class ProductItem
        {
            public string Type { get; set; } //use to specify the type: Good, DownLoad or Service
            public decimal Price { get; set; }
        }
        public class Good : ProductItem
        { 
            public string Id { get; set; } 
            public string count { get; set; } 
            public string BranchId { get; set; } 
            //public decimal Price { get; set; } 
        }
    
        public class Download : ProductItem
        { 
            public string Id { get; set; }
    
            public string Path { get; set; }
    
            //public decimal Price { get; set; }
    
        }
    
        public class Service : ProductItem
        { 
            public string Id { get; set; }
    
            public TimeSpan Time {  get; set;  }
    
            //public decimal Price {  get; set; }
    
        }
    

    Then, create a custom model binder to handle the transfer data.

        public class ProductItemModelBinderProvider : IModelBinderProvider
        {
            public IModelBinder GetBinder(ModelBinderProviderContext context)
            {
                if (context.Metadata.ModelType != typeof(ProductItem))
                {
                    return null;
                }
    
                var subclasses = new[] { typeof(Good), typeof(Download),typeof(Service) };
    
                var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
                foreach (var type in subclasses)
                {
                    var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
                    binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
                }
    
                return new ProduceItemModelBinder(binders);
            }
        }
    
        public class ProduceItemModelBinder : IModelBinder
        {
            private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;
    
            public ProduceItemModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
            {
                this.binders = binders;
            }
    
            public async Task BindModelAsync(ModelBindingContext bindingContext)
            {
                var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(ProductItem.Type));
                
                var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;
              
                IModelBinder modelBinder;
                ModelMetadata modelMetadata;
                if (modelTypeValue == "Good")
                {
                    (modelMetadata, modelBinder) = binders[typeof(Good)];
                }
                else if (modelTypeValue == "Download")
                {
                    (modelMetadata, modelBinder) = binders[typeof(Download)];
                }
                else if (modelTypeValue == "Service")
                {
                    (modelMetadata, modelBinder) = binders[typeof(Service)];
                }
                else
                {
                    bindingContext.Result = ModelBindingResult.Failed();
                    return;
                }
    
                var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
                    bindingContext.ActionContext,
                    bindingContext.ValueProvider,
                    modelMetadata,
                    bindingInfo: null,
                    bindingContext.ModelName);
    
                await modelBinder.BindModelAsync(newBindingContext);
                bindingContext.Result = newBindingContext.Result;
    
                if (newBindingContext.Result.IsModelSet)
                {
                    // Setting the ValidationState ensures properties on derived types are correctly 
                    bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
                    {
                        Metadata = modelMetadata,
                    };
                }
            }
        }
    
    

    After that, register the custom model binder:

    builder.Services.AddControllers(opt =>
    {
        opt.ModelBinderProviders.Insert(0, new ProductItemModelBinderProvider());
    });
    

    In the Controller, use the following code to receive the data:

        [Route("api/[controller]")]
        [ApiController]
        public class TodoController : ControllerBase
        {
            [HttpPost]
            public IActionResult Post(ProductItem item)
            {
                 
                return Ok(item);
            }
        }
    

    The result as below:

    image2

    Note: In the above demo, the data was transferred via the Form, if you want to transfer data from request body, you need to change the code in the ProduceItemModelBinder method, and use the StreamReader to read the request body and then base on the model type to convert the json string to related object.


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Dillion


1 additional answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 64,161 Reputation points
    2023-05-23T15:33:05.45+00:00

    action parameters can not be interfaces. as the action parameter binder needs to create an instance of the parameter to set its values, the parameter datatype must have a parameterless constructor.

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.