Reputation: 589
I've written up the nuance and the details here, but here's the gist of what I'm struggling with:
I have a third-party component that returns any one of a variety of POCO objects that basically look the same. By look the same, I mean that if you skimmed through the JSON of the objects, you would assume they are the exact same class. But they aren't the same class. And despite having the same shape, they do not implement an interface or inherit from a base class. Something like this:
// Order V1
public class ThirdPartyOrderV1
{
public decimal Amount { get; set; }
public ThirdPartyOrderItemV1[] LineItems { get; set; }
public ThirdPartyOrderAddressV1 BillingAddress { get; set; }
public ThirdPartyOrderAddressV1 ShippingAddress { get; set; }
public string SomeV1ThingIDontCareAbout { get; set; }
public decimal AnotherV1ThingIDontCareAbout { get; set; }
// ...
}
public class ThirdPartyOrderItemV1
{
public int ItemID { get; set; }
public int Quantity { get; set; }
// ...
}
public class ThirdPartyOrderAddressV1
{
public string Name { get; set; }
public string Street { get; set; }
// ...
}
// Order V2
public class ThirdPartyOrderV2
{
public decimal Amount { get; set; }
public ThirdPartyOrderItemV2[] LineItems { get; set; }
public ThirdPartyOrderAddressV2 BillingAddress { get; set; }
public ThirdPartyOrderAddressV2 ShippingAddress { get; set; }
public object SomeV2ThingIDontCareAbout { get; set; }
// ...
}
public class ThirdPartyOrderItemV2
{
public int ItemID { get; set; }
public int Quantity { get; set; }
// ...
}
public class ThirdPartyOrderAddressV2
{
public string Name { get; set; }
public string Street { get; set; }
// ...
}
The library has 3 or 4 of these versions. Right now when I get the object from the library, I do type checking on them and pass them to a method to handle the specific type:
public static void Main()
{
var parser = new ThirdPartyOrderParser();
var order = parser.ParseOrder("text");
if (order is ThirdPartyOrderV1)
{
IngestOrderV1(order as ThirdPartyOrderV1);
}
else if (order is ThirdPartyOrderV2)
{
IngestOrderV2(order as ThirdPartyOrderV2);
}
// ...
}
This is not ideal since I end up duplicating my method for each type. I want to have a single method that can handle any arbitrary object that looks like an order (or a line item, or an address, etc).
I know I can't be the only one that has faced this type of issue. I have a few ideas, like casting it to a dynamic
object and referencing the properties that way. Or serialize it to JSON and traverse through the [JObject representation](https://www.newtonsoft.com/json/help/html/QueryingLINQtoJSON.htm (or XML or YAML or something else)
But these solutions still feel brittle and just wrong. Is there a better way to handle this? Maybe even some tool that can generate a class the represents the commonalities of the classes and translates back and forth between the actual class and this "common" class?
Thanks!
Upvotes: 1
Views: 159
Reputation: 29262
It's rarely a good idea to work against strong typing by using dynamic
. It's unfortunate that you have to deal with all of these types, but at least they are defined so you know what each of them represents.
I'd start by defining your own classes that meet your needs. They may or may not resemble the classes in this 3rd-party library. If you receive any input in any other form, immediately convert it to your own models. That way you avoid being tightly coupled to someone else's classes. If they change, or if you want to integrate with some other library, you primarily just have to add mapping from their models to your models.
Suppose these are the external models:
public class ThirdPartyOrderV1
{
public decimal Amount { get; set; }
public ThirdPartyOrderItemV1[] LineItems { get; set; }
}
public class ThirdPartyOrderItemV1
{
public int ItemID { get; set; }
public int Quantity { get; set; }
}
And these are your models, which allow you to handle items from multiple sources:
public class MyOrder
{
public Money Amount { get; set; }
public MyOrderItem Lines { get; set; }
}
public class MyOrderItem
{
public ItemId ItemId { get; set; }
public int Quantity { get; set; }
}
public class ItemId
{
public OrderItemType ItemType { get; set; }
public string Value { get; set; }
}
public enum OrderItemType
{
Internal,
Acme,
BobsThirdPartyItems
}
You can use Automapper for that, or even just write your own methods to convert them. If it's not really obvious 1:1 mapping I'd rather write my own. You have to write the same logic either way.
public static class BobsThirdPartyItemExtensions
{
public static MyOrder ToMyOrder(this ThirdPartyOrderV1 source)
{
return new MyOrder
{
Amount = new Money(source.Amount, Currency.USD),
Lines = source.LineItems.Select(item => item.ToMyOrderItem()).ToArray()
};
}
public static MyOrderItem ToMyOrderItem(this ThirdPartyOrderItemV1 source)
{
return new MyOrderItem
{
ItemId = new ItemId
{
ItemType = OrderItemType.BobsThirdPartyItems,
Value = source.ItemID.ToString("0")
},
Quantity = source.Quantity
};
}
}
var myOrder = someThirdPartyV1order.ToMyOrder();
So far this protects your application from unwanted external complexity by ensuring that internally you only have to deal with one set of classes which have been defined by you for your application. That still leaves the detail of handling all of those redundant V1
and V2
types. You could just write additional extension methods for those, which would be identical. That's not really code duplication. They are all different types that need to be mapped. I'd probably lean toward that.
If they really are identical then you could use Automapper to convert one external type to the other external type.
Suppose you have
public class ThirdPartyOrderV2
{
public decimal Amount { get; set; }
public ThirdPartyOrderItemV2[] LineItems { get; set; }
public string DontNeedThis { get; set; }
}
public class ThirdPartyOrderItemV2
{
public int ItemID { get; set; }
public int Quantity { get; set; }
}
You could configure Automapper like this:
Mapper.Initialize(config =>
{
config.CreateMap<ThirdPartyOrderItemV2, ThirdPartyOrderItemV1>();
config.CreateMap<ThirdPartyOrderV2, ThirdPartyOrderV1>()
.ForSourceMember(source => source.DontNeedThis, option => option.Ignore());
});
This basically says to map all identical properties from V2 to V1, and ignore the one we don't care about.
Now, given an instance of V2
, you can do this:
var v2 = new ThirdPartyOrderV2
{
Amount = 5.00M,
DontNeedThis = "Who cares",
LineItems = new ThirdPartyOrderItemV2[]
{
new ThirdPartyOrderItemV2 {ItemID = 5, Quantity = 200},
new ThirdPartyOrderItemV2 {ItemID = 4, Quantity = 50}
}
};
var v1 = Mapper.Map<ThirdPartyOrderV1>(v2);
var myOrder = v1.ToMyOrder();
You can't get around doing some mapping, but I'd keep it strongly-typed and avoid doing anything with dynamic
unless you have no choice.
Your comments mentioned that these classes are large, so creating your own class would be a lot of extra work to maintain. I'd consider doing it anyway. If there's maintenance that means that something is changing, and you're going to have to change some code somewhere to account for that. I'd rather handle that change at the boundary of my application where I receive inputs than have my internal logic breaking and having to do maintenance there. At least your maintenance is done when you've mapped the external input to something you've defined and you control.
If you're mapping between strongly-typed classes and properties then even if it's slightly tedious, it will be comprehensible to the next person who has to maintain it. It also accounts for the reality that while the classes seem identical today, you can't control that. (If the third-party library made rational choices then maybe they wouldn't duplicate so many classes. Can you trust them to do what makes sense tomorrow?) If you deviate from that then it will be both tedious and harder to figure out what's going on.
Upvotes: 1