November Happy Hour will be moved to Thursday December 5th.

dave.ketnor@twentysixdigital.com
Aug 12, 2014
  3353
(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
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog