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:
As you can see, the request body contains "categoryId": 2
and the return body contains "categoryId": 13
Here are screenshots of my database tables:
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());
}
}
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);