Resolved What's the best way to do this string replace?

Donal23

Member
Joined
Dec 28, 2022
Messages
6
Programming Experience
Beginner
Hello! I am writing a client that talks to HTTPS APIs. The configuration can add sources from which to pull data, and I want it to be as flexible as possible. So I have a "Source" class that is the API source config. It has vars like:

RootUrl - the base of the API's URL, e.g. "https://www.service.com/api.php";
QueryPosts - the construction of the post query URL, e.g. "%RootUrl%%Query%%Page%%PostResultLimit%"
ComponentQuery - the query component, e.g. "tag=%value%"
ComponentPage - the page number component, e.g. "pid=%value%"
ComponentPostResultLimit - the post result limit component, e.g. "limit=%value%"

I was doing:
C#:
public string GetQueryString(string query, string page = "0", string limit = "") {
  if (RootUrl.Trim() == "") return "";

  string ret = QueryPosts.Replace("%RootUrl%", RootUrl);
  ret = ret.Replace("%Query%", ComponentQuery.Trim() == "" ? "" : ((ret.IndexOf("?") == -1 ? "?" : "&") + ComponentQuery.Replace("%value%", query)));
  ret = ret.Replace("%Page%", ComponentPage.Trim() == "" ? "" : (page.Trim() == "" ? "" : (ret.IndexOf("?") == -1 ? "?" : "&") + ComponentPage.Replace("%value%", page)));
  ret = ret.Replace("%PostResultLimit%", ComponentPostResultLimit.Trim() == "" ? "" : (limit.Trim() == "" ? "" : (ret.IndexOf("?") == -1 ? "?" : "&") + ComponentPostResultLimit.Replace("%value%", limit)));
  return ret;
}
It worked great until I realized if the QueryPosts had the order of components different than what my replace was doing, it breaks the decision of whether or not to use ? or & when adding vars to the querystring. So like if I had QueryPosts = "%RootUrl%%Page%%Query%%PostResultLimit%" and I passed in page 0, query of hi and limit of 50 the resultant URL would be:
C#:
https://www.service.com/api.php&pid=0?tag=hi&limit=50
I realized then I need to process the tag replacement in the order they appear in the QueryPosts string. This is where I hit my conundrum. I'm sure there must be some easy way to do that, but I can't think of it. This was my solution:
C#:
public string GetQueryString(string query, string page = "0", string limit = "") {
  if (RootUrl.Trim() == "") return "";

  SortedDictionary<int, string> keyorder = new SortedDictionary<int, string>();
  foreach (string var in new string[] { "query", "page", "postresultlimit" }) {
    if (QueryPosts.ToLower().IndexOf("%" + var + "%") == -1) continue;

    keyorder.Add(QueryPosts.ToLower().IndexOf("%" + var + "%"), var);
  }

  string ret = QueryPosts.Replace("%RootUrl%", RootUrl);
  foreach (KeyValuePair<int, string> keys in keyorder) {
    switch (keys.Value) {
    case "query":
      ret = ret.Replace("%Query%", ComponentQuery.Trim() == "" ? "" : ((ret.IndexOf("?") == -1 ? "?" : "&") + ComponentQuery.Replace("%value%", query)));
      break;

    case "page":
      ret = ret.Replace("%Page%", ComponentPage.Trim() == "" ? "" : (page.Trim() == "" ? "" : (ret.IndexOf("?") == -1 ? "?" : "&") + ComponentPage.Replace("%value%", page)));
      break;

    case "postresultlimit":
      ret = ret.Replace("%PostResultLimit%", ComponentPostResultLimit.Trim() == "" ? "" : (limit.Trim() == "" ? "" : (ret.IndexOf("?") == -1 ? "?" : "&") + ComponentPostResultLimit.Replace("%value%", limit)));
      break;
    }
  }

  return ret;
}
This works, but seems like a real kludge lol. Is there a more efficient way?
 
Use Flurl.Http from nuget

All that code you've written there would come down to something like:

C#:
//your config file contains a json like
/*
{
    "rootUrl": "blah",
    "queryParams": {
      "key1": "value1",
      "key2": "value2"
    }
}*/

//You have a class that you deserialize your config to

class Site{
  string RootUrl {get;set;}
  Dictionary<string,string> QueryParams {get;set;}
}

//you deser it
var site = JsonConvert.DeserializeObject<Site>(json_string_read_from_config_file);

//you use it with flurl, including doing the post and getting the result etc

var result = await site.RootUrl.SetQueryParams(site.QueryParams).GetXxxAsync();

Lines 1-22 are doing whatever the other code (you haven't put it here) does to read BaseUrl, QueryString etc from files

The code that builds the url up (and also calls the http service) is just on line 23. The RootUrl.SetQueryParams is done by Flurl and is what you're trying to achieve here, to build a url that has a query parameters collection turned into a url encoded list of key value pairs. Flurl allows you to call methods on a string to build functioning urls. Flurl.Http uses an http client to call those URLs and give you results

The last call on line 23, GetString (or GetJson, PostString, PutJson etc etc) is the http action to take and the kind of response you want. If all your APIs will return data in the same format you can make a class for it and do eg GetJson<YourClass>; if they return different it's a bigger question of how to handle it, but not in scope for this answer
 
Last edited:
Awesome, I knew there had to be a better way :)

I'm confused though; RootUrl is a string so VS is complaining that string doesn't contain a definition for SetQueryParams...?
 
Last edited:
If you start with the "Learn It" link here: Fluent URL Building - Flurl
The example starts with using Flurl; and explains:
The example above (and most on this site) uses an extension method off String to implicitly create a Url object.
That's how most extension methods work, they add functionality to existing and often common types, and for those extensions to become available the namespace they are in must be imported first (and the library they are defined in referenced of course). Linq is the most common extension library in .Net, without using System.Linq namespace none of that can be used.
 
Extension methods are a way of apparently extending/adding new functionality to other types, even ones that cannot be extended. it's a compiler smoke and mirrors trick; the compiler rewrites your code for you.

An extension method on string (not an extendable class) might look like:

C#:
public static class ExtensionMethods { //ext methods must be defined in a static class
  public static string ToSentenceCase(this string s){
    return Char.ToUpper(s[0]) + s.Substring(1).ToLower();
  }
}

The pivotal thing is the keyword `this` in the first argument. Declared thus, the compiler makes it look like the method is callable on a string:

C#:
  var s = "hello world";
  var t = s.ToSentenceCase();

What actually happens is the compiler rewrites your code before it compiles it, so it looks like this:

C#:
  var s = "hello world";
  var t = ExtensionMethods.ToSentenceCase(s);

All is good wth the world; there is no violation of C# inheritance principles etc.. It's just a way of making a more fluid looking syntax. You could have many extension methods, perhaps one that removes everything except numbers, one that took a string and churned out the int parse of it, and one that took an int and churned out the dutch currency formatting of it:

C#:
  string OnlyNumbers(this string x) { ... }
  int ParseInteger(this string x) { ... }
  string ToDutchCurrencyFormat(this int x) { ... }

  var s = "The amount is 1234";
  var t  = s.OnlyNumbers().ParseInteger().ToDutchCurrencyFormat();

  //what would really actually be formed by the compiler
  var t = ExtensionMethods.ToDutchCurrencyFormat(ExtensionMethods.ParseInteger(ExtensionMethods.OnlyNumbers(s)));

The extension method syntax is much more readable than the nested nested nested, because nested is read "backwards" from innermost to outermost.

---

Just like any other method, in order to use that method you have to have the class that implements the method accessible to whatever namespace you're in, and that probably involves a using. In this way, if you using Xxx and that Xxx namespace includes a class that carries extension methods, they suddenly become available to call on your objects, just like all the other normal methods on the classes in that namespace that was using'd
 
Last edited:
Wow, the extension methods sound like a really cool feature, a lot of possibilities there. I'll read up on that and learn how to use it. Appreciate the help!
 
Back
Top Bottom