We did a first .NET Core migration of Optimizely for one of our client last year. We were one of the early adopters - As far as I know, I think we were told we were like the forth or fifth migration to be ever initiated as part of this effort. Optimizely first released their first stable .NET Core release back on Q4 2021 if my memory is correct. In Q1-2 of 2022 we then started to migrate our client and we did it successfully. The thing though, at the time we did the migration, only .NET 5 was targeted for certain Optimizely libraries, so had to do this migration in .NET 5, even though it was about to be EOL by the beginning of May 2022.
This lead us to migrate our client a second time when all libraries were targeting .NET 6 later on the same year. This is where we faced a major issue with some of the carts on the website. Not systematically all of them, but it was quite a deal breaker.
Under this solution, we have a custom IPayment
, which was being used to secure different payment data. For simplifying my explanations, we'll call this custom payment a CustomPayment
(ooohh super original). This CustomPayment
was rarely used, thus the reason the problem wasn't occurring all the time. I ended up finding the root cause by these simple reproduction steps:
CustomPayment
with a call on IOrderGroupFactory.CreatePayment(orderGroup, typeof(CustomPayment));
IOrderRepository.Save(cart);
with the user's cart now containing the CustomPayment
Thereafter, every following http request would provoke a crash. They were all caused by any call being made on OrderRepository.Load<ICart>([...])
.
Upon further investigating the problem, I found out the source of the issue was coming from Optimizely's library, which leads me to find a behavioral change on how the payments are deserialized between version 14.2.0 and 14.6.0 of Episerver.Commerce.Core. When decompiling both versions of the class PaymentConverter
, the method Create
does things quite differently:
Version 14.2.0 and similar:
private IPayment Create(JObject jObject, Type objectType)
{
string typeName = JToken.op_Explicit(jObject["ImplementationClass"]);
if (string.IsNullOrEmpty(typeName))
return this._paymentFactory.Service(objectType);
Type type = Type.GetType(typeName);
return type == (Type) null || typeof (Payment).IsAssignableFrom(type) && this._featureSwitch.Service.IsSerializedCartsEnabled() ? this._paymentFactory.Service(objectType) : this._paymentFactory.Service(type);
}
Version 14.6.0:
private IPayment Create(JObject jObject, Type objectType)
{
string str = JToken.op_Explicit(jObject["ImplementationClass"]);
if (string.IsNullOrEmpty(str))
return this._paymentFactory.Service(objectType);
Type type = TypeResolver.GetType(str);
return type == (Type) null ? this._paymentFactory.Service(objectType) : this._paymentFactory.Service(type);
}
Explanation time! So under the working version, the variable type
is indeed assignable from a Payment
and the feature switch is enabled. Since this is the case, the condition is using the variable objectType
, which at runtime, is assigned with the value EPiServer.Commerce.Order.Internal.SerializablePayment
. The paymentFactory
is a wrapper around the IoC ServiceProvider, so basically it creates an empty instance of a SerializablePayment
. A SerializablePayment
is registered elsewhere by Optimizely inside the IoC container, so this is why this is working.
As for the non-working version, the issue is quite evident. At the return statement, the variable type
is never null, so it calls the second end of the condition, the paymentFactory
with the variable type
as parameter. As we have never registered our custom payment under the IoC container, this line will return null and will cause a crash elsewhere in Optimizely's code.
At first, I thought I could simply add our CustomPayment
inside the IoC container, but that caused another issue in relation to the deserialization logic of Optimizely. The type must absolutely be compatible with a SerializablePayment
. This, of course, isn't the case; our custom payment is only inheriting from Mediachase.Commerce.Orders.Payment
. To counter this issue, I used a very obscure and a "I wouldn't recommend it" approach to resolve the problem. Before moving with the solution though, let me first tell you about type forwarding: As you're probably already aware, there's the existence of extension methods allowing you to "Forward" certain types to others under the IoC container, it is known to be available under the Optimizely libraries. It's also a practice other third party IoC containers have implemented, e.g., Windsor, which also has this forward type concept. All in all, the idea is quite straightforward: You want to have a certain service, to be registered under multiple different types. Any of the registered types will resolve the same service instance, based on its configured lifetime. This is what gave me the idea to resolve the issue like this:
// The following two lines are the equivalence of this call: services.Forward<SerializablePayment, CustomPayment>();
// Except that on the second line, I cast the instance as IPayment instead of a SerializablePayment.
// This is a hack, it will force Epi to use its in house object when deserializing a CustomPayment.
var serviceDescriptor = services.First((Func<ServiceDescriptor, bool>)(s => s.ServiceType == typeof(SerializablePayment)));
// ReSharper disable once RedundantCast -> Keep it, important.
services.Add(typeof(CustomPayment), s => s.GetService<SerializablePayment>() as IPayment, serviceDescriptor.Lifetime);
Super sketchy, but it works. Like mentioned inside the comments, the code is inspired from the "Forward" extension method of Optimizely. It does the following:
SerializablePayment
service.CustomPayment
which resolves to the existing SerializablePayment
already registered within the IoC container.
SerializablePayment
as IPayment
Since both CustomPayment
and SerializablePayment
inherits from IPayment
, this resolves our deserialization issue. At runtime, the call onto _paymentFactory.Service(type);
will simply load a SerializablePayment
, even though the value of the variable type
is CustomPayment
.
Edit: The bug has been fixed: https://world.optimizely.com/support/bug-list/bug/COM-16500.