SaveChangesAsync fails with assigned users to object AspNetUsers duplicate key violation - asp.net-core

I am trying to create a Project item where a project can have multiple users. You assign the user by typing their name and pressing the button; the controller will find the user and return the user if they exist. They are then added to the List which is then assigned to the project on project creation submit. When the POST is engaged for the project the SaveChangesAsync() produces a http response 500 with the error:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
---> Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_AspNetUsers'. Cannot insert duplicate key in object 'dbo.AspNetUsers'. The duplicate key value is (fa2f3cdf-6147-4973-9c1e-988a21408610).
The statement has been terminated.
I cannot seem to figure out why this might be happening, I feel the form is being submitted twice or the Editform is incorrectly being submitted via the two buttons. I very new to Blazor and EF so I would appreciate any insight into what might be causing this and how I might go about resolving this issue. Thanks in advance
Create razor page
<EditForm Model="#projectItem" OnValidSubmit="#CreateNewProject">
<DataAnnotationsValidator />
<div class="form-group">
<label class="custom-control-label"> Name </label>
<InputText id="title" #bind-Value="projectItem.ProjectName" class="form-control" />
<InputText id="title" #bind-Value="projectItem.ProjectDescription" class="form-control" />
<ValidationMessage For="#(() => projectItem.ProjectName)" />
<button type="submit" class="btn btn-primary">Create Project</button>
</div>
<ValidationSummary />
</EditForm>
<EditForm Model="#AssignUser" OnValidSubmit="#AddUserToProject">
<DataAnnotationsValidator />
<div class="form-group">
<label class="custom-control-label"> Assign users </label>
<InputText id="title" type="email" #bind-Value="#AssignUser.UserName" class="form-control" />
<button type="submit" class="btn btn-primary">Add User</button>
</div>
</EditForm>
<p> Currently assigned users </p>
#if (ListOfAllUsers.Count() == 0)
{
<p> No users assigned yet</p>
}
else
{
#for(int i = 0; i < ListOfAllUsers.Count(); i++)
{
<li> #ListOfAllUsers[i].UserName </li>
}
}
private Project projectItem { get; set; } = new Project();
private ApplicationUser AssignUser { get; set; } = new ApplicationUser();
private List<ApplicationUser> ListOfAllUsers { get; set; } = new List<ApplicationUser>();
//submit and create project with the assigned users
private async void CreateNewProject()
{
projectItem.AssignedUsersToProject = ListOfAllUsers;
try
{
var response = await Http.PostAsJsonAsync("Projects", projectItem);
var content = await response.Content.ReadAsStringAsync();
var project = JsonConvert.DeserializeObject<Project>(content);
if(response.IsSuccessStatusCode)
Navigation.NavigateTo($"/projects");
}
catch (Exception e)
{
}
}
// find and add user
private async void AddUserToProject()
{
try
{
var response = await Http.PostAsJsonAsync($"Projects/{AssignUser.UserName}", AssignUser);
var content = await response.Content.ReadAsStringAsync();
var userobject = JsonConvert.DeserializeObject<ApplicationUser>(content);
if (response.IsSuccessStatusCode)
{
if (!ListOfAllUsers.Any(n => n.UserName.Equals(userobject.UserName)))
{
ListOfAllUsers.Add(userobject);
} else
{
AssignUser.UserName = "This user is already added to the project.";
}
} else if (!response.IsSuccessStatusCode)
{
AssignUser.UserName = "User not found, try again";
}
}
catch (Exception e)
{
}
}
Controller: Create Project
[HttpPost]
public async Task<ActionResult<Project>> PostProject(Project project)
{
_context.Projects.Add(project);
await _context.SaveChangesAsync(); // error occurs when reaching this point
return CreatedAtAction("GetProject", new { id = project.ProjectId }, project);
}
Controller: find and return user
[HttpPost("{userEmail}")]
public async Task<ActionResult<ApplicationUser>> AddUserToProject(string userEmail)
{
try
{
// Find user
ApplicationUser userValid = _userManager.Users.Where
(s => s.Email == userEmail).First();
return userValid;
}
catch (InvalidOperationException)
{
return BadRequest();
}
}
Models
public class Project
{
[Key]
public Guid ProjectId { get; set; }
[Required]
public string ProjectName { get; set; }
[JsonIgnore]
public ICollection<ApplicationUser> AssignedUsersToProject { get; set; }
public Company assignedCompanyForProject { get; set; }
}
public class ApplicationUser : IdentityUser
{
public string firstName { get; set; }
public string lastName { get; set; }
[JsonIgnore]
public ICollection<Project> Projects { get; set; }
}
Form image
You type the name of the user you want to add to the project and click "Add User" which will find and return the user if they are a user that exists (This does not create the project yet only finds user). This user is then added to the ListOfAllUsers list which is then looped through to display all the current users assigned. Once a project title and description is given alongside the relevant users the "Create Project" button should be pressed which will create the whole project with the assigned users which have been determined. This is what is submitted to the database to add a new Project item
Suggested changes
Implementing EntityState.Unchanged;
foreach(var x in project.AssignedUsersToProject)
{
_context.Entry(x).State = EntityState.Unchanged;
}
_context.Projects.Add(project);
Causing following error:
ystem.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32

Implementing the following in the controller when creating the project solved the issue.
foreach(var x in project.AssignedUsersToProject)
{
_context.Entry(x).State = EntityState.Unchanged;
}
_context.Projects.Add(project);

Related

IValidationAttributeAdapterProvider is called only for EmailAddressAttribute

What I was doing with ASP.NET MVC 5
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MaxLengthAttribute), typeof(MyMaxLengthAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute), typeof(MyRequiredAttributeAdapter));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MinLengthAttribute), typeof(MyMinLengthAttribute));
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAddressAttribute), typeof(MyEmailAddressAttributeAdapter));
Now I'm migrating it to ASP.NET core 6
We can't use DataAnnotationsModelValidatorProvider anymore so I'm trying to use IValidationAttributeAdapterProvider, which doesn't work properly for me.
My codes
My IValidationAttributeAdapterProvider is below.
public class MyValidationAttributeAdapterProvider : ValidationAttributeAdapterProvider, IValidationAttributeAdapterProvider
{
IAttributeAdapter? IValidationAttributeAdapterProvider.GetAttributeAdapter(
ValidationAttribute attribute,
IStringLocalizer? stringLocalizer)
{
return attribute switch
{
EmailAddressAttribute => new MyEmailAddressAttributeAdapter((EmailAddressAttribute)attribute, stringLocalizer),
MaxLengthAttribute => new MyMaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer),
MinLengthAttribute => new MyMinLengthAttribute((MinLengthAttribute)attribute, stringLocalizer),
RequiredAttribute => new MyRequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer),
_ => base.GetAttributeAdapter(attribute, stringLocalizer),
};
}
}
My model class is below.
public class LogInRequestDTO
{
[Required]
[EmailAddress]
[MaxLength(FieldLengths.Max.User.Mail)]
[Display(Name = "mail")]
public string? Mail { get; set; }
[Required]
[MinLengthAttribute(FieldLengths.Min.User.Password)]
[DataType(DataType.Password)]
[Display(Name = "password")]
public string? Password { get; set; }
}
And in my Program.cs, I do like below.
builder.Services.AddControllersWithViews()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Resources));
});
builder.Services.AddSingleton<IValidationAttributeAdapterProvider, MyValidationAttributeAdapterProvider>();
What happed to me
I expect GetAttributeAdapter is called for each attribute like EmailAddressAttribute, MaxLengthAttribute, etc.
But it's called only once with EmailAddressAttribute.
So, all other validation results are not customized by my adaptors.
If I remove [EmailAddress] from the model class, GetAttributeAdapter is never called.
Am I missing something?
Added on 2022/05/24
What I want to do
I want to customize all the validation error message.
I don't want to customize for one by one at the place I use [EmailAddress] for example.
I need the server side validation only. I don't need the client side validation.
Reproducible project
I created the minimum sample project which can reproduce the problem.
https://github.com/KuniyoshiKamimura/IValidationAttributeAdapterProviderSample
Open the solution with Visual Studio 2022(17.2.1).
Set the breakpoint on MyValidationAttributeAdapterProvider.
Run the project.
Input something to the textbox on the browser and submit it.
The breakpoint hits only once with EmailAddressAttribute attribute.
The browser shows the customized message for email and default message for all other validations.
Below is a work demo, you can refer to it.
In all AttributeAdapter, change your code like below.
public class MyEmailAddressAttributeAdapter : AttributeAdapterBase<EmailAddressAttribute>
{
// This is called as expected.
public MyEmailAddressAttributeAdapter(EmailAddressAttribute attribute, IStringLocalizer? stringLocalizer)
: base(attribute, stringLocalizer)
{
//attribute.ErrorMessageResourceType = typeof(Resources);
//attribute.ErrorMessageResourceName = "ValidationMessageForEmailAddress";
//attribute.ErrorMessage = null;
}
public override void AddValidation(ClientModelValidationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-must-be-true", GetErrorMessage(context));
}
// This is called as expected.
// And I can see the message "Input the valid mail address.".
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
In homecontroller:
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index([FromForm][Bind("Test")] SampleDTO dto)
{
return View();
}
Index view:
#model IV2.Models.SampleDTO
#{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<h4>SampleDTO</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Test" class="control-label"></label>
<input asp-for="Test" class="form-control" />
<span asp-validation-for="Test" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Result1:
Result2:
I found the solution.
What I have to use is not ValidationAttributeAdapterProvider but IValidationMetadataProvider.
This article describes the usage in detail.
Note that some attributes including EmailAddressAttribute have to be treated in special way as describe here because they have default non-null ErrorMessage.
I confirmed for EmailAddressAttribute and some other attributes.
Also, there's the related article here.

Razor Pages - Return Error on Duplicate Name

I'm working on a Razor Pages form that takes in a string to create a new customer in a SQL Server Database. I want to make it work so that if the string that is the customer already exists, a prompt comes up that says "This Customer Already Exists". Just to be safe for data integrity.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
int customerCheck = -1; //No Customer ID is -1
try
{
using (var context = new DataWarehouseContext())
{
customerCheck = context.Customer //Tries to grab a Customer with this name
.Where(a => a.Name == Customer.name)
.Select(b => b.CustomerId)
.FirstOrDefault();
}
}
catch (Exception)
{
}
if(customerCheck == -1)
{
_context.Customer.Add(Customer);
await _context.SaveChangesAsync();
return RedirectToPage("/Customer/List");
}
else
{
return Page();
}
}
This is the code I have so far in my backend. What happens is that when a user tries to create a new customer, the backend of the page tries to see if it can grab a customer ID that correlates to this name. If it can, then the value of customerCheck is not -1, therefore some error should get printed out.
I don't know what methods can be used to do this, so any help would be great!
I found a solution, and it wasn't hard to implement. When a duplicate customer was found in the backend, I create a ModelState.AddModelError object and fill it with a key and a description of the error. Next, in the frontend, I put it within an H3 tag to print it out like so:
Backend OnPost() Code
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
int customerCheck = 0; //No Customer ID is 0
try
{
using (var context = new DataWarehouseContext())
{
customerCheck = context.Customer //Tries to grab a Customer with this name
.Where(a => a.Name == Customer.name)
.Select(b => b.CustomerId)
.FirstOrDefault();
}
}
catch (Exception)
{
}
if(customerCheck == 0)
{
_context.Customer.Add(Customer);
await _context.SaveChangesAsync();
return RedirectToPage("/Customer/List");
}
else
{
ModelState.AddModelError("DuplicateCustomer", "This Customer Already Exists");
return Page();
}
}
So on the frontend, it gets implemented like this:
<h3 align="center" style="color:yellowgreen">#Html.ValidationMessage("DuplicateCustomer")</h3>
When return Page(); is hit, the page is reloaded and the DuplicateCustomer Error appears.
At first, glad to hear you have found a solution.
Besides, I think you could also use the Remote Validation to check whether the Customer is exist or not. Check the following sample code:
Remote validation in ASP.NET (Core) relies on Unobtrusive AJAX, so you will need to install that first. The easiest way to do this is via LibMan. Right click on the lib folder in wwwroot, choose Add ยป Client-side Library, and then choose jsdelivr as the source, and type in jquery-ajax-unobtrusive, click the "Install" button to install the package.
In the CreateCustomer.cshtml.cs page, add a Email property and use the PageRemote attribute, then, add a handler method to perform the validation.
public class CreateCustomerModel : PageModel
{
private readonly IRepository _repository;
public CreateCustomerModel(IRepository repository)
{
_repository = repository;
}
[PageRemote(ErrorMessage = "Email Address already exists", AdditionalFields = "__RequestVerificationToken", HttpMethod = "post",PageHandler = "CheckEmail")]
[BindProperty]
public string Email { get; set; }
public void OnGet()
{
}
public IActionResult OnPost()
{
if (ModelState.IsValid)
{
//insert data into database.
}
return Page();
}
#pragma warning disable MVC1001 // Filters cannot be applied to page handler methods.
[ValidateAntiForgeryToken]
#pragma warning restore MVC1001 // Filters cannot be applied to page handler methods.
public JsonResult OnPostCheckEmail()
{
//query database and check whether the email is exist or not.
var existingEmails = _repository.GetCustomers().Select(c => c.Email.ToLower()).ToList();
var valid = !existingEmails.Contains(Email.ToLower());
return new JsonResult(valid);
}
In the CreateCustomer.cshtml razor page, add JQuery reference and add a form to enter the values.
#page
#model RazorSample.Pages.CreateCustomerModel
#{
}
<div class="row">
<div class="col-md-4">
<form method="post" asp-antiforgery="true">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
#* add other fields *#
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#section scripts{
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<partial name="_ValidationScriptsPartial" />
<script src="~/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js"></script>
}
After submit the button, the result as below: if the email is exist, it will show the prompt:
[Note] In the above sample, we are adding the properties in the PageModel (instead of nested model), and use it to validate the field. Because, if using nested object, we might meet the 400 Bad Request result. The 400 error is related to the AntiForgeryToken, if you meet this error, try to ignore validate the AntiForgeryToken or custom add the __RequestVerificationToken token at the body or header, check this link.
More detail information about Remote Validation in Razor Pages, check the following articles:
Remote Validation in Razor Pages
Improved Remote Validation in Razor Pages

Any alternative to hidden fields when updating from a viewmodel? I don't want to have all fields in the edit page

I know there is a way to do this the "right" way, but for some reason I can't find an answer. I even saw on Microsofts guide that hidden fields are the way to go, but it feels "wrong".
I am finding my update works fine when I put all the hidden fields in the Edit form:
<input type="hidden" asp-for="OrgUnits.Organizations" />
<input type="hidden" asp-for="OrgUnits.Address" />
<input type="hidden" asp-for="OrgUnits.AlternateId" />
<input type="hidden" asp-for="OrgUnits.Category" />
<input type="hidden" asp-for="OrgUnits.City" />
<input type="hidden" asp-for="OrgUnits.FriendlyPath" />
<input type="hidden" asp-for="OrgUnits.IsTop" />
<input type="hidden" asp-for="OrgUnits.Name" />
<input type="hidden" asp-for="OrgUnits.NextChildId" />
<input type="hidden" asp-for="OrgUnits.RecoveryOverride" />
<input type="hidden" asp-for="OrgUnits.RowStatus" />
<input type="hidden" asp-for="OrgUnits.RowVersion" />
<input type="hidden" asp-for="OrgUnits.State" />
<input type="hidden" asp-for="OrgUnits.UseAppVersion" />
<input type="hidden" asp-for="OrgUnits.ZipCode" />
But, that seems like a poor way to write code. I only want a few of the fields in this table to be editable.
Here is my controller:
public async Task<IActionResult> Edit(string id, [Bind("OrgUnits")] OrgUnitsViewModel orgUnitsViewModel)
{
id = Uri.UnescapeDataString(id);
if (id != orgUnitsViewModel.OrgUnits.OrgUnitId)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
//Get org for the DbCatalog
var org = await _opkCoreContext.Organizations.FindAsync(orgUnitsViewModel.OrgUnits.OrgId);
_serverConnectionHelper.SetDatabaseConnectStringToSession(org.DbCatalog);
_opkDataContext.Update(orgUnitsViewModel.OrgUnits);
await _opkDataContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!OrgUnitsExists(orgUnitsViewModel.OrgUnits.OrgUnitId))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index), new { currentSearchFilter = orgUnitsViewModel.OrgUnits.OrgUnitId });
}
return View(orgUnitsViewModel);
}
Is this really how this is supposed to be done. I went the route of AutoMapper, but that was failing for me and I don't quite understand how to use it. Anyways, here is my error:
DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
Hopefully one of you smart people out there know the answer. I am surprised I can't find anything on Google or SO because I know this is extremely common. It is just that hidden fleids seems so wrong because what if you miss one?
Thank you very much in advance.
I personally do following For partially update an entity:
If I don't want to send an entire model to action to change entire entity, I would make an endpoint(API action) that partially update entity and return success status code instead of View. I would use ajax request to the endpoint to change the entity without refreshing the page.
This is my code for partially updating a Employee entity:
Employee.cs
public class Employee
{
public int Id { get; set; }
[Required(ErrorMessage ="Employee name is a required field.")]
[MaxLength(30,ErrorMessage ="Maximum length for the Name is 30 chrachters.")]
public string Name { get; set; }
[Required(ErrorMessage = "Age is a required field.")]
public int Age{ get; set; }
[Required(ErrorMessage = "Position is a required field.")]
[MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 chrachters.")]
public string Position { get; set; }
public int CompanyId { get; set; }
public Company Company { get; set; }
}
EmployeeUpdateDto.cs
public class EmployeeUpdateDto
{
[Required(ErrorMessage = "Employee name is required")]
[MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters")]
public string Name { get; set; }
[Range(18, int.MaxValue, ErrorMessage = "Minimum age must be 18")]
public int Age { get; set; }
[Required(ErrorMessage = "Employee position is required")]
[MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 characters")]
public string Position { get; set; }
}
Controller.cs
public class EmployeesController : ControllerBase
{
private IRepositoryManager _repository;
private ILoggerManager _logger;
private IMapper _mapper;
public EmployeesController(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)
{
_repository = repository;
_logger = logger;
_mapper = mapper;
}
[HttpPatch("{id}")]
public async Task<IActionResult> PartiallyUpdateEmployee(int id, JsonPatchDocument<EmployeeUpdateDto> employeePatches)
{
if (employeePatches is null)
{
_logger.LogError("JsonPatchDocument object sent from client is null");
return BadRequest();
}
var employeeEntity = await _repository.EmployeeRepository.GetEmployeeAsync(employeeId, trackChanges:true);
if (employeeEntity null)
{
_logger.LogInfo($"Employee with id {id} doesn't exist in the database.");
return NotFound();
}
var employeeUpdateDto = _mapper.Map<EmployeeUpdateDto>(employeeEntity);
employeePatches.ApplyTo(employeeUpdateDto, ModelState);
TryValidateModel(employeeUpdateDto);
if (!ModelState.IsValid)
{
_logger.LogError("invalid model state for the patch document");
return UnprocessableEntity(ModelState);
}
_mapper.Map(employeeUpdateDto, employeeEntity);
await _repository.SaveAsync();
return NoContent();
}
//other action methods
}
You must send your request body in the following standard patch format (json):
[
{ "op": "replace", "path": "/name", "new_name": "new name" },
{ "op": "remove", "path": "/position" }
]
That's it. the sample request above would change the Employee name to "new_name" and set the Position to its default value (in this case null).
Above sample needs these prerequisites to work:
Microsoft.AspNetCore.JsonPatch to support JsonPatchDocument type.
Microsoft.AspNetCore.Mvc.NewtonsoftJson to support mapping request to JsonPatchDocument<T>. Configure this in ConfigureServices() method:
services.AddControllersWithViews
.AddNewtonsoftJson();
AutoMapper.Extensions.Microsoft.DependencyInjection to map EmployeeUpdateDto to Employee. Add a mapping profile class and configure AutoMapper in ConfigureServices() method:
services.AddAutoMapper(typeof(Startup));
and
public class MappingpProfile : Profile
{
public MappingpProfile()
{
CreateMap<CompanyUpdateDto, Company>();
CreateMap<CompanyCreationDto, Company>();
CreateMap<Employee, EmployeeDto>();
CreateMap<EmployeeCreationDto, Employee>();
CreateMap<EmployeeUpdateDto, Employee>().ReverseMap();
}
}
In above code we use CreateMap<EmployeeUpdateDto, Employee>().ReverseMap(); for our needs.

ASP.Net Core Razor Pages: How to return the complex model on post?

I created a new ASP.Net Core 2 (Razor Pages) Project
My model is:
public class FormularioGenerico
{
public FormularioGenerico()
{
}
public string IP { get; set; }
public List<string> items { get; set; } = new List<string>();
}
On the page I put
on the page.cshtml.cs
public class EditarModel : PageModel
{
[BindProperty]
public FormularioGenerico ff { get; set; }
[BindProperty]
public string Message { get; set; }
public void OnGet()
{
this.ff = new FormularioGenerico();
ff.IP = "C# FORM";
ff.items.Add("OK1");
ff.items.Add("OK2");
ff.items.Add("OK3");
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var m = ModelState.IsValid; // true
Debug.WriteLine(this.ff.IP); // is Always returning null
Debug.WriteLine(this.ff.items.Count); // is Always returning null
}
}
on the page.cshtml:
#model Formulario.Pages.EditarModel
...
<h1>#Model.ff.IP</h1>
#foreach (var i in Model.ff.items)
{
<div>#i</div>
}
<button type="submit">Enviar</button>
The items are correctly output. But the complete object does not go to the OnPost.
The problem is: The model is not coming fully populated on the OnPost.
How to receive the full object that was created on the OnGet, plus the changes made by the user on the form, on the post to OnPostAsync() ?
The BindProperty attribute is used to inform ASP.NET Core that the values that the form submitted should be mapped to the specified object. In your case you set the values for the ff property but you do not have the equivalent input values so that ASP.NET Core will get these values in order to store them back to the ff property.
In order to make it work you will have to replace your razor code with the following code:
<form method="post">
<h1>#Model.ff.IP</h1>
<input asp-for="#Model.ff.IP" type="hidden" /> #* create a hidden input for the IP *#
#for (int i = 0; i < Model.ff.items.Count(); i++)
{
<input asp-for="#Model.ff.items[i]" type="hidden" /> #* create a hidden input for each item in your list *#
<div>#Model.ff.items[i]</div>
}
<button type="submit">Enviar</button>
</form>
Very important. To make this work you can not use the foreach loop because ASP.NET core will not be able to find the values. You will have to use a for loop.
The inputs that I added are hidden because I guess you do not want them to be visible but you can remore the type="hidden" so that you will be able to see them. Every change that you make to these inputs will be submitted to the OnPostAsync method.

ASP.NET MVC 4 DropDownListFor error: Null Values

I am a beginner programmer and having trouble with the #Html.DropDownListFor helper...
I am using a General Repository and Unit of Work pattern based off of the tutorial here:
http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
Here is my code for the Repository:
public class GenericRepository<TEntity> where TEntity : class
{
internal UsersContext context;
internal DbSet<TEntity> dbSet;
public GenericRepository(UsersContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual TEntity GetByID(object id)
{
return dbSet.Find(id);
}
public virtual void Insert(TEntity entity)
{
dbSet.Add(entity);
}
// Delete methods not shown
public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}
Here is my code for my UnitOfWork class:
public class UnitOfWork : IDisposable
{
private UsersContext context = new UsersContext();
private GenericRepository<UserProfile> userProfileRepository;
private GenericRepository<Lead> leadRepository;
private GenericRepository<UnitedStatesState> unitedStatesStateRepository;
public GenericRepository<UserProfile> UserProfileRepository
{
get
{
if (this.userProfileRepository == null)
{
this.userProfileRepository = new GenericRepository<UserProfile(context);
}
return userProfileRepository;
}
}
public GenericRepository<Lead> LeadRepository
{
get
{
if (this.leadRepository == null)
{
this.leadRepository = new GenericRepository<Lead>(context);
}
return leadRepository;
}
}
public GenericRepository<UnitedStatesState> UnitedStatesStateRepository
{
get
{
if (this.unitedStatesStateRepository == null)
{
this.unitedStatesStateRepository = new GenericRepository<UnitedStatesState>(context);
}
return unitedStatesStateRepository;
}
}
I am trying to use strongly typed views and models in order to pass the selectlist data to the view without using ViewData/ViewBag. From what I understand, the best practice is to do something similar to what I saw here:
validate a dropdownlist in asp.net mvc
I tried following that as closely as possible and this is what I came up with
My View Model looks like this:
public class Lead
{
public int LeadID { get; set; }
public int CompanyID { get; set; }
[Required(ErrorMessage = "Please enter state")]
[Display(Name = "State")]
[MaxLength(2)]
public string State { get; set; }
[Display(Name = "Assigned To")]
public string AssignedTo { get; set; }
[Timestamp]
public Byte[] Timestamp { get; set; }
public virtual Company Company { get; set; }
// IEnumerables for Dropdown Lists passed to views
public IEnumerable<UnitedStatesState> UnitedStatesStates { get; set; }
public IEnumerable<UserProfile> UserProfiles { get; set; }
// Objects passed to views
public Lead lead { get; set; }
}
These IEnumerables for my dropdown lists are then populated in my controller from my database through my repository. The odd part is that I am using these dropdown lists in two different views, Create and Edit. When I use the dropdown lists in the Create view they work perfectly both on the GET and POST ActionResults. When I try and use the same dropdown lists for my Edit view they work for the GET ActionResult (the view loads and the dropdowns work) but when I try to POST them to my Edit ActionResult I get the following error:
{"Value cannot be null.\r\nParameter name: items"} // This is the error as shown in Visual Studio 2012
System.ArgumentNullException: Value cannot be null.
Parameter name: items // This is the error shown in Google Chrome
Below is my Lead Controller with the Edit and Create ActionResults:
public class LeadController : Controller
{
// create instance of Repository Unit of Work
private UnitOfWork unitOfWork = new UnitOfWork();
public ActionResult Create()
{
// Get the current users profile
UserProfile userProfile = UserProfile.GetCurrentUserProfile();
// Creates Dropdown Lists to pass to view
var model = new Lead
{
UnitedStatesStates = unitOfWork.UnitedStatesStateRepository.Get(u => u.StateAbbreviation != null),
UserProfiles = unitOfWork.UserProfileRepository.Get(u => u.CompanyID == userProfile.CompanyID)
};
// Return View
return View(model);
}
[HttpPost]
public ActionResult Create(Lead model)
{
try
{
if (ModelState.IsValid)
{
// Call the current users profile
UserProfile userProfile = UserProfile.GetCurrentUserProfile();
// Create a new lead and apply all attirbutes that were entered
Lead lead = new Lead();
lead.CompanyID = userProfile.CompanyID;
lead.State = model.State;
lead.AssignedTo = model.AssignedTo;
// Add the lead and save the changes. Redirect to Lead Index.
unitOfWork.LeadRepository.Insert(lead);
unitOfWork.Save();
return RedirectToAction("Index");
}
}
catch (DataException)
{
ModelState.AddModelError("", "Unable to save changes. Try again and if the problem persists, see your system administrator.");
}
// Return view if ModelState is not valid
return View();
}
public ActionResult Edit(int id = 0)
{
// Get Users Profile
UserProfile userProfile = UserProfile.GetCurrentUserProfile();
// Check to see if Lead Exists
if (unitOfWork.LeadRepository.GetByID(id) == null)
{
return HttpNotFound();
}
// Creates Dropdown Lists and Gets current lead values to pass to view
var model = new Lead
{
lead = unitOfWork.LeadRepository.GetByID(id),
UnitedStatesStates = unitOfWork.UnitedStatesStateRepository.Get(u => u.StateAbbreviation != null),
UserProfiles = unitOfWork.UserProfileRepository.Get(u => u.CompanyID == userProfile.CompanyID)
};
return View(model);
}
[HttpPost]
public ActionResult Edit(Lead lead)
{
try
{
// Update lead if model state is valid
if (ModelState.IsValid)
{
unitOfWork.LeadRepository.Update(lead);
unitOfWork.Save();
return RedirectToAction("Index");
}
}
// Catch any concurrency exceptions
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = (Lead)entry.GetDatabaseValues().ToObject();
var clientValues = (Lead)entry.Entity;
if (databaseValues.State != clientValues.State)
ModelState.AddModelError("State", "Current value: "
+ databaseValues.State);
if (databaseValues.AssignedTo != clientValues.AssignedTo )
ModelState.AddModelError("Assigned To ", "Current value: "
+ databaseValues.AssignedTo );
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
lead.Timestamp = databaseValues.Timestamp;
}
catch (DataException)
{
//Log the error (add a variable name after Exception)
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
// Return View if Model State is not valid
return View(lead);
}
The POST Edit ActionResult includes code to catch concurrencies which I created following the tutorial shown here:
http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application
Below is my view for Create (this works perfectly):
#model SolarToolbase.Models.Lead
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<div>
<div>
#Html.LabelFor(model => model.State)
#Html.DropDownListFor(model => model.State, new SelectList(Model.UnitedStatesStates, "StateAbbreviation", "UnitedStatesStateName"),"Choose State")<br />
#Html.ValidationMessageFor(model => model.State)
</div>
<div>
#Html.LabelFor(model => model.AssignedTo)
#Html.DropDownListFor(model => model.AssignedTo, new SelectList(Model.UserProfiles, "FullName", "FullName"),"Choose User")<br />
#Html.ValidationMessageFor(model => model.AssignedTo)
</div>
<p>
<input type="submit" value="Create" />
</p>
</div>
}
Below is my view for Edit(this throws the aforementioned errors when I hit the submit button. I inserted a comment below to show the line that the error is being thrown from):
#model SolarToolbase.Models.Lead
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.lead.LeadID)
#Html.HiddenFor(model => model.lead.Timestamp)
<div>
<div>
#Html.LabelFor(model => model.lead.State)
#Html.DropDownListFor(model => model.lead.State, new SelectList(Model.UnitedStatesStates, "StateAbbreviation", "UnitedStatesStateName"))<br /> // Error thrown from this line
#Html.ValidationMessageFor(model => model.lead.State)
</div>
<div>
#Html.LabelFor(model => model.lead.AssignedTo)
#Html.DropDownListFor(model => model.lead.AssignedTo, new SelectList(Model.UserProfiles, "FullName", "FullName"))<br />
#Html.ValidationMessageFor(model => model.lead.AssignedTo)
</div>
<p>
<input type="submit" value="Save" />
</p>
</div>
}
I apologize in advance for posting so much code, I just honestly don't know where this error is coming from and I've beat my head against the wall trying to figure it out for about 4 hours now. Free virtual high fives and good karma for anyone that can help.
Thanks!
In the case of a POST to both the Create and Edit actions, when there's an error or the ModelState is invalid, you catch any exceptions and return the default View with the constructed Lead view model, created and populated by the model binder.
In the Edit POST action though, if there is an error condition, you return the lead object to the View as its Model. Note that the UnitedStatesStates and the UserProfiles properties are not repopulated upon a POST. You populate them in the GET actions, but you have to do that in the POST actions too. You need to be careful that whatever model you are sending to the view is in proper shape, and it has all expected members populated.
Also notice your view model is of type Lead which has a property called lead. That's a code smell there; I wouldn't have a view model class having a reference to an instance of its own class. It's causing confusion for you already. I'd have Lead be LeadViewModel to be explicit and just have it hold all the properties and values it needs when going to and from the views, with no lead property.
In your Edit view, you're referencing the model's properties as model.lead.State for example, but in the Create view you're referencing the parent-level properties, as in model.State. But in the Edit view, when it comes to the SelectListItems you're using Model.UnitedStatesStates instead of Model.lead.UnitedStatesStates. As I said I'd do away with this pattern and do what the Create view does now, not having a child lead property at all. Just do model.State for example, for all properties and in both views.
So make sure your collection properties are populated whenever you pass the model to the view, as in
[HttpPost]
public ActionResult Edit(Lead lead)
{
try
{
// Update lead if model state is valid
if (ModelState.IsValid)
{
unitOfWork.LeadRepository.Update(lead);
unitOfWork.Save();
return RedirectToAction("Index");
}
}
// Catch any concurrency exceptions
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = (Lead)entry.GetDatabaseValues().ToObject();
var clientValues = (Lead)entry.Entity;
if (databaseValues.State != clientValues.State)
ModelState.AddModelError("State", "Current value: "
+ databaseValues.State);
if (databaseValues.AssignedTo != clientValues.AssignedTo )
ModelState.AddModelError("Assigned To ", "Current value: "
+ databaseValues.AssignedTo );
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
lead.Timestamp = databaseValues.Timestamp;
}
catch (DataException)
{
//Log the error (add a variable name after Exception)
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
// Return View if Model State is not valid
/////////// CHANGES HERE
lead.UnitedStatesStates = unitOfWork.UnitedStatesStateRepository.Get(u => u.StateAbbreviation != null),
lead.UserProfiles = unitOfWork.UserProfileRepository.Get(u => u.CompanyID == userProfile.CompanyID)
return View(lead); // pass the model to the view for Create and Edit POST actions when there's an error
}
Do that in both POST actions. If there's an error, the view will be instantiated by the action with a populated model. Also change the Edit view to work just like the Create view, and not use the Lead view model's lead property. Presumably that will take care of any null reference exceptions in the views.