DEV Community

Bradley Wells
Bradley Wells

Posted on • Originally published at wellsb.com on

OData API Multiple Endpoints, Multiple Parameters

Original Article

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.

Source

Top comments (0)