skip to Main Content

Im trying to do a post request from Reactjs to ASP.NET API, using axios. At first, I got an 400 Bad request error. But after I changed the DTO in the backend, it worked, but the data receive is empty or null (I think this is one of those Model binding errors).
This is my React.Js file:

const defaultImgPath = '/assets/ImgStarter.jpg';
const initialFieldValues = {
  productName: 'Fried spring rolls',
  productPrice: 50000,
  productStatus: 1,
  productDescription: 'A short description',
  imagePath: defaultImgPath,
  imgName: '',
  ProductImg: null
}

const ApiUploadImg = () => {
  const [input, setInput] = useState(initialFieldValues) 
  const handleAddProduct = (e) => {
    e.preventDefault();
    axios.post('http://localhost:5273/api/product', input)
    .then((response) => {
      console.log(response);
    })
    .catch((error) => {
      console.log(error.response);
    })

return (
    <div className='form-uload'>
      <form className='p-5' onSubmit={handleAddProduct}>
        <Input
          inputType="text"
          labelname="Product name"
          name="productName"
          setInput={setInput}
          input={input}
        />
        <Input
          inputType="number"
          labelname="Price"
          name="productPrice"
          setInput={setInput}
          input={input}
        />
        <Input
          inputType="text"
          labelname="Status (Is available?)"
          name="productStatus"
          setInput={setInput}
          input={input}
        />
        <Input
          inputType="text"
          labelname="Description"
          name="productDescription"
          setInput={setInput}
          input={input}
        />
        <Input
          inputType="file"
          labelname="Image"
          name="productImg"
          setInput={setInput}
          input={input}
          onChange={handlePreviewImg}
        />
        <div>
          <img
            src={input.imagePath}
            className='preview-img'
          />
        </div>
        <button type='submit' className='form-controls btn btn-primary'>Submit</button>
      </form>
    </div>
  )
  }

My Input.js component:

const Input = ({ setInput, input, labelname, name, inputType, onChange=null}) => {
    const [value, setValue] = useState('');

    const onChangeHanlder = (e) => {
        setInput({...input, [name]: e.target.value})
    }
    var id = {name};
    return (
        <Fragment>
            <label htmlFor={id} className='font-semibold text-white'>
                {labelname}:&nbsp;
            </label>
            <input
                id={id}
                type={inputType}
                name={name}
                className='rounded input_form form-control mt-2 w-100'
                // placeholder={placeholder} 
                onChange={onChange ? onChange : onChangeHanlder}
            />
        </Fragment>
    )
}

My api function:

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] CreateProductRequestDTO requestDTO)
        {
            if(!ModelState.IsValid) 
            {
                return BadRequest(ModelState);
            }
            var productModel = requestDTO.ToProductsFromCreateDTO();
            await _productRepo.CreateAsync(productModel);
            return CreatedAtAction(nameof(GetById), new {id = productModel.Id}, productModel.ToProductDTO());
        }

And the main character, the DTO that I mentioned above. I really thought this would be the last place an error could happen, cause I literally copy paste the "name" attribute from the ReactJs file mentioned above and create this DTO (Solution (1)). But it didn’t work. So I paste the attributes from class Products, but as I said, this method make data received empty (Solution (2))

    public class CreateProductRequestDTO
    {
        //Solution (1)
        // public string? ProductName {get; set; } = string.Empty;
        // public int ProductPrice {get; set; } 
        // public bool ProductStatus {get; set; }
        // public string? ProductDescription {get; set; } = string.Empty;

        //Solution (2)
        public string Name {get; set; } = string.Empty;
        public int Price {get; set; } 
        public bool Status {get; set; }
        public string Description {get; set; } = string.Empty;
    }

And in case you need it, the "input" object look exactly like "initialFieldValues"
Can someone explain it to me what is going on?

2

Answers


  1. Try using FromForm.

    [HttpPost]
        public async Task<IActionResult> Create([FromForm] CreateProductRequestDTO requestDTO)
        {
            if(!ModelState.IsValid) 
            {
                return BadRequest(ModelState);
            }
            var productModel = requestDTO.ToProductsFromCreateDTO();
            await _productRepo.CreateAsync(productModel);
            return CreatedAtAction(nameof(GetById), new {id = productModel.Id}, productModel.ToProductDTO());
        }
    

    If this doensn’t help, try give an example of the parsed call.

    Login or Signup to reply.
  2. ModelBinding is part of the lifecycle of the request of .net

    I am not sure if you are using .NetFramework or .net core, but the idea is the same, I will try to explain with .Net core

    Your Controller is receiving a Dto that in this case because it is a POST request you are explicitly saying to read or to bind the parameters from the Body.
    .Net core has a default binder that assigns all the values you are passing in your request based on a naming convention. For example, your Post request has this input which name is "productPrice" but in your DTO is Price

    <Input
              inputType="number"
              labelname="Price"
              name="productPrice"
              setInput={setInput}
              input={input}
    

    if you match the same naming convention it should work

    <Input
              inputType="number"
              labelname="Price"   //change the name here
              name="Price"
              setInput={setInput}
              input={input}
    

    Now, if you want to keep your react definition as it is and want to change the binding without changing your controller and your DTO, you can extend .Net core to register a model binder for you. I am going to update here only the Price so you can get the idea

    This is an example:

    public class YourDtoModelBinder : IModelBinder
    {
        public VehicleRequestDtoModelBinder()
        {
    
        }
    
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }
    
            CreateProductRequestDTO requestDto = null;
    
            var request = bindingContext.HttpContext.Request ?? throw new ArgumentNullException(nameof(bindingContext));
    
            //reading the value you passed
            var priceValue = bindingContext.ValueProvider.GetValue("productPrice").FirstValue;
            CreateProductRequestDTO  requestDto = null;
    
            try
            {
                requestDto = new CreateProductRequestDTO 
                {
                    Price = priceValue,
                    //bind the rest of the values here
                };
    
                bindingContext.Result = ModelBindingResult.Success(requestDto);
            }
            catch (Exception ex)
            {
                bindingContext.Result = ModelBindingResult.Failed();
            }
    
            return Task.CompletedTask;
        }
    }
    

    }

    Now you need to register the model binder and implement a modelbinder provider for your dto input and let .net core that everytime it sees your dto to use this model binder

    public class YourDtoModelBinder: IModelBinderProvider
    {
        /// <summary>
        /// Method that performs the request binding
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        public IModelBinder? GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            if (context.Metadata.ModelType == typeof(CreateProductRequestDTO))
            {
                return new BinderTypeModelBinder(typeof(YourDtoModelBinder));
            }
    
            return null;
        }
    }
    

    finally in your dependency injection on .net core, insert your model binder as part of the default list of modelbinders, that gives you the flexibility to work with your request and don’t change the contracts you have already, so you have 2 options, follow the default convention and change the UI and pass the correct names, or register a model binder with your custom logic.

    services.AddControllers()
        .AddMvcOptions(mvcOptions =>
        {
            mvcOptions.ModelBinderProviders.Insert(0, new VehicleRequestDtoModelBinderProvider());
        });
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search