This project is read-only.

Implementation of custom container for use outside of the NO framework

Nov 17, 2012 at 8:09 PM
Edited Nov 27, 2012 at 9:59 PM

I love the Naked Objects pattern and the framework. In my experience so far it truly facilitates the rapid development and delivery of value to the customer, specifically in an enterprise environment, leveraging and reflecting the domain language of the business.

However, what we have found is that every so often we have to write code that will run outside of the framework, but we still want to leverage the existing entities and business logic already developed. Specifically windows service components, functional tests, both for test data preparation for XATs, and some logic testing, and even in some WCF services where it is not required to use the full framework.

To this end, I have written a custom implementation of IDomainObjectContainer that basically just wraps the Entity Framework model (DbContext), and allows you to reuse your rich domain entities as per usual to a large extent. I am sharing it here in case it would be useful to anybody else into the future.

The Container manages the injection of itself into the registered repositories and services, and also injects the registered repositories and services into each other where required as well as into any new transient or materialised entities.

Furthermore, it also fires the following Naked Objects life cycle methods on your entities if they are defined: Created, Loaded, Updating, Persisting, Deleting, Updated, Persisted, and Deleted. The last three fires in separate transactions as the NO Framework also does.
 What it specifically does not do, at this stage, is to fire any Validate, Hide, or Disable methods that are defined on the entities or repositories.

Disclaimer: Use this code at your own risk. I had to make some changes to it when extracting it from my project, and I haven't run this specific version, so no guarantees.
 
Here is how you use it.

IContainer container = new Container<Model>(new Repository1(), new Repository2());

As you can see, it takes the generic type of the specific EF Model that your entities are defined in. Note, this type must derive from DbContext. (You can add a DbContext generator template to your project).

It takes a params array of services / repositories in the constructor.  This is the equivalent of what is registered in the RunWeb class as MenuServices and System services. You can also pass them in directly as follows:

IContainer container = new Container<Model>(this.SystemServices.GetServices())

Now, obviously the NO Framework is not around to manage the saving of your changes for you, so you have to explicitly call container.SaveChanges() and also dispose the container when you are done. Utilising the using construct is usually the best way to go as shown in the following code snippet:

using (var container = this.NewContainer())
{
    // Retreive the fully initialized repository of the specified type.
    var repository = container.Service<Repository1>();
   
    // Do the entity related work here as per usual.
    // Eg. use the repository methods, or create new entity instances and persist them.
    // (An understanding of the normal NO container usage is assumed here.)
   
    container.SaveChanges();
}


Besides the use of "using", there are 2 things to note in the above code.
1) I have abstracted the spinning up of an appropriately constructed container to a factory method called NewContainer(). That takes care of specifying which model to use, as well as passing in the appropriate services and repositories as needed. You could also configure your favorite IOC container (unfortunate ambiguity of terms) to give you back a properly initialised instance of your Container whenever you ask for an IContainer.
2) You will see the use of container.Service<Repository1>(). This retrieves the fully initialised instance of Repository1 that you can use as you would usually use a repository. Note, if an instance of Repository1 was not provided to the constructor of the Container when it was created, this method will throw an exception.

And that is all you need to know to use it.

A bit more detail, for those interested.
I defined 2 new interfaces:
1) IContainer that just combines IDomainObjectContainer and IDisposable, and also exposes the SaveChanges() method to the underlying DbContext and some other methods and properties that are useful, such as DbContext, that returns the instance of your model as a DbContext.
2) IContainer<T> that enhances IContainer with the generics to strongly type the model it relates to. It is this latter interface that my custom container implements. It also defines a property, ModelContext, that returns your specific model instance of type T. The same instance, if not the same type, as the DbContext property in IContainer, should you need it.

The interfaces with the implementation follows below. There are obviously some improvements that can be made to this code, but I hope it is a start that would be helpful to the NO MVC community.

Update: Download a zipped project containing more up to date code with some improvements: https://docs.google.com/open?id=0B4Q8uCMjruK5UFFwVmtoVFFoa00

Feedback is welcome.

(Update: I'm not sure what is up with the strikethrough of some of the code blocks below.)

The interfaces

 

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Objects;
using NakedObjects;

namespace CustomContainer
{
	/// <summary>
	/// This is an interface to combine IDomainObjectContainer and IDisposable for use in unit tests so that we can easily reuse our domain entities
	/// as they are out side of the Naked Objects Framework.
	/// </summary>
	public interface IContainer : IDomainObjectContainer, IDisposable
	{
		/// <summary>
		/// Contains the messages passed to the InformUser() method.
		/// </summary>
		IEnumerable<string> Messages { get; }
		/// <summary>
		/// Contains the warning messages passed to the WarnUser() method.
		/// </summary>
		IEnumerable<string> Warnings { get; }
		/// <summary>
		/// Returns true if InformUser() has been called at least once on this instance.
		/// The actual messages can be retrieved from the Messages property.
		/// </summary>
		bool HasMessages { get; }
		/// <summary>
		/// Returns true if WarnUser() has been called at least once on this instance.
		/// The actual warnings can be retrieved from the Warnings property.
		/// </summary>
		bool HasWarnings { get; }

		/// <summary>
		/// Retrieves the repository or service class of the specified type that was registered with this container.
		/// If no such type was registered, an exception is thrown.
		/// </summary>
		/// <typeparam name="S"></typeparam>
		/// <returns></returns>
		S Service() where S : class;

		ObjectContext ObjectContext { get; }
		DbContext DbContext { get; }

		int SaveChanges();
		bool IsDetached(object obj);
		bool Attatch(object obj);

		void InjectContainerAndServices(object o);
	}

	public interface IContainer<T> : IContainer
		where T : DbContext, new()
	{
		T ModelContext { get; set; }
	}
}

The implementation

 

 

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Web;

namespace CustomContainer
{
	public class Container<T> : IContainer<T>
		where T : DbContext, new()
	{
		// Holds the registered services.
		private object[] _services;

		// Collections for tracking changes to entities so that we can invoke the post SaveChanges life cycle methods.
		private IList<object> _updatedEntities = new List<object>();
		private IList<object> _persistedEntities = new List<object>();
		private IList<object> _deletedEntities = new List<object>();

		private IList<string> _messages = new List<string>();
		private IList<string> _warnings = new List<string>();

		public bool HasMessages
		{
			get
			{
				return this._messages.Count() > 0;
			}
		}

		public bool HasWarnings
		{
			get
			{
				return this._warnings.Count() > 0;
			}
		}

		public IEnumerable<string> Messages
		{
			get
			{
				return this._warnings;
			}
		}

		public IEnumerable<string> Warnings
		{
			get
			{
				return this._messages;
			}
		}

		public object[] SystemServices
		{
			get
			{
				return _services;
			}
		}

		public ObjectContext ObjectContext
		{
			get
			{
				return ((IObjectContextAdapter)this.ModelContext).ObjectContext;
			}
		}

		public Container(params object[] services)
			: base()
		{
			// Instantiate the specific EF model we are working with.
			this.ModelContext = new T();
			this.RegisterServices(services);
			this.ObjectContext.ObjectMaterialized += new ObjectMaterializedEventHandler(ObjectContext_ObjectMaterialized);
			this.ObjectContext.SavingChanges += ObjectContext_SavingChanges;
		}

		private void ObjectContext_SavingChanges(object sender, EventArgs e)
		{
			// Capture the new entities that have been added and that are being persisted,
			// so that we can invoke Persisted() on them after SaveChanges() is complete.
			this._persistedEntities = this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added)
				.Select(entry => entry.Entity)
				.ToList();

			// Capture the entities that have been modified and that are being saved,
			// so that we can invoke Updating() on them as they are being saved, and Updated() after SaveChanges() is complete.
			this._updatedEntities = this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)
				.Select(entry => entry.Entity)
				.ToList();
			foreach (var entity in this._updatedEntities)
				this.InvokeMethodIfExists(entity, "Updating");

			// Capture the entities that are being deleted,
			// so that we can invoke Deleting() on them as they are being deleted, and Deleted() after SaveChanges() is complete.
			this._deletedEntities = this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
				.Select(entry => entry.Entity)
				.ToList();
			foreach (var entity in this._deletedEntities)
				this.InvokeMethodIfExists(entity, "Deleting");
		}

		private void ObjectContext_ObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
		{
			// Inject the container and services into each entity as it is loaded from DB, whether explicitly or lazily.
			if (e.Entity != null && !e.Entity.GetType().IsValueType)
			{
				this.InjectContainerAndServices(e.Entity);
				this.InvokeMethodIfExists(e.Entity, "Loaded");
			}
		}

		#region Injection Methods

		public void InjectContainerAndServices(object target)
		{
			if (target != null)
			{
				InjectDependency(target, this);
				InjectDependencies(target, _services);
			}
		}

		private static void InjectDependencies(object target, object[] services)
		{
			foreach (var s in services)
				InjectDependency(target, s);
		}

		private static void InjectDependency(object target, object service)
		{
			var properties = target.GetType()
				.GetProperties()
				.Where(p =>
					p.PropertyType != typeof(object)
					&& p.PropertyType.IsAssignableFrom(service.GetType())
					&& p.CanWrite);

			foreach (PropertyInfo pi in properties)
			{
				pi.SetValue(target, service, null);
				//LOG.DebugFormat("Injected service {0} into instance of {1}", service, target.GetType().FullName);
			}
		}

		#endregion Injection Methods

		/// <summary>
		/// Injects the container into each of the registered system services.
		/// It also injects each service into any other service that has a public property of the type.
		/// </summary>
		public void RegisterServices(params object[] services)
		{
			if (services == null)
				throw new ArgumentNullException("No services were provided.");
			this._services = services;

			foreach (var service in _services)
			{
				InjectDependency(service, this);
				InjectDependencies(service, _services);
			}
		}

		#region IDomainObjectContainer Members

		public S Service()
			where S : class
		{
			var service = this._services
				.OfType()
				.FirstOrDefault();
			if (service == null)
				throw new ApplicationException(string.Format("No service of type {0} was registered with the container.", typeof(S).FullName));
			return service;
		}

		public void DisposeInstance(object persistentObject)
		{
			var entry = this.ModelContext.Entry(persistentObject);
			entry.State = System.Data.EntityState.Deleted;
		}

		public void InformUser(string message)
		{
			this._messages.Add(message);
		}

		public IQueryable Instances(Type type)
		{
			return this.ModelContext.Set(type);
		}

		public IQueryable<T> Instances<T>() where T : class
		{
			return this.ModelContext.Set<T>();
		}

		public bool IsPersistent(object obj)
		{
			return this.ModelContext.Entry(obj).State != System.Data.EntityState.Detached;
		}

		public object NewTransientInstance(Type type)
		{
			var instance = this.CreateInstance(type);
			InjectContainerAndServices(instance);
			this.InvokeMethodIfExists(instance, "Created");
			return instance;
		}

		public T NewTransientInstance<T>()
		{
			var instance = this.CreateInstance<T>(typeof(T));
			InjectContainerAndServices(instance);
			this.InvokeMethodIfExists(instance, "Created");
			return instance;
		}

		public void ObjectChanged(object obj)
		{
			throw new NotImplementedException("This is a legacy method that should not have been called.");
		}

		public void Persist<T>(ref T transientObject)
		{
			this.InvokeMethodIfExists(transientObject, "Persisting");
			this.ModelContext.Set(transientObject.GetType()).Add(transientObject);
		}

		public System.Security.Principal.IPrincipal Principal
		{
			get
			{
				if (HttpContext.Current != null)
				{
					return HttpContext.Current.User;
				}
				else // Not in web context
				{
					// Use the current windows account under which this is running.
					WindowsIdentity identity = WindowsIdentity.GetCurrent();
					WindowsPrincipal principal = new WindowsPrincipal(identity);
					return principal;
				}
			}
		}

		/// <summary>
		/// Rather throw exceptions directly instead of calling this method.
		/// In that way stack traces will be more accurate.
		/// </summary>
		/// <param name="message"></param>
		public void RaiseError(string message)
		{
			throw new ApplicationException(message);
		}

		/// <summary>
		/// Reload the entity from the DB.
		/// </summary>
		/// <param name="obj"></param>
		public void Refresh(object obj)
		{
			this.ModelContext.Entry(obj).Reload();
		}

		public void Resolve(object obj, object field)
		{
			throw new NotImplementedException("This is a legacy method that should not have been called.");
		}

		public void Resolve(object obj)
		{
			throw new NotImplementedException("This is a legacy method that should not have been called.");
		}

		public void WarnUser(string message)
		{
			this._warnings.Add(message);
		}

		#endregion

		#region IContainer Members

		public T ModelContext { get; set; }

		public bool IsDetached(object obj)
		{
			return this.ModelContext.Entry(obj).State == System.Data.EntityState.Detached;
		}

		public bool Attatch(object obj)
		{
			if (IsDetached(obj))
			{
				this.ModelContext.Set(obj.GetType()).Attach(obj);
				return true;
			}
			else
				return false;
		}

		public void AbortCurrentTransaction()
		{
			//TODO: Find a better way of rolling back changes in the DbContext. (It doesn't have something like RejectChanges().
			throw new ApplicationException("We blew up, so that the transaction is not committed to the database.");
		}

		#endregion

		public int SaveChanges()
		{
			var count = this.ModelContext.SaveChanges();

			this.InvokePostSaveChangesLifeCycleMethodsAndResetTracking();

			// If InvokePostSaveChangesLifeCycleMethods caused new entity changes in the current context, call SaveChanges recursively until there is nothing more to save.
			count = +SaveChangesAgainIfNeeded();

			return count;
		}

		private int SaveChangesAgainIfNeeded()
		{
			this.ObjectContext.DetectChanges();
			bool saveAgain =
				this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified).Count() > 0
				|| this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Count() > 0
				|| this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).Count() > 0;
			if (saveAgain)
				return this.SaveChanges();
			else
				return 0;
		}

		private void InvokePostSaveChangesLifeCycleMethodsAndResetTracking()
		{
			// Grab a reference to the entities that have changed, and reset the primary collections.
			var persistedEntities = this._persistedEntities;
			var updatedEntities = this._updatedEntities;
			var deletedEntities = this._deletedEntities;
			this._persistedEntities = new List<Object>();
			this._updatedEntities = new List<Object>();
			this._deletedEntities = new List<Object>();

			foreach (var entity in updatedEntities)
				this.InvokeMethodIfExists(entity, "Updated");

			foreach (var entity in persistedEntities)
				this.InvokeMethodIfExists(entity, "Persisted");

			foreach (var entity in deletedEntities)
				this.InvokeMethodIfExists(entity, "Deleted");
		}

		public void InvokeMethodIfExists(
			object instance,
			string methodName)
		{
			if (instance == null || methodName == null)
				return;

			// Getting the method information using the method info class
			var type = instance.GetType();
			MethodInfo mi = type.GetMethod(methodName);
			if (mi != null)
			{
				// null- no parameter for the function [or] we can pass the array of parameters
				mi.Invoke(instance, null);
			}
		}

		/// <summary>
		/// Creates an instance of the type.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		public object CreateInstance(Type type)
		{
			return type.Assembly.CreateInstance(type.FullName);
		}


		/// <summary>
		/// Creates an instance of the type.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		public T CreateInstance<T>(Type type)
		{
			return (T)type.Assembly.CreateInstance(type.FullName);
		}

		#region IDisposable Members

		public void Dispose()
		{
			this.ModelContext.Dispose();
		}

		#endregion

		#region IContainer Members

		public DbContext DbContext
		{
			get
			{
				return this.ModelContext;
			}
		}

		#endregion IContainer Members
	}
}


Nov 18, 2012 at 2:01 PM

Many thanks for sharing this, Jacques.  I concur that in an enterprise system there may well be situations where you want to re-use your behaviourally-rich domain model, alongside a Naked Objects application -  and this is a great way to do it.

Dan H (if you read this)  -  might it be appropriate to include this code within your NakedObjects.Contrib project?

Richard