I have 2 models, one for Poll
and for PollOption
. In the create action, I want to add 1 Poll
to the database and a list of PollOptions
as well. Each PollOption
has a foreign key PollId
.
When I submit the form, an error occurs because the foreign key is null
or 0
. I assume this is because the Id
of the poll is not ready before the Save function
is called thus it is 0
but how can I fix that? The error I get is:
The MERGE statement conflicted with the FOREIGN KEY constraint "FK_PollOptions_Polls_PollId". The conflict occurred in database "favpolls", table "dbo.Polls", column ‘Id’
Models:
public class Poll
{
[Key]
public int Id { get; set; }
[Required]
public string Question { get; set; } = "";
}
public class PollOption
{
[Key]
public int Id { get; set; }
[Required]
public string Option { get; set; } = "";
[Required]
public int PollId { get; set; }
[ForeignKey("PollId")]
[ValidateNever]
public Poll Poll { get; set; }
}
View model:
public class PollVM
{
public Poll Poll { get; set; }
[ValidateNever]
public List<PollOption> PollOptions { get; set; }
}
Action:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(PollVM pollVM)
{
_unitOfWork.Poll.Add(pollVM.Poll);
foreach (var option in pollVM.PollOptions)
{
_unitOfWork.PollOption.Add(option);
}
_unitOfWork.Save();
return View(pollVM);
}
View:
@model PollVM
<div class="index-container">
<div class="subcontainer">
<h1 class="title">Poll Creator</h1>
<h2 class="subtitle">Complete the form below to create your poll!</h2>
<form method="POST" asp-action="Create">
<input asp-for="Poll.Id" type="hidden" />
<div class="form-container">
<div class="form-input">
<label asp-for="Poll.Question">Question:</label>
<input asp-for="Poll.Question" type="text" placeholder="Type the poll question here" required>
</div>
<div class="form-input">
<div class="form-input form-options">
<label>Answer Options:</label>
<div class="form-option">
<input asp-for="PollOptions[0].Id" type="hidden" />
<input asp-for="PollOptions[0].PollId" type="hidden" />
<input asp-for="PollOptions[0].Option" type="text" placeholder="Option 1" required>
<img src="~/images/icons/cross.svg" alt="remove-option" width="30px" class="remove-option" onclick="removeOption(this)">
</div>
<div class="form-option">
<input asp-for="PollOptions[1].Id" type="hidden" />
<input asp-for="PollOptions[1].PollId" type="hidden" />
<input asp-for="PollOptions[1].Option" type="text" placeholder="Option 2" required>
<img src="~/images/icons/cross.svg" alt="remove-option" width="30px" class="remove-option" onclick="removeOption(this)">
</div>
</div>
<button type="button" class="button side-button icon-button">Add Option <img src="~/images/icons/plus.svg" alt="add-option" width="25px"></button>
</div>
<button type="submit" class="button submit-button">Create!</button>
</div>
</form>
</div>
</div>
@section Scripts {
<script src="~/js/answer-options.js" asp-append-version="true"></script>
}
If I remove the poll option "add" functions, the action works correctly and only adds 1 poll which is what I want but I want it to add the poll options as well.
2
Answers
I’m not sure what _unitOfWork is in your question. EF provides the Unit of Work pattern so there is usually no need to write your own on top of it.
Poll should contain the PollOptions collection in your model.
Then you would just add the pollOptions to the Poll and add that to your datacontext. EF will take care of the keys for you. Slightly simplistic example:
It would be better to change the structure of your vm while you are at it.
It’s generally better not to use domain objects in your VM, in order to maintain separation of layers, and to map one to the other. AutoMapper is useful for mapping between layers without writing a load of boilerplate. If you map PollViewModel to Poll then you wouldn’t need to manually set the pollOptions collection as I have in the example above.
If
_unitOfWork.PollOption
provides aDbSet
that does not need to be changed, but if you use methods that you yourself such asRepositories
, you should add the method with the following signature toUnitOfWork
AddRange(IEnumrable<PollOption> polOptions);
and change your action method as follows: