Search code examples
c#asp.net-coreentity-framework-coreasp.net-core-webapi

I have an issue passing the correct data to my SQL database through my ASP.NET Core 9 Web API


I am new to ASP.NET so I'm having a hard time figuring out what the issue is.

I have a database with 2 tables, Items and Categories.

An Item contains Id, Name, Description, Quantity, Price, and CategoryId.

A Category contains Id and Name.

Here is the issue: when a request is made to create and item, for some reason, a new Category is created, then the Id of the new Category is being used as the CategoryId for the Item. That should not happen. I do not want a new Category to be created.

Here's an example in Postman:

Postman Screenshot

As you can see, the request body contains "categoryId": 2 and the return body contains "categoryId": 13

Here are screenshots of my database tables:

Items Table

Categories Table

As you can see, the Items table has 13 as the categoryId and the Categories table has a brand new row with an empty string under Name.

Please. I am trying to learn. My first post got a down vote within the first minute because it contained "a lot of code" and I had to take it down because it wasn't "focused" enough. I was just trying to be as detailed as possible to show how I'm routing my backend.

I'm not sure what part of my code I should put here since the backend has a lot of moving parts and I'm still learning ASP.NET so I'm not sure exactly is causing the bug but I'll try to show only what I think is relevant.

Here is also a link to the repo in case you would like to see it in its entirety.

I am showing less code - it's all relevant.

Models > Item.cs

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    [ForeignKey("CategoryId")]
    public int CategoryId { get; set; }
    public Category Category { get; set; } = new Category();
}

Dtos > Item > ItemDto.cs

public class ItemDto
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

Dtos > Item > CreateItemDto.cs

public class CreateItemDto
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

Repositories > ItemRepository.cs

public class ItemRepository : IItemRepository
{
    private readonly ApplicationDBContext _context;
    public ItemRepository(ApplicationDBContext context)
    {
        _context = context;
    }
    public async Task<Item> CreateAsync(Item itemModel)
    {
        await _context.Items.AddAsync(itemModel);
        await _context.SaveChangesAsync();
        return itemModel;
    }
}

Controllers > ItemRepository.cs

[Route("api/item")]
[ApiController]
public class ItemController : ControllerBase
{
    private readonly IItemRepository _itemRepo;

    public ItemController(IItemRepository itemRepo)
    {
        _itemRepo = itemRepo;
    }

    [HttpGet]
    [Route("get/{id}")]
    // This function will return a specific category from the database according to its Id
    public async Task<IActionResult> GetById([FromRoute] int id) {
        var item = await _itemRepo.GetByIdAsync(id);
        if(item == null) {
            return NotFound();
        }
        return Ok(item.ToItemDto());
    }

    [HttpPost]
    [Route("create")]
    public async Task<IActionResult> CreateAsync([FromBody] CreateItemDto itemDto) {
        if(!ModelState.IsValid) {
            return BadRequest(ModelState);
        }

        var itemModel = itemDto.ToItemFromCreate();
        await _itemRepo.CreateAsync(itemModel);
        return CreatedAtAction(nameof(GetById), new { id = itemModel.Id }, itemModel.ToItemDto());
    }
}

Solution

  • The issue stems from initializing the Category when creating an Item. Within entities do not initialize singular navigation properties, only collections:

    public Category Category { get; set; } // = new Category(); <- no *new*
    

    With nullable references, C# will whine at you about Category not being initialized, which likely resulted in adding the initialization to keep it happy. With entities this can be a nuisance since you don't want these initialized. Options to handle this are that you can use a null-able property marked as Required:

    [Required]
    public Category? Category { get; set; }
    

    Or simply tell C# to shut up about the warning: (can do individual fields, or for multiple fields just mark a constructor)

    #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
            /// <summary>
            /// Constructor used by EF.
            /// </summary>
            public Item()
            { }
    #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
    

    When you add the item, EF will add any related navigation properties, so while you might set a CategoryId to "2", your Item will have a new Default Category navigation property set (with an ID defaulted to "0") and it will treat that as a new record. If category has an Identity PK then you'll end up with a new Category record with the next new ID.

    Just setting the FK and ignoring the navigation property will work. Though the issue you might run into by just setting the FK is that after saving the new Item, if you want to populate a DTO to refresh your view, the item.Category will not be set. To create an Item and populate that Category:

    var itemModel = itemDto.ToItemFromCreate();
    var category = _itemRepo.GetCategoryById(itemDto.CategoryId);
    itemModel.Category = category;
    await _itemRepo.CreateAsync(itemModel);
    

    Where the "GetCategoryById" essentially does:

    return _context.Categories.Single(c => c.CategoryId == categoryId);