Create custom response in .NET Core 5 with OData 8 controller - asp.net

I'm using OData 8 in .NET Core 5 with the [EnableQuery] attribute. Problem is when I want to return Problem() or BadRequest("Some message"), the API always returns some default OData BadRequest and never the message I wanted (only when [EnableQuery] attribute is there).
Example:
[HttpGet]
[EnableQuery(EnsureStableOrdering = false )]
public IActionResult GetList(ODataQueryOptions<List> queryOptions)
{
if (queryOptions.Filter == null)
{
return BadRequest( "Filter is required for this endpoind!" );
}
try
{
queryOptions.Filter.Validator = new OdataCustomFilterValidator();
this.BaseValidateQueryOptions(queryOptions);
}
catch (ODataException ex)
{
return this.BadRequest(ex.Message);
}
IQueryable<List> list = this._service.GetList();
return this.Ok(list);
}
So in the above example, if the code gets to the first IF, i do not recieve this message but ALWAYS the same Odata error:
{
"error": {
"code": "",
"message": "The query specified in the URI is not valid. The requested resource is not a collection. Query options $filter, $orderby, $count, $skip, and $top can be applied only on collections.",
"details": [],

Hi you ran into this bug which was fixed in OData/WebApi https://github.com/OData/WebApi/issues/2511 but it seems like it has not yet been migrated to AspNetCoreOData, A pull request exists which when merged and published should allow you to continue with your use case which is valid.

Related

Write our API end point in a standard way to cover all the business scenarios, in our ASP.NET MVC 5 web application

I have this Action method in ASP.NET MVC 5:
namespace LDAPMVCProject.Controllers
{
public class HomeController : Controller
{
public ActionResult UsersInfo(string username, string password)
{
DomainContext result = new DomainContext();
try
{
// create LDAP connection object
DirectoryEntry myLdapConnection = createDirectoryEntry();
string ADServerName = System.Web.Configuration.WebConfigurationManager.AppSettings["ADServerName"];
string ADusername = System.Web.Configuration.WebConfigurationManager.AppSettings["ADUserName"];
string ADpassword = System.Web.Configuration.WebConfigurationManager.AppSettings["ADPassword"];
using (var context = new DirectoryEntry("LDAP://mydomain.com:389/DC=mydomain,DC=com", ADusername, ADpassword))
using (var search = new DirectorySearcher(context))
{
// validate username & password
using (var context2 = new PrincipalContext(ContextType.Domain, "mydomain.com", ADusername, ADpassword))
{
bool isvalid = context2.ValidateCredentials(username, password);
if !(isvalid)
return **** // authentication error
}
// create search object which operates on LDAP connection object
// and set search object to only find the user specified
// DirectorySearcher search = new DirectorySearcher(myLdapConnection);
// search.PropertiesToLoad.Add("telephoneNumber");
search.Filter = "(&(objectClass=user)(sAMAccountName=test.test))";
// create results objects from search object
// user exists, cycle through LDAP fields (cn, telephonenumber etc.)
SearchResult r = search.FindOne();
ResultPropertyCollection fields = r.Properties;
foreach (String ldapField in fields.PropertyNames)
{
if (ldapField.ToLower() == "telephonenumber")
{
foreach (Object myCollection in fields[ldapField])
{
result.Telephone = myCollection.ToString();
}
}
else if (ldapField.ToLower() == "department")
{
foreach (Object myCollection in fields[ldapField])
{
result.Department = myCollection.ToString();
}
}
// }
}
if (result.Telephone == null)
return ***** //Telephone is empty
if (result.Department)
return **** // department is empty
string output = JsonConvert.SerializeObject(result);
return Content(output, "application/json");//success
}
}
catch (Exception e)
{
Console.WriteLine("Exception caught:\n\n" + e.ToString());
}
return View(result);
}
}
}
The action method acts as an API endpoint for our web application, where the API accepts username & password, and does the following:
Validate the username/password against Active Directory
If valid; check if the telephone number is empty >> if so return an error
If valid; check if department is empty >> if so return an error
If valid and info found; return the department & telephone for the user
Now I am a bit confused on how I need to return the JSON for the first 3 points? Should I always return http 200 with a status message (Status : "success" OR Status: "failed")? or if the username/password validation failed then i should return http 401 without having to return any JSON content?
Can anyone help me with this?
I need to write the action method in a standard way that can be consumed by 3rd party application.
Second question: what do I need to return in case the code raised an exception?
Thanks
This is an API error handling and logging design, and the following type of approach works well, to separate the concerns and keep your main logic clean:
DESIGN ERROR RESPONSES
These should be useful to clients, eg if they need to display an error or do something based on a specific cause. A 4xx error might have this payload, along with an HTTP status:
{
"code": "authentication_failed",
"message": "Invalid credentials were provided"
}
A 500 error is often given a different payload based on what a UI will display in this case, and how you look the error up in logs:
{
"code": "authentication_error",
"message": "A problem was encountered during a backend authentication operation",
"area": "LDAP",
"id": 12745,
"utcTime": "2022-07-24T10:27:33.468Z"
}
DESIGN API LOGS
In the first case the server logs might have fields such as these:
{
"id": "7af62b06-8c04-41b0-c428-de332436d52a",
"utcTime": "2022-07-24T10:27:33.468Z",
"apiName": "MyApi",
"operationName": "getUserInfo",
"hostName": "server101",
"method": "POST",
"path": "/userInfo",
"errorData": {
"statusCode": 401,
"clientError": {
"code": "authentication_failed",
"message": "Invalid credentials were provided",
"context": "The account is locked out"
}
}
}
In the second case the server logs might have fields such as these:
{
"id": "7af62b06-8c04-41b0-c428-de332436d52a",
"utcTime": "2022-07-24T10:27:33.468Z",
"apiName": "MyApi",
"operationName": "getUserInfo",
"hostName": "server101",
"method": "POST",
"path": "/userInfo",
"errorData": {
"statusCode": 500,
"clientError": {
"code": "authentication_error",
"message": "A problem was encountered during a backend authentication operation",
"area": "LDAP",
"id": 12745,
"utcTime": "2022-07-24T10:27:33.468Z"
},
"serviceError": {
"details": "Host not found: error MS78245",
"stack": [
"Error: An unexpected exception occurred in the API",
"at DirectorySearcher: 871 ... "
]
}
}
CODE
Perhaps aim to use code similar to this, to represent your desired error and logging behaviour. The ClientError and ServiceError classes enable the above responses and logs. When errors are thrown this should enable you to add useful contextual info:
public class HomeController : Controller
{
public ActionResult UsersInfo(string username, string password)
{
DomainContext result = new DomainContext();
try
{
DirectoryEntry myLdapConnection = createDirectoryEntry();
string ADServerName = System.Web.Configuration.WebConfigurationManager.AppSettings["ADServerName"];
string ADusername = System.Web.Configuration.WebConfigurationManager.AppSettings["ADUserName"];
string ADpassword = System.Web.Configuration.WebConfigurationManager.AppSettings["ADPassword"];
using (var context = new DirectoryEntry("LDAP://mydomain.com:389/DC=mydomain,DC=com", ADusername, ADpassword))
using (var search = new DirectorySearcher(context))
{
using (var context2 = new PrincipalContext(ContextType.Domain, "mydomain.com", ADusername, ADpassword))
{
bool isvalid = context2.ValidateCredentials(username, password);
if !(isvalid)
throw new ClientError(401, "authentication_failed", "Invalid credentials were provided", "optional context goes here");
}
DirectorySearcher search = new DirectorySearcher(myLdapConnection);
search.Filter = "(&(objectClass=user)(sAMAccountName=test.test))";
SearchResult r = search.FindOne();
ResultPropertyCollection fields = r.Properties;
foreach (String ldapField in fields.PropertyNames)
{
if (ldapField.ToLower() == "telephonenumber")
{
foreach (Object myCollection in fields[ldapField])
{
result.Telephone = myCollection.ToString();
}
}
else if (ldapField.ToLower() == "department")
{
foreach (Object myCollection in fields[ldapField])
{
result.Department = myCollection.ToString();
}
}
}
if (result.Telephone == null)
throw new ClientError(400, "invalid_user_data", "User data is invalid", "Telephone is missing");
if (result.Department)
throw new ClientError(400, "invalid_user_data", "User data is invalid", "Department is missing");
string output = JsonConvert.SerializeObject(result);
return Content(output, "application/json");
}
}
catch (Exception e)
{
throw new ServiceError("authentication_error", "A problem was encountered during a backend authentication operation", "LDAP", e);
}
return View(result);
}
}
MIDDLEWARE
The usual pattern is then to use small middleware classes to deal with processing exceptions, returning error responses and writing error logs:
logging filter
exception filter
The type of logic written here will depend a little on your preferences, but might look similar to this:
public class ErrorFilterAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
var logEntry = new ErrorLogEntry();
var jsonResponse = ""
var statusCode = 500;
if (filterContext.Exception is ClientError)
{
var clientError = filterContext.Exception as ClientError;
logEntry.AddClientErrorDetails(clientError);
statusCode = clientError.StatusCode;
jsonResponse = clientError.toResponseFormat();
}
if (filterContext.Exception is ServiceError)
{
var serviceError = filterContext.Exception as ServiceError;
logEntry.AddServiceErrorDetails(serviceError);
statusCode = serviceError.StatusCode;
jsonResponse = serviceError.toResponseFormat();
}
logEntry.Write();
filterContext.Result = new JsonResult(jsonResponse);
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = statusCode;
filterContext.ExceptionHandled = true;
}
}
There are a lot of ways to go about this and ultimately you want to have your endpoint behave in a way that whoever is consuming your endpoint expects.
I stumbled across this as an interesting way to handle nuanced errors in a request to your endpoint. Even though this is used for Graph API, you could use the concept for your needs. https://developers.facebook.com/docs/graph-api/guides/error-handling. The TL;DR is to have a standardized json response like:
{
"error": {
"message": "Message describing the error",
"type": "OAuthException",
"code": 190,
"error_subcode": 460,
"error_user_title": "A title",
"error_user_msg": "A message",
"fbtrace_id": "EJplcsCHuLu"
}
}
The HTTP statuse codes are very flexable and can be confused to tell when to use what.
My advice:
Identify the Http status family (X00)
100s: Informational codes: the server acknowledges the request
initiated by the browser and that it is being processed (100–199).
200s: Success codes: request received, understood, processed and
expected info relayed to browser (200–299).
300s: Redirection codes: a different destination has been substituted
for the requested resource; further action by the browser may be
required (300–399).
400s: Client error codes: website or page not reached; page
unavailable or there was a technical problem with the request
(400–499).
500s: Server error codes
Search for the specific Http status code for your response (2XX) here some exemples for the 200 family:
201: Created. Request fulfilled; new resource created. Typical response
after POST requests.
202: Accepted. Browser request accepted, still in process. May or may not
succeed.
For your example I would return:
403: Forbidden - if the user credentials are wrong.
200: Ok - if everythig works well (all the info returned).
The other option is a little tricky, when the user is authenticate but have no valid data.
you can return:
204: No content - because the user is auth but has no data
500: internal server error - because the server cant return the requested
object
404: Not found - (not my personal chois but it is an option)
It also depends on your client and you preferences.
Happy coddind :)

Creating a Graph Webhook Subscription, why am I getting this message? "Response must exactly match validationToken query parameter"

I am new to Graph. I'm attempting to subscribe to changes in /users. Here's my Headers and POST to Graph:
//HTTP POST
//Host: https://graph.microsoft.com/v1.0/subscriptions
//Content-Type:applicaton/json
//Authorization: {auth key}
{
"changeType":"updated"
,"clientState":"myClientState"
,"resource":"/users"
,"notificationUrl":"[ngrok URL tunneling back to my local debug api instance]"
,"expirationDateTime":"2020-05-23T04:30:28.2257768+00:00"
}
And this is the relevant code on my core web API that gets the response from Graph:
// POST api/values
public IHttpActionResult Post ([FromUri]string validationToken)
{
Debug.WriteLine("validationToken string is: " + validationToken);
if (!String.IsNullOrEmpty(validationToken))
{
Debug.WriteLine("Token received, sending back token: " + validationToken);
return Ok(validationToken);
}
else
{
//...
}
}
I've tested this by hitting it up via Postman and it sends back exactly whatever query parameter it receives, as expected. When I send the POST call to Graph, my Web API gets the response. It has one parameter:
key: validationToken
value: 'Validation: Testing client application reachability for subscription Request-Id: ea95e0a8-55c6-42db-b7e6-441920ae9c15'
So that's what I send back. I always get this error after:
{
"error": {
"code": "InvalidRequest",
"message": "Subscription validation request failed. Response must exactly match validationToken query parameter.",
"innerError": {
"request-id": "ea95e0a8-55c6-42db-b7e6-441920ae9c15",
"date": "2020-05-18T20:07:17"
}
}
}
I've tried different encodings and confirmed that my Auth token is valid, and all endpoints are reaching each other OK. Honestly that doesn't really look like what I expect a validation token to look like - is that even it? I see nothing else in the POST body and there's no other params. Here's the exact request URI with the query parameter included:
https://localhost:44391/api/values?validationToken=Validation%3a+Testing+client+application+reachability+for+subscription+Request-Id%3a+ea95e0a8-55c6-42db-b7e6-441920ae9c15
Ensure validation token is returned as plain/text content-type. EG:
public IHttpActionResult Post([FromUri] string validationToken) {
Debug.WriteLine("validationToken string is: " + validationToken);
if (!String.IsNullOrEmpty(validationToken)) {
Debug.WriteLine("Token received, sending back token: " + validationToken);
return this.ResponseMessage(new HttpResponseMessage(HttpStatusCode.OK) {
Content = new StringContent(validationToken)
});
}
else {
return this.InternalServerError();
}
}
Replace return Ok(validationToken); by return Content(validationToken);.
Ok expects the parameter to be an object and might wrap what is passed in with JSON or XML depending on the request and the pipeline configuration.
Content expects the parameter to be a string and returns it as is.
You can use the postman collection to validate your endpoint's validation implementation and look at the sample for more information.

use json format in an URL

I am building a rest API with asp.net my problem is that when I try to add a student to my database like that :
http://localhost:50001/api/Students?&FirstName=cc&LastName=cc&Email=student10#gmail.com&DropOut=false&Live=false&ClassId=1&ImageId=1
I get "the value variable is null",
this is my code to add a student:
// Get All Students
[Route("api/Students")]
public IEnumerable<Student> Get()
{
return _StudentService.Queryable().ToList();
}
// Insert Student
[Route("api/Students/")]
public IEnumerable<Student> Post(Student value)
{
cc.Students.Add(value);
cc.SaveChanges();
return Get();
}
I have used "Fiddler web Debugger" to test my URLs an it works only in this way:
now If I have an angularJS client that tries to add a new student to the database,how can I send data as a json format in an URL
this is how I add a new student from my client angularJS:
$http({method: 'POST', url: 'http://localhost:50001/api/Students?&FirstName=cc&LastName=cc&Email=student10#gmail.com&DropOut=false&Live=false&ClassId=1&ImageId=1})
.success(function (data) {
console.log("success");
}).error(function (data, status, headers, config) {
console.log("data error ...");
});
thanks a lot for help
If you are saying you want a true Rest API you should continue to use the POST verb as it is more semantically right for creating a new student.
Passing a new student on the URL is possible but not in the configuration you have provided.
Your API method expects a POST request and that the new student be located in the HTTP body.
Just configure your angular call to use jsonData and post it to your API.

POST action taking JSON and returning JSON with ASP.NET

I'm developing a web service in which I have resources of type A owning resources of type B.
I want to develop an API for getting stats about an A, but only considering a subset of its Bs.
So I'd have a route taking an A's ID, a collection of IDs of Bs, and returns the stats. I'd use it like the following:
POST /api/StatsAboutA/{aId}
JSON payload:
[1, 4, 12]
And it'd return something like that:
[
{"key": ..., "value": ...},
...
]
Here is my controller:
class StatsAboutAController : ApiController
{
// ...
// POST api/StatsAboutA/{aId}
[HttpPost]
public Stats Post(long aId, IEnumerable<long> bIds)
{
A a = _aRepository.SelectById(aId);
if (a == null)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
}
return _aRepository.CollectStats(a, bIds);
}
// ...
}
I can't manage to configure my router so that my controller matches my route.
Here is the error message I get:
{
"message": "No HTTP resource was found that matches the request URI 'http://localhost:51398/api/StatsAboutA/1'.",
"messageDetail": "No action was found on the controller 'StatsAboutA' that matches the request."
}
Change the method signature to
public Stats Post(long aId, [FromBody]IEnumerable<long> bIds)
Because it is trying to get the bIds from the URI and that route is not defined.

Strange Behavior when using jQuery/asp.net WebApi

I have 2 Web API Projects:
Api1 is a testing-Environment for the JavaScript Front-End, but has a API
Back-end(the default ValuesController), also for testing.
Api2 is the "true" Back-end, from which the Experimental JavaScript UI schould pull Data. For Testing, i use the default ValuesController here too, because, i want to have the same Output.
Status Quo
The Api1-UI can query the Data from the ValuesController of the own API
The Api2 returns the Correct Data(tested in Firefox and with Fiddler)
The Code
JavaScript Client:
var _load = function (url) {
$.ajax({
url: url,
method: 'GET',
accepts: "application/json; charset=utf-8",
success: function (data) {
alert("Success: " + data);
},
error: function (data) {
alert("Error :" + data);
}
});
};
WebApi Controller method:
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Problem
The JavaScript UI of the experimental Front-End is not able to display, or even receive, the data from the API 2, which is, according to Fiddler, sent correct.
My first thought was, I am using the wrong Method, but i tried $.getJSON and $.ajax. But i always end up with an error. It just says statusText= "Error"
I don't get, why it can display Data from the own ApiController, but not from the "External"...
Thanks for any Help/Suggestions
You seem to be accessing data from X from a different domain Y using ajax. This seems to be a classic cross domain access issue.
You need to set Access-Control-Allow-Origin to value " * " in your response header.
Response.Headers.Add("Access-Control-Allow-Origin", "*")
There various ways you can solve this
defining this header in IIS
using a actionfilter attribute like below
FilterAttribute
public class AllowCrossSiteJsonAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Response != null)
actionExecutedContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
base.OnActionExecuted(actionExecutedContext);
}
}
Using Attribute on Controller Action
[AllowCrossSiteJson]
public Result Get(int id)
{
//return appropriate result
}

Resources