Home > Back-end >  Creating a complex object using Web Api in Blazor
Creating a complex object using Web Api in Blazor

Time:12-29

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);
}
  • Related