Friday, August 12, 2011

How to prevent your extension methods from polluting the base libary types?

I got constants and some useful Helper(extension methods) that makes sense on my component(s) domain only.

On common assembly, I have these common constants and helpers

Assembly A:


namespace Ienablemuch.ToTheEfnhX
{
    public static class RepositoryConstants
    {
        // rationales: 
        // http://msdn.microsoft.com/en-us/library/ms182256(v=vs.80).aspx
        // http://10rem.net/articles/net-naming-conventions-and-programming-standards---best-practices
        public readonly static string IdSuffix = "Id";


        public readonly static string RowversionName = "RowVersion";

    }


    public static class Helper
    {
        public static object GetDefault(this Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }
            return null;
        }


        public static void DetectIdType(this Type ent, string primaryKeyName, object id)
        {
            if (ent.GetProperty(primaryKeyName).PropertyType != id.GetType())
                throw new IdParameterIsInvalidTypeException("Id is invalid type: " + id.GetType() + ", didn't matched the repository's primary key. Most IDs are of primitive type. Contact the dev to fix this problem");
        }
    }
}


namespace Ienablemuch.ToTheEfnhX
{
    public interface IRepository<TEnt> where TEnt : class
    {
        IQueryable<TEnt> All { get; }
        
        void Save(TEnt ent, byte[] version);
        void Merge(TEnt ent, byte[] version);
        TEnt Get(object id);
        void Delete(object id, byte[] version);
        void DeleteCascade(object id, byte[] version);
        void Evict(object id);

        TEnt LoadStub(object id);

        string PrimaryKeyName { get; set; }
        string VersionName { get; set; }
    }
}

Here are the implementors of that repository interface:

Assembly B:
namespace Ienablemuch.ToTheEfnhX.EntityFramework
{
    public class EfRepository<TEnt> : IRepository<TEnt> where TEnt : class
    {
        DbContext _ctx = null;
        public EfRepository(DbContext ctx)
        {
            _ctx = ctx;
            // Convention-over-configuration :-)
            PrimaryKeyName = typeof(TEnt).Name + RepositoryConstants.IdSuffix;
            VersionName = "RowVersion";
        }
        
        public string PrimaryKeyName { get; set; }
        public string VersionName { get; set; }
        
        public void Delete(object id, byte[] version)
        {
            try
            {
                typeof(TEnt).DetectIdType(PrimaryKeyName, id);
                Evict(id);
                _ctx.Set<TEnt>().Remove(LoadDeleteStub(id, version));
                _ctx.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new DbChangesConcurrencyException();
            }
        }
    }    
}    


Assembly C:
namespace Ienablemuch.ToTheEfnhX.NHibernate
{
    
    public class NhRepository<TEnt> : IRepository<TEnt> where TEnt : class
    {

        ISession _session = null;

       
        public NhRepository(ISession sess)
        {
            _session = sess;
            PrimaryKeyName = typeof(TEnt).Name + RepositoryConstants.IdSuffix;
            VersionName = RepositoryConstants.RowversionName;


            
        }
        
        public string PrimaryKeyName { get; set; }
        public string VersionName { get; set; }
        
        
        public void Delete(object id, byte[] version)
        {
            try
            {
                typeof(TEnt).DetectIdType(PrimaryKeyName, id);
                using (var tx = _session.BeginTransaction())
                {
                    object objStub = LoadDeleteStub(id, version);
                    _session.Delete(objStub);
                    tx.Commit();
                }
            }
            catch (StaleObjectStateException)
            {
                throw new DbChangesConcurrencyException();
            }
        }
    }
}



The Type's GetDefault and DetectIdType are potential polluters on base library types, especially the DetectId type, it's only useful on the component's domain.

At first attempt, I'm thinking of making these constants and helpers be made as internals..

namespace Ienablemuch.ToTheEfnhX
{

    internal static class RepositoryConstants
    {
        internal readonly static string IdSuffix = "Id";
        internal readonly static string RowversionName = "RowVersion";
    }


    internal static class Helper
    {
        internal static object GetDefault(this Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }
            return null;
        }


        internal static void DetectIdType(this Type ent, string primaryKeyName, object id)
        {
            if (ent.GetProperty(primaryKeyName).PropertyType != id.GetType())
                throw new IdParameterIsInvalidTypeException("Id is invalid type: " + id.GetType() + ", didn't matched the repository's primary key. Most IDs are of primitive type. Contact the dev to fix this problem");
        }
    }
}



..,then make Ienablemuch.ToTheEfnhX.NHibernate and Ienablemuch.ToTheEfnhX.EntityFramework as friend assemblies of repository interface(on Assembly A), putting these on assembly A:

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Ienablemuch.ToTheEfnhX.NHibernate")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Ienablemuch.ToTheEfnhX.EntityFramework")]

Prior to that, I attempted if it's possible to include wildcard on assembly name, tee hee :D

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Ienablemuch.ToTheEfnhX.*")]

Alas no joy, that didn't work.


However, as any discerning component makers will notice, using friend assemblies will produce tight coupling between interface and its implementors, as you have to specify what specific assembly can be a friend of your assembly. What will happen if there's another ORM vendor who wanted to implement your repository interface? if you used friend assemblies that would entail on your part to recompile repository interface to enable new vendor(s) on implementing your repository interface, which is a complicated routine to do so, entails coordination too. So we must not use friend assemblies, don't use InternalsVisibleTo. The only case that InternalsVisibleTo makes sense is when you don't want anyone to inherit functionalities from your components.



On a bit related note, though it's a bit of disappointment that wildcard on InternalsVisibleTo is not allowed, I think it's a good decision on Microsoft part, politically-wise you don't want to implement someone's interface and at the same time you must rigidly follow their assembly's name just for you to able to use their constants and extension methods sauce, isn't it?



So, the easiest way is just make things public, so those constants and helpers will be available to any vendors who wanted to implement your components. But as most of us wanted to avoid, we don't want components that pollutes the base library types with extension methods that are very domain-specific or has very localized uses only.



However, making things public is the first step to the solution, the last step is to put those RepositoryConstants and Helpers to their own namespace. That way, those constants won't be accidentally exposed to the end-user devs, and those extension methods won't be accidentally polluting the base library types. Alleviating your worries and/or frustrations on non-conforming citizen components.



namespace Ienablemuch.ToTheEfnhX.ForImplementorsOnly
{

	public static class RepositoryConstants
	{
		public readonly static string IdSuffix = "Id";
		public readonly static string RowversionName = "RowVersion";
	}


	public static class Helper
	{
		public static object GetDefault(this Type type)
		{
			if (type.IsValueType)
			{
				return Activator.CreateInstance(type);
			}
			return null;
		}


		public static void DetectIdType(this Type ent, string primaryKeyName, object id)
		{
			if (ent.GetProperty(primaryKeyName).PropertyType != id.GetType())
				throw new IdParameterIsInvalidTypeException("Id is invalid type: " + id.GetType() + ", didn't matched the repository's primary key. Most IDs are of primitive type. Contact the dev to fix this problem");
		}
	}
}

Then on an implementor side, they can use the constants and helpers by using a particular namespace.

using Ienablemuch.ToTheEfnhX.ForImplementorsOnly;

namespace Ienablemuch.ToTheEfnhX.EntityFramework
{
    public class EfRepository<TEnt> : IRepository<TEnt> where TEnt : class
    {
        DbContext _ctx = null;
        public EfRepository(DbContext ctx)
        {
            _ctx = ctx;
            // Convention-over-configuration :-)
            PrimaryKeyName = typeof(TEnt).Name + RepositoryConstants.IdSuffix;
            VersionName = "RowVersion";
        }
        
        public string PrimaryKeyName { get; set; }
        public string VersionName { get; set; }
        
        public void Delete(object id, byte[] version)
        {
            try
            {
                typeof(TEnt).DetectIdType(PrimaryKeyName, id);
                Evict(id);
                _ctx.Set<TEnt>().Remove(LoadDeleteStub(id, version));
                _ctx.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new DbChangesConcurrencyException();
            }
        }
    }    
}    


Then on end-user devs, the following will be the typical code they will write. Notice that on end-user devs side, typically, they will not have a need to use the namespace Ienablemuch.ToTheEfnhX.ForImplementorsOnly, the constants will not be accidentally leaked to end-user devs, and extension methods will not pollute the base library types then. Though we cannot enforce end-user devs from using the namespace ForImplementorsOnly, at least it would take a conscious effort on their part if they wish to do so.

using Ienablemuch.ToTheEfnhX;
// Notice the end-user dev didn't include the ForImplementorsOnly. Constants
// and extension methods that makes sense to component implementors(e.g.  Ienablemuch.ToTheEfnhX.NHibernate) only,
// won't be exposed to end-user devs.
using Ienablemuch.ToTheEfnhX.NHibernate;
using Ienablemuch.ToTheEfnhX.EntityFramework;
using Ienablemuch.ToTheEfnhX.Memory;

namespace TestProject
{
    [TestClass()]
    public class TestTheIRepository
	{
		[TestMethod]
		public void Memory_CanSave()
		{
			IRepository<Product> db = new MemoryRepository<Product>();
			Common_CanSave(db);
		}
		[TestMethod]
		public void Ef_CanSave()
		{
			EmptyDatabase();
			IRepository<Product> db = new EfRepository<Product>(new EfDbMapper(connectionString)); 
			Common_CanSave(db);
		}
		[TestMethod]
		public void Nh_CanSave()
		{
			EmptyDatabase();
			IRepository<Product> db = new NhRepository<Product>(NhModelsMapper.GetSession(connectionString));
			Common_CanSave(db);                
		}
		void Common_CanSave(IRepository<Product> db)
		{            
			db.Save(new Product { ProductName = "Optimus", Category = "Autobots", MinimumPrice = 7 }, null);
			db.Save(new Product { ProductName = "Bumble Bee", Category = "Autobots", MinimumPrice = 8 }, null);
			db.Save(new Product { ProductName = "Megatron", Category = "Decepticon", MinimumPrice = 9 }, null);
						

						
			Assert.AreEqual(7 + 8 + 9, db.All.Sum(x => x.MinimumPrice));
			Assert.AreEqual(3, db.All.Count());
		}
	}
}

No comments:

Post a Comment