Using reflection to create a dynamic OnModelCreating in Entity Framework

Published: Tuesday 24 March 2020

Reflection isn't something new to the .NET library, but it's as important as ever. Reflection provides objects that tells you more about the assemblies, types and modules in your project.

You can use reflection for many things. For example, finding properties in a particular class. Or, invoking a method within a class instance. You can even create a new instance of a class. This allows for more dynamic logic, and less code.

In this article, we are going to showcase an example where we invoke static "OnModelCreating" methods from entities that are part of our Entity Framework's DB Context using reflection.

System.Reflection.Assembly

When using reflection, you will need to identify which assembly the types resides in. When I say the "types", I mean any types you are using for reflection.

Now, if you are using reflection on the type directly, it will know which assembly it's calling from. That is, the assembly that the type resides in.

But, if you want to perform a search on getting many different types based on a particular criteria, you need to specify which assembly you wish to perform your search.

The method I tend to use is "Assembly.GetExecutingAssembly()". The "Assembly" class resides in "System.Reflection" and it calls the static method "GetExecutingAssembly". This will load the assembly where the code is currently being executed. For example, if I was to call the "GetExecutingAssembly" method in "RoundTheCode.Assemblies", it would load that particular assembly.

In addition, the "Load" method is something I use a lot in the "Assembly" class. This allows us to load in an assembly using the full qualified name.

Assembly.Load("RoundTheCode.Assemblies");

With the "Load" method, you need to make sure that the assembly you are using is referenced in your project. Otherwise, it won't be able to find the assembly and will throw an error.

Creating a Base Class

When we use reflection on our Entity Framework DB context, we want to find all types that inherit an "IBase" interface. We will create a separate "Base" abstract class, that will have it's own "OnModelCreating" method and an "Id" property. The "Base" class's main intention is to set the primary key of the entity, which is "Id".

public abstract partial class Base : IBase
{
	public virtual int Id { get; }

	public static void OnModelCreating<TBase>(ModelBuilder builder)
		where TBase : class, IBase
	{
		builder.Entity<TBase>().HasKey(_base => _base.Id);
	}
}

The "OnModelCreating" method is a generic method with a "TBase" type. When invoked, the "TBase" type will be the entity that we are using in our Entity Framework DB context.

Using the Base Class for an Entity

Next, we are going to show an example of an entity that inherits the "Base" class. It will include the properties we want for our entity and a separate "OnModelCreating" method, like so:

[Table("Blog", Schema = "b")]
public partial class Blog : Base
{
	public virtual string Title { get; set; }

	public virtual string Article { get; set; }

	public virtual string Slug { get; set; }

	public virtual DateTimeOffset? Publish { get; set; }

	[JsonIgnore]
	public ICollection<BlogCategory> BlogCategories { get; set; }

	[JsonIgnore]
	public ICollection<BlogTag> BlogTags { get; set; }


	public static void OnModelCreating(ModelBuilder builder)
	{
		builder.Entity<Blog>().Property(blog => blog.Title).HasMaxLength(200);
	}
}

Using Reflection in the DB Context

The final step is to override the "OnModelCreating" method in our DB context and use reflection to execute all the DB context entities "OnModelCreating" methods. This will also call the "OnModelCreating" method is the base context, using our entity as the generic type.

Get All Types

We use this reflection code to get all our types:

var types = Assembly.GetExecutingAssembly().GetTypes().Where(s => s.GetInterfaces().Any(_interface => _interface.Equals(typeof(IBase)) && s.IsClass && !s.IsAbstract && s.IsPublic));

This code gets all the types that contain the "IBase" interface. Additionally, the type has to be a public class, which is not abstract.

Executing the "OnModelCreating" method

The next task is to execute the static "OnModelCreating" that resides in our types. We can do that in the following code:

foreach (var type in types)
{
	// On Model Creating
	var onModelCreatingMethod = type.GetMethods().FirstOrDefault(x => x.Name == "OnModelCreating" && x.IsStatic);

	if (onModelCreatingMethod != null)
	{
		onModelCreatingMethod.Invoke(type, new object[] { builder });
	}

	// On Base Model Creating
	if (type.BaseType == null || type.BaseType != typeof(Base))
	{
		continue;
	}

	var baseOnModelCreatingMethod = type.BaseType.GetMethods().FirstOrDefault(x => x.Name == "OnModelCreating" && x.IsStatic);

	if (baseOnModelCreatingMethod == null)
	{
		continue;
	}

	var baseOnModelCreatingGenericMethod = baseOnModelCreatingMethod.MakeGenericMethod(new Type[] { type });

	if (baseOnModelCreatingGenericMethod == null)
	{
		continue;
	}

	baseOnModelCreatingGenericMethod.Invoke(typeof(Base), new object[] { builder });
}

So for each of the types, it uses reflection to get all the methods and finds one that's named "OnModelCreating" and is static.

The "OnModelCreating" method is invoked if found, passing in a "builder" variable. Subsequently, the "builder" variable is a parameter called in the "OnModelCreating" method and is a "ModelBuilder" type.

protected override void OnModelCreating(ModelBuilder builder);

Next, we look at the BaseType of our type. If it's not an instance of "Base" type, we continue to the next type in our loop. However, if it is, it looks to see if the "OnModelCreating" method exists in the "Base" class.

With the "OnModelCreating" method existing in "Base", we have to make a generic method. The generic method uses our type as the generic type. The generic method is part of the "OnModelCreating" reflection method. Thereafter, it's a case of invoking it, once again passing in the "builder" parameter.

Here is the full example of our Entity Framework DB Context:

public class MyContext : DbContext
{
	protected IConfiguration _configuration;

	public MyContext()
	{
		_configuration = new ConfigurationBuilder().AddJsonFile(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + @"\appsettings.json").Build();
	}

	public MyContext([NotNull] IConfiguration configuration)
	{
		_configuration = configuration;
	}

	protected override void OnModelCreating(ModelBuilder builder)
	{
		// Loads all types from an assembly which have an interface of IBase and is a public class
		var types = Assembly.GetExecutingAssembly().GetTypes().Where(s => s.GetInterfaces().Any(_interface => _interface.Equals(typeof(IBase)) && s.IsClass && !s.IsAbstract && s.IsPublic));
	  
		foreach (var type in types)
		{
			// On Model Creating
			var onModelCreatingMethod = type.GetMethods().FirstOrDefault(x => x.Name == "OnModelCreating" && x.IsStatic);

			if (onModelCreatingMethod != null)
			{
				onModelCreatingMethod.Invoke(type, new object[] { builder });
			}

			// On Base Model Creating
			if (type.BaseType == null || type.BaseType != typeof(Base))
			{
				continue;
			}

			var baseOnModelCreatingMethod = type.BaseType.GetMethods().FirstOrDefault(x => x.Name == "OnModelCreating" && x.IsStatic);

			if (baseOnModelCreatingMethod == null)
			{
				continue;
			}

			var baseOnModelCreatingGenericMethod = baseOnModelCreatingMethod.MakeGenericMethod(new Type[] { type });

			if (baseOnModelCreatingGenericMethod == null)
			{
				continue;
			}

			baseOnModelCreatingGenericMethod.Invoke(typeof(Base), new object[] { builder });
		}
		 
	}
}

Runtime

It's worth noting that Reflection is ran at run time. So, compiling may not throw all the errors that are present. However, a good way to test reflection is to take advantage of Test-Driven Development (TDD). Writing good tests should help identify any issues with your Reflection calls.