I have an ASP.NET Core controller with multiple action methods. Each action method has an {id} path parameter and do the following in almost every request:
[HttpPost("action1/{id}")]
public async Task<ActionResult<JsonNode>> Action1Async(int id)
{
var model = await _dbService.FindModelAsync(id); // Entity Framework Core
if (model is null)
{
return NotFound();
}
// actual code for action1
}
[HttpPost("action2/{id}")]
public async Task<ActionResult<JsonNode>> Action2Async(int id)
{
var model = await _dbService.FindModelAsync(id); // Entity Framework Core
if (model is null)
{
return NotFound();
}
// actual code for action2
}
[HttpPost("action3/{id}")]
public async Task<ActionResult<JsonNode>> Action3Async(int id)
{
var model = await _dbService.FindModelAsync(id); // Entity Framework Core
if (model is null)
{
return NotFound();
}
// actual code for action3
}
As you can see, all actions do exactly the same first step: retrieves a model from a DbService, returns a 404 error if the model is not found, and do some specific logic otherwise.
For me, this code is very repetitive. Basically, my only goal is just the following:
- Retrieve the model based on an id
- If the model is not found, do not enter the controller and return a 404 error
- Otherwise, enter the controller, supply the retrieved model, and do some specific logic
I would like to have something like this:
// this will not execute if the model is not found.
[HttpPost("action1/{id}")]
public async Task<ActionResult<JsonNode>> Action1Async(Model model)
{
// actual code for action1
}
I am currently exploring filters/middleware to do this, but it would seem that they are used for validations only, not really initializing data and passing them to a controller action, but I am willing to explore all approaches.
Is what I want possible? Can we bind Entity Framework Core models directly to controller action parameters?
Is it possible? Yes. Is it recommended? No. The main issues with passing entities to and from views is that the entity instance you send to populate the view is not the same instance that comes back. Whether a Form POST or an Ajax POST, what comes back are the values stripped from the HTML elements that ASP.Net then builds a fresh, detached instance of an entity. This means that every property that is expected to come back to the controller must be rendered somewhere in the HTML, such as a hidden input control. The
@modelyou see in the cshtml means nothing to what the client browser can send back for a POST, if the values are not in the HTML, or in the JavaScript then it doesn't come back. Serializing the model in the JavaScript is not recommended as this both exposes your entire entity structure to client inspection, and can end up tripping lazy loading.The main reasons to avoid sending these entities to the view and quasi-entities back to the controller are that:
This amounts to sending far more data with each action than you need. If you are expecting to treat the passed in entity as a complete representation of the row, it needs to be complete or there are consequences that you end up erasing state or otherwise have null reference exceptions when values or relationships aren't set. Basically any method that accepts an Entity should receive a complete, or complete-able entity, not a potentially incomplete stub.
When used to update data it exposes your system to unintended tampering. Controllers that accept an entity might be tempted to use something like
context.Update(entity);orcontext.Attach(entity); context.Entry(entity).State = EntityState.Modified;which leaves the system open to tampering. Calls to the server can be intercepted in the browser debugging tools and altered to update values in the "entity" that you don't intend to be modified. If the server trusts that quasi-entity and uses it to update the row, data can be compromised.Working with detached entities, even if validated and trusted, can be error prone. Deserialized data creates separate references for the same related data so you technically have to go through each reference and handle the fact that a DbContext instance could be tracking an instance for that row, and need to substitute it. This results in a fair bit of code to do things correctly, or seemingly intermittent runtime errors.