In many many repositories you can find tons of controllers, completely similar code with the only difference of serving different types.
Here's one approach to fix this. If you just want the full code, skip the article and look here -> https://github.com/DeeJayTC/samples
Using one generic controller for all the types in your project
First step is to create a generic controller, this is a really simple part, just implement a controller as usually and add T to make it generic.
We also need to add a common interface to all the classes, I just named it IObjectBase. This is used to make sure all classes share the same ID Property.
The Interface:
public interface IObjectBase<TId>
{
[Key]
TId Id { get; set; }
}
The Controller:
[Route("api/[controller]")]
[Produces("application/json")]
public class GenericController<T> : Controller where T : class,
IObjectBase
{
private readonly GenericDbContext db;
public GenericController(GenericDbContext context)
{
this.db = context;
}
[HttpGet]
public IQueryable<T> Get()
{
return this.db.Set<T>();
}
....excluded for brevity, full sample in repo
We also add a DBContext pretty much as usually, similar to this:
public class GenericDbContext : DbContext
{
public static IModel StaticModel { get; } = BuildStaticModel();
public DbSet<Something> Somethings { get; set; }
public DbSet<SomeOtherThing> OtherThing { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured) optionsBuilder.UseInMemoryDatabase("ApplicationDb");
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
private static IModel BuildStaticModel()
{
using var dbContext = new GenericDbContext();
return dbContext.Model;
}
}
Don't forget to add the DBContext to your startup file!
Lets put things together
To tell .NET Core that we want to add additional controllers and routes we need to change the AddMVC call a bit
builder.Services.AddMvc(o =>
o.Conventions.Add(new GenericControllerRouteConvention()))
.ConfigureApplicationPartManager(m => m.FeatureProviders.Add(
new GenericTypeControllerFeatureProvider(new[] { Assembly.GetEntryAssembly().FullName}))
);
The ApplicationPartManager and FeatureProvider allows you to add new controllers at runtime ( See here )
In the feature provider we need to find a way to find all the classes we want to use as a controller, here we are again using our Interface. This can be done using a custom attribute or anything similar thats shared by all classes supposed to create a controller.
Here's a sample code for this:
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
foreach (var assembly in this.Assemblies)
{
var loadedAssembly = Assembly.Load(assembly);
var customClasses = loadedAssembly.GetExportedTypes()
.Where(x => x.IsAssignableTo(typeof(IObjectBase)) && x.Name != nameof(IObjectBase));
foreach (var candidate in customClasses)
{
// Ignore BaseController itself
if (candidate.FullName != null && candidate.FullName.Contains("BaseController")) continue;
// Generate type info for our runtime controller, assign class as T
var propertyType = candidate.GetProperty("Id")
?.PropertyType;
if (propertyType == null) continue;
var typeInfo = typeof(GenericController<,>).MakeGenericType(candidate, propertyType)
.GetTypeInfo();
// Finally add the new controller via FeatureProvider ->
feature.Controllers.Add(typeInfo);
}
}
}
Last but not least we need to make AttributeRouting work, this can be done quite easily as well, there's a function called IControllerModelConvention.
We can use this to apply route conventions to all GenericController instances.
public void Apply(ControllerModel controller)
{
if (controller.ControllerType.IsGenericType)
{
var genericType = controller.ControllerType.GenericTypeArguments[0];
controller.ControllerName = genericType.Name;
controller.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel(new RouteAttribute($"/{genericType.Name}"))
});
}
}
Final Words
Implementing things like this allows you to have a shared controller for all types that don't need any specific work done. You can still add a normal controller for special cases and everything else, swagger for example, keeps working as usually.
Just check the sample here -> https://github.com/DeeJayTC/samples/tree/main/GenericControllers