OData API Multiple Parameters

The Open Data Protocol is a flexible protocol for creating APIs. This tutorial will demonstrate how to pass multiple parameters to an API endpoint. You can use this technique to create multiple GET endpoints instead of relying on OData filters.

Example Models

Suppose you have a project where you are tracking your favorite open source projects and your favorite open source contributors. You might have a database table that holds a list of projects, a table that holds a list of contributors, and a table of pull requests. The model pull request table might be as follows:

public class PullRequest
{
	public int Id { get; set; }
	public string Title { get; set; }
	
	public int ProjectId { get; set; }
	public int ContributorId { get; set; }
	
	public Project Project { get; set; }
	public Contributor Contributor { get; set; }
}

The model includes the pull request title, project ID, and ID of the contributor. It also includes navigational properties that will link to the related data, namely the Project and Contributor. In some ways, the pull requests table can be seen as a many-to-many join table with payload that links projects and contributors. A project can have many contributors, and a contributor can contribute to many projects.

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

Using OData Filters

Suppose, you wanted to retrieve a list of pull requests for a specific project, by a specific contributor. The classical approach would be to retrieve a specific project, including the PullRequests collection, and then filter the collection by ContributorId.

First, in the GetProject (or equivalent) method of ProjectsController, you must make certain to include PullRequests when fetching from the database.

[EnableQuery]
[ODataRoute("({id})")]
public async Task GetProject([FromODataUri] int id)
{
	...
	var project = await _context.Projects.Where(s => s.Id == id)
		.Include(s => s.PullRequests)
		.FirstOrDefaultAsync();
	...
}

Then, expand the list of Pull Requests when you fetch a specific Project. You could then filter the pull requests by ContributorId to return only those by the specific contributor.

GET api/projects(1)?$expand=PullRequests&$filter=PullRequests/ContributorId eq 5

One thing worth noting about this approach: Because this query is hard-coded as a string, if you change the name of a variable in the backend model, Visual Studio’s IntelliSense will not be able to help you refactor this instance.

Using OData Functions

If the pull requests themselves are the primary focus of your project, you might have a PullRequestsController with a GET endpoint to fetch all pull requests and a GET endpoint to fetch a specific pull request by its ID. In this case, it could be useful to create an API endpoint to help you filter by ProjectId and ContributorId.

// GET: api/pullrequests/ByProjectByContributor(projectId=1,contributorId=5)
[ODataRoute("ByProjectByContributor(projectId={projectId},contributorId={contributorId})")]
public IQueryable<PullRequest> ByProjectByContributor([FromODataUri] int projectId, [FromODataUri] int contributorId)
{
    var pullRequests = _context.PullRequests.Where(
        pr => pr.ProjectId == projectId
        && pr.ContributorId == contributorId);
	return pullRequests;
}

Before it works, you must register the function in your OData EDM model. Open Startup.cs of your OData API project. I am using endpoint routing with route parsing performed by an EDM model fetched from a method GetEdmModel().

app.UseEndpoints(endpoints =>
{
    endpoints.Select().Filter().OrderBy().Expand().Count().MaxTop(50);
    endpoints.MapODataRoute("api", "api", GetEdmModel());
});

Locate your GetEdmModel() (or equivalent) method and register the function you created. You must also register each of the parameters your function uses. You can configure parameters as optional or required.

private IEdmModel GetEdmModel()
{
    var builder = new ODataConventionModelBuilder();
    builder.EntitySet<Project>("Projects");
    builder.EntitySet<Contributor>("Contributors");
    builder.EntitySet<PullRequest>("PullRequests");
 
    var pullRequestsByProjectByContributor = builder.EntityType<Project>().Collection
        .Function("ByProjectByContributor")
        .ReturnsCollectionFromEntitySet<Project>("Projects");
    pullRequestsByProjectByContributor.Parameter<int>("projectId").Required();
	pullRequestsByProjectByContributor.Parameter<int>("contributorId").Required();
 
    return builder.GetEdmModel();
}

If you set an optional parameter, be sure your controller has logic for handling it, such as a default value or a conditional check.

Now you have an extra GET endpoint you can use to pass multiple parameters to your OData API.

GET: api/pullrequests/ByProjectByContributor(projectId=1,contributorId=5)

This approach is especially useful if you plan to create a filtering component on your frontend. Rather than building a complex query string for filtering, you can simply pass the parameters from the frontend filter component to this custom function to fetch only the results your end user wants.

The Bottom Line

In this tutorial, you learned how to add multiple GET endpoints to an OData API. You also learned how to pass multiple parameters to an OData endpoint by registering a custom function in your EDM model. You can use this technique to create as many custom endpoints as your project requires, thus helping keep your data processing on the backend.


Was this article helpful? Share it with your friends:

Share on email
Share on facebook
Share on twitter
Share on reddit

1 thought on “OData API Multiple Parameters”

  1. Hello,
    Great tutorial. Thank you. The question I have is related to the operation of Blazor with a remote Odata Api.

    Lets say the Odata Api sits on top of an Entity Framework Core context that comprises a central table and numerous one to one relationships back to the central table.

    For example lets say the central table is about applications (Application) and the other tables are about Deployment Country, Classification, Assigned Manager etc.

    The goal of the web application (Http Client) is to present a flexible Query front end to the remote Api, so the ability to select from multiple pull down lists e.g. select Applications from this Geography with these other factors, defined by pull down lists.

    First question is whether in an Odata context, it makes sense to have one big Application table, with all content, or a smaller application table with numerous one to one mappings?

    Odata seems to like Ids for queries, hence the consideration of a smaller central table and many one to one mappings, rather than buikding an application and using string matching within the Odata Uri’s.

    Next question is how to manage the Id’s in the scenario where there is a smaller Application table and many one to one mappings. E.g. how to make use of SelectListItem from Blazor based on SelectListItems from the Api application, and whether that even makes sense?

    In order to populate the Http Client Query interface, the possible content of each pull down search option needs to be represented in the Blazor application and therefore come from Entity Framework Core in some way. I don’t thing Json serialisation of SelectListItem is the correct approach, maybe it is.

    Welcome your thoughts and Regards

    Reply

Leave a Comment

Looking for more?

You have visited our site before, and we appreciate you!

If you found the tutorials helpful, enter your email address for more free C# tips and tricks.

Can't get enough C#?

Enter your email address for more free C# tutorials and tips.