MongoDb Ninjitsu: Using ObjectId as a Timestamp

Whilst reading around yesterday I stumbled upon this little gem of knowledge:

Mongo’s ObjectIds contains a Utc timestamp with 1 second resolution.

This means, that if we don’t need millisecond accuracy, we can drop all those “CreatedOn” fields from our schemas, and by doing so we win twice:

  • Storage – no need to store a seperate time stamp field means less to store on disk and less for the server to shunt around.
  • Free Index – assuming your ObjectId is your primary key, it already has an index on it by default, so not only are you saving the space of having to store a seperate time stamp, but it’s also indexed for free.

To make it a little easier, I created an extension class to convert DateTimes to and from ObjectIds. This makes it insanely simple to query for objects created before/after a certain date. Here’s the code:

/// Author	: Daniel Harman (http://www.danharman.net)
/// Date	: 26.Oct.2011
/// License : Public Domain with no warranty given. Please maintain author credit.
using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;

namespace Mmphs.Utils.MongoDb
{
	public static class DateTimeExtensions
	{
		/// <summary>
		/// Converts a DateTime to an ObjectId.
		/// n.b. missing values that would ensure uniqueness. This is only intended for time comparisons.
		/// </summary>
		/// <param name="dateTime"></param>
		/// <returns></returns>
		public static ObjectId ToObjectId(this DateTime dateTime)
		{
			var timestamp = (int)(dateTime - BsonConstants.UnixEpoch).TotalSeconds;
			return new ObjectId(timestamp, 0, 0, 0);
		}

		/// <summary>
		/// Convert an ObjectId to a DateTime.
		/// </summary>
		/// <param name="id"></param>
		/// <returns></returns>
		public static DateTime ToDateTime(this ObjectId id)
		{
			return id.CreationTime;
		}

		static readonly int DATETIME_TRUNCATE_FACTOR = 10000;

		/// <summary>
		/// Truncate the accuracy of a datetime to the same resolution as a mongo db datetime.
		/// </summary>
		/// <param name="dateTime"></param>
		/// <returns></returns>
		public static DateTime MongoTruncate(this DateTime dateTime)
		{
			long ticks = dateTime.Ticks / DATETIME_TRUNCATE_FACTOR;
			return new DateTime(ticks * DATETIME_TRUNCATE_FACTOR, dateTime.Kind);
		}
	}
}

I’ve thrown in a little bonus there too – a method to truncate a DateTime in the same way MongoDb does when it persists one. This is handy for unit tests where you want to compare an object you’ve created and persisted and then loaded back up e.g. when testing a query brings back the right record.

Here is an example of using the ObjectId to get items after a certain date:

		public IEnumerable<Drop> GetStreamActivities(ObjectId streamId, DateTime utcSince)
		{
			return _drops
				.AsQueryable()
				.Where(a => a.StreamId == streamId && a.Id >= utcSince.ToObjectId())
				.OrderBy(a => a.Id);
		}

n.b. I’m using FluentMongo, but you certainly don’t need to.

and here are the unit tests:

/// Author	: Daniel Harman (http://www.danharman.net)
/// Date	: 26.Oct.2011
/// License : Public Domain with no warranty given. Please maintain author credit.

using System;
using System.Collections.Generic;
using System.Linq;
using MbUnit.Framework;
using MongoDB.Bson;

namespace Mmphs.Utils.MongoDb.Test.DateTimeExtensions
{
	[TestFixture]
	public class DateTimeExtensionsTest
	{
		[Test]
		public void Can_Convert_DateTime_To_ObjectId()
		{
			// Arrange
			DateTime dateTime = DateTime.UtcNow;

			// Act
			ObjectId result = dateTime.ToObjectId();

			// Assert
			Assert.AreApproximatelyEqual(dateTime, result.CreationTime, TimeSpan.FromSeconds(1));
		}

		[Test]
		public void Can_Convert_ObjectId_To_DateTime()
		{
			// Arrange
			ObjectId objectId = ObjectId.GenerateNewId();
			DateTime dateTime = DateTime.UtcNow;

			// Act
			var result = objectId.ToDateTime();

			// Assert
			Assert.AreApproximatelyEqual(dateTime, result, TimeSpan.FromSeconds(1));
		}
	}
}
/// Author	: Daniel Harman (http://www.danharman.net)
/// Date	: 26.Oct.2011
/// License : Public Domain with no warranty given. Please maintain author credit.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MbUnit.Framework;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;

namespace Mmphs.Utils.MongoDb.Test.DateTimeExtensions
{
	[TestFixture]
	public class When_Truncating_DateTime
	{
		[Test]
		public void Should_Match_Mongo()
		{
			// Arrage
			DateTime dt = DateTime.UtcNow;
			var asJson = dt.ToJson();

			// Act
			var asTrunc = dt.MongoTruncate();
			
			// Assert
			var fromJson = BsonSerializer.Deserialize<DateTime>(asJson);
			Assert.AreEqual(fromJson, asTrunc);
		}
	}
}

One thought on “MongoDb Ninjitsu: Using ObjectId as a Timestamp

Leave a Reply