Example of handling cookies Domain-driven

A lot of web applications use cookies for enabling special features. For my company (Exact) for example, we use a cookie to store the latest division (code) that was selected by a user. This is not modelled DDD right now. How should it look like if it did? How to tackle the fact that an HTTP cookie is sealed, as we don’t want to introduce some (static) tooling or helper class?

First of all, I created an abstract wrapped cookie class. This allows developers to add extra (factory) methods, constructors, and properties based on the domain where the specific cookie has to be used. A cookie is a quiet generic thing, and we want to model domain specific implementations. Note that, as a result of that, the Value and Values property of the HTTP cookie are not publicly exposed.

In this case, the base class has besides the required wrapping a constructor with a user ID (GUID) and a static method to create a cookie name, as that is the default for our company. That is not necessarily something everybody should need.

For the specific cookie we have one public constructor, that creates an instance based on the user ID and its division code (a custom value object, containing that logic). Because, in the end, that is what this cookie is all about in our domain.

In this case it is extremely important that is always possible to (implicitly) cast between the wrapped and the original cookie. That makes the usage if the class way more easy.

using System;
using System.Web;

namespace HelloWorld.Web
{
	/// <summary>Represents a cookie that stores the last division visited by an user (ID).</summary>
	public class DivisionCodeCookie : WrappedCookie
	{
		/// <summary>The key for the division code.</summary>
		private const string DivisionCodeKey = "Division";

		/// <summary>Initializes a new division code cookie.</summary>
		/// <param name="userId">The user ID.</param>
		/// <param name="code">The division code.</param>
		public DivisionCodeCookie(Guid userId, DivisionCode code)
			: base(userId)
		{
			this.Code = code;
		}

		/// <summary>Initializes a new division code cookie.</summary>
		private DivisionCodeCookie(HttpCookie cookie) : base(cookie) { }

		/// <summary>Gets and set the division code of the cookie.</summary>
		public DivisionCode Code
		{
			get { return DivisionCode.TryParse(UnderlingCookie.Values[DivisionCodeKey]); }
			set { UnderlingCookie.Values[DivisionCodeKey] = value.ToString(); }
		}

		/// <summary>Creates a copy of the division code cookie.</summary>
		public DivisionCodeCookie Copy() { return new DivisionCodeCookie(this.UserId, this.Code); }

		/// <summary>Casts an HTTP Cookie to a Division code cookie.</summary>
		/// <remarks>
		/// Making the cast implicit allows the use of wrapped cookie when a HTTP cookie is asked.
		/// </remarks>
		public static implicit operator DivisionCodeCookie(HttpCookie http) { return new DivisionCodeCookie(http); }
	}
}

The base class.

using System;
using System.Diagnostics;
using System.Web;

namespace HelloWorld.Web
{
	/// <summary>Represents a cookie.</summary>
	/// <remarks>
	/// It is a wrapper that allows to add custom logic to the cookie.
	/// </remarks>
	[DebuggerDisplay("{DebuggerDisplay}")]
	public abstract class WrappedCookie
	{
		/// <summary>Initials a new wrapped cookie based on an HTTP cookie.</summary>
		protected WrappedCookie(HttpCookie httpCookie)
		{
			if (httpCookie == null) { throw new ArgumentNullException("httpCookie"); }
			this.UnderlingCookie = httpCookie;
		}

		/// <summary>Initials a new wrapped cookie based on an user ID.</summary>
		protected WrappedCookie(Guid userId) : this(GetCookieName(userId)) { }

		/// <summary>Initials a new wrapped cookie based on cookie name.</summary>
		protected WrappedCookie(string name): this(new HttpCookie(name)){}

		/// <summary>Gets or set the underlying HTTP cookie.</summary>
		protected HttpCookie UnderlingCookie { get; set; }

		/// <summary>Gets or set the user ID of the cookie.</summary>
		public Guid UserId
		{
			get
			{
				Guid userid;

				if (this.Name.StartsWith("ExactServer{")&& Guid.TryParseExact(this.Name.Substring(11), "B", out userid))
				{
					return userid;
				}
				return Guid.Empty;

			}
			set { this.Name = GetCookieName(value); }
		}

		/// <summary>Gets or set the name of the cookie.</summary>
		public string Name 
		{ 
			get { return UnderlingCookie.Name;  }
			set{ UnderlingCookie.Name = value;}
		}
		/// <summary>Gets or set the domain of the cookie.</summary>
		public string Domain
		{
			get { return UnderlingCookie.Domain; }
			set { UnderlingCookie.Domain = value; }
		}
		/// <summary>Gets or set the path of the cookie.</summary>
		public string Path
		{
			get { return UnderlingCookie.Path; }
			set { UnderlingCookie.Path = value; }
		}
		/// <summary>Gets or set the expiration date of the cookie.</summary>
		public DateTime Expires
		{
			get { return UnderlingCookie.Expires; }
			set { UnderlingCookie.Expires = value; }
		}
		
		/// <summary>Gets or set a value that specifies whatever a cookie is accessible by client-side script.</summary>
		public bool HttpOnly
		{
			get { return UnderlingCookie.HttpOnly; }
			set { UnderlingCookie.HttpOnly = value; }
		}
		/// <summary>Gets or set a value indicating specifies whatever to transmit the cookie Secure Sockets Layers (SSL)--that is, over HTTPS only.</summary>
		public bool Secure
		{
			get { return UnderlingCookie.Secure; }
			set { UnderlingCookie.Secure = value; }
		}
		/// <summary>Determines whatever the cookie is allowed to participate in output caching.</summary>
		public bool Shareable
		{
			get { return UnderlingCookie.Shareable; }
			set { UnderlingCookie.Shareable = value; }
		}

		/// <summary>Casts a wrapped cookie (back) to an HTTP cookie.</summary>
		/// <remarks>
		/// Making the cast implicit allows the use of wrapped cookie when an HTTP cookie is asked.
		/// </remarks>
		public static implicit operator HttpCookie(WrappedCookie wrapped) { return wrapped.UnderlingCookie; }

		/// <summary>Cleans the cookie up by clearing the value and set the expire date in the past.</summary>
		public void Cleanup()
		{
			UnderlingCookie.Expires = DateTime.Now.AddMinutes(-1);
			UnderlingCookie.Values.Clear();
		}

		/// <summary>Gets the name for the cookie based on the user ID.</summary>
		public static string GetCookieName(Guid userId)
		{
			return string.Format("ExactServer{0:B}", userId);
		}

		/// <summary>Gets a debugger display for the wrapped cookie.</summary>
		protected virtual string DebuggerDisplay { get { return string.Format("Cookie[{0}], Value: {1}, Expires: {2:yyyy-MM-dd HH:mm}", this.Name, this.UnderlingCookie.Value, this.Expires); } }
	}
}

Leave a Reply