3

I am attempting to modify data via WebAPI using a automapped resource model on my EFCore entity class. I have managed to get GET, GET(by id), DELETE and POST working fine using this method.

Now the problem I face is with an Automapper Serialization and deserialization of 'System.Type' error message I get when trying to do a PUT with this Resource. I managed to narrow this error down my ID field which is a primary key identity field in SQL Server. Omitting this ID in the resource model makes the PUT work.

However, I require the ID field for my other GETS and DELETE so cannot omit it from the resource model.

Here is my Entity:

  [Table("BlazorWebApiDemo")]
    public partial class BlazorEntity
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        [Column("First Name")]
        [StringLength(50)]
        public string FirstName { get; set; } = string.Empty;
        [Column("Last Name")]
        [StringLength(50)]
        public string LastName { get; set; } = string.Empty;
        [StringLength(50)]
        public string Address { get; set; } = string.Empty;
    }

Here is my API Resource Model:

    public class BlazorEntityModel
    {    
        public int Id { get; set; } // Commenting this out makes PUT work.
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Address { get; set; }
    }

Automapper Profile:

    public class BlazorEntityProfile : Profile
    {
        public BlazorEntityProfile()
        {
            this.CreateMap<BlazorEntity, BlazorEntityModel>()

                .ForMember(dest => dest.FirstName, opt => opt.NullSubstitute(""))
                .ForMember(dest => dest.LastName, opt => opt.NullSubstitute(""))
                .ForMember(dest => dest.Address, opt => opt.NullSubstitute(""))
                //.ForMember(src => src.Id, opts => opts.Ignore())
                .ReverseMap();
        }
    }

Controller PUT method:

        [HttpPut("{id:int}")]
        public async Task<ActionResult<BlazorEntityModel>> UpdateBlazorItem(int id, BlazorEntityModel blazorEntityModel)
        {
            try
            {
                var oldBlazorEntity = await blazorRepository.GetBlazorById(id);
                if (oldBlazorEntity == null) return NotFound($"LetItem with the ID: {id} not found");

                mapper.Map(blazorEntityModel, oldBlazorEntity);

                if(await blazorRepository.SaveChangesAsync())
                {
                    return mapper.Map<BlazorEntityModel>(oldBlazorEntity);
                }

            }
            catch (Exception e)
            {
                return StatusCode(StatusCodes.Status500InternalServerError,
                                  e);
            }

            return BadRequest(); //Error getting caught here.
        }

This is a screenshot of the error I get, in swagger: enter image description here

Any ideas?

Riku Das
  • 91
  • 1
  • 14

2 Answers2

1

Code looks fine, this issue could be on the client side; i.e. how you're serializing it from the Front end/Client while calling/sending to the backend API.

Double check thats you are using contentType:"application/json" & using JSON.stringify method to convert it to JSON string when you send it.

Look here and here similar issue


Update 2: In your blazor entity try adding a key attribute

public class BlazorEntityModel
{    
    [Key]  // add this to force it to recognize it as key
    public int Id { get; set; } // Commenting this out makes PUT work.
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Address { get; set; }
}

Update 3: Configure/override the serialization handling

Can you try to add the following in your global.asax

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;  
GlobalConfiguration.Configuration.Formatters.Remove(GlobalConfiguration.Configuration.Formatters.XmlFormatter);

If this works, let me know we may have to move to a DTO or VM so we trace and loops/circular refs.

Transformer
  • 6,963
  • 2
  • 26
  • 52
  • This is actually all happening before I serialize it. I haven't got a front end yet. Just the Data project (repos) and API project. This test is running in only swagger. – Riku Das May 27 '21 at 09:54
  • @RikuDas Try to add the `key` attribute on `ID` and let me know what happens – Transformer May 29 '21 at 00:48
  • if you look at my OP you'll see that I did decorate the entity ID field with the Key attribute. Unfortunately this did not make it work. – Riku Das May 30 '21 at 01:07
  • @RikuDas try the configuration option in your `global.asax` above. I am running out of ideas, we may have to decouple the objects to VMs. But I am very optimistic this may work. – Transformer May 30 '21 at 04:53
0

One way to handle that is to use two classes with inheritance.

public class MyObject {
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public string Address { get; set; }
}

public class CreatedVersionOfMyObject : MyObject  {
    public int Id { get; set; }
}

Then your controller can easily expose multiples endpoints with those entitities

    [Route("objects")]
    public class MyObjectControLler : Controller
    {
        [Route("{id}")]
        [HttpGet]
        [ProducesResponseType((int)HttpStatusCode.NotFound)]
        [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
        [ProducesResponseType(typeof(CreatedVersionOfMyObject), (int)HttpStatusCode.OK)]
        public Task<IActionResult> GetByIdAsync(Guid id)
        {
            // Do what you have to do to retrieve the object by id
        }

        [Route("{id}")]
        [HttpDelete]
        [ProducesResponseType((int)HttpStatusCode.NotFound)]
        [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
        [ProducesResponseType((int)HttpStatusCode.NoContent)]
        public Task<IActionResult> DeleteByidAsync(Guid id)
        {
            // Do what you have to do to delete the object by id
        }

        [Route("{id}")]
        [HttpPut]
        [ProducesResponseType((int)HttpStatusCode.NotFound)]
        [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
        [ProducesResponseType(typeof(MyObject), (int)HttpStatusCode.OK)]
        public Task<IActionResult> PutAsync([FromRoute]Guid id, [FromBody]MyObject theModifiedObject)
        {
            // Do what you have to do to retrieve the object by id
        }

        [Route("")]
        [HttpPost]
        [ProducesResponseType((int)HttpStatusCode.NotFound)]
        [ProducesResponseType((int)HttpStatusCode.InternalServerError)]
        [ProducesResponseType(typeof(CreatedVersionOfMyObject), (int)HttpStatusCode.Created)]
        public Task<IActionResult> PostAsync(MyObject theModifiedObject)
        {
            // Do what you have to do to create the object and return the version with the id
        }
    }

Finally, regarding the mapping you can also easily handle that (see original doc https://docs.automapper.org/en/stable/Mapping-inheritance.html)

CreateMap<BaseEntity, BaseDto>()
   .ForMember(dest => dest.SomeMember, opt => opt.MapFrom(src => src.OtherMember));

CreateMap<DerivedEntity, DerivedDto>()
    .IncludeBase<BaseEntity, BaseDto>();
j_freyre
  • 4,623
  • 2
  • 30
  • 47