SaaS CMS has officially launched! Learn more now.

BusinessManager.Load throws NullReferenceException when called within IBackgroundContext

Vote:
 

Hi,

As in title - the BusinessManager.Load throws NullReferenceException when called within IBackgroundContext.
The BusinessManager.Load method is called by IPromotionEngineExtensions.GetDiscountPrices within a service, that we'd like to parallelize using IBackgroundContext and Task.WhenAll.
On the other hand, if we're not using IBackgroundContext, we're getting errors mentioned in:
https://world.optimizely.com/blogs/Johan-Bjornfot/Dates1/2023/8/parallel-tasks-and-backgroundcontext/

Is the there a way to utilize the GetDiscountPrices (and by extent the BusinessManager.Load) within IBackgroundContext?

Code to reproduce:

var range = Enumerable.Range(0, 10).Select(_ => "4263b71a-69a4-43ff-b9a1-59a1d0cfbf9a"); // valid Customer Contact id
var customerContactTasks = range.Select(reference => Task.Run(() =>
{
    using var backgroundContext = _backgroundContextFactory.Create();

    // example to reproduce the problem, here is requested service which relies on BusinessManager.Load
    // using var service = backgroundContext.Services.GetRequiredService<IProblematicService>();
    // and then the service is called, internally using BusinessManager.Load via IPromotionEngine / IPromotionEngineExtensions

    var contactId = new PrimaryKeyId(new Guid(reference));
    var customerContact = BusinessManager.Load("Contact", contactId) as CustomerContact;
    return customerContact;
}));
var customerContacts = await Task.WhenAll(customerContactTasks);

The stack trace of above example:

   System.NullReferenceException: Object reference not set to an instance of an object.
   at Mediachase.BusinessFoundation.Data.Business.BusinessManager.Execute(Request request)
   at Mediachase.BusinessFoundation.Data.Business.BusinessManager.Load(String metaClassName, PrimaryKeyId primaryKeyId)
   at Project.Commerce.Features.Start.Builders.SomeViewModelBuilder.<>c__DisplayClass4_0.<Build>b__4() in C:\repo\src\Project.Commerce\Features\Start\Builders\SomeViewModelBuilder.cs:line 42
   at System.Threading.Tasks.Task`1.InnerInvoke()
   at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)

The stack trace of real-world example (anonimized):

System.NullReferenceException: Object reference not set to an instance of an object.
   at Mediachase.BusinessFoundation.Data.Business.BusinessManager.Load(String metaClassName, PrimaryKeyId primaryKeyId)
   at Mediachase.Commerce.Customers.CustomerContext.InnerGetContactById(Guid contactId)
   at Mediachase.Commerce.Customers.CustomerContext.<>c__DisplayClass32_0.<GetContactById>b__0()
   at Mediachase.Commerce.Customers.CustomersCache.<>c__DisplayClass6_0`1.<ReadThrough>g__WrapperAction|0()
   at EPiServer.Framework.Cache.ObjectInstanceCacheExtensions.ReadThrough[T](IObjectInstanceCache cache, String key, Func`1 readValue, Func`2 evictionPolicy)
   at EPiServer.Framework.Cache.ObjectInstanceCacheExtensions.ReadThrough[T](IObjectInstanceCache cache, String key, Func`1 readValue, Func`1 evictionPolicy)
   at Mediachase.Commerce.Extensions.ObjectInstanceCacheExtensions.ReadThrough[T](IObjectInstanceCache cache, Boolean useCache, String cacheKey, IEnumerable`1 masterKeys, TimeSpan duration, Func`1 load)
   at Mediachase.Commerce.Extensions.ObjectInstanceCacheExtensions.ReadThrough[T](IObjectInstanceCache cache, String cacheKey, IEnumerable`1 masterKeys, TimeSpan duration, Func`1 load)
   at Mediachase.Commerce.Customers.CustomersCache.ReadThrough[T](String key, IEnumerable`1 masterKeys, TimeSpan timeout, Func`1 load)
   at Mediachase.Commerce.Customers.CustomerContext.ExecuteThroughCache[T](String key, TimeSpan timeout, Func`1 load)
   at Mediachase.Commerce.Customers.CustomerContext.GetContactById(Guid contactId)
   at EPiServer.Commerce.Marketing.RedemptionLimitService.GetContactById(Guid customerId)
   at EPiServer.Commerce.Marketing.RedemptionLimitService.GetPerPromotionAndCustomerLimit(IEnumerable`1 promotions, Guid customerId, Int32 orderFormId)
   at EPiServer.Commerce.Marketing.RedemptionLimitService.GetRemainingRedemptions(IEnumerable`1 promotions, Guid customerId, Int32 orderFormId)
   at EPiServer.Commerce.Marketing.RedemptionLimits.Load(IEnumerable`1 promotions, Guid customerId, Int32 orderformId)
   at EPiServer.Commerce.Marketing.PromotionEngine.Run(IOrderGroup orderGroup, PromotionEngineSettings settings)
   at EPiServer.Commerce.Marketing.PromotionEngine.Evaluate(IEnumerable`1 entryLinks, IMarket market, Currency currency, RequestFulfillmentStatus requestFulfillmentStatus)
   at EPiServer.Commerce.Marketing.IPromotionEngineExtensions.Evaluate(IPromotionEngine promotionEngine, IEnumerable`1 entryLinks, IMarket market, Currency currency, RequestFulfillmentStatus requestFulfillmentStatus)
   at EPiServer.Commerce.Marketing.IPromotionEngineExtensions.GetDiscountedEntries(IPromotionEngine promotionEngine, ContentReference entryLink, IMarket market, Currency currency, ReferenceConverter referenceConverter, ILineItemCalculator lineItemCalculator)
   at EPiServer.Commerce.Marketing.IPromotionEngineExtensions.<>c__DisplayClass27_0.<ReadThroughCache>b__0()
   at EPiServer.Framework.Cache.ObjectInstanceCacheExtensions.ReadThrough[T](IObjectInstanceCache cache, String key, Func`1 readValue, Func`2 evictionPolicy)
   at EPiServer.Framework.Cache.ObjectInstanceCacheExtensions.ReadThrough[T](IObjectInstanceCache cache, String key, Func`1 readValue, Func`1 evictionPolicy)
   at EPiServer.Commerce.Marketing.IPromotionEngineExtensions.ReadThroughCache(IPromotionEngine promotionEngine, ContentReference contentLink, IMarket market, Currency currency, Guid contextId, ReferenceConverter referenceConverter, ILineItemCalculator lineItemCalculator, Func`5 getCacheKey, Func`7 readValues)
   at EPiServer.Commerce.Marketing.IPromotionEngineExtensions.GetDiscountPrices(IPromotionEngine promotionEngine, IEnumerable`1 entryLinks, IMarket market, Currency marketCurrency, ReferenceConverter referenceConverter, ILineItemCalculator lineItemCalculator)
   at EPiServer.Commerce.Marketing.IPromotionEngineExtensions.GetDiscountPrices(IPromotionEngine promotionEngine, IEnumerable`1 entryLinks, IMarket market, Currency marketCurrency)
   at Project.Commerce.Core.Prices.Services.PricingService.GetDiscountedPrice(String code) in C:\repo\src\Project.Commerce.Core\Prices\Services\PricingService.cs:line 54
   at Project.Commerce.Features.Product.Builders.SomeVariationViewModelBuilder.BuildDiscountedPrice(SomeVariationViewModel viewModel, SomeVariation variation, UserInfo user) in C:\repo\src\Project.Commerce\Features\Product\Builders\SomeVariationViewModelBuilder.cs:line 128
   at Project.Commerce.Features.Product.Builders.SomeVariationViewModelBuilder.Build(SomeVariationViewModel viewModel, SomeVariation variation, IIdentity identity) in C:\repo\src\Project.Commerce\Features\Product\Builders\SomeVariationViewModelBuilder.cs:line 82



#319482
Mar 25, 2024 14:10
Quan Mai - Mar 26, 2024 11:17
Just want to say that nicely formatted question - you clearly put effort into that. Well done
Vote:
 

think (just a thought, nothing concrete ) that the problem is BusinessManager will try to access the HttpContext which will be shared between the threads. even if you don't have that kind of problem you might run into other (HttpContext is not thread safe IIRC).

Why do you need to load the contacts in parallel? Can you show your code code GetDiscountedPrice(String code) ?

#319527
Mar 26, 2024 11:22
Vote:
 

Hi Quan,

The shortened version of Project.Commerce.Core.Prices.Services.PricingService.GetDiscountedPrice(String code)

public IPriceValue? GetDiscountedPrice(string code)
{
    // (skipped) getting contentReference from code
    var currentMarket = _currentMarket.GetCurrentMarket();
    var discountedPrices = _promotionEngine.GetDiscountPrices(new []{contentReference}, currentMarket, currentMarket.DefaultCurrency).ToList();
    // (skipped) more logic
    return priceValue;
}

And the _promotionEngine.GetDiscountPrices is just a call to the standard EPiServer.Commerce.Marketing.IPromotionEngineExtensions.GetDiscountPrices.
We don't use the contacts at all (in this scenario), but calling the IPromotionEngineExtensions.GetDiscountPrices calls internally loading contact with BusinessManager.Load and that's where the exception occurs. I tried to go down the chain to isolate what exactly is causing exception, that's why I focused on BusinessManager.Load.

#319531
Edited, Mar 26, 2024 12:00
Vote:
 

I think you can preload the contact using CustomerContext.Current.GetContactById, which would cache the contact and avoid calling to BusinessManager.Load

Not every API is meant to be run in parallel 

#319532
Mar 26, 2024 12:25
Vote:
 

Hi Quan,

That's great idea - if I call just once the IPromotionEngineExtensions.GetDiscountPrices on any product before running it inside the IBackgroundContext, then it works correctly 🥳

So it's a bit workaround, but it works 😉

#319537
Mar 26, 2024 13:09
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.