How to handle Concurrency Conflicts with HttpClient? - c#

I can't figure out how to edit the row after seeing the changes in DB.
I have an API-project and an MVC-project. I use CRUD in my API and call them with my MVC with HttpClient
I have a public byte[] RowVersion { get; set; } property with the attribute [Timestamp].
I have a clientFactory where I do CreateClient() to perform PutAsync("api.example.com/{id}") action.
The HttpResponseMessage variable on my putasync action returns StatusCode(409) because my API successfully detected a concurrency conflict.
I managed to display error messages before updating the concurrency; showing the newly updated rows in the database(newsDb) with help of a new client, clientFactory.CreateClient(), and comparing them with the inputs(news).
Then I set the news.RowVersion = newsDb.RowVersion and re-display View(news).
And after clicking Save again, nothing happens - no redirects, no changes - the concurrency errors are still there:
[HttpPost("edit/{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditNewsArticle(int id, [Bind("NewsId,Author,Title,Content,CreatedDate,HashTags,RowVersion")] News news)
{
if (id != news.NewsId)
{
return NotFound();
}
if (ModelState.IsValid)
{
news.UpdatedDate = DateTime.Now;
string json = JsonConvert.SerializeObject(news);
HttpResponseMessage putTask = await clientFactory.CreateClient().PutAsync($"https://localhost:44331/api/News/{id}", new StringContent(json, Encoding.UTF8, "application/json"));
if (putTask.IsSuccessStatusCode)
{
return RedirectToAction(nameof(Index));
}
else if (putTask.StatusCode == HttpStatusCode.Conflict)
{
string jsonDb = await clientFactory.CreateClient().GetStringAsync($"https://localhost:44331/api/News/{id}");
News newsDb = JsonConvert.DeserializeObject<News>(jsonDb);
if (newsDb is null)
{
ModelState.AddModelError(string.Empty, $"Unfortunately, the news item you edited has already been deleted by another user.");
}
if (newsDb.Title != news.Title)
{
ModelState.AddModelError("Title", $"Title in database: {newsDb.Title}");
}
if (newsDb.Author != news.Author)
{
ModelState.AddModelError("Author", $"Author in database: {newsDb.Author}");
}
if (newsDb.Content != news.Content)
{
ModelState.AddModelError("Content", $"Content in database: {newsDb.Content}");
}
if (newsDb.HashTags != news.HashTags)
{
ModelState.AddModelError("HashTags", $"HashTags in database: {newsDb.HashTags}");
}
ModelState.AddModelError(string.Empty,
"Editing was canceled as the selected news item was changed by someone else in the meantime." +
"The values ​​of the change are now shown below, which are derived from the database" +
"If you still want to edit the user, click Save again.");
news.RowVersion = newsDb.RowVersion;
}
else
{
ModelState.AddModelError(string.Empty, "Unknown error. Contact a support.");
return View(news);
}
}
return View(news);
}
API Put:
[HttpPut("{id}")]
public async Task<IActionResult> PutNews(int id, [FromBody] News news)
{
if (id != news.NewsId)
{
return BadRequest();
}
context.Entry(news).State = EntityState.Modified;
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!NewsExists(id))
{
return NotFound();
}
else
{
return StatusCode(409);
}
}
return CreatedAtAction("GetNews", new { id = news.NewsId }, news);
}

I found my issue. I needed to call ModelState.Clear(); after de-serializing the 'jsonDb', and also remove RowVersion from Bind in the attribute.

Related

Displaying custom error on View on primary key violation with Entity Framework Core

I want a custom error message to be displayed just as is the case for required, regex etc, but for primary key violation.
The check is done so as to never be able to insert a primary key. What do I put in else?
I don't want to use Ajax or ViewBag/ViewState, else I know how to do it that way. I want to display that error "This code already exists " on form post.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Code,Libelle")] Marque marque)
{
if (ModelState.IsValid)
{
var codeExists = _context.Marque.Where(s => s.Code == marque.Code)
.FirstOrDefault()
.Code == marque.Code ? "yes" : "no";
if (codeExists == "no")
{
_context.Add(marque);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
else
{
}
}
return View(marque);
}
You can add a model error to object, like this:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Code,Libelle")] Marque marque)
{
if (ModelState.IsValid)
{
var codeExists = _context.Marque.Where(s => s.Code == marque.Code).FirstOrDefault().Code == marque.Code ? "yes" : "no";
if (codeExists == "no")
{
_context.Add(marque);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
else
{
ModelState.AddModelError("Error occurred", "Your custom error exception");
}
}
return View(marque);
}
I am assuming that you have a web application and not web api - because this is not specified in your code.
In Asp.net core, you can configure Error handling middleware:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
Then you can have Error controller as shown below, which returns view based on the exception type.
[AllowAnonymous]
public IActionResult Error()
{
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
{
ExceptionMessage = "File error thrown";
}
if (exceptionHandlerPathFeature?.Path == "/index")
{
ExceptionMessage += " from home page";
}
// Either show exception message on page or return different views based on conditions.
}
Hope this helps!
TIP - even if you have web APIs, you can choose to return business error codes form API to UI, and then UI can show appropriate error messages based on those codes.
Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-3.0
Thanks Guilherme.
Below is the code that works.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Code,Libelle")] Marque marque)
{
if (ModelState.IsValid)
{
var codeExists = _context.Marque.Where(s => s.Code == marque.Code).FirstOrDefault().Code == marque.Code ? "yes" : "no";
if (codeExists == "no")
{
_context.Add(marque);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
else
{
ModelState.AddModelError("Code","This code already exists");
}
}
return View(marque);
}

Can't seem to send string over a PUT request

For some reason my PUT request gives an BadRequest error. I have checked what the cause is and the cause is that "gebruikersnaam" when arriving in the .NET side is null, instead of its value.
[HttpPut("naam")]
public async Task<IActionResult> VeranderNaam(string gebruikersnaam)
{
IdentityUser user = await this._userManager.FindByNameAsync(User.Identity.Name);
Gebruiker gebruiker = this._gebruikerRepository.GetBy(user.UserName);
Brouwer brouwer = this._brouwerRepository.GetBy(user.UserName);
user.UserName = gebruikersnaam;
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
if(brouwer != null)
{
brouwer.Naam = gebruikersnaam;
this._brouwerRepository.Update(brouwer);
this._brouwerRepository.SaveChanges();
return Ok();
}
else if(gebruiker != null)
{
gebruiker.Gebruikersnaam = gebruikersnaam;
this._gebruikerRepository.Update(gebruiker);
this._gebruikerRepository.SaveChanges();
return Ok();
}
}
return BadRequest();
}
angular code
onSubmitNaam() {
console.log(this.gebruikersnaam.value.gebruikersnaam);
this.authService.veranderNaam(this.gebruikersnaam.value.gebruikersnaam).subscribe(
() => {
this.success = "Uw naam is met success aangepast";
}, err => {
this.error = "Uw naam is niet aangepast";
}
)
this.gebruikersnaam.reset();
}
veranderNaam(gebruikersnaam: string) {
return this.http.put(`${environment.apiUrl}/gebruikers/naam`, gebruikersnaam);
}
I know my form gets the value, it is sent from a form through the request, but upon arriving on the request it "becomes" null.
In this line of code you are submit the body not in a query string
return this.http.put(`${environment.apiUrl}/gebruikers/naam`, gebruikersnaam);
So if you want your code work you can change into this
public async Task<IActionResult> VeranderNaam([FromBody] string gebruikersnaam)
Or you can use queryParams

Edit Action in ASP.Net Core controller using another field

I have an URL field in my table for each of the courses. And i am using it as route parameter. I did this to make Urls user-friendly and as per my understanding this might also help me in SEO ( Please correct me if i am wrong ). With such as setup, i am unable to figure-out how do i create Edit / Delete actions.
Course.cs : The model of the course
public partial class Course
{
public int Id { get; set; }
public string Title { get; set; }
// This is set as Unique Key in the table.
public string Url { get; set; }
public string InnerHtml { get; set; }
}
CourseController.cs : The controller and Edit action for our reference.
[HttpPost("Edit/{courseUrl}")]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Administrator")]
public async Task<IActionResult> Edit(string courseUrl, [Bind("Id,Title,Url,InnerHtml")] Course course)
{
var OriginalCourse = await _context.Courses.SingleOrDefaultAsync(m => m.Url == courseUrl);
if (OriginalCourse.Id != course.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(course);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CourseExists(course.Url))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(course);
}
The problem : I am getting the following error on this action
InvalidOperationException: The instance of entity type 'Course' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
The WorkAround : Commenting the below code in the action, get's the application working. But, the below code is to check if the model being edited is contained in DB.
var OriginalCourse = await _context.Courses.SingleOrDefaultAsync(m => m.Url == courseUrl);
if (OriginalCourse.Id != course.Id)
{
return NotFound();
}
What's the correct way to handle this scenario ?
As the error message explains, there is already a model loaded from the search which is being track by the ORM. You need to copy desired properties over to the tracked model if you intend to save it.
//...code removed for brevity
var OriginalCourse = await _context.Courses.SingleOrDefaultAsync(m => m.Url == courseUrl);
if (OriginalCourse.Id != course.Id) {
return NotFound();
}
if (ModelState.IsValid) {
try {
Populate(OriginalCourse, course);
_context.Update(OriginalCourse);
await _context.SaveChangesAsync();
} catch (DbUpdateConcurrencyException) {
if (!CourseExists(course.Url)) {
return NotFound();
} else {
throw;
}
}
return RedirectToAction(nameof(Index));
}
//...code removed for brevity
Where Populate could look like this
void Populate(Course original, Cource source) {
original.Title = source.Title;
original.Url = source.Url;
original.InnerHtml = source.InnerHtml;
}
Another option would be to not load up an instance by not selecting/returning an item from the context
//...code removed for brevity
var exists = await _context.Courses.AnyAsync(m => m.Url == courseUrl);
if (!exists) {
return NotFound();
}
//...code removed for brevity
and then update the provided course

ASP.Net & EF Core 2 and Concurrency token error

Entity Framework Core 2, added a concurrency token property to model
[Timestamp]
public byte[] Timestamp { get; set; }
Controller Edit fails
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("Id,Name,Description,IsDeleted,ParentId")] ItemStatus itemStatus)
{
if (id != itemStatus.Id)
return NotFound();
if (ModelState.IsValid)
{
try
{
_context.Update(itemStatus);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ItemStatusExists(itemStatus.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
ViewData["ParentId"] = new SelectList(_context.ItemStatus, "Id", "Description", itemStatus.ParentId);
return View(itemStatus);
}
the specific error I am receiving occurs when SaveChangesAsync occurs. catch pops and when I step in it goes straight to the throw.
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. See
http://go.microsoft.com/fwlink/?LinkId=527962 for information on
understanding and handling optimistic concurrency exceptions.
Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ThrowAggregateUpdateConcurrencyException(int
commandIndex, int expectedRowsAffected, int rowsAffected)
searching for the error message doesn't help. did find this article but it appears to be no help.
https://learn.microsoft.com/en-us/ef/core/saving/concurrency
as indicated in the comments, I was missing a hidden field to 'save' the timestamp in the view.
Followed this example: https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/concurrency
for clarity added my altered Edit. I had to do something similar to Delete too. this needs to be added to the Edit view <input type="hidden" asp-for="Timestamp" />
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("Id,Name,Description,ParentId,Timestamp")] ItemStatus itemStatus)
{
if (id != itemStatus.Id)
return NotFound();
if (ModelState.IsValid)
{
try
{
_context.Update(itemStatus);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ItemStatusExists(itemStatus.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
ViewData["ParentId"] = new SelectList(_context.ItemStatus, "Id", "Description", itemStatus.ParentId);
return View(itemStatus);
}

Web API 2 Http Post Method

I am disgusted not have found a solution to this problem.
I started creating a new api using Web API 2 and just cannot get the POST and PUT to work. The Get all and Get single item works perfectly fine.
There are no related articles anywhere, and those that i've found relates only to Gets and Web API, but not Web API 2.
Any assistance would do please.
// POST: api/checkOuts
[HttpPost]
[ResponseType(typeof(checkOut))]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IHttpActionResult> PostcheckOut(checkOut co)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.checkOuts.Add(checkOut);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (checkOutExists(checkOut.id))
{
return Conflict();
}
else
{
throw;
}
}
return CreatedAtRoute("DefaultApi", new { id = checkOut.id }, checkOut);
}
So basically, I'm just attempting to get a debug into the method.
Was especially disappointed in this link as it covered almost everything, but ai. http://www.asp.net/web-api/overview/web-api-routing-and-actions/create-a-rest-api-with-attribute-routing
Regards
This is a working code
// POST api/values
[HttpPost]
[ResponseType(typeof(CheckOut))]
public async Task<IHttpActionResult> Post([FromBody] CheckOut checkOut)
{
if (checkOut == null)
{
return BadRequest("Invalid passed data");
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.checkOuts.Add(checkOut);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (checkOutExists(checkOut.id))
{
return Conflict();
}
else
{
throw;
}
}
return CreatedAtRoute("DefaultApi", new { id = checkOut.Id }, checkOut);
}
I've declared CheckOut class to be like this :
public class CheckOut
{
public int Id { get; set; }
public string Property2 { get; set; }
}
The Key things here are :
1- You need to add [FromBody] to your Api method.
2- I've tested it using Fiddler,
i- by choosing POST action.
ii- content-type: application/json.
iii- passing {"Id":1,"Property2":"Anything"} in the message body.
Hope that helps.

Categories

Resources