Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# method able to handle/accept different class types

Tags:

c#

I have a method that accepts a simple class object and builds a URL used in an API call. I would like this method to be able handle/accept different class types that are similar yet have different properties.

public class ClientData
{
   public string Name {get; set;}
   public string Email {get; set;}
   ...
}

public class PaymentData
{
   public decimal PaymentAmount {get; set;}
   public string Description {get; set;}
   ...
}

Below are two example methods. As you can see they are very similar. Would it better to implement these as different methods accepting different parameters or can one method be written that can deal with parameter object differences?

public string BuildApiCall(ClientData clientDataObject)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("http://mytestapi.com/");
  sb.append("name=" + clientDataObject.Name);
  sb.append("email=" + clientDataObject.Email);
  return sb.ToString();
}

public string BuildApiCall(PaymentData paymentDataObject)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("http://mytestapi.com/");
  sb.append("payment=" + paymentDataObject.PaymentAmount );
  sb.append("description=" + paymentDataObject.Description );
  return sb.ToString();
}
like image 645
webworm Avatar asked Dec 20 '22 12:12

webworm


2 Answers

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.

like image 193
Groo Avatar answered Jan 07 '23 04:01

Groo


Define an interface:

public interface IWhatsit
{
     string ToApiString();
}

Now have your data objects implement it. ToApiString should return the query string part for this particular object:

public class ClientData : IWhatsit
{
   public string Name {get; set;}
   public string Email {get; set;}
   ...

   public string ToApiString()
   {
       // Do whatever you need here - use a string builder if you want
       return string.Format("Name={0}&Email={1}",Name,Email);
   }
}

And now you can have a single method for making the API call:

public string BuildApiCall(IWhatsit thing)
{
  StringBuilder sb = new StringBuilder();
  sb.Append("http://mytestapi.com/");
  sb.append(thing.ToApiString());
  return sb.ToString();
}

Note: You could use a property instead of a method in the interface if you so wished.

An alternative approach would be to use an abstract base class and inherit from that. Then you could do something like this:

public abstract class BaseData
{
    protected abstract string ToApiString();

    public string BuildApiCall()
    {
        StringBuilder sb = new StringBuilder();
        sb.Append("http://mytestapi.com/");
        sb.append(ToApiString());
        return sb.ToString();
    }
}

And then each class would look something like this:

public class ClientData : BaseData
{
   public string Name {get; set;}
   public string Email {get; set;}
   ...

   protected override string ToApiString()
   {
       // Do whatever you need here - use a string builder if you want
       return string.Format("Name={0}&Email={1}",Name,Email);
   }
}

Which lets you put BuildApiCall inside the class itself and have a base implementation. Of course, if you do need BuildApiCall to be outside of these classes, then you could do that. It would just take a BaseData and you'd have to make ToApiString public rather than protected.

like image 21
Matt Burland Avatar answered Jan 07 '23 05:01

Matt Burland