Calling all developers! We invite you to provide your input on Feature Experimentation by completing this brief survey.

 

dave.ketnor@twentysixdigital.com
Aug 12, 2014
  3371
(6 votes)

ICurrentMarket Storage Providers

This is my first ever blog post, so be gentle ;)

In this post we’re going to discuss how we can create a number of different storage provider’s for use with our ICurrentMarket implementation. The main objectives for this post will be to:

  • Provide implementations for a number of different storage providers, including Session, Profile, Cookie
  • Highlight how clean, well architected code will allow your implementation to serve numerous clients and provide more extensible solutions

So without further ado, we’ll get started.

ICurrentMarket

The purpose of this interface is to return the market of the current request as detailed here.

Here is a typical implementation taken from the commerce demo site, which persists the users current market within their membership profile.

namespace EPiServer.Commerce.Sample.Helpers
{
    /// 
    /// Implementation of current market selection that stores information in user profile.
    /// 
    public class MarketStorage : ICurrentMarket
    {
        private const string _marketIdKey = "MarketId";
        private readonly IMarketService _marketService;
 
        /// 
        /// Initializes a new instance of the  class.
        /// 
        public MarketStorage()
            : this(ServiceLocation.ServiceLocator.Current.GetInstance())
        { }
 
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The market service.
        public MarketStorage(IMarketService marketService)
        {
            _marketService = marketService;
        }
 
        /// 
        /// Gets the  selected in the current user profile, if the value is
        /// set and the indicated market is valid; ; otherwise, gets the default .
        /// 
        /// The current .
        public IMarket GetCurrentMarket()
        {
            var profileStorage = GetProfileStorage();
            var profileMarketId = profileStorage == null ? null : profileStorage[_marketIdKey] as string;
            var marketId = string.IsNullOrEmpty(profileMarketId) ? MarketId.Default : new MarketId(profileMarketId);
            var market = _marketService.GetMarket(marketId);
 
            if (market == null && marketId != MarketId.Default)
            {
                market = _marketService.GetMarket(MarketId.Default);
            }
 
            UpdateProfile(market);
 
            return market;
        }
 
        /// 
        /// Sets the current market, if  represents a valid market;
        /// otherwise, performs no action.
        /// 
        /// The market id.
        /// This will also set the current currency for the ECF context.
        public void SetCurrentMarket(MarketId marketId)
        {
            var market = _marketService.GetMarket(marketId);
            if (market != null)
            {
                UpdateProfile(market);
                SiteContext.Current.Currency = market.DefaultCurrency;
                Globalization.ContentLanguage.PreferredCulture = market.DefaultLanguage;
            }
        }
 
        private void UpdateProfile(IMarket market)
        {
            var profileStorage = GetProfileStorage();
            if (profileStorage != null)
            {
                var originalMarketId = profileStorage[_marketIdKey] as string;
                var currentMarketId = market == null || market.MarketId == MarketId.Default ? string.Empty : market.MarketId.Value;
                if (!string.Equals(originalMarketId, currentMarketId, StringComparison.Ordinal))
                {
                    profileStorage[_marketIdKey] = currentMarketId;
                    profileStorage.Save();
                }
            }
        }
 
        private ProfileBase GetProfileStorage()
        {
            var httpContext = HttpContext.Current;
            return httpContext == null ? null : httpContext.Profile;
        }
    }
}

This works well, but includes the underlying persistence within the actual implementation of ICurrentMarket. If we had further clients who had different persistence requirements, we need to implement ICurrentMarket again which would result in repeated ICurrentMarket implementations (DRY).

If we abstracted the Current Market storage implementation out of ICurrentMarket we’d be in a better place.

Enter ICurrentMarketStorageProvider

ICurrentMarketStorageProvider

The purpose of this interface is to and store and retrieve the current market.

public interface ICurrentMarketStorageProvider
    {
        string MarketIdKey { get; }
 
        string Get();
 
        void Set(string value);
    }

This is a pretty simple implementation, so how does it change our implementation of ICurrentMarket?

/// 
    /// Implementation of current market selection.
    /// 
    public class SiteCurrentMarket : ICurrentMarket
    {
        private readonly IMarketService marketService;
        private readonly ICurrentMarketStorageProvider storageProvider;
 
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The market service.
        /// The market storage provider
        public SiteCurrentMarket(IMarketService marketService, ICurrentMarketStorageProvider storageProvider)
        {
            this.marketService = marketService;
            this.storageProvider = storageProvider;
        }
 
        /// 
        /// Gets the  selected within the storage provider, if the value is
        /// set and the indicated market is valid; ; otherwise, gets the default .
        /// 
        /// The current .
        public IMarket GetCurrentMarket()
        {
            string currentMarketId;
 
            try
            {
                currentMarketId = this.storageProvider.Get();
            }
            catch (Exception ex)
            {
                currentMarketId =  MarketId.Default;
            }
 
            var marketId = string.IsNullOrEmpty(currentMarketId) ? MarketId.Default : new MarketId(currentMarketId);
            var market = this.marketService.GetMarket(marketId);
 
            if (market == null && marketId != MarketId.Default)
            {
                market = this.marketService.GetMarket(MarketId.Default);
            }
 
            return market;
        }
 
        /// 
        /// Sets the current market, if  represents a valid market;
        /// otherwise, performs no action.
        /// 
        /// The market id.
        /// This will also set the current currency for the ECF context.
        public void SetCurrentMarket(MarketId marketId)
        {
            var market = this.marketService.GetMarket(marketId);
            if (market != null)
            {
                this.storageProvider.Set(market.MarketId.Value);
                SiteContext.Current.Currency = market.DefaultCurrency;
                EPiServer.Globalization.ContentLanguage.PreferredCulture = market.DefaultLanguage;
            }
            else
            {
                throw new ArgumentException("Unknown MarketId when setting Current Market", "marketId");
            }
        }
    }

Note the ICurrentMarketStorageProvider interface being passed as a constructor parameter and also note the implementations of GetCurrentMarket() and SetCurrentMarket() now use the storage provider to retrieve and persist the current market. Now that we’ve abstracted the persistence out of ICurrentMarket lets implement some example storage providers.

MembershipCurrentMarketStorageProvider

using System;
    using System.Web;
    using System.Web.Profile;
 
    /// 
    /// A membership based provider for storing a users current market.
    /// 
    public class MembershipCurrentMarketStorageProvider : ICurrentMarketStorageProvider
    {
        private readonly HttpContextBase context;
 
        public HttpContextBase Context
        {
            get
            {
                return context;
            }
        }
 
        public virtual string MarketIdKey
        {
            get
            {
                return "MarketId";
            }
        }
 
        public MembershipCurrentMarketStorageProvider()
        {
            this.context = new HttpContextWrapper(HttpContext.Current);
        }
 
        public MembershipCurrentMarketStorageProvider(HttpContextBase context)
        {
            this.context = context;
        }
 
        public virtual string Get()
        {
            var profileMarketId = string.Empty;
 
            if (context != null && context.Profile != null)
            {
                if (context.Profile.GetPropertyValue(this.MarketIdKey) != null)
                {
                    profileMarketId = context.Profile[this.MarketIdKey] as string;
                }
            }
 
            return profileMarketId;
        }
 
        public virtual void Set(string value)
        {
            if (context != null && context.Profile != null)
            {
                var originalMarketId = context.Profile[this.MarketIdKey] as string;
                if (!string.Equals(originalMarketId, value, StringComparison.Ordinal))
                {
                    context.Profile[this.MarketIdKey] = value;
                    context.Profile.Save();
                }
            }
        }
    }

This is pretty simple and is more or less a simple refactoring of the original MarketStorage example above.

SessionCurrentMarketProvider

using System.Web;
 
    /// 
    /// A session based provider for storing a users current market.
    /// 
    public class SessionCurrentMarketStorageProvider : ICurrentMarketStorageProvider
    {
        private readonly HttpSessionStateBase session;
 
        public HttpSessionStateBase Session
        {
            get
            {
                return session;
            }
        }
 
        public virtual string MarketIdKey
        {
            get
            {
                return "MarketId";
            }
        }
 
        public SessionCurrentMarketStorageProvider()
        {
            this.session = new HttpSessionStateWrapper(HttpContext.Current.Session);
        }
 
        public SessionCurrentMarketStorageProvider(HttpSessionStateBase session)
        {
            this.session = session;
        }
 
        public string Get()
        {
            if (Session != null && Session[this.MarketIdKey] != null)
            {
                return Session[this.MarketIdKey].ToString();
            }
            
            return string.Empty;
        }
 
        public void Set(string value)
        {
            if (Session != null)
            {
                Session[this.MarketIdKey] = value;
            }
        }
    }

Again another simple implementation, that utilises Http session.

NOTE: If you’re thinking of using this within production environments that are load balanced ensure server affinity is configured accordingly or you use permanent session storage techniques such as SQL Server or a State Server.

CookieCurrentMarketStorageProvider

using System;
    using System.Web;
 
    /// 
    /// A cookie based provider for storing a users current market.
    /// 
    public class CookieCurrentMarketStorageProvider : ICurrentMarketStorageProvider
    {
        private readonly HttpContextBase context;
 
        public HttpContextBase Context
        {
            get
            {
                return this.context;
            }
        }
 
        public virtual string CookieName
        {
            get
            {
                return "Market";
            }
        }
 
        public virtual string MarketIdKey
        {
            get
            {
                return "MarketId";
            }
        }
 
        public virtual bool StoreAsSecureCookie
        {
            get
            {
                return false;
            }
        }
 
        public CookieCurrentMarketStorageProvider()
        {
            this.context = new HttpContextWrapper(HttpContext.Current);
        }
 
        public CookieCurrentMarketStorageProvider(HttpContextBase context)
        {
            this.context = context;
        }
 
        public virtual string Get()
        {
            if (this.Context != null)
            {
                var httpCookie = this.Context.Request.Cookies[this.CookieName];
                if (httpCookie != null)
                {
                    return httpCookie[this.MarketIdKey];
                }
            }
 
            return string.Empty;
        }
 
        public virtual void Set(string value)
        {
            if (this.Context != null)
            {
                var httpCookie = this.context.Request.Cookies[this.CookieName] ?? new HttpCookie(this.CookieName);
 
                httpCookie[this.MarketIdKey] = value;
                httpCookie.Expires = DateTime.Now.AddHours(2d);
                httpCookie.Secure = this.StoreAsSecureCookie;
                context.Response.Cookies.Set(httpCookie);
            }
        }
    }

Another fairly standard implementation.

Configuration

The only thing we need to do now is register the implementation within our IConfigurableModule and we’re away.

A point worth mentioning is that whenever you use a Storage Provider which utilises the HttpContext ensure that ICurrentMarketStorageProvider and ICurrentMarket are scoped accordingly.

In Summary

This approach allows your implementation of ICurrentMarket to remain unchanged across multiple clients/persistence requirements and allows your persistance mechanism to be interchangeable. This also provides the benefit of allowing your ICurrentMarket and your storage providers to be all testable.

So there you go… a pretty fundamental blog post which should hopefully highlight how small architectural changes can provide big benefits further down the line.

Thanks for reading

Aug 12, 2014

Comments

Aug 15, 2014 02:28 PM

Great first post David!

Good use of abstracting the underlying storage from the service. Makes this clean and testable.

Son Do
Son Do Aug 21, 2014 11:52 AM

+1, very useful David!

Please login to comment.
Latest blogs
Level Up with Optimizely's Newly Relaunched Certifications!

We're thrilled to announce the relaunch of our Optimizely Certifications—designed to help partners, customers, and developers redefine what it mean...

Satata Satez | Jan 14, 2025

Introducing AI Assistance for DBLocalizationProvider

The LocalizationProvider for Optimizely has long been a powerful tool for enhancing the localization capabilities of Optimizely CMS. Designed to ma...

Luc Gosso (MVP) | Jan 14, 2025 | Syndicated blog

Order tabs with drag and drop - Blazor

I have started to play around a little with Blazor and the best way to learn is to reimplement some old stuff for CMS12. So I took a look at my old...

Per Nergård | Jan 14, 2025

Product Recommendations - Common Pitfalls

With the added freedom and flexibility that the release of the self-service widgets feature for Product Recommendations provides you as...

Dylan Walker | Jan 14, 2025

My blog is now running using Optimizely CMS!

It's official! You are currently reading this post on my shiny new Optimizely CMS website.  In the past weeks, I have been quite busy crunching eve...

David Drouin-Prince | Jan 12, 2025 | Syndicated blog

Developer meetup - Manchester, 23rd January

Yes, it's that time of year again where tradition dictates that people reflect on the year gone by and brace themselves for the year ahead, and wha...

Paul Gruffydd | Jan 9, 2025