I have a custom object that is based in the following models:
public class Filter<T> where T : new()
{
public T Object { get; set; }
public int Page { get; set; }
public int ItemsPerPage { get; set; }
}
public class TransactionFilter<T> : Filter<T> where T : new()
{
public DateTime InitialDate { get; set; }
public DateTime EndDate { get; set; }
public DateTime InitialPayment { get; set; }
public DateTime EndPayment { get; set; }
public List<string> Filters { get; set; }
}
public class Transaction<T> : Base where T : new()
{
public T PaymentObject { get; set; }
}
public class Base
{
public bool Sandbox { get; set; }
public PaymentMethod PaymentMethod { get; set; }
}
public class PaymentMethod
{
public string Code { get; set; }
}
And is built on:
var queryObj = new TransactionFilter<Transaction<object>>()
{
Object = new Transaction<object> { PaymentMethod = new PaymentMethod { Code = "1" }, Sandbox = false },
InitialDate = new DateTime(2019, 03, 01),
EndDate = new DateTime(2019, 04, 01),
InitialPayment = new DateTime(2019, 03, 01),
EndPayment = new DateTime(2019, 04, 01),
Filters = new List<string>() { "ID", "Customer", "Reference", "PaymentMethod", "Application", "Ammount", "Vendor", "Status", "PaymentDate", "CreatedDate" }
};
And should be transformed in a querystring like this:
?Filter=ID&Filter=Customer&Filter=Reference&Filter=PaymentMethodCode&Filter=Application&Filter=Amount&Filter=Vendor&Filter=Status&Filter=PaymentDate&Filter=CreatedDate&Filter=PaymentMethod&Page1=PaymentMethod&Page=1&ItensPerPage100&Object.Sandbox=False&PaymentMethod.Code=1
Yes, Filter property is correct, the API needs to receive it this way... and that was my big issue to find a way to properly mount as needed based on the models.
Based on a lot of research, questions, debugging, exceptions and a lot of coffee, I finally got this as a result to build it as it should be:
private string QueryString(object request, string propertyName = null)
{
if (request == null) throw new ArgumentNullException(nameof(request));
var queryString = new StringBuilder();
var properties = request.GetType().GetProperties()
.Where(x => x.CanRead)
.Where(x => x.GetValue(request, null) != null)
.Where(x => !x.PropertyType.IsClass || x.PropertyType.IsClass && x.PropertyType.FullName == "System.String")
.ToDictionary(x => x.Name, x => x.GetValue(request, null));
foreach (var (key, value) in properties)
{
if (string.IsNullOrEmpty(propertyName))
queryString.AppendFormat("{0}={1}", Uri.EscapeDataString(key), Uri.EscapeDataString(value.ToString()));
else
queryString.AppendFormat("{0}.{1}={2}", Uri.EscapeDataString(propertyName), Uri.EscapeDataString(key), Uri.EscapeDataString(value.ToString()));
queryString.AppendFormat("&");
}
var classTypes = request.GetType().GetProperties()
.Where(x => x.CanRead)
.Where(x => x.GetValue(request, null) != null && x.PropertyType.IsClass && x.PropertyType.FullName != "System.String" && !(x.GetValue(request, null) is IEnumerable))
.ToDictionary(x => x.Name, x => x.GetValue(request, null));
var collectionTypes = request.GetType().GetProperties()
.Where(x => x.CanRead)
.Where(x => x.GetValue(request, null) != null)
.ToDictionary(x => x.Name, x => x.GetValue(request, null))
.Where(x => !(x.Value is string) && x.Value is IEnumerable)
.ToDictionary(x => x.Key, x => x.Value);
foreach (var (key, value) in collectionTypes)
{
var valueType = value.GetType();
var valueElemType = valueType.IsGenericType
? valueType.GetGenericArguments()[0]
: valueType.GetElementType();
if (valueElemType.IsPrimitive || valueElemType == typeof(string))
{
if (!(value is IEnumerable enumerable)) continue;
foreach (var obj in enumerable)
{
if (string.IsNullOrEmpty(propertyName))
queryString.AppendFormat("{0}={1}", Uri.EscapeDataString(key), Uri.EscapeDataString(obj.ToString()));
else
queryString.AppendFormat("{0}.{1}={2}", Uri.EscapeDataString(propertyName), Uri.EscapeDataString(key), Uri.EscapeDataString(obj.ToString()));
queryString.AppendFormat("&");
}
}
else if (valueElemType.IsClass)
{
var count = 0;
foreach (var className in (IEnumerable) value)
{
var queryKey = $"{key}[{count}]";
queryString.AppendFormat(QueryString(className, queryKey));
count++;
}
}
}
foreach (var (key, value) in classTypes)
queryString.AppendFormat(QueryString(value, key));
return "?" + queryString;
}
It's called in a quite simple way to mount the query and combine with the URL on API request:
var query = QueryString(queryObj);
var response = await client.GetAsync("https://api_address.com/Transaction/Get" + query);
It works as expected and provides the desired result, which is good enough to me, but I believe that it could be achieved on a simple/better way, so I would really appreciate to see a different view about that, if that could be considered as a good approach for this situation, both function and the call for it, to achieve the expected result.
Thanks!
Filtersshould be aHashSet<string>instead of aList, because it doesn't make sense that it may contain duplicates. \$\endgroup\$