4

I'm looking to build URIs such as https://example.com/data/customers?$top=100.

Is there a UriBuilder for creating OData URIs (i.e. which can handle characters such as $ appropriately)?

Full info

I have code like this (simplified example):

public Uri CreateMyApiUri(string rootUri, string apiPath, string entity, int pageSize)
{
    var builder = new UriBuilder(rootUri);
    builder.Path = ConcatPathParts(builder.Path, apiPath, entity); //basically string.Join("/", args), plus code to remove superfluous slashes
    var parameters = HttpUtility.ParseQueryString(builder.Query);
    if (pageSize > 0) parameters["$top"] = pageSize.ToString();
    builder.Query = parameters.ToString();
    return builder.Uri; 
}
//called like this
var uri = CreateMyApiUri("https://example.com", "data", "customers", 100);

However, the OData special character $ gets encoded for use in the URI as %24.

I've found OData.Net on GitHub which seems a helpful library for such things, but it's not part of the standard library and looks quite heavyweight for my simple need, so I'm hoping to find something simpler before committing to that going down the OData.Net path...

Of course, I could avoid this by doing a simple var uri = string.Join("/", new [] {rootUri, apiPath, entity, $"?$top={pageSize}"});... but I want to ensure I'm taking advantage of the .net library's character escaping features / not creating a solution for something the framework already gives me.

NB: I'm aware that you can generate classes from OData services, but I don't want to use this approach since that requires that I regenerate the client code if the API is changed (e.g. new fields are added to the target entity). Instead I want to use a more "pure" HTTP approach.

JohnLBevan
  • 22,735
  • 13
  • 96
  • 178
  • OData publishes its schema. If the API changes, the schema will also change. That's no less "pure HTTP" than any other approach. In fact, GraphQL and Open API try to bring back that discoverable schema – Panagiotis Kanavos Nov 22 '18 at 09:26
  • @PanagiotisKanavos by "non pure HTTP" I mean where you have a C# client class, such as `ProductClient` in https://docs.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-client-app. I'm writing middleware which picks up the data and converts it to XML, which can then be transformed via XLSTs before being consumed by other systems. By avoiding generated C# I don't have to redeploy assemblies; I can update the middleware using only configuration (XSLT / URI) for the affected service. There may be a better way though / I'm open to ideas. – JohnLBevan Nov 22 '18 at 10:09
  • 1
    that's not a matter of pureness, it's a matter of versioning that won't be solved by using XSLT or JSON Path - you need to know the schema to write the proper transformation. The ODATA *client* allows you to write LINQ queries, retrieve only the fields you want and map them to whatever *client* entity you want, or even use anonymous types. There's no real difference between an XSLT mapping and a LINQ query, except for ease of use, compile-time checking etc – Panagiotis Kanavos Nov 22 '18 at 10:53

1 Answers1

1

I found a solution; I didn't need a special ODataUriBuilder; rather there was a bug in my use of query.ToString(), as explained here: https://stackoverflow.com/a/26789977/361842

Applying that fix to the above code solves the issue:

public Uri CreateMyApiUri(string rootUri, string apiPath, string entity, int pageSize)
{
    var builder = new UriBuilder(rootUri);
    builder.Path = ConcatPathParts(builder.Path, apiPath, entity); //basically string.Join("/", args), plus code to remove superfluous slashes
    var parameters = HttpUtility.ParseQueryString(builder.Query);
    if (pageSize > 0) parameters["$top"] = pageSize.ToString();

    //the fix:
    builder.Query = Uri.EscapeUriString(HttpUtility.UrlDecode(parameters.ToString()));
    //instead of:
    //builder.Query = parameters.ToString();

    return builder.Uri; 
}
//called like this
var uri = CreateMyApiUri("https://example.com", "data", "customers", 100);
JohnLBevan
  • 22,735
  • 13
  • 96
  • 178