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 class="btn btn-outline-secondary oi oi-arrow-left" #onclick="GoToHome"></button>
<h3 class="text-center">Create a board</h3>
</div>
<hr />
<EditForm Model="Board" OnValidSubmit="#HandleValidSubmit">
<ObjectGraphDataAnnotationsValidator />
<div class="form-group row">
<label for="Company" class="col-sm-2 col-form-label">Company</label>
<div class="col-sm-10">
<InputSelect id="Company" class="form-control" #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 class="form-group row">
<label for="Name" class="col-sm-2 col-form-label">Name</label>
<div class="col-sm-10">
<InputText id="Name" class="form-control" placeholder="Name" #bind-Value="Board.Name" />
<ValidationMessage For="#(() => Board.Name)" />
</div>
</div>
<div class="form-group row">
<label for="Description" class="col-sm-2 col-form-label">Description</label>
<div class="col-sm-10">
<InputText id="Description" class="form-control" placeholder="Description" #bind-Value="Board.Description" />
<ValidationMessage For="#(() => Board.Description)" />
</div>
</div>
<button type="submit" class="btn btn-primary">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");
}
}
}
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" class="form-control" #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" class="form-control" #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
Faced such peculiar problem. For a better understanding, I will try to describe in more detail.
I have two metod in ArticleController:
[HttpGet]
public async Task<IActionResult> Create()
{
var sources = await _sourceServices.GetSourceNameAndId();
var listSources = new List<SourceNameAndIdModel>();
foreach (var source in sources)
{
listSources.Add(_mapper.Map<SourceNameAndIdModel>(source));
}
var viewModel = new ArticleCreateViewModel()
{
SourceNameAndIdModels = listSources
};
return View(viewModel);
}
[HttpPost]
public async Task<IActionResult> Create(ArticleCreateViewModel viewModel)
{
await _articleService.CreateArticle(_mapper.Map<ArticleDTO>(viewModel));
return RedirectToAction("Index", "Article");
}
As you can see, in the Get-method, I get the names and ids of all Sources from the database via _sourceService in the form of IEnumerable :
public class SourceNameAndIdDTO
{
public Guid Id { get; set; }
public string Name { get; set; }
}
Next, I enumerate them in a foreach loop and add each SourceNameAndIdDTO object to the List listSources I created before:
public class SourceNameAndIdModel
{
public string Name { get; set; }
public Guid Id { get; set; }
}
Next, I create an instance of the ArticleCreateViewModel model, which I will use further in the View:
public class ArticleCreateViewModel
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Title { get; set; }
public string Description { get; set; }
public string Body { get; set; }
public Guid SourceId { get; set; }
public DateTime CreationDate { get; set; }
public List<SourceNameAndIdModel> SourceNameAndIdModels { get; set; }
}
And I assign to the field public List SourceNameAndIdModels { get; set; } List listSources values:
var viewModel = new ArticleCreateViewModel()
{
SourceNameAndIdModels = listSources
};
You can see this in the controller code I posted above. Next, I send the viewModel to the View.
Code of my View:
#model ArticleCreateViewModel
<div class="container">
<h2>Edit article</h2>
<div class="row gx-5">
<form asp-controller="Article" asp-action="Create" asp-antiforgery="true" method="post">
<div>
<input type="hidden" asp-for="Id" />
<div class="mb-3">
<label class="col-sm-2 col-form-label" asp-for="Title"></label>
<input class="form-control" type="text" asp-for="Title">
</div>
<div class="mb-3">
<label class="col-sm-2 col-form-label" asp-for="Description"></label>
<textarea class="form-control" asp-for="Description"></textarea>
</div>
<div class="mb-3">
<label class="col-sm-2 col-form-label" asp-for="Body"></label>
<textarea class="form-control" asp-for="Body"></textarea>
</div>
<div class="dropdown">
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Source
</a>
<ul class="dropdown-menu">
#foreach (var item in #Model.SourceNameAndIdModels)
{
<li><select class="dropdown-item" asp-for="SourceId" asp-items="#item.Id">#item.Name</select></li>
}
</ul>
</div>
<div class="mb-3">
<label class="col-sm-2 col-form-label" asp-for="CreationDate"></label>
<input type="datetime-local" class="form-control" asp-for="CreationDate">
</div>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
And finally, in this place of the code, in which I want to set the Source of the article I am creating, I have an error:
enter image description here
Can you please tell me how in my case to make friends with my code with dropdown on my request? What am I doing wrong?
Using asp-items in this way is incorrect.
The Select Tag Helper asp-items specifies the option elements
Details and example:
https://learn.microsoft.com/...netcore-6.0#the-select-tag-helper
I have a Blazor server project where we have a Game and it has related child entities such as Venue, Season and GameType.
public Game()
{
Results = new HashSet<Result>();
}
public int GameId { get; set; }
public int SeasonId { get; set; }
public int VenueId { get; set; }
public int GameTypeId { get; set; }
[Required]
public DateTime GameDate { get; set; } = DateTime.Today;
public int BuyIn { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
public virtual Season Season { get; set; }
public virtual Venue Venue { get; set; }
public virtual GameType GameType { get; set; }
public virtual ICollection<Result> Results { get; set; }
I have a repo that is injected into my page to handle the update:
...
public async Task UpdateGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
context.Games.Update(game);
await context.SaveChangesAsync();
}
}
...
#page "/Settings/Games/EditGame/{GameId:int}"
#using SuttonPokerBlazor.Components
#using SuttonPokerBlazor.Models
#using SuttonPokerBlazor.Repositories.Interfaces
#inject IGameRepository gamesRepository
#inject NavigationManager NavigationManager
#inject ISeasonRepository seasonsRepository
#inject IVenueRepository venueRepository
#inject IGameTypeRepository gameTypeRepository
#if (Game != null)
{
<h3>Add new game</h3>
<EditForm Model="#Game" OnValidSubmit="Save">
<DataAnnotationsValidator />
<div class="mb-3">
<label for="Season" class="form-label">Season</label>
<div class="col-md-4">
<InputSelect #bind-Value="#Game.SeasonId" class="form-control">
#foreach (Season season in seasons)
{
<option value="#season.SeasonId">#season.SeasonDescription</option>
}
</InputSelect>
</div>
<ValidationMessage For="#(() => Game.SeasonId)" />
</div>
<div class="mb-3">
<label for="Season" class="form-label">Venue</label>
<div class="col-md-4">
<InputSelect #bind-Value="#Game.VenueId" class="form-control">
#foreach (Venue venue in venues)
{
<option value="#venue.VenueId">#venue.VenueName</option>
}
</InputSelect>
</div>
<ValidationMessage For="#(() => Game.VenueId)" />
</div>
<div class="mb-3">
<label for="Season" class="form-label">Game Type</label>
<div class="col-md-4">
<InputSelect #bind-Value="#Game.GameTypeId" class="form-control">
#foreach (GameType gameType in gameTypes)
{
<option value="#gameType.GameTypeId">#gameType.GameTypeDescription</option>
}
</InputSelect>
</div>
<ValidationMessage For="#(() => Game.GameTypeId)" />
</div>
<div class="mb-3">
<label for="GameDate" class="form-label">Game Date</label>
<div class="col-md-4">
<InputDate class="form-control" #bind-Value="Game.GameDate" />
</div>
<ValidationMessage For="#(() => Game.GameDate)" />
</div>
<div class="mb-3">
<label for="BuyIn" class="form-label">Buy In</label>
<div class="col-md-4">
<InputNumber class="form-control" #bind-Value="Game.BuyIn" />
</div>
<ValidationMessage For="#(() => Game.BuyIn)" />
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" #onclick="Cancel">Back</button>
<button type="button" class="btn btn-danger" #onclick="Delete">Delete</button>
</div>
</EditForm>
<Confirm #ref="DeleteConfirmation" ConfirmationChanged="ConfirmDelete"
ConfirmationMessage=#($"Are you sure you want to delete {Game.GameDate} - {Game.GameType.GameTypeDescription}?")>
</Confirm>
}
else
{
<h3>Not found</h3>
}
#code {
[Parameter]
public int GameId { get; set; }
public Game Game { get; set; }
ConfirmBase DeleteConfirmation { get; set; }
List<Season> seasons { get; set; }
List<Venue> venues { get; set; }
List<GameType> gameTypes { get; set; }
protected override async Task OnInitializedAsync()
{
Game = await gamesRepository.GetGame(GameId);
seasons = await seasonsRepository.GetSeasons();
venues = await venueRepository.GetVenues();
gameTypes = await gameTypeRepository.GetGameTypes();
}
private async Task Save()
{
await gamesRepository.UpdateGame(Game);
}
private void Cancel()
{
NavigationManager.NavigateTo("/Settings/Games/");
}
private void Delete()
{
DeleteConfirmation.Show();
}
private async void ConfirmDelete(bool deleteConfirmed)
{
if (deleteConfirmed)
{
await gamesRepository.DeleteGame(Game);
NavigationManager.NavigateTo("/Settings/Games/");
}
}
}
However, when the Game entity is updated it sets the Id of Season, GameType and/or Venue back to what they were before the update occurred.
For example:
Pre SaveChangesAsync():
Post SaveChangesAsync():
SQL produced:
In the SQL above, my assumption was that I would only see a update request to my Game entity. Why is it making updates to the other related tables and then why is it reverting anything that was changed back to what it was pre Save?
Any other changes to things like dates or strings is persisted as expected. It just seems that where I've used a drop down <InputSelect> this effect is taking place.
Update:
This is an updated version of my repo that seems to work but
Caius Jard was asking why I was doing what had fixed my issue. I'm happy to correct something if what I've done is incorrect for some reason:
public class GameRepository : IGameRepository
{
private readonly IDbContextFactory<SuttonPokerDbContext> _suttonPokerDbContext;
public GameRepository(IDbContextFactory<SuttonPokerDbContext> suttonPokerDbContext)
{
_suttonPokerDbContext = suttonPokerDbContext;
}
public async Task<Game> GetGame(int GameId)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
return await context.Games.Include(q => q.Results).Include(q => q.GameType).Include(q => q.Venue).Include(q => q.Season).FirstOrDefaultAsync(q => q.GameId == GameId); ;
}
}
public async Task<List<Game>> GetGames()
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
return await context.Games.Include(q => q.GameType).Include(q => q.Venue).Include(q => q.Season).OrderByDescending(q => q.GameDate).ToListAsync();
}
}
public async Task<Game> AddGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
context.Games.Add(game);
await context.SaveChangesAsync();
return game;
}
}
public async Task<Game> UpdateGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
// First we need to get the game from the database as we need to see if it's been modified already
Game dbGame = await context.Games.Include(q => q.Results).Include(q => q.GameType).Include(q => q.Venue).Include(q => q.Season).FirstOrDefaultAsync(q => q.GameId == game.GameId);
// Compare the byte arrays
if (!dbGame.RowVersion.SequenceEqual(game.RowVersion))
{
game = dbGame;
return game;
}
else
{
// We have to detach the dbGame version otherwise we get a conflict of tracked games.
context.Entry(dbGame).State = EntityState.Detached;
context.Entry(game).State = EntityState.Modified;
await context.SaveChangesAsync();
return game;
}
}
}
public async Task DeleteGame(Game game)
{
using (var context = _suttonPokerDbContext.CreateDbContext())
{
context.Remove(game);
await context.SaveChangesAsync();
}
}
}
need your thoughts on how the following can be implemented.
I need to process on the OnPost a list of <string,file> objects and do some business logic after, but at this stage, I am unsure of how to implement it either using RazorPages logic or what was usually done with MVC.
At this stage what I can't do is to get on OnPostAsync the picked values on selectedCompany and inputFile, which I was expecting to come from the formData.
Any thoughts? TY
View
(...)
<form method="post" enctype="multipart/form-data">
<div class="border p-3 mt-4">
<table class="table" id="filesToProcess">
<tr>
<td>
<div class="mb-3">
<select>
<option value="" name="selectedCompany">Pick a company ...</option>
#foreach (var company in Model.Companies)
{
<option value="#company.Id">#company.Name</option>
}
</select>
</div>
</td>
<td>
<div class="mb-3">
<div>
<input type="file" name="inputFile" />
</div>
</div>
<td>
</tr>
</table>
</div>
<button type="submit" class="btn btn-primary" style="width:150px">Calculate</button>
</form>
(...)
ViewModel
public class CalculatorModel : PageModel
{
private IHostingEnvironment _environment;
private ICompaniesService _companyService;
private IIndicatorsService _indicatorsService;
//To be presented on the front-end
public List<CompanyDto> Companies { get; set; }
//The initial idea would be that one row on the table of the front-end corresponds to one instance of IndicatorsRequest
[BindProperty]
public List<IndicatorsRequest> IndicatorsRequests { get; set; }
public class IndicatorsRequest
{
public Guid CompanyGuid { get; set; }
public IFormFile File { get; set; }
public List<IndicatorDto> CalculatedIndicators { get; set; }
}
public CalculatorModel(IHostingEnvironment environment, ICompaniesService companyService, IIndicatorsService indicatorsService)
{
_environment = environment;
_companyService = companyService;
_indicatorsService = indicatorsService;
}
public async Task OnGet()
{
Companies = await this._companyService.GetCompanies();
}
public async Task OnPostAsync(IFormCollection formData)
{
try
{
var selectedCompanies = formData.Where(f => f.Key.Contains("selectedCompany")).ToList();
var inputFiles = formData.Where(f => f.Key.Contains("inputFile")).ToList();
//Do some business logic with provided companies and files;
}
catch (Exception ex)
{
throw ex;
}
}
}
Solution - https://www.learnrazorpages.com/razor-pages/model-binding#binding-complex-collections
View
The '0' on [0].CompanyGuid and [0].File has obviously to be an auto-generated sequencial number.
<td>
<div class="mb-3">
<select name="[0].CompanyGuid"> <<<<<<<<<<<<<<<
<option value="">Pick a company ...</option>
#foreach (var company in Model.Companies)
{
<option value="#company.Id">#company.Name</option>
}
</select>
</div>
</td>
<td>
<div class="mb-3">
<div>
<input type="file" name="[0].File" /> <<<<<<<<<<<<<
</div>
</div>
<td>
ViewModel
public async Task OnPostAsync(List<IndicatorsRequest> requests)
{
Console.WriteLine(requests.ElementAt(0).CompanyGuid);
}
public class IndicatorsRequest
{
public Guid CompanyGuid { get; set; }
public IFormFile File { get; set; }
}
I am trying to add data to the database. I experimenting with Blazor and .NET core:
This is my code in the controller:
[Route("AddCarBlazor")]
[HttpPost]
public IActionResult PostBlazor(Car car)
{
if (car.CarId == 0)
{
// New
car.Created = DateTime.Now;
_context.Cars.Add(car);
_context.SaveChanges();
return Ok();
}
else
{
// Update
var c = _context.Cars.First(e => e.CarId == car.CarId);
c.Brand = car.Brand;
c.Color = car.Color;
c.Model = car.Model;
c.LastChange = DateTime.Now;
c.TopSpeed = car.TopSpeed;
_context.SaveChanges();
return Ok();
}
}
My car model looks like this:
public class Car
{
[Key]
public long CarId { get; set; }
public string Created { get; set; }
public string LastChange { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
public string Color { get; set; }
public long TopSpeed { get; set; }
}
I call this method like this:
private async Task AddCar()
{
await Http.PostJsonAsync(baseUrl + "/AddCarBlazor/", carobject);
await Refresh();
}
When I fill in the form and press add button the car object is always null
This is my form with the databinding:
<form>
<div class="row">
<div class="form-group col-sm-3">
<label>Brand</label>
<input input type="text" #bind="#carobject.Brand" class="form-control" placeholder="Enter brand" />
</div>
</div>
<div class="row">
<div class="form-group col-sm-3">
<label>Model</label>
<input type="text" #bind="#carobject.Model" class="form-control" placeholder="Enter model" />
</div>
</div>
<div class="row">
<div class="form-group col-sm-3">
<label>Color</label>
<input type="text" #bind="#carobject.Color" class="form-control" placeholder="Enter color" />
</div>
</div>
<div class="row">
<div class="form-group col-sm-3">
<label>TopSpeed</label>
<input type="number" #bind="#carobject.TopSpeed" class="form-control" placeholder="Enter speed" />
</div>
</div>
<div class="btn-group mr-2">
<button class="btn btn-danger mr-1" onclick=#AddCar>Save changes</button>
</div>
</form>
I have put a breakpoint on the addCar method. I get the values from the fields but when it goes to the controller it becomes null.
I have following this tutorial:
https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-3.0
How can I save the values from the fields and send it to the database?
I test a demo which works well, you could refer to my code below:
1.Car.cs (namespace Blazor.Models)
public class Car
{
public long CarId { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
}
2. AddCar.razor
#page "/car"
#using System.Net.Http
#inject HttpClient Http
#using Blazor.Models
<Editform Model="carobject">
<div class="row">
<div class="form-group col-sm-3">
<label>Brand</label>
<input #bind="#carobject.Brand" class="form-control" placeholder="Enter brand" />
</div>
</div>
<div class="row">
<div class="form-group col-sm-3">
<label>Model</label>
<input #bind="#carobject.Model" class="form-control" placeholder="Enter model" />
</div>
</div>
<div class="btn-group mr-2">
<button class="btn btn-danger mr-1" onclick="#AddCar">Save changes</button>
</div>
</Editform>
#functions {
[Parameter]
private Car carobject { get; set; } = new Car();
private async Task AddCar()
{
await Http.PostJsonAsync(baseUrl + "/AddCarBlazor/", carobject);
//await Refresh();
}
}
3.Web API CORS configuration:
app.UseCors(corsbuilder => {
corsbuilder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
});
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
4.action:
[Route("AddCarBlazor")]
[HttpPost]
public IActionResult PostBlazor([FromBody]Car car)
After a weekend of research I have the solution!
I have changed my method in CarService.cs like this:
public async Task AddCar(Car car)
{
var client = new HttpClient { BaseAddress = new Uri("https://localhost:44369/api/car/") };
await client.SendJsonAsync(HttpMethod.Post, "AddCar", car);
}
Then I call this method in my razor page like this:
async Task AddCar()
{
await CarService.AddCar(car);
car = new CarService.Car();
await LoadCarData();
}
I also made a new object of the service like this:
CarService.Car car = new CarService.Car();
And I moved the model of Car.cs into CarService.cs
I'm using Asp .Net Core Razor Pages and i scaffolded a model using CRUD and generated me pages for create, edit, delete etc...
Problem is, all of them work except for edit.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(Utilizador).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!UtilizadorExists(Utilizador.UserID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
whenever it hits save changes it gives exception of trying to insert
SqlException: Cannot insert duplicate key row in object
'dbo.xColaboradores' with unique index 'IX_xColaboradores_UserID'. The
duplicate key value is (8). The statement has been terminated.
here is the models
public class Colaborador
{
[Key]
[ForeignKey("Utilizador")]
public int ColaboradorID { get; set; }
public string Nome { get; set; }
public string Email { get; set; }
public int UserID { get; set; }
public virtual Utilizador Utilizador { get; set; }
public int DepartamentoID { get; set; }
public virtual Departamento Departamento { get; set; }
}
public class Utilizador
{
[Key]
[ForeignKey("Colaborador")]
public int UserID { get; set; }
public string Username { get; set; }
public bool IsActivo { get; set; }
public DateTime? UltimoAcesso { get; set; }
public virtual Colaborador Colaborador { get; set; }
}
I tried attaching the related entities but it gives another exception
InvalidOperationException: The property 'ColaboradorID' on entity type
'Colaborador' has a temporary value while attempting to change the
entity's state to 'Unchanged'. Either set a permanent value explicitly
or ensure that the database is configured to generate values for this
property.
EDIT
I'm still not surely how this works on entity framework but it seems that it gets the object directly from the page that is binded.
I noticed that some properties had no values like the IDs of the related entity. they were at 0 hence why it was trying to insert. To fix this i had to put the primary and foreign Ids in a hidden input or it will try to insert them and gives exception duplicate
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Utilizador.UserID" />
<input type="hidden" asp-for="Utilizador.Colaborador.ColaboradorID" />
<input type="hidden" asp-for="Utilizador.Colaborador.UserID" />
<div class="form-group">
<label asp-for="Utilizador.Username" class="control-label"></label>
<strong class="text-danger">*</strong>
<input asp-for="Utilizador.Username" class="form-control form-control-sm" />
<span asp-validation-for="Utilizador.Username" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Utilizador.Colaborador.Nome" class="control-label"></label>
<strong class="text-danger">*</strong>
<input asp-for="Utilizador.Colaborador.Nome" class="form-control form-control-sm" />
<span asp-validation-for="Utilizador.Colaborador.Nome" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Utilizador.Colaborador.Email" class="control-label"></label>
<strong class="text-danger">*</strong>
<input asp-for="Utilizador.Colaborador.Email" class="form-control form-control-sm" />
<span asp-validation-for="Utilizador.Colaborador.Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Utilizador.Colaborador.Departamento.Designacao" class="control-label"></label>
<strong class="text-danger">*</strong>
<select asp-for="Utilizador.Colaborador.DepartamentoID" asp-items="Model.DepartamentosList" class="form-control form-control-sm">
<option value="">Selecione um departamento</option>
</select>
<span asp-validation-for="Utilizador.Colaborador.DepartamentoID" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Guardar" class="btn btn-block text-white text-uppercase text-bold" style="background-color:#124475; box-shadow:0 5px 5px 0 rgba(0, 0, 0, 0.2), 0 8px 5px 0 rgba(0, 0, 0, 0.19);" />
</div>
</form>
and on back end
_context.Attach(Utilizador).State = EntityState.Modified;
_context.Attach(Utilizador.Colaborador).State = EntityState.Modified;
with this i can change the related entity and it is updated normally.
Not sure if this is the best practice cause i tried changing the value on browser with dev tools and this can be easily hacked or if there is another way that it gets these values without storing them on input hidden but i noticed the generated scaffolded item pages was already storing the primary key id on a hidden input.