OData Actions in .NET Core

ODATA API AND .NET CORE: CREATING AND USING ODATA ACTIONS

by

In the previous tutorial, you learned how to create a function in an OData API. Such a function could accept multiple parameters and return relevant data corresponding to those parameters. An OData action, on the other hand, is better suited for modifying a database entity. In this tutorial, you will learn how to create and configure an OData action.

Create OData Action

Whereas an OData function is generally associated with an Http GET request, an Action can be associated with a POST request. For this example, you will build off the previous tutorial where you created a project to track your favorite open source contributors. In that tutorial, you created a standard GET method to return a specific project by its Project Id. Then you used a special OData function to add a GET endpoint ByProjectByContributor to filter and return only a project's pull requests that were made by a specific contributor.

Recall, we are using the following model for the Project entity in this example.

public class Project
{
	public int Id { get; set; }
	public string Title { get; set; }
	public List PullRequests {get; set; }
}

Now, suppose you want to change the Title of a given project. This is a trivial example, as you could simply PUT or PATCH a specific project with key Id. For example, PUT: api/projects(5)

Instead, we will create an OData action with its own endpoint that is designed to update the Title of a specific project. Let's create an action with the following endpoint:

POST: api/projects(5)/SetTitle

Your controller action may look like the following

// POST: api/projects(5)/SetTitle       
[HttpPost]
[ODataRoute("({id})/SetTitle")]
public async Task<IActionResult> SetTitle([FromODataUri] int id, ODataActionParameters parameters)
{
    if (!ModelState.IsValid)
    {
        return BadRequest();
    }

    string newtitle = (int)parameters["Title"];            
 
    var project = await _context.Projects
        .Where(p => p.Id == id)
        .FirstOrDefaultAsync();
 
    project.Title = newTitle;
	
	_context.Update(project);
	await _context.SaveChangesAsync();
}

Of course, you would want to enclose the SaveChangesAsync() command in a try-catch block in order to catch any DbUpdateConcurrencyException exceptions.

Register OData Action

To use your new OData action, you must first register it in your EDM model builder, just like you did with the OData functions. If you are using endpoint routing and the GetEdmModel() method from the previous tutorial, bind your action to the Project entity type and register the Title parameter.

private IEdmModel GetEdmModel()
{
    var builder = new ODataConventionModelBuilder();
    builder.EntitySet<Project>("Projects");
	
	...
 
    builder.EntityType<Project>()
        .Action("SetTitle")
		.Parameter("Title").Required();
		
	...
 
    return builder.GetEdmModel();
}

Make Call to OData Action

You might be wondering how to pass the parameters to the controller action. For OData actions, all parameters are passed in the payload of the POST request as Json. For example:

POST: api/projects(5)/SetTitle

{
   "Title": "Super Cool C# Project"
}

This can be made easier by creating a service class with a method that handles the Json encoding for you. Such a method might look like:

public class ApiService
{
	public HttpClient _httpClient;

	public ApiService(HttpClient client)
	{
		_httpClient = client;
	}
	
	...
	
	public async Task<HttpResponseMessage> SetTitle(int projectId, string newTitle)
	{
		TitleModel title = new TitleModel() { Title = newTitle };
		string json = JsonSerializer.Serialize(title);
		var stringContent = new StringContent(json, System.Text.Encoding.UTF8, "application/json");

		return await _httpClient.PostAsync($"api/projects({projectId})/SetTitle", stringContent);            
	}
	
	...
}

Where the TitleModel object is derived from a class that contains any parameters you want to pass to the controller action.

public class TitleModel
{
    public string Title { get; set; }
}

Once you register the service, you can inject it into a Blazor page, view, or even a controller as a dependency and invoke the SetTitle() method.

Why Use OData Actions?

Again, this is a somewhat trivial example of how to use an OData controller action. In real-world usage, your action may include complex logic and calculations. It can even be used to modify multiple entities across different entity types. For example, suppose you want to become GDPR compliant and allow a user to delete all their data that is stored on your server? You could create an OData action to generate a list of entities they control and remove them by issuing one simple API call.

As you can see, OData actions are a powerful tool to add to your API building toolbelt. Let me know how you plan to use OData actions, and, as always, if you have any questions or thoughts, let me know in the comments below.


Don't stop learning!

There is so much to discover about C#. That's why I am making my favorite tips and tricks available for free. Enter your email address below to become a better .NET developer.


Did you know?

Our beautiful, multi-column C# reference guides contain more than 150 tips and examples to make it even easier to write better code.

Get your cheat sheets