0

I have an Update method in my repository which I'm using to update articles on my project. I was initially using this method only to carry out admin edits for articles. It handles that correctly, but I decided I'd like to add a simple mechanism to calculate "most read" articles. In order to do that, I'd like to update TimesRead property each time an article has been viewed. This has been giving me trouble with the updates which seem to revolve around using ObjectStateManager.ChangeObjectState. Here's my Update method:

public void Update(Article article)
{
    if (article == null) return;

    db.Articles.Attach(article);
    db.ObjectStateManager.ChangeObjectState(article, EntityState.Modified);
    db.SaveChanges();
}

In my AdminController the following method updates correctly:

[HttpPost]
public ActionResult Edit(AdminEditViewModel viewModel)
{
    if (ModelState.IsValid)
    {
        Article article = Mapper.Map<AdminEditViewModel, Article>(viewModel);
        articleRepository.Update(article);

        return RedirectToAction("Index");
    }

    viewModel.Categories = new SelectList(categoryRepository.GetAll(), "CategoryID", "Name", viewModel.CategoryID);

    return View(viewModel);
}

However, in the TimesRead scenario, the update will trigger an exception of:

The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.

Relevant code from that controller method:

var model = articleRepository.GetByID(id);

model.TimesRead++;
articleRepository.Update(model);

return View(model);

After having a look around to see what I can do to solve this, I came across the answer to this SO question. So I implemented that answer by replacing my Update method with the code suggested. This also works correctly in my admin scenario but not in the TimesRead scenario. The following exception is thrown:

An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.

The exceptions are quite clear in their meaning but it does leave me wondering how I am supposed to handle simple updates such as these. I found that I can "fool" the EF into thinking the model is unchanged by setting EntityState.Unchanged and that will update TimesRead but give an exception for admin updates, stating the ObjectStateManager doesn't hold a reference to the object.

It's also clear to me how these scenarios differ. The Edit action is mapping properties from a ViewModel onto a new, unattached Article object, whereas, ArticleController is dealing with an object retrieved directly from the context. That leaves me with the feeling I should refactor one of those controller methods so the steps taken to update are the same. I'm just not really sure how I should even approach that as both approaches seem like they should be able to coexist to me. So my question is, what can I change to get both types of update to work correctly?

Thanks for your time and I'm really sorry for the amount of code posted. I just feel it is all relevant to the problem.

Community
  • 1
  • 1
John H
  • 13,157
  • 4
  • 33
  • 68
  • How are you instantiating your db context? Your Update method doesn't seem to be doing it. – Erik Funkenbusch Jul 23 '11 at 21:55
  • It's a private member within the `ArticleRepository` class, so it's instantiated as soon as the repository is constructed. `ArticleRepository.Update` is the method in question. Sorry if I didn't make that clear enough. – John H Jul 23 '11 at 21:59
  • And how are you constructing your ArticleRepository? My point is, HTTP is stateless, and you can't keep a reference to your db context across requests. So, I'm making sure your problem isn't being caused by an old copy of the db context. – Erik Funkenbusch Jul 23 '11 at 22:01
  • I just deleted a comment I made because I misread what you'd written. ArticleRepository is a private member within my controller, so it is getting constructed when the controller is constructed. `private ArticleRepository articleRepository = new ArticleRepository();`. – John H Jul 23 '11 at 22:27

1 Answers1

0

The primary difference between your two methods is that the Admin Edit method creates a new Article from your AdminEditViewModel, then it attaches this newly created Article to your database. This works because it's a new object that has never been attached to a dc.

In the second case, you get an Article from the repository, update that Article, then try and attach it again, this fails because it's not a newly created Article, it's an Article returned from the db Context in the first place so it's already attached. and you are trying to attach it again.

Erik Funkenbusch
  • 90,480
  • 27
  • 178
  • 274
  • I understand this, but what do I do about it? Should I refactor the `Edit` method to something like: `Article article = articleRepository.GetByID(id);` and map the ViewModel to that article object instead? It seems a bit odd to query for the object when all of the information I need is in the ViewModel already. – John H Jul 23 '11 at 22:17
  • Re: my comment above, I just tried that and it makes no difference. However, it did illustrate what you said above about the way the context is constructed between requests, but now I really don't know how to solve that. Thanks a lot for the help so far. – John H Jul 23 '11 at 22:50
  • 1
    @johnh - There are several solutions that are hackish. For instance, you could detach the article after you get it from the repository. However, I think you want to design a better solution overall and that requires knowing a lot more about the design than is feasible to post here. – Erik Funkenbusch Jul 24 '11 at 00:08
  • I've marked this as the answer as you're correct, the design does need to be improved. I've spent the past 7 hours working on that and that's become quite apparent. Thank you very much for the help. You've put me on the right track. :) – John H Jul 24 '11 at 07:29