Deciding which approach to take
Your problem is, essentially, to create a custom serializer for your classes, based on the provided API (presumably fixed).
To separate concerns as much as possible, this functionality is commonly implemented separately from your entity classes, leaving them (if anyhow possible) as POCOs, or dumb DTOs which are serialization independent, as much as possible. So, just like you would use a XmlSerializer
or a DataContractSerializer
to serialize a class into XML, or Protobuf.NET to serialize it into protocol buffers, arguably the most general approach would be to create your own serializer.
Of course, as with all other problems you encounter in daily programming, you need to weigh potential benefits and decide how much effort you want to invest into refactoring. If you have a small number of cases, nobody will get hurt by a couple of copy/pasted hard-coded methods, similar to what you're doing now. Also, if this is just a tiny "pet project", then you might decide that you don't want to waste time on potential issues you might encounter while trying to refactor into a more general solution (which you might never need again).
Your goal is to have to write as little as possible
However, if you do choose to invest some time in writing a serializer, then you will quickly note that most serialization frameworks try to rely on convention for serialization as much as possible. In other words, if your class is:
public class ClientData
{
public string Name { get; set; }
public string Email { get; set; }
}
Then a XmlSerializer
will, without any configuration at all, produce the following XML:
<ClientData>
<Name>...</Name>
<Email>...</Email>
</ClientData>
It would also be really cool to have a class which would simply spit out ?name=...&email=...
for that object, with absolutely no additional work on your side. If that works, than you have a class which will not only remove duplication from existing code, but seriously save time for all future extensions to the API.
So, if you are writing your classes based on the API, then it might make sense to name properties exactly like API members whenever possible (and use convention-based serialization), but still keep it open enough to be able to handle a couple of edge cases separately.
Example code
public class ClientData
{
public string Name {get; set;}
public string Email {get; set;}
}
// customer really insisted that the property is
// named `PaymentAmount` as opposed to simply `Amount`,
// so we'll add a custom attribute here
public class PaymentData
{
[MyApiName("payment")]
public decimal PaymentAmount {get; set;}
public string Description {get; set;}
}
The MyApiName
attribute is really simple, just accepts a single string argument:
public class MyApiNameAttribute : Attribute
{
private readonly string _name;
public string Name
{ get { return _name; } }
public MyApiNameAttribute(string name)
{ _name = name; }
}
With that in place, we can now use a bit of reflection to render the query:
public static string Serialize(object obj)
{
var sb = new StringBuilder();
foreach (var p in obj.GetType().GetProperties())
{
// default key name is the lowercase property name
var key = p.Name.ToLowerInvariant();
// we need to UrlEncode all values passed to an url
var value = Uri.EscapeDataString(p.GetValue(obj, null).ToString());
// if custom attribute is specified, use that value instead
var attr = p
.GetCustomAttributes(typeof(MyApiNameAttribute), false)
.FirstOrDefault() as MyApiNameAttribute;
if (attr != null)
key = attr.Name;
sb.AppendFormat(
System.Globalization.CultureInfo.InvariantCulture,
"{0}={1}&",
key, value);
}
// trim trailing ampersand
if (sb.Length > 0 && sb[sb.Length - 1] == '&')
sb.Length--;
return sb.ToString();
}
Usage:
var payment = new PaymentData()
{
Description = "some stuff",
PaymentAmount = 50.0m
};
// this will produce "payment=50.0&description=some%20stuff"
var query = MyApiSerializer.Serialize(payment)
Performance
As noted in the comments, the power of reflection does incur a performance penalty. In most cases, this should not be of much concern. In this case, if you compare the cost of building the query string (probably in the range of 10s of microseconds) with the cost of executing the HTTP request, you'll see that it's pretty much negligible.
If, however, you decide that you want to optimize, you can easily do it at the end, after profiling, by changing that single method which does all the work by caching property information or even compiling delegates. That's good about separation of concerns; duplicated code is hard to optimize.