Entity Framework / WebApi 2 permission check implementation

asp.net-web-api asp.net-web-api2 c# entity-framework entity-framework-6

Question

Using Entity Framework 6.1.0-alpha1 and ASP.NET WebApi 2, I'm creating a straightforward "Todo App." I want to limit access such that users can only read and change their own Todos.

Example:

    // GET api/Todo/5
    [ResponseType(typeof(Todo))]
    public async Task<IHttpActionResult> GetTodo(int id)
    {
        var todo = await _db.Todos.FindAsync(id);

        if (todo == null)
        {
            return NotFound();
        }

        if (todo.CreatorId != _currentUser.Id)
        {
            return StatusCode(HttpStatusCode.Forbidden); 
        }

        return Ok(todo);
    }

That's okay. A similar check has been implemented for deletion, and on creation, the CreatorId is set to the ID of the current user. I struggle with updating, though.

I gave it a try:

    // PUT api/Todo/5
    public async Task<IHttpActionResult> PutTodo(int id, Todo todo)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        if (id != todo.Id)
        {
            return BadRequest();
        }


        // --- No exception if I remove this block - BEGIN ---
        var original = await _db.Todos.FindAsync(id);

        if (original.CreatorId != _currentUser.Id || original.CreatorId != todo.CreatorId)
        {
            return StatusCode(HttpStatusCode.Forbidden);
        }
        // --- No exception if I remove this block - END ---

        _db.Entry(todo).State = EntityState.Modified; // Exception thrown here

        try
        {
            await _db.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!TodoExists(id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return StatusCode(HttpStatusCode.NoContent);
    }

Even so, aSystem.InvalidOperationException it hurled towards the designated line:

System.InvalidOperationException

Attaching an entity of type 'ModernWeb.Domain.Models.Todo' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

The FindByAsync() block will not throw an exception if I remove it.

I also tried utilizing_db.Entry(todo).OriginalValue but was unable to locate a suitable syntax.

How can I solve this issue? Is there a best practice in circumstances like this?

1
0
1/24/2014 11:55:23 PM

Accepted Answer

When you callFindAsync the entity instance that was returned has already been associated with the context. There is therefore no need for_db.Entry(todo).State = EntityState.Modified;

Update

I believe I understand what you are attempting to do. Instead, try this:

var original = await _db.Todos.AsNoTracking()
    .SingleOrDefaultAsync(x => x.Id == id);

if (original.CreatorId != _currentUser.Id || original.CreatorId != todo.CreatorId)
{
    return StatusCode(HttpStatusCode.Forbidden);
}
// --- No exception if I remove this block - END ---

_db.Entry(todo).State = EntityState.Modified;

When you call.AsNoTracking().SingleOrDefaultAsync in place ofFindAsync , theoriginal The returning entity won't be connected to the context. The one that was passed into the controller action can then be set asModified You should no longer receive that exception because the context is no longer tracking a distinct entity with the same id.

In addition, there shouldn't be a requirement to supply the Id attribute as a separate argument in the controller action because the Todo entity you passed as your argument already has one. This should be doable for you:

public async Task<IHttpActionResult> PutTodo(Todo todo)
{
    if (!ModelState.IsValid || todo == null)
    {
        return BadRequest(ModelState);
    }

    var original = await _db.Todos.AsNoTracking()
        .SingleOrDefaultAsync(x => x.Id == todo.Id);

    if (original == null) return NotFound();

    if (original.CreatorId != _currentUser.Id || original.CreatorId != todo.CreatorId)
    {
        return StatusCode(HttpStatusCode.Forbidden);
    }

    _db.Entry(todo).State = EntityState.Modified; // Exception thrown here

    try
    {
        await _db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!TodoExists(todo.Id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return StatusCode(HttpStatusCode.NoContent);
}
2
1/25/2014 1:36:48 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow