Search code examples
asp.net-coreautomapperasp.net-core-3.1ef-core-3.0ef-core-3.1

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.


Solution

  • 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:

    enter image description here

    • 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.