I am trying to create a complex object using the Post method within my web api. However I'm struggling to do this as when I create a Board object I require it to have a Board.Company.Name which associates it with a company. However when I select an already existing company name and handle the valid submit a new company is created with the Board.Company.Name I have chosen. I then display the board I have created and it appears like no company is in fact associated with it. Below I have included the relevant code. This is my first project with C# and Blazor so let me know if I have left out anything important and I will include it.
Company Model
public class Company
{
[Key]
public Guid Id { get; set; }
[Required]
public string Name { get; set; }
public DateTime Founded { get; set; }
}
Board Model
public class Board
{
[Key]
public Guid Id { get; set; }
[ValidateComplexType]
public Company Company { get; set; } = new();
[Required]
public string Name { get; set; }
public string Description { get; set; }
public List<Ticket> Tickets { get; set; }
public Board()
{
}
}
Api POST Method
[HttpPost]
public async Task<ActionResult<Board>> PostBoard(Board board)
{
_context.Boards.Add(board);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBoard", new { id = board.Id }, board);
}
Create_Board
@page "/create_board"
@inject NavigationManager Navigation
@inject HttpClient Http
<div>
<button @onclick="GoToHome"></button>
<h3 >Create a board</h3>
</div>
<hr />
<EditForm Model="Board" OnValidSubmit="@HandleValidSubmit">
<ObjectGraphDataAnnotationsValidator />
<div >
<label for="Company" >Company</label>
<div >
<InputSelect id="Company" @bind-Value="Board.Company.Name">
<option value="" disabled selected>Company</option>
@foreach (var company in Companies)
{
<option>@company.Value.Name</option>
}
</InputSelect>
<ValidationMessage For="@(() => Board.Company.Name)" />
</div>
</div>
<div >
<label for="Name" >Name</label>
<div >
<InputText id="Name" placeholder="Name" @bind-Value="Board.Name" />
<ValidationMessage For="@(() => Board.Name)" />
</div>
</div>
<div >
<label for="Description" >Description</label>
<div >
<InputText id="Description" placeholder="Description" @bind-Value="Board.Description" />
<ValidationMessage For="@(() => Board.Description)" />
</div>
</div>
<button type="submit" >Submit</button>
</EditForm>
@code {
private void GoToHome()
{
Navigation.NavigateTo("/");
}
private Board Board { get; set; } = new Board();
private Dictionary<Guid, Company> Companies = new Dictionary<Guid, Company>();
protected override async Task OnInitializedAsync()
{
try
{
Companies = await Http.GetFromJsonAsync<Dictionary<Guid, Company>>("api/Companies");
}
catch (Exception)
{
Console.WriteLine("Exception occurred for GET companies");
}
}
private async void HandleValidSubmit()
{
try
{
var response = await Http.PostAsJsonAsync("/api/Boards", Board);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var board = JsonConvert.DeserializeObject<Board>(content);
Navigation.NavigateTo($"/read_board/{board.Id}");
}
catch (Exception)
{
Console.WriteLine("Exception occurred for POST board");
}
}
}
CodePudding user response:
I don't think you'll get any joy out of setting the name; even if the context is living long enough (it shouldn't; contexts should only live as long as a request to the API does) to see you using a company name it has previously downloaded it'll be seeing a Guid.Empty (the default) and (presumably you've told EF that it's database generated) that will make the context think the company is new with Name X
Instead, I think I'd have the entity follow the typical "have CompanyId be a member of the Board and set it there" route, rather than setting the name on a new related entity:
<InputSelect id="Company" @bind-Value="Board.CompanyId">
<option value="" disabled selected>Company</option>
@foreach (var company in Companies)
{
<option value="@company.Key">@company.Value.Name</option>
}
</InputSelect>
This should save, and EF will see the company id and wire up the related company.
If you're averse to this (adding a CompanyId entity to Board) you can adopt either:
- download that company by ID before you save, and assign it as the Company - you'll then be using a Company instance the change tracker has seen before and it will know how to wire up to the existing company rather than creating a new e.g.
<InputSelect id="Company" @bind-Value="Board.Company.Id">
<option value="" disabled selected>Company</option>
@foreach (var company in Companies)
{
<option value="@company.Key">@company.Value.Name</option>
}
</InputSelect>
[HttpPost]
public async Task<ActionResult<Board>> PostBoard(Board board)
{
board.Company = _context.Companies.Find(board.Company.Id); // download existing co with that ID
_context.Boards.Add(board);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBoard", new { id = board.Id }, board);
}
or
- look at tricking the change tracker/context into thinking it's already seen the new company you created with Id X. Personally I'm not a fan, but:
[HttpPost]
public async Task<ActionResult<Board>> PostBoard(Board board)
{
_context.Boards.Add(board);
_context.Entries(board.Company).State = EntityState.Unchanged; //don't try to save the Company
await _context.SaveChangesAsync();
return CreatedAtAction("GetBoard", new { id = board.Id }, board);
}