ASP.NET Core OData Action With Two Parameters - asp.net-core

I have this action method available to OData:
[HttpPost]
[ODataRoute("({id})/Replace")]
public Blog Replace([FromODataUri] int id, Blog blog)
This responds for POST requests on /odata/Blogs(1)/Replace.
It is working except for the part that the blog parameter is never bound from the POST, that is, it is not null, but has default values for all properties. If I add [FromBody] to the parameter, it becomes null. I also tried with a parameter of type ODataActionParameters, but it is always null.
This is the relevant part of my model:
var replace = builder.EntitySet<Blog>("Blogs").EntityType.Action("Replace");
//replace.Parameter<int>("id").Required(); //this can be added or not, it doesn't matter
replace.EntityParameter<Blog>("blog").Required();
replace.Returns<int>();
I read somewhere that an OData action cannot have two parameters, but I am not sure of that. I need the id parameter to be present, so I need to find a solution for this.
I am using ASP.NET Core 3.1 and the latest versions of all packages.
What am I doing wrong?

The solution turned out to be simple:
The parameter declaration was wrong: the name of the entityset is “blogs”, not “blog”
I was posting the JSON for the “Blog” entity, but I had to modify it so as to be included in a parameter “blog”, as this:
{
“blog”: {
“BlogId”: 1,
<< rest of the Blog properties >>
}
}
This solved my problem.

Related

Api Platform pagination custom page_parameter_name

I have very specific question on which I cannot find any answer and/or solution provided for Api Platform.
By default, the documentation states, that if you want to pass a page parameter for paging action, you must do the following:
pagination:
page_parameter_name: _page
However, due to the nature of our frontend we're not able to pass this variable to the request. It is hardcoded to the frontend request and is something like page[number]=1.
Is it possible to configure page_parameter_name to receive this variable or we need to transform it somehow in the Api itself?
Thank you!
ApiPlatform\Core\EventListener\ReadListener::onKernelRequest gets $context['filters'] from the request through ApiPlatform\Core\Util\RequestParser::parseRequestParams which ultimately uses PHP's parse_str function so the value of 'page[number]' will be in $context$context['filters']['page']['number'].
ApiPlatform\Core\DataProvider\Pagination::getPage retrieves the page number from $context['filters'][$parameterName] so whatever the value of [$parameterName] it will at best retrieve the array ['number'=> 1].
Then ::getPage casts that to int, which happens to be 1. But will (at least with PHP7) be 1 for any value under 'number'.
Conclusion: You need to transform it somehow in the Api itself. For example by decoration of the ApiPlatform\Core\DataProvider\Pagination service (api_platform.pagination).
API_URL?page[number]=2
print_r($request->attributes->get('_api_pagination'));
Array(
[number] => 2
)
The value of the "page_parameter_name" parameter should be "number" .
api_platform.yaml
collection:
pagination:
page_parameter_name: number
This may not work in version 3
vendor/api-platform/core/src/JsonApi/EventListener/TransformPaginationParametersListener.php
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$pageParameter = $request->query->all()['page'] ?? null;
...
/* #TODO remove the `_api_pagination` attribute in 3.0 */
$request->attributes->set('_api_pagination', $pageParameter);
}

How to bind optional querystring string parameter on POST

I have the following controller in ASP.NET WebApi 2:
[RoutePrefix("Validations")]
public partial class ValidationsController
{
[HttpPost, Route("Bsb")]
public IHttpActionResult ValidateBsb(string value)
{
var validator = new BankStateBranchValidator(DbContext.BankStateBranches);
var data = new ValidationsResult
{
IsValid = validator.IsValid(value ?? string.Empty)
};
data.Error = data.IsValid
? null
: "The BSB you have entered does not appear to be valid. Please check the value and try again.";
return Ok(data);
}
}
For historical reasons, the value parameter needs to be in the querystring, rather than the form body, which should be empty. So the expected API call would be POST /Validate/Bsb?value=012345.
That all works fine, and I get the expected result; however, sometimes we are getting clients calling the API with POST /Validate/Bsb or POST /Validate/Bsb?value=, and that is resulting in a 400 Bad Request response from WebAPI itself, because, as far as I can tell, the model binder is failing to bind the missing value to the parameter. If I put a breakpoint inside the method, it never gets hit.
So, given that I can't change the API contract, how can I handle this scenario? I've tried adding a [ValueProvider(typeof(RouteDataValueProviderFactory))] attribute to the parameter, and my test case for the missing value works, but then the valid value test cases break since the value isn't in the route but in the querystring.
Update
Based on Craig H's suggestion, I've added a default value to the value parameter. So the various scenarios are:
POST /Validate/Bsb?value=012345 - pass (valid value)
POST /Validate/Bsb?value=000000 - pass (invalid value)
POST /Validate/Bsb?value= - fail (empty value)
POST /Validate/Bsb - pass (missing value)
You should be able to make the parameter optional by specifying a default value in the method signature.
e.g.
[HttpPost, Route("Bsb")]
public IHttpActionResult ValidateBsb(string value = null)
Your question says that a query with ?value= was throwing a bad request.
When I tried this locally my breakpoint was hit and value was null.
If I omitted the QS parameter completely, then I received a method not allowed response.
This page makes mention of optional route parameters with attribute routing, although you are not specifying the parameter like that here.
I cannot find the document which describes the other options with regards to routing and optional parameters. I have seen one which indicates the differences between defining it as optional in the route definition, and optional in the method signature. If I find it, I will update this answer!

AspNet Core WebApi OData - Many to Many join tables

As is the recommended pattern in EF Core these days when you have a many to many join we do something like this:
Fluent API, many-to-many in Entity Framework Core
Having done that I'm faced with the issue as to how I go about exposing that in the OData model.
Since technically the Entity type definition has no key property (as it uses a composite key the OData framework doesn't like me adding the set to the model.
Whats the recommended approach to this problem?
It seems that EF and OData have become somewhat synced up, what would be even better would be if they could literally share model building code.
To that end I found that in the OData model build after calling AddSet I was able to define the key in the same way as I did in EF ...
Builder.EntityType<UserRole>().HasKey(ac => new { ac.UserId, ac.RoleId });
This is somewhat clean, I have not yet tried posting or directly requesting such a type yet, but expanding from either side of the relationship chain seems to work fine.
EDIT: Moredetails on the definition of this in controller ...
The Url needs to contain both key parts ...
HTTP GET ~/UserRole(123, 456)
the same with PUTs and DELETEs but POST's don't contain the key they are part of the object in the body.
The method signature must contain both key parts, here's some examples ...
public IActionResult Get([FromODataUri]int keyUserId, [FromODataUri]Guid keyRoleId)
{
...
}
public IActionResult Put([FromODataUri]int keyUserId, [FromODataUri]Guid keyRoleId)
{
...
}
public IActionResult Post(UserRole entity)
{
...
}

Howto select a ASP.NET Core action from a JSON property in the body?

I have a REST interface endpoint like
POST /items/12345/actions
I utilize a generic actions sub collection to be apply to apply changes to 12345 which are not easily mapped to the content or direct other sub collections of it.
My question is the following: Since there could be multiple different action types I identify the action by a JSON property of the content of an uploaded document.
How do I select a action by a part of the JSON body of the request. Is there something possible like...
[Route("api/v1/items")
public class ItemsController : Controller
{
[HttpPost("{id}/actions")]
[CheckJsonBody("type", "ActionA")]
public ActionResult DoActionA(int id, ActionA a)
{
// do something
}
[HttpPost("{id}/actions")]
[CheckJsonBody("type", "ActionB")]
public ActionResult DoActionB(int id, ActionB b)
{
// do something
}
}
The request would look like ...
{
"type": "ActionA",
"abc": "xyz"
}
I have digged myself up into the code till Microsoft.AspNetCore.Mvc.ActionConstraints.ActionMethodSelectorAttribute (GitHub).
However starting from there, I am a bit lost to reach a high-performance solution. Do I need to decode the body or is that something which is already done at that time the constraint is evaluated?
ps: And yes, I know I could handle them in one action and do a switch on the "type" property.
An ASP.NET team member was so friendly to direct me to an answer: In the ActionMethodSelectorAttribute you can read the body into a memory stream, read till the property for the selection filter. Then you seek the memory stream to zero and replace it in the request (for later model binding). You can cache the criteria value in HttpContext.Items to speed it up if you use the same property for multiple actions.

Optional query string parameters in URITemplate in WCF?

I'm developing some RESTful services in WCF 4.0. I've got a method as below:
[OperationContract]
[WebGet(UriTemplate = "Test?format=XML&records={records}", ResponseFormat=WebMessageFormat.Xml)]
public string TestXml(string records)
{
return "Hello XML";
}
So if i navigate my browser to http://localhost:8000/Service/Test?format=XML&records=10, then everything works as exepcted.
HOWEVER, i want to be able to navigate to http://localhost:8000/Service/Test?format=XML and leave off the "&records=10" portion of the URL. But now, I get a service error since the URI doesn't match the expected URI template.
So how do I implement defaults for some of my query string parameters? I want to default the "records" to 10 for instance if that part is left off the query string.
Note: This question is out of date, please see the other answers.
This does not appear to be supported.
However, Microsoft has been made aware of this issue and there is a work-around:
You can get the desired effect by
omitting the Query string from the
UriTemplate on your WebGet or
WebInvoke attribute, and using
WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters
from within your handlers to inspect,
set defaults, etc. on the query
parameters.
https://connect.microsoft.com/VisualStudio/feedback/details/451296/
According to this answer this is fixed in .NET 4.0. Failing to supply the query string parameter seems to result in its being given the default value for the type.
Check this blog post out. Makes sense to me, and comes with a class to parse out the query string parameters.
http://blogs.msdn.com/b/rjacobs/archive/2009/02/10/ambiguous-uritemplates-query-parameters-and-integration-testing.aspx
Basically don't define the query string parameters in the UriTemplate so it matches with/without the parameters, and use the sample class to retrieve them if they're there in the method implementation.
This seems to work in WCF 4.0.
Just make sure to set your default value in your "Service1.svc.cs"
public string TestXml(string records)
{
if (records == null)
records = "10";
//... rest of the code
}
While this is an old question, we still come to this scenario from time to time in recent projects.
To send optional query parameters, I created WCF Web Extensions nuget package.
After installation, you can use the package like this:
using (var factory = new WebChannelFactory<IQueryParametersTestService>(new WebHttpBinding()))
{
factory.Endpoint.Address = new EndpointAddress(ServiceUri);
factory.Endpoint.EndpointBehaviors.Add(new QueryParametersServiceBehavior());
using (var client = factory.CreateWebChannel())
{
client.AddQueryParameter("format", "xml");
client.AddQueryParameter("version", "2");
var result = client.Channel.GetReport();
}
}
Server side you can retrieve the parameters using WebOperationContext:
WebOperationContext.Current.IncomingRequest.UriTemplateMatch.QueryParameters;