What is best practice for deleting multiple records by optional parameters in Core Web API?

Viewed 1356

I'm creating a web API for an application and I have a DELETE action for a resource. The resource is Names, all of which are in a SQL Names table. Each name has the foreign key of a Record (all in a Records table) and each Record has a FileName and IsValid column.

This means that I can delete a bunch of Names by FileName, IsValid, or a combination of these values. This is exactly what the application that will use the API requires.

However, in all the examples of API DELETE endpoints I've seen they are always deleting one record by its id and it's making me doubt if my approach is not considered best practice.

This also brings up a question on how would I even do what I want to do? At the moment, my DELETE endpoint for Names is api/Names/{fileName} and I'm not sure how to also include IsValid into this. At least one or both FileName and IsValid values should be required. I do not want the user to be able to call api/Names and delete every name in the database.

Delete action in NamesController:

[HttpDelete("{fileName}")]
public void DeleteBySourceFileName(string fileName)
{
    _repo.DeleteNamesByFileName(sourceFileName);
}

I've thought about adding IsValid as a query parameter in the action, but that would still keep fileName required.

What would be the best approach to do this, and would such an endpoint be an appropriate for a RESTful API?

2 Answers

I tested this code in postman. All 3 routs are working properly. Thanks @PavelAnikhouski for isValid:bool idea.

        [Route("~/api/Delete/{fileName}")]
        [Route("~/api/Delete/{isValid:bool}")] 
        [Route("~/api/Delete/{fileName}/{isValid}")] 
        public  void Delete(string fileName, bool? isValid)
        {
            .....
        }

If you like, instead of Delete you can use different names for each route for example "DeleteByFileName" or "DeleteIsValid". It still be working fine.

If you want to follow a RESTful approach, I think you can follow one of three ways to delete several items at once:


  1. DELETE with query string, as already mentioned: DELETE api/names?filenames={file name(s)}&isvalid={true|false}.
    • It is simple, but has the disadvantage of stating the filters as optional, which you already said are not, since you do not allow the deletion of all the Names. The action code would be something like:
[HttpDelete]
public Task Delete([FromQuery] string[] fileNames, [FromQuery] bool? isValid)
{
    // code to delete, but avoid delete with null/empty parameters (avoid delete all)
}

  1. POST to a deletions resource, return an identifier and then DELETE with the identifier, as the workflow below:

---> a) Post request like:

POST api/names/deletions
{
   'filenames': [],
   'isValid': false
}

to an endpoint coded like:

public class DeletionInput
{
    public string[] FileNames { get; set; }
    public bool? IsValid { get; set; }
}

[HttpPost("deletions")]
public Task<IActionResult> CreateDeletion([FromBody] DeletionInput input)
{
    // Based on the inputs, gather a list of Names to be deleted
    // and assign an ID to it. The ID is returned in the response
}

The response of this endpoint returns a location with the deletion ID (e.g. api/names/deletions/3abf5784), so that the client can order the effective delete afterwards.

---> b) Delete request to deletion item created: DELETE api/names/deletions/3abf5784

  • This request deletes the Names list related with the ID 3abf5784. The action would be like:
[HttpDelete("deletions/{id}")]
public Task<IActionResult> DeleteByDeletionId([FromRoute] string id)
{
    // the Names in the deletion list are effectively deleted only now
}

(More info on this option here)


  1. DELETE with body. HTTP delete requests may have body (reference).
  • Request ex.:
DELETE api/names
{
   'filenames': [],
   'isValid': false
}
  • Action ex.:
public class DeletionInput
{
    public string[] FileNames { get; set; }
    public bool? IsValid { get; set; }
}

[HttpDelete]
public Task<IActionResult> Delete([FromBody] DeletionInput input)
{
    // code to delete, but avoid delete with null/empty parameters (avoid delete all)
}
  • It is also a simple option. ASP.NET supports it, but there may be vendors that do not. However, it is allowed by the HTTP specs.
Related