Having issues with documenting the dynamic keyword in C# ASP.NET Core - dynamic

We utilize Pagination, and we have a handy little class that helps us, some of it looks like this:
public class PagedResponse<T>
{
public PagedResponse(HttpRequest request, IQueryable<dynamic> queryable,
string maxPageSizeKey, dynamic options, int pageNumber, int pageSize)
{
//code
}
public dynamic Data { get; set; }
At some point, we execute this and assign it to Data:
List<dynamic> dataPage = queryable
.Skip(skip)
.Take(take)
.ToList();
Because this class utilizes the dynamic type, here is what Swagger generates (imagine the type we pass to our PagedResponse class is Foo):
{
"PageNumber": 0,
"PageSize": 0,
"MaxPageSizeAllowed": 0,
"FirstPage": "string",
"NextPage": "string",
"LastPage": "string",
"TotalPages": 0,
"TotalRecords": 0,
"Data": {} <---- Here we want it to describe Foo
}
I also notice that when you click on 'schema' it gives it the concatenated name FooPagedResponse.
The fact that swagger doesn't give any information about Data is becoming a sticking point for our React developers who utilize some utility to grab schemas.
Now, here's the thing, if you replaced anywhere that I used dynamic with T, swagger, of course, would be happy. But that's no solution, because we have to use dynamic.
Why? OdataQueryOptions and the possibility of using the odata $select command.
Before we are executing the queryable (the .ToList above), we are doing something like this (as well as other odata commands):
if (options.SelectExpand != null)
{
queryable = options.SelectExpand.ApplyTo(queryable, settings) as IQueryable<dynamic>;
}
(options was the "dynamic options" passed to the constructor, this is the ODataQueryOptions)
Once you apply a $select you can no longer assign the .ToList to List<T> it has to be List<dynamic>, I get an exception otherwise about not being able to convert types.
I can't take away the ability to $select just to get proper documentation. What can I do here?

While this is not a direct answer to the original post, there is a lot to unpack here, too much for a simple comment.
Firstly, the original question is essentially: How to customise the swagger documentation when returning dynamic typed responses
To respond to this directly would require the post to include the swagger configuration as well as an example of the implementation, not just the type.
The general mechanism for extending Swagger docs is by implementing IDocumentFilter or IOpertationFilter so have a read over these resources:
IOperationFilter and IDocumentFilter in ASP.NET Core
Is there a way to get Swashbuckle to add OData parameters
Because you are using a dynamic typed data response the documentation tools cannot determine the expected type so you would need to provide that information, if it is available, in another way, there are many different addons and code examples out there, but I haven't come across an implementation like this, so can't provide a specific example. Most of OData configuration is derived through reflection, so if you have obfuscated the specific implementation through dynamic most of the OData tools and inner workings will simply fail.
Now, here's the thing, if you replaced anywhere that I used dynamic with T, swagger, of course, would be happy. But that's no solution, because we have to use dynamic.
The only reason that your code requires you to use a dynamic typed response is because it has been written in a way as to require that. This is a constraint that you have enforced on the code, it has nothing to do with EF, OData or any other constraints outside of your code. When a $select operation is applied to project a query result, we are not actually changing the shape of the data at all, we are simply omitting non-specified columns, in a practical sense their values will be null.
OData is very specific about this concept of projection, to simplify the processing at both the server and client ends, a $select (or $expand) query option can not modify the schema at all, it only provides a mask that is applied to the schema. It is perfectly valid to deserialise a response from a $select projection into the base type, however the missing fields will not be initialized.
It is not always practical to process the projected response in the base type of the request, so many clients, especially any late-bound languages will receive the data in the same shape that it was transferred over the wire in.
While you could go deep into customising the swagger output, this post smells strongly like an XY problem, OData has a built in mechanism for paging IQueryable<T> responses OOTB, yes the response is different to the PagedResponse<T> class that you are using, it is more than adequate for typical data paging scenarios and it's implementation is very similar to your custom implementation.
If you use the in-built paging mechanism in OData, using the $skip and $top query options, then your code implementation will be simpler and the swagger documentation would be correct.
You have admitted to being new to OData, so before blindly customising standard endpoints it is a valuable experience to first gain an understanding of the default behaviours and see if you can re-align your requirements with the standards.
A key driving reason to adopt OData in the first place is to be standards compliant, so that clients can make calls against your API following standard conventions and code generation tools can create reliable client-side interfaces. Once you start customising the in-built behaviours you must then customise the documentation or $metadata if you want to maintain compatibility with those types of processes.
By default, when a $top query option is provided, the response will include a link to get the next page of results. If you have not enabled the count to be provided automatically, you can use the $count=true query option to enable the overall count to be provided in the output.
The key difference between your custom implementation and the OData implementation is that the client is responsible for managing or maintaining the list of Page Numbers and translating them into $top and $skip values.
This is a deliberate push from the OData team to support virtual paging or load on demand scrolling. The expectation is that the user might get to the bottom of a list and either click to see more, or by virtue of reaching the end of the current list more records would be dynamically added to the previous set, becoming a delayed load yet continuous list.
To select the 3rd page, if the page size is 5, with both a projection and a filter we could use this url:
~/OData/Companies?$top=5&$skip=10&$select=CompanyName,TradingName&$filter=contains(CompanyName,'Bu')&$count=true
{
"#odata.context": "~/odata/$metadata#Companies(CompanyName,TradingName)",
"#odata.count": 12,
"value": [
{
"CompanyName": "BROWERS BULBS",
"TradingName": "Browers Bulbs"
},
{
"CompanyName": "BUSHY FLOWERS",
"TradingName": "Bushy Flowers"
}
],
"#odata.nextLink": "~/OData/Companies?$top=5&$skip=10&$select=CompanyName%2CTradingName&$filter=contains%28CompanyName%2C%27Bu%27%29"
}
This table has thousands of rows and over 30 columns, way to much to show here, so its great that I can demonstrate both $select and $filter operations.
What is interesting here is that this response represents the last page, the client code should be able to interpret this easily because the number of rows returned was less than the page count, but also because the total number of rows that match the filter criteria was 12, so 2 rows on page 3 when paging by 5 was expected.
This is still enough information on the client side to build out links to the specific individual pages but greatly reduces the processing on the server side and the repetitive content returned in the custom implementation. In other words the exact response from the custom paging implementation can easily be created from the standard response if you had a legacy need for that structure.
Don't reinvent the wheel, just realign it.
Anthony J. D'Angelo

Related

How to register two controllers same name, different namespace in same ODATA EDM Model?

Objective
The objective is to better understand how to develop modular (plugins by 3rd parties) approach to developing APIs, on top of ASP.NET Core.
To make plugin development easy to pick up it should rely on Conventions where possible, and deliver high value.
OData's queryability and being an Standard remains a compelling improvement to REST, providing a lot of bang for effort.
Issues
OData may be powerful, but the availability of current documentation remains sub par, hence unsure of its limits/capabilities.
Hence questions regarding what options one has to untangling routing issues due to conflicting endpoints from controllers in 3rd party modules.
Specifically:
Q1: Can a single OData EDM model disambiguate between two Controllers with the same name in two different namespaces?
Q2: Can one register an ODataController with a different Route than the ODataController name (eg route Foo points to BarController, when the convention is it would look for a FooController) without breaking default functionality? (eg: $count stops working for me)
Q2b: Even if we take over parsing incoming Uris into its odata path components as well as the logic it uses to find the relevant Controller?
Q3: If it can't, would the more practical approach to loading Plugins be for each Plugin to register its own EDM models?
Q4: Will using multiple EDM models work even if the second plugin/edm model has Controllers that exposes Models that has properties that refer back to Models exposed by Controllers in the base (or another dependency Plugin) EDM model?
Eg: Base EDM exposes Persons and Addresses, and second plugin EDM exposes maybe Customers, which has properties referencing Person and Addresses types, provided by the base EDM?
(I'm guessing it will work, but not 100% sure...anybody see an issue with this?)
Q5: How can one add new EDM models dynamically, resetting the routes?
For example if one uploaded a nuget package that was a plugin, and it contained its own EDM model describing its Models and Controllers...all this happening way after after.Run() was invoked, how can one kick the system in the head to refind/relearn what are valid routes?
Q6: Why is $count only available on ODataControllers that are registered by Convention, but doesn't work on those registered by RouteAttribute?
Background
The latest(?) documentation I found in a blog post here:
https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/
But it's just a blog post, and a lot remains missing to understand all the pieces and how they fit together.
Process So Far
I'm listing my notes below to help others see better what I am trying, in case someone can see what I am obviously getting wrong.
And if it helps and you want working code, I've uploaded my investigation efforts (actually thrashingArounds):
https://github.com/skysgh/Spikes.AspNetCore.ODataRouting
Setup
The default scenario in OData is to register EDM models with Controller Routes equal to the prefix of the name of the Controller.
Eg:
static string ODataPrefixWithSlash = "api/odata/v{version}/"
class SomeModelController : ODataController {...}
//is registered in an EDM model using convention of matching prefix of controller name:
var builder = new ODataConventionModelBuilder();
//to build a whole model:
builder.EntitySet<SomeModel>("SomeModel"); //This will get found
builder.EntitySet<SomeModel>("Renamed"); //Without further work causes 404 as route string != controller prefix
var edmModelA = builder.Build();
//and the model later registered as the source of OData routing info:
var mvcBuilder = builder.Services
.AddControllers()
.AddOData(
opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)
//Add Module/PluginA Routes:
.AddRouteComponents(AppAPIConstants.ODataPrefixWithSlash,edmModelA);
if one has enabled
builder.Services.AddSwaggerGen();
app.UseODataRouteDebug();
one can navigate to ~odata and see the controller's Endpoints listed as:
~api/odata/v{version}/SomeModel
~api/odata/v{version}/SomeModel/$count <- note, showing for now (will break later...why?!)
Controller Choice Process
An article exists that talks about how routing has been updated in OData v8 RC, relying on RouteAttribute
https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/
After a lot of messing around, appears one can use a different name but
only IF one starts the route with the same prefix used to register the EDM Model (eg: api/odata/v{version}/
[ODataAttributeRouting]
// Half Works
// because the route starts with the same base as what the model was registered under
// fools it(?) (where?) into accepting it as an Odata controller.
// a) listed in ~/$odata as an odata controller (under api/odata/v{version}
// b) acting as an Odata controller (returning odata wrapper in json)
// c) but no default queryability (doesn't accept or list /$count)
// as the route starts with same prefix
// as Convention used when registereing EDM model
// But $count doesn't work!
[Route(AppAPIConstants.ODataPrefixWithSlash + "Renamed5")]
public class ValuesA5Controller : ODataController{
[EnableQuery(PageSize = 100)]
[HttpGet("")]
[HttpGet("Get")]
public IActionResult Get()
{
return Ok(FakeDataBuilder.Get());
}
}
The above permits registering the ODataController in the EDM model as follows:
builder.EntitySet<SomeModel>("Renamed5");
But it only HALF works.
it is listed as an OData Controller in $odata. ok.
it is acting (mostly) as an ODataController in that it accepts most OData commands $select, $filter, etc.
But it is failing at offering $count for some reason:
~api/odata/v{version}/Renamed5 <- showing, same as it was doing for earlier SomeModelController
~api/odata/v{version}/Renamed5/$count <- not showing, even though endpoint decorated with [EnableQuery].
Options?
Option A: More Route information
I suspect that one could decorate the Get method with more routes to enable $count:
[EnableQuery(PageSize = 100)]
[HttpGet("")]
[HttpGet("Get")]
[HttpGet("$count") ??? <- really???
public IActionResult Get() {....}
Certainly wish to avoid adding more codes as workaround (eg: what was tried here: https://stackoverflow.com/a/73042175/19926885)
but even if either worked, its more code, more sources of errors, etc. to watch out for.
If at all possible I'd like the least code, the most convention, while not locking in controller names.
Option B: Controller Selection process
As I said earlier, I don't know how or where the odata framework is magically matching "SomeModel" to "SomeModelController".
In an article I came across (https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/)
I saw mention of AttributeRoutingConvention: IODataControllerActionConvention which maybe could be put to use, but the blog post
didn't show when/how it could be registered, or replaced so have not been able to progress in that direction yet.
Also, iterating through registered Services I don't see anything inheriting from IODataControllerActionConvention. What's going on?
What I do see is:
//not sure yet what these do
IODataQueryRequestParser
IODataTemplateTranslator : Microsoft.AspNetCore.OData.Routing.Template.DefaultODataTemplateTranslator
IODataPathTemplateParser : Microsoft.AspNetCore.OData.Routing.Parser.DefaultODataPathTemplateParser`
But I don't have documentation on how they work, what they are for, etc.
Where next?
First of all THANK YOU for taking the time to read this long question!
Second, if you have advice as how I not thinking right as to how to solve the problem, that would help.
Third, if you are able to provide answers to the questions...wow. It's been a while I've looked for answers to this!
Took me a while to recognise the following basic misunderstandings on my part.
OData routing not something else but is an instance of the underlying WebAPI Routing
When one registers an EDM and providing the route prefix ("api/odata/v{version}"), it's just registering "~/api/odata/v{version}/~" as an action/route filter.
If you don't provide a [RouteAttribute] the convention is that it will make it up the Route segment from the Controller name (eg: 'PersonController' means that 'Person' will be used.
If you do provide a [RouteAttribute] on the Controller, you are saying this is the full (not a suffix!) route -- even if you provided a prefix when registering the EDM model.... To underline that point: just because HttpGet(...) acts as a suffix to the Route, doesn't meant the Route does too.
Which means that if it doesn't start with [Route("api/odata/v{version}")] -- maybe is just [Route("Renamed6")] -- it **doesn't match the route prefix used to register the edm model, so won't be handled by the models handler...so all it's doing is acting like all WebAPI controllers are doing and registers another route (whatever you gave), but won't consider it worthy of being OData queryability enabled....
So your route has to be [Route("api/odata/v{version}/Renamed6")].
Other than that, basics are that you'll get it confused if you register two models one with
"api/odata/v{version}", and another with
"api/odata/v{version}/plugin".
The first handler/whatever (i don't know what's behind the scenes here) will probably try to capture requests to plugin and not find a controller called "PluginController". I'm just guessing.
Finally, I think I saw issues with leaving dynamics tokens at the end for some reason. I had to change from using api/odata/PluginA/v{version} to api/odata/v{version}/PluginA . Not clear why.
Admittedly I still need to know a LOT more about the routing mechanism, but for now, this gets me forward again.
PS: Also...use Constants for building route strings. Turns out that Typos can really waste a lot of time :-(

Patterns when designing REST POST endpoint when resource has a computed property

I have a resource, as an example a 'book'.
I want to create a REST POST endpoint to allow consumers to create a new book.
However, some of the properties are required and computed by API, and others were actually taken as they are
Book
{
name,
color,
author # computed
}
Let's say the author is somehow calculated in API based on the book name.
I can think of these solutions each has its drawbacks:
enforce consumer to provide the author and just filter it (do not take into account as an input) # bad because it is very unpredictable why the author was changed
allow the user to provide author # same problem
do not allow the user to provide an author and show an exception if the user provides it
The last solution seems to be the most obvious one. The main problem I can see is that it is inconsistent and can be bizarre for consumers to see the author later on GET request.
I want my POST endpoint to be as expressive as possible. So the POST and GET data transfer objects will look almost the same.
Are there any simple, expressive, and predictable patterns to consider?
Personally I'm a big fan of using the same format for a GET request as well as a PUT.
This makes it possible for a client to do a GET request, add a property to the object they received and immediately PUT again. If your API and clients follow this pattern, it also means it can easily add new properties to GET requests and not break clients.
However, while this is a nice pattern I don't really think that same expectation exists at much for 'creation'. There's usually many things that make less less to require as a property when creating new items (think 'id' for example), so I usually:
Define a schema for PUT and GET.
Define a separate schema for POST that only contains the relevant properties for creation.
If users supply properties not in the schema, always error with a 422.
some of the properties are required and computed by API
Computed properties are neither required nor optional, by definition. No reason to ask consumers to pass such properties.
do not allow the user to provide an author and show an exception if the user provides it
Indeed, DTO should not contain author-property. Consumers can send over network whatever they want, however it is the responsibility of the API-provider to publish contract (DTO) for consumers to use properly. API-provider controls over what properties to consider, and no exception should be thrown, as the number of "bad" properties that can be sent by consumers is endless.
So the POST and GET data transfer objects will look almost the same
Making DTOs of the same resource look the same is not a goal. In many cases, get-operation exposes a lot more properties than post-operation for the same resource, especially when designing domain-driven APIs.
Are there any simple, expressive, and predictable patterns to consider?
If you want your API to express the fact that author is computed, you can have the following endpoints:
POST http://.../author-computed-books
GET http://.../books/1
Personally, I wouldn't implement that way since it does not look natural, however you can get the idea.
I want my POST endpoint to be as expressive as possible. So the POST
and GET data transfer objects will look almost the same.
Maybe just document it instead of relying explicit stuff like it must be almost the same as the GET endpoint.
E.g. my POST endpoint is POST /number "1011" and my GET endpoint is GET /number -> 11. If I don't document that I expect binary and I serve decimal, then nobody will know and they would guess for example decimal for both. Beyond documentation another way of doing this and to be more explicit is changing the response for GET to include the base {"base":10, value:"11"} or changing the GET endpoint GET /number/decimal -> 11.
As of the computed author I don't understand how you would compute it. I mean either a book is registered and the consumer shouldn't register it again or you don't know much about the author of it. If the latter, then you can guess e.g. based on google results for the title, but it will be a guess, not necessarily true. The same with consumer data, but at least that is what the consumers provided. There is no certainty. So for me it would be a complex property not just a primitive one if the source of the information matters. Something like "author": {name: "John Wayne", "source": "consumer/service"} normally it is complex too, because authors tend to have ids, names, other books, etc.
Another thought that if it is weird for the consumers instead of expected, then I have no idea why it is a feature at all. If author guessing is a service, then a possible solution is making the property mandatory and adding a guessing service GET /author?by-book-name={book-name}, so they can use the service if they want to. Or the same with a completely optional property. This way you give back the control to the consumers on whether they want to use this service or not.

How to convert a LinqExpression into OData query URI

There are a lot of answers on how to convert ODataQuery into an Expression or into a Lambda, but what I need is quite the opposite, how to get from a Linq Expression the OData query string.
Basically what I want is to transcend the query to another service. For example, having 2 services, where your first service is not persisting anything and your second service is the one that will return the data from a database. Service1 sends the same odata request to Service2 and it can add more parameters to the original odata request to Service2
What I would like:
public IActionResult GetWeatherForecast([FromServices] IWeatherForcastService weatherForcastService)
{
//IQueryable here
var summaries = weatherForcastService.GetSummariesIQ();
var url = OdataMagicHelper.ConvertToUri(summaries);
var data = RestClient2.Get(url);
return data;
}
OP Clarified the request: generate OData query URLs from within the API itself.
Usually, the queries are so specific or simple, that it's not really necessary to try and generate OData urls from within the service, the whole point of the service configuration is to publish how the client could call anything, so it's a little bit redundant or counter-intuitive to return complex resource query URLs from within the service itself.
We can use Simple.OData.Client to build OData urls:
If the URL that we want to generate is:
{service2}/api/v1/weather_forecast?$select=Description
Then you could use Simple.OData.Client:
string service2Url = "http://localhost:11111/api/v1/";
var client = new ODataClient(service2Url);
var url = await client.For("weather_forecast")
.Select("Description")
.GetCommandTextAsync();
Background, for client-side solutions
If your OData service is a client for another OData Service, then this advice is still relevant
For full linq support you should be using OData Connected Services or Simple.OData.Client. You could roll your own, or use other derivatives of these two but why go to all that effort to re-create another wheel.
One of the main drivers for a OData Standard Compliant API is that the meta data is published in a standard format that clients can inspect and can generate consistent code and or dynamic queries to interact with the service.
How to choose:
Simple.OData.Client provides a lightweight framework for dynamically querying and submitting data to OData APIs. If you already have classes that model the structure of the API then you can use typed linq style query syntax, if you do not have a strongly typed model but you do know the structure of the API, then you can use either the untyped or dynamic expression syntax to query the API.
If you do not need full compile-time validation of your queries or you already have the classes that represent the resources served by the API then this is a simple enough interface to use.
This library is perfect for use inside your API logic if you have need of generating complex URLs in a strongly typed style of code without trying to generate a context to manage the connectivity to the server.
NOTE: Simple.OData.Client is sometimes less practical when developing against a large API that is rapidly evolving or that does not have a strict versioned route policy. If the API changes you will need to diligently refactor your code to match and will have to rely on extensive regression testing.
OData Connected Services follows a pattern where some or all of the API is modelled in the client with strongly typed client side proxy interfaces. These are POCO classes that have the structure necessary to send to and receive data from the server.
The major benefit to this method is that the POCO structures, requests and responses are validated against the schema of the API. This effectively gives you full intellisense support for the API and allows you to explor it's structure, the generated code becomes your documentation. It also gives you compile time checking and runtime safety.
The general development workflow after the API is deployed or updated is:
Download the $metadata document
Select the Operations and Types from the API that you want to model
Generate classes to represent the selected DTO Types as defined in the document, so all the inputs and outputs.
Now you can start using the code.
In VS 2022/19/17 the Connected Services interface provides a simple wizard for establishing the initial connection and for updating (or re-generating) when you need to.
The OData Connected Service or other client side proxy generation pattern suits projects under these criteria:
The API definition is relatively stable
The API definition is in a state of flux
You consume many endpoints
You don't want to manually code the types to serialize or deserialze payloads
Full disclosure, I prefer the connected service approach, but I have my own generation scripts. However if you are trying to generate OData query urls from inside your API, its not really an option, it creates a messy recursive dependency... just don't go there.
Connected services is the low-(manual)-code and lazy approach that is perfect for a stable API, generate once and never do it again. But the Connected Service architecture is perfect for a rapidly changing API because it will manage the minute changes to the classes for you, you just need to update your client side proxy classes more frequently.

Exposing Sequelize filtering mechanics on API

I've been looking into some Node ORMs for use with PostgreSQL lately, and would like to expose some type of flexible filtering on the front end.
I'm quite enjoying the flexibility provided by Sequelize's where/include filtering (e.g. filtering a model based on some relation N levels deep).
Is the filtering mechanism safe at all to expose to any front end API? I haven't had much experience with it, so I"m not sure what types of fields can be passed through to the filter query.
Otherwise, for more complex querying I may go with something like Knex instead.
Generally if you need to ask if it is safe to expose where/include parameter passing directly to your frontend, I would suggest you shouldn't do it. If you don't know how it will behave you'll end up fast leaking all your users and password hashes to the world.
So you'll be better covered by validating incoming filtering parameters and only after that pass them to the query. You can use for example json schema to validate the incoming parameters.
For example in objection.js ORM this thing is handled in a way that you can give the query inside your code certain pattern what data user can include to response, and then incoming user input is automatically reduced to that subset (this works only when one requests additional relations to the requested row).
var houseWithPossiblePetsAndOwner = await House.query()
.allowEager('[pets, owner]')
.eager(eagerParamDirectlyFromEndUser)
.where('id', id);
You could extend your preferred ORM to support that kind of extra method, which allows you to declare for the query which parameters can be passed to it from the user input.

Searching with WebAPI

I have made a web API class for my Customer model. I have the standard methods (GET, POST, PUT, DELETE). The problem is, I want to implement another GET method which is a search. Something like this:
[HttpGet]
public IEnumerable<Customer> Search(string id)
{
var customers = customerRepository.Search(id);
return customers;
}
The search method performs a search based on the account number of my customers, using the .Contains() method.
The problem is, when I navigate to: mySite.com/api/Customers/Search/123 I get a 404. What am I doing wrong here?
While Darin's answers are always of top quality this question would actually benefit from an answer that explains how searching, paging and filtering should actually be done in any API and how it should be done using the most current version of Web API (v2).
This is a post which I consider a good resource on the matter (technology indenpendent):
http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
The answer should also reflect what's new in ASP.NET Web API v2 because Darin's answer is quite old.
Since this question comes up at the top when doing Google search for "asp.net web api searching" I will try to explain few things here.
To get as close as possible to REST principles with the latest version of ASP.NET Web API (v2) one should take a serious look at attribute routing that was introduced in the latest version. It is very hard to achieve RESTful routing with the old, classic, convention based routing (in global.asax.cs or RouteConfig.cs).
You should read more about that here
http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Now, to go into details how to implement the specifics you ask about.
The most common practice is to expose these types of functionality through query string parameters.
Per REST principles, you should have one endpoint for your Customers resource, for instance
/api/customers
To achieve this you would decorate your GetCustomers() action in your Web API controller like this
[HttpGet]
[Route("/api/customers")]
public HttpResponseMessage GetCustomers(string q="", string sortBy="", string sortDirection="", bool active=true, ...)
{
// q = being optional search query
// sortBy = optional sort by column/property
// sortDirection = optional sort direction
// active = filter on 'active' column/property
// ... other filters may be applicable
}
You would implement this action closely to what you did in classic MVC if you wanted to provide filtered Views.
I would only introduce new controllers and custom actions if really needed, for some custom edge cases.
with regards to a comment about SearchFilter strongly typed object, let's explain that this won't work out of the box because the default model binder will not bind to this class when using GET requests.
So I'd either take those properties out of SearchFilter class and put them on the action itself so they'd bind via query string binder or use the [FromBody] binder if you wanted to bind from the request body. As per http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
HTH
As per the default route setup only the standard controller action names are allowed (the RESTful ones and the dispatching is done based on the HTTP verb). If you want to violate the RESTful conventions and use some custom action names then you will have to modify your route setup in order to include the action name in the url: api/{controller}/{action}/{id}. Now you can send a request to /api/Customers/Search/123 which will invoke the Search action on the Customers API controller.