Items count in OData v4 WebAPI response - count

How to return number of items in OData v4 HTTP response?
I need this number to pagination, so it should be number of items after filtering, but before 'skip' and 'top'.
I already tried passing '$inlinecount=allpages' and '$count=true' parameters in query options in url (https://damienbod.wordpress.com/2014/06/13/web-api-and-odata-v4-queries-functions-and-attribute-routing-part-2/ - "Example of $count"), but my responses from WebAPI always have only query results (collection) - whole response looks like:
[
{
"Name":"name1",
"age":5
},
{
"Name":"name2",
"age":15
}
]
There is nothing like "odata.count" in the response.
I also tried returning PageResult instead of IQueryable in my WebAPI controller action (like described here: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#server-paging), but Request.GetInlineCount() is deprecated and its value is always null.
Any ideas?
[Update] I just found the same problem here: WebApi with Odata NextPage and Count not appearing in the JSON response and I removed [EnableQuery] attribute and now my response looks like:
{
"Items":
[
{
"Name":"name1",
"age":5
},
{
"Name":"name2",
"age":15
}
],
"NextPageLink":null,
"Count":null
}
But still "Count" is always null. :(
Edit: After debugging and searching for count value in Request properties in my controller, I found out that correct Count value is in property named "System.Web.OData.TotalCount". So right now I exctract this value from that request property and my controller looks like that:
public PageResult<People> Get(ODataQueryOptions<People> queryOptions)
{
var query = _context.People.OrderBy(x => x.SomeProperty);
var queryResults = (IQueryable<People>)queryOptions.ApplyTo(query);
long cnt = 0;
if (queryOptions.Count != null)
cnt = long.Parse(Request.Properties["System.Web.OData.TotalCount"].ToString());
return new PageResult<People>(queryResults, null, cnt);
}
And it works fine, but I still don't know why I have to use workarounds like that.

For future reference (OData v4):
First of all $inlinecount it's not supported in OData v4 so you should use $count=true instead.
Second, if you have a normal ApiController and you return a type like IQueryable<T> this is the way you can attach a count property to the returned result:
using System.Web.OData;
using System.Web.OData.Query;
using System.Web.OData.Extensions;
//[EnableQuery] // -> If you enable globally queries does not require this decorator!
public IHttpActionResult Get(ODataQueryOptions<People> queryOptions)
{
var query = _peopleService.GetAllAsQueryable(); //Abstracted from the implementation of db access. Just returns IQueryable<People>
var queryResults = (IQueryable<People>)queryOptions.ApplyTo(query);
return Ok(new PageResult<People>(queryResults, Request.ODataProperties().NextLink, Request.ODataProperties().TotalCount));
}
Note:
OData functionality does not supported by ApiControllers so you
cannot have things like count or $metadata. If you choose to
use simple ApiController the way above is the one you should use
to return a count property.
For a full support of OData functionality you should implement a ODataController the following way:
PeopleController.cs
using System.Web.OData;
using System.Web.OData.Query;
public class PeopleController : ODataController
{
[EnableQuery(PageSize = 10, AllowedQueryOptions = AllowedQueryOptions.All)]
public IHttpActionResult Get()
{
var res = _peopleService.GetAllAsQueryable();
return Ok(res);
}
}
App_Start \ WebApiConfig.cs
public static void ConfigureOData(HttpConfiguration config)
{
//OData Models
config.MapODataServiceRoute(routeName: "odata", routePrefix: null, model: GetEdmModel(), batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
config.EnsureInitialized();
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder
{
Namespace = "Api",
ContainerName = "DefaultContainer"
};
builder.EntitySet<People>("People").EntityType.HasKey(item => item.Id); //I suppose the returning list have a primary key property(feel free to replace the Id key with your key like email or whatever)
var edmModel = builder.GetEdmModel();
return edmModel;
}
Then you access your OData Api this way (example):
encoded uri:
http://localhost:<portnumber>/People/?%24count=true&%24skip=1&%24top=3
decoded:
http://localhost:<portnumber>/People/?$count=true&$skip=1&$top=3
References:
How to Use Web API OData to Build an OData V4 Service without Entity Framework
Web API OData V4 Pitfalls
Create an OData v4 Endpoint Using ASP.NET Web API 2.2

This can also be achieved by an action filter:
/// <summary>
/// Use this attribute whenever total number of records needs to be returned in the response in order to perform paging related operations at client side.
/// </summary>
public class PagedResultAttribute: ActionFilterAttribute
{
/// <summary>
///
/// </summary>
/// <param name="actionExecutedContext"></param>
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Response != null)
{
dynamic responseContent=null;
if (actionExecutedContext.Response.Content != null)
responseContent = actionExecutedContext.Response.Content.ReadAsAsync<dynamic>().Result;
var count = actionExecutedContext.Response.RequestMessage.ODataProperties().TotalCount;
var res = new PageResult<dynamic>() {TotalCount=count,Items= responseContent };
HttpResponseMessage message = new HttpResponseMessage();
message.StatusCode = actionExecutedContext.Response.StatusCode;
var strMessage = new StringContent(JsonConvert.SerializeObject(res), Encoding.UTF8, "application/json");
message.Content = strMessage;
actionExecutedContext.Response = message;
}
}
}
And the custom PageResult class is:
public class PageResult<T>
{
public long? TotalCount { get; set; }
public T Items { get; set; }
}
Usage:
[PagedResult]
[EnableQuery()]

Will you please take a look at the sample service TripPin web api implementation at https://github.com/OData/ODataSamples/blob/master/Scenarios/TripPin. You can follow the code in Airports controller and the service with the code http://services.odata.org/TripPinWebApiService/Airports?$count=true can return the count correctly.

That's what I am using with oData v4:
Request.ODataProperties().NextLink,
Request.ODataProperties().TotalCount

If you are using OData conventional routing, $odata.count is not returned when your routes are not known to odata. Add 'app.UseODataRouteDebug();' to your ConfigureServices-method and then invoke 'https://localhost:5001/$odata'. If your route is not in the OData-route table, your route is not known to OData and you are not using correct naming conventions for your controller and EDM-type to be included in OData conventional routing.

Related

Consuming asp.net core web api in asp.net core mvc web app

I'm trying to consume my asp.net web api in my asp.net core mvc web app which are on the same solution. I configured the solution for multi-project start and they start both.
next I tried to consume the API in the Web part but I'm getting the following error.
InvalidOperationException: A suitable constructor for type 'ProjectName.Web.Services.Interfaces.IAdminService' could not be located. Ensure the type is concrete and all parameters of a public constructor are either registered as services or passed as arguments. Also ensure no extraneous arguments are provided.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType, Type[] argumentTypes, out ConstructorInfo matchingConstructor, out Nullable[] matchingParameterMap)
Here is the complete Stack trace
The Projects are structure like this
SolutionName:
Name.API
Name.Web
each with its own respective structure
This is my Helper Class
public static class HttpClientExtensions
{
public static async Task<T> ReadContentAsync<T>(this HttpResponseMessage response)
{
//if (response.IsSuccessStatusCode == false) return StatusCodes = 300;
//throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}");
var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var result = JsonSerializer.Deserialize<T>(
dataAsString, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
return result;
}
}
The IAdmin Inter Face
Task<IEnumerable<Admins>> GetAllAdmins();
The AdminService(Implementation)
private readonly HttpClient _client;
public const string BasePath = "api/Admins";
public AdminService(HttpClient client)
{
_client = client; // ?? throw new ArgumentNullException(nameof(client));
}
public async Task<IEnumerable<Admins>> GetAllAdmins()
{
var response = await _client.GetAsync(BasePath);
return await response.ReadContentAsync<List<Admins>>();
}
Admins Controller
private readonly IAdminService _adminService;
public AdminController(IAdminService adminService)
{
_adminService = adminService;
}
public async Task<IActionResult> Index()
{
var adminsList = await _adminService.GetAllAdmins();
if(adminsList == null)
{
return new JsonResult("There are now Admins");
}
return View(adminsList);
}
Program.cs
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient<IAdminService, IAdminService>(c =>
c.BaseAddress = new Uri("https://localhost:<port-Num>/"));
var app = builder.Build();
What Could I be doing wrong???
I'm using .NET 6 adn both Projects are in the same solution
NB My end points are working fine, I test them using Postman.
It is failing because DI cannot instantiate your AdminService with parameterized constructor. This is possibly a duplicate of Combining DI with constructor parameters? .
Essentially, you should avoid parameterized constructor injection where possible. Either control it through configuration or have the configuration loaded through common infrastructure such as host configuration.
According to your codes, I found you put two interface inside the AddHttpClient method which caused the issue.
I suggest you could modify it like this and then it will work well.
builder.Services.AddHttpClient<IAdminService, AdminService>(c =>
c.BaseAddress = new Uri("https://localhost:3333/"));

How can I use a default value/model on WebAPI EmptyBody?

I have dotnet WebAPI and I'm trying to get a specific behaviour but am constantly getting 415 responses.
I have reproduced this by starting a new webapi project using dotnet new webapi on the command line. From there, I added two things: a new controller, and a model class. In my real project the model class is obviously a bit more complex, with inheritance and methods etc...
Here they are:
[HttpGet("/data")]
public async Task<IActionResult> GetModel(BodyParams input)
{
var response = new { Message = "Hello", value = input.valueOne };
return Ok(response);
}
public class BodyParams {
public bool valueOne { get; set; } = true;
}
My goal is that the user can call https://localhost:7222/data with no headers or body needed at all, and will get the response - BodyParams will be used with the default value of true. Currently, from postman, or from the browser, I get a 415 response.
I've worked through several suggestions on stack and git but nothing seems to be working for me. Specifically, I have tried:
Adding [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] into the controller, but this makes no difference unless I provide an empty {} json object in the body. This is not what I want.
Making BodyParams nullable - again, no change.
Adding .AddControllers(opt => opt.AllowEmptyInputInBodyModelBinding = true)... again, no change.
I Implemented the solution suggested here using the attribute modification in the comment by #HappyGoLucky. Again, this did not give the desired outcome, but it did change the response to : 400 - "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true."
I tried modifying the solution in (4) to manually set context.HttpContext.Request.Body to an empty json object... but I can't figure out the syntax for this because it need to be a byte array and at that point I feel like I am way over complicating this.
How can I get the controller to use BodyParams with default values in the case that the user provides no body and no headers at all?
You can achieve that using a Minimal API.
app.MapGet("/data",
async (HttpRequest httpRequest) =>
{
var value = true;
if (Equals(httpRequest.GetTypedHeaders().ContentType, MediaTypeHeaderValue.Parse("application/json")))
{
var bodyParams = await httpRequest.ReadFromJsonAsync<BodyParams>();
if (bodyParams is not null) value = bodyParams.ValueOne;
}
var response = new {Message = "Hello", value};
return Results.Ok(response);
});
So, as there doesn't seem to be a more straightforward answer, I have currently gone with the approach number 5) from the OP, and just tweaking the code from there very slightly.
All this does is act as an action which checks the if the user has passed in any body json. If not, then it adds in an empty anonymous type. The behaviour then is to use the default True value from the BodyParams class.
The full code for the action class is:
internal class AllowMissingContentTypeForEmptyBodyConvention : Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
action.Filters.Add(new AllowMissingContentTypeForEmptyBodyFilter());
}
private class AllowMissingContentTypeForEmptyBodyFilter : IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (!context.HttpContext.Request.HasJsonContentType()
&& (context.HttpContext.Request.ContentLength == default
|| context.HttpContext.Request.ContentLength == 0))
{
context.HttpContext.Request.ContentType = "application/json";
var str = new { };
//convert string to jsontype
var json = JsonConvert.SerializeObject(str);
//modified stream
var requestData = Encoding.UTF8.GetBytes(json);
context.HttpContext.Request.Body = new MemoryStream(requestData);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
// Do nothing
}
}
}
Then you can add this to any of your controllers using [AllowMissingContentTypeForEmptyBodyConvention]

Trying to query "traces" in Application Insights via the REST API

I am attempting to query the Application Insights "traces" data via the API by using the C# example given on the API Quickstart page (https://dev.applicationinsights.io/quickstart) and I think I am having an issue understanding the path to make the call work.
The following was taken from the Quickstart page...
public class QueryAppInsights
{
private const string URL = "https://api.applicationinsights.io/v1/apps/{0}/{1}/{2}?{3}";
public static string GetTelemetry(string appid, string apikey, string queryType, string queryPath, string parameterString)
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("x-api-key", apikey);
var req = string.Format(URL, appid, queryType, queryPath, parameterString);
HttpResponseMessage response = client.GetAsync(req).Result;
if (response.IsSuccessStatusCode)
{
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
Console.WriteLine(response.StatusCode);
return response.Content.ReadAsStringAsync().Result;
}
else
{
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
Console.WriteLine(response.StatusCode);
return response.ReasonPhrase;
}
}
}
When I call it using the following parameters I get the following error.
{"error":{"message":"The requested path does not exist","code":"PathNotFoundError"}}
NotFound
public class TestSuite
{
public void CallTest()
{
QueryAppInsights.GetTelemetry("My Application ID", "My API Key", "query", "traces", "timespan=P7D&query=traces%7C%20where%20message%20contains%20%1111a11aa1-1111-11aa-1a1a-11aa11a1a11a%22");
}
}
When I call it replacing the "query" param with "events" I get over 500 rows returned with the following at the top
{"#odata.context":"https://api.applicationinsights.io/v1/apps/GUID PLACE HOLDER/events/$metadata#traces","#ai.messages":[{"code":"AddedLimitToQuery","message":"The query was limited to 500 rows"}
public class TestSuite
{
public void CallTest()
{
QueryAppInsights.GetTelemetry("My Application ID", "My API Key", "events", "traces", "timespan=P7D&query=traces%7C%20where%20message%20contains%20%1111a11aa1-1111-11aa-1a1a-11aa11a1a11a%22");
}
}
When I call it replacing the "events" param with "metrics" I get the following error:
{"error":{"message":"The requested item was not found","code":"ItemNotFoundError","innererror":{"code":"MetricNotFoundError","message":"Metric traces does not exist"}}}
NotFound
public class TestSuite
{
public void CallTest()
{
QueryAppInsights.GetTelemetry("My Application ID", "My API Key", "metrics", "traces", "timespan=P7D&query=traces%7C%20where%20message%20contains%20%1111a11aa1-1111-11aa-1a1a-11aa11a1a11a%22");
}
}
So I don't know if the way I am passing the query is incorrect or if I am trying something that is not possible. The query was taken from the API Explorer page (https://dev.applicationinsights.io/apiexplorer/query) in the "Query" > "GET /query" section and it does work as expected returning the correct row:
traces
| where message contains "1111a11aa1-1111-11aa-1a1a-11aa11a1a11a" (I've replaced the real GUID's with made up ones)
Just in case anyone ever comes across this I wanted to share how I did it successfully. Basically, I was using the wrong URL constant provided by the example on the quickstart (https://dev.applicationinsights.io/quickstart) page. I had to modify it in order to query Traces:
The given example on the quickstart:
private const string URL = "https://api.applicationinsights.io/v1/apps/{0}/{1}/{2}?{3}";
My implementation:
private const string URL = "https://api.applicationinsights.io/v1/apps/{0}/{1}?{2}{3}";
essentially moving the query string params to match what the GET/query API Explorer (https://dev.applicationinsights.io/apiexplorer/query) does when sending a query.

Web API httpget with many parameters

I am trying to create my first REST service using WEB API to replace some of my postbacks in a web forms asp.net project. In the web forms project, when I browse to a new web page, I always get an ASP.net Application variable and a querystring value that helps me determine which database to connect to. In this old app, it connects to several different databases that all have the same schema and database objects but the data is different in each database
I am not sure the best way to pass these variables to a REST Service or if they should be part of the route or some other method.
So in a REST method like the one below
// GET api/<controller>/5
public string GetCategoryByID(int id)
{
return "value";
}
I can get the category id and pass that to my database layer, but I also need the two variables mentioned above. I will need to obtain these variables in every call to my REST api in order to access the appropriate database. Should I use something like the following:
// GET api/<controller>/5
public string GetCategoryByID(int id, string applicationEnvironment, string organization)
{
return "value";
}
Or should they be part of the route with something like this:
api/{appEnvironment}/{organization}/{controller}/{id}
This seems like a simple problem, but I am having trouble figuring out a solution.
I ended up passing extra parameters with my httpget call. I will probably follow this pattern unless I get some additional feedback.
[HttpGet]
public Company[] GetProgramCompanies(int id, [FromUri] string org, [FromUri] string appEnvir)
{
DataLayer dataAccess = new DataLayer(Utilities.GetConnectionString(org, appEnvir));
IEnumerable<BudgetProgramCompanyListing> companies = dataAccess.GetProgramCompaniesListing(id).OrderBy(o => o.Company_Name);
Company[] returnComps = new Company[companies.Count()];
int count = 0;
foreach (BudgetProgramCompanyListing bpc in companies)
{
returnComps[count] = new Company
{
id = bpc.Company_ID,
name = bpc.Company_Name
};
count++;
}
return returnComps;
}
Calling the above service with this url:
api/programcompanies/6?org=SDSRT&appEnvir=GGGQWRT
In .Net core 1.1 you can specify more parameters in HttGet attribute like this:
[HttpGet("{appEnvironment}/{organization}/{controller}/{id}")]
It may work in other .Net versions too.
I used to follow the below two method to pass multiple parameter in HttpGet
public HttpResponseMessage Get(int id,[FromUri]int DeptID)
{
EmpEntity = new EmpDBEntities();
var entity = EmpEntity.USP_GET_EMPINFO(id, DeptID).ToList();
if(entity.Count()!=0)
{
return Request.CreateResponse(HttpStatusCode.OK, entity);
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Employee With ID=" + id.ToString() + " Notfound");
}
}
and the webapi url will be http://localhost:1384/api/emps?id=1&DeptID=1
in the above methode USP_GET_EMPINFO is the stored procedure with two parameters.
in second method we can use the class with [FromUri] to pass multiple parameter.
the code snippet is as below
public HttpResponseMessage Get(int id,[FromUri]Employee emp)
{
EmpEntity = new EmpDBEntities();
var entity = EmpEntity.USP_GET_EMPINFO(id,emp.DEPTID).ToList();
if(entity.Count()!=0)
{
return Request.CreateResponse(HttpStatusCode.OK, entity);
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Employee With ID=" + id.ToString() + " Notfound");
}
}
and the webapi url will be http://localhost:1384/api/emps?id=1&DEPTID=1
here the DEPTID is one of the property of the class. we can add multiple parameters separated with & in the url
You could also define a model and send that with the request and bind it to a variable in your api function using [FromBody].
Something like:
[HttpGet]
public Company[] GetProgramCompanies([FromBody] YourModel model) { ... }
As explained here Model binding in Asp.Net Core

How can i unit test an EntitySetController

i try to unit test an EntitySetController. I can test Get but have problems in testing the Post Method.
I played around with SetODataPath and SetODataRouteName but when i call this.sut.Post(entity) i get a lot of errors regarding missing Location Header, missing OData-Path, missing Routes.
I am at my wit's end.
Is there anybody out there who has successfully testet their EntitySetController?
Has anybody an idea for me?
Maybe should i test only the protected overrided methods from my EntitySetController implementation? But how can i test protected methods?
Thanks for your help
Came here looking for a solution aswell. This seems to work however not sure if there is a better way.
The controller needs a minimum of CreateEntity and GetKey overrides:
public class MyController : EntitySetController<MyEntity, int>
{
protected override MyEntity CreateEntity(MyEntity entity)
{
return entity;
}
protected override int GetKey(MyEntity entity)
{
return entity.Id;
}
}
Where MyEntity is really simple:
public class MyEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
Looks like you need at least:
+ Request with a URI
+ 3 keys in the request header, MS_HttpConfiguration, MS_ODataPath and MS_ODataRouteName
+ A HTTP configuration with a route
[TestMethod]
public void CanPostToODataController()
{
var controller = new MyController();
var config = new HttpConfiguration();
var request = new HttpRequestMessage();
config.Routes.Add("mynameisbob", new MockRoute());
request.RequestUri = new Uri("http://www.thisisannoying.com/MyEntity");
request.Properties.Add("MS_HttpConfiguration", config);
request.Properties.Add("MS_ODataPath", new ODataPath(new EntitySetPathSegment("MyEntity")));
request.Properties.Add("MS_ODataRouteName", "mynameisbob");
controller.Request = request;
var response = controller.Post(new MyEntity());
Assert.IsNotNull(response);
Assert.IsTrue(response.IsSuccessStatusCode);
Assert.AreEqual(HttpStatusCode.Created, response.StatusCode);
}
I'm not too sure about the IHttpRoute, in the aspnet source code (I had to link to this to figure this all out) the tests use mocks of this interface. So for this test I just create a mock of this and implement the RouteTemplate property and GetVirtualPath method. All the others on the interface were not used during the test.
public class MockRoute : IHttpRoute
{
public string RouteTemplate
{
get { return ""; }
}
public IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values)
{
return new HttpVirtualPathData(this, "www.thisisannoying.com");
}
// implement the other methods but they are not needed for the test above
}
This is working for me however I am really not too sure about the ODataPath and IHttpRoute and how to set it correctly.
In addition to the answer from #mynameisbob, I have found you also may need to set the HttpRequestContext as well on the Request properties:
var requestContext = new HttpRequestContext();
requestContext.Configuration = config;
request.Properties.Add(HttpPropertyKeys.RequestContextKey, requestContext);
I needed the above additions for example when creating an HttpResponseMessage as follows:
public virtual HttpResponseException NotFound(HttpRequestMessage request)
{
return new HttpResponseException(
request.CreateResponse(
HttpStatusCode.NotFound,
new ODataError
{
Message = "The entity was not found.",
MessageLanguage = "en-US",
ErrorCode = "Entity Not Found."
}
)
);
}
Without having the HttpRequestContext set, the above method will throw an Argument Null Exception as the CreateResponse extension method attempts to get the HttpConfiguration from the HttpRequestContext (rather than directly from the HttpRequest).
OK updated answer.
I've also found to support executing a returned IHttpActionResult successfully, a few more things are needed.
Here is the best approach I found so far, I'm sure there is a better way but this works for me:
// Register OData configuration with HTTP Configuration object
// Create an ODataConfig or similar class in App_Start
ODataConfig.Register(config);
// Get OData Parameters - suggest exposing a public GetEdmModel in ODataConfig
IEdmModel model = ODataConfig.GetEdmModel();
IEdmEntitySet edmEntitySet = model.EntityContainers().Single().FindEntitySet("Orders");
ODataPath path = new ODataPath(new EntitySetPathSegment(edmEntitySet));
// OData Routing Convention Configuration
var routingConventions = ODataRoutingConventions.CreateDefault();
// Attach HTTP configuration to HttpRequestContext
requestContext.Configuration = config;
// Attach Request URI
request.RequestUri = requestUri;
// Attach Request Properties
request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, config);
request.Properties.Add(HttpPropertyKeys.RequestContextKey, requestContext);
request.Properties.Add("MS_ODataPath", path);
request.Properties.Add("MS_ODataRouteName", "ODataRoute");
request.Properties.Add("MS_EdmModel", model);
request.Properties.Add("MS_ODataRoutingConventions", routingConventions);
request.Properties.Add("MS_ODataPathHandler", new DefaultODataPathHandler());
Also, to get the correct Location header values etc, you really want to call your Web Api application OData configuration code.
So rather than using:
config.Routes.Add("mynameisbob", new MockRoute());
You should separate the portion of the WebApiConfig class that sets up your OData routes into a separate class (e.g. ODataConfig) and use that to register the correct routes for your tests:
e.g.
ODataConfig.Register(config);
The only things you then have to watch out for is that the following lines match your routing configuration:
request.Properties.Add("MS_ODataPath", new ODataPath(new EntitySetPathSegment("MyEntity")));
request.Properties.Add("MS_ODataRouteName", "mynameisbob");
So if your Web API OData configuration is as follows:
config.Routes.MapODataRoute("ODataRoute", "odata", GetEdmModel());
private static IEdmModel GetEdmModel()
{
ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<MyEntity>("MyEntities");
IEdmModel model = modelBuilder.GetEdmModel();
return model;
}
Then this is the correct configuration:
request.Properties.Add("MS_ODataPath", new ODataPath(new EntitySetPathSegment("MyEntities")));
request.Properties.Add("MS_ODataRouteName", "ODataRoute");
With this in place, your Location header will be generated correctly.
In addition to everything here, I had to manually attach the context to the request, as well as create route data. Unfortunately there is no way I found to unit-test without a dependency on route/model configuration.
So using a route called "ODataRoute" which is all part of the normal configuration established in my static ODataConfig.Configure() method (same as above, it creates the model and calls a bunch of MapODataServiceRoute), the following code works to prepare a controller for a test:
protected static void SetupControllerForTests(ODataController controller,
string entitySetName, HttpMethod httpMethod)
{
//perform "normal" server configuration
var config = new HttpConfiguration();
ODataConfig.Configure(config);
//set up the request
var request = new HttpRequestMessage(httpMethod,
new Uri(string.Format("http://localhost/odata/{0}", entitySetName)));
//attach it to the controller
//note that this will also automagically attach a context to the request!
controller.Request = request;
//get the "ODataRoute" route from the configuration
var route = (ODataRoute)config.Routes["ODataRoute"];
//extract the model from the route and create a path
var model = route.PathRouteConstraint.EdmModel;
var edmEntitySet = model.FindDeclaredEntitySet(entitySetName);
var path = new ODataPath(new EntitySetPathSegment(edmEntitySet));
//get a couple more important bits to set in the request
var routingConventions = route.PathRouteConstraint.RoutingConventions;
var pathHandler = route.Handler;
//set the properties of the request
request.SetConfiguration(config);
request.Properties.Add("MS_ODataPath", path);
request.Properties.Add("MS_ODataRouteName", "ODataRoute");
request.Properties.Add("MS_EdmModel", model);
request.Properties.Add("MS_ODataRoutingConventions", routingConventions);
request.Properties.Add("MS_ODataPathHandler", pathHandler);
//set the configuration in the request context
var requestContext = (HttpRequestContext)request.Properties[HttpPropertyKeys.RequestContextKey];
requestContext.Configuration = config;
//get default route data based on the generated URL and add it to the request
var routeData = route.GetRouteData("/", request);
request.SetRouteData(routeData);
}
This took me the better part of a few days to piece together, so I hope this at least saves someone else the same.

Resources