UPDATE 1: Apparently HttpContext.Current.Request.AnonymousID
already uses the cookie internally, so there might be something that makes it stop working. I’ll update when I found out.
Today we received a support ticket as customers seeing corrupted carts data being lost – line items with invalid data, duplicated line items etc. “Corrupted data” is one of the alarming words that we take very seriously, so I decided to jump on it right away.
The setup is a load balancing environment, and the problem only happens with anonymous users. However, it can be “fixed” by turning on the sticky sessions mode. So basically, instead of having sessions on the memory of a server (so sessions on server A can’t be seen by server B, and vice versa), they need a mechanism (can be a database) to share sessions between servers.
When looking at their code, I found out what was wrong:
var orderFactory = ServiceLocator.Current.GetInstance<IOrderFactory>(); var contactId = PrincipalInfo.CurrentPrincipal.GetContactId(); //Create cart var cart = orderFactory.LoadOrCreateCart(contactId, "Default");
This code is actually from Episerver official documentation. Keep in mind that most of sample code from Episerver is not tested in a load balancing configuration – they are supposed to be examples of using the APIs, so if you are running on load balancer, just don’t take sample code as-is, without testing. Load balancing is hard!
PrincipalInfo.CurrentPrincipal.GetContactId()
will return the contact id of the current user, if he or she is registered. Otherwise it’ll create a new Guid
value, based on HttpContext.Current.Request.AnonymousID
That explains why the problem only happens with anonymous users, and why sticky session fixes it. When an anonymous user’s request is sent to another server, GetContactId
sees that as a different user and returns a new Guid
value. That ends up IOrderFactory.LoadOrCreateCart
to create a new cart instead of loading the existing one. (if you don’t already know, then a cart is supposed to be unique with a combination of name, customer id, and the market it’s in, so you can’t have two carts with same name of “default”, for same customer, and in same market, but you can have two carts for same customer with same name, if they are in “US” and “SWE” markets). Sticky session fixes that by ensuring that every server sees a same AnonymousID
, therefore same cart is loaded correctly, and the problem disappears.
So can we just stick with sticky sessions, and be done with it?
Well, it depends. Sticky session is not without overhead, and it limits the ability for you to scale up. If you add more servers to your load balancer, your session server can easily be the single point of failure. In most of the cases, you won’t want that. Also, enabling sticky session to fix this specific issue is a bit overkill.
So is there an effective, nice way to fix the issue? Well, in this case, I think cookie is a very good choice – it’s lightweight, it’s easy to work with, and it solve the load balancing problem nicely.
public static class PersistentCustomerId { const string CookieKey = "AnonymousId"; public static Guid GetCustomerId() { if (PrincipalInfo.CurrentPrincipal.Identity.IsAuthenticated) { return PrincipalInfo.CurrentPrincipal.GetCustomerContact().PrimaryKeyId.Value; } else { var cookie = HttpContext.Current.Request.Cookies[CookieKey]; if (cookie == null) { var anonynousId = HttpContext.Current.Request.AnonymousID; cookie = new HttpCookie(CookieKey, anonynousId); cookie.Expires = DateTime.UtcNow.AddDays(30); HttpContext.Current.Response.Cookies.Add(cookie); return new Guid(anonynousId); } return new Guid(cookie.Value); } } }
I’m using a static class here, but you can easily change it to a normal class, or an extension of IPrincipal
, to replace the GetContactId
(you can name it GetPersistentContactId
, for instance). The idea is simple: if the current user is authenticated (i.e. logged in) just return the normal contact id. Otherwise check if the current request has a cookie with specific name. If there is not (first request), we create one and add back to the response. Otherwise, just take that cookie value and return.
Will Episerver include this in the framework? I doubt it. Using cookie is an implementation and it does not work in every case (customers might choose to reject cookies, rendering this broken). For the time being, make sure to take the sessions into consideration if you are running on load balancing configuration.
Shameless plug: this will be converted to a recipe, in my new book. If this blog post helps you, consider support my book. Thank you!
Another example of why you don’t want Sticky Sessions is that you can’t re route your traffic in real time to another server, leading to rolling deploys being next to impossible. You’ll have to shut down the IIS, cancelling all current TCP connections, which is very bad experience for the users stuck on that server.
Wheras if you don’t have sticky sessions, you can simlpy tell the loadbalancer to not send requests to that server anymore, and you can then turn it off without any customers realizing.
Official Episerver documentation has not even just “load balancing” issue in the code, but it also uses `ServiceLocator` 😉
And – I would recommend to better show people instance based solution (for `PersistentCustomerId`) and note, that this could be turned into static one as well, if you will.
Re static – I intentionally left that as a practice! IMO an extension method would work very nicely here.