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.