Search code examples
c#.net-coreodata.net-6.0

Register controllers dynamically for each model in my project .NET 6


I need an API that would expose all my DB tables as an OData API's. For this I have used scaffold db to generate all the existing models, and I have already managed to create a correct EDM model using reflection for the OData to work with. I have also created a generic controller that I can use as a base controller for each of my models. And it looks like this:

public class GenericController<TEntity> : ODataController
       where TEntity : class, new()
    {
        private readonly DbContext _context;

        public GenericController(DbContext context)
        {
            _context = context;
        }

        [HttpGet]
        [EnableQuery(PageSize = 1000)]
        [Route("odata/{Controller}")]
        public async Task<ActionResult<IEnumerable<TEntity>>> GetObjects()
        {
            try
            {
                if (_context.Set<TEntity>() == null)
                {
                    return NotFound();
                }
                var obj = await _context.Set<TEntity>().ToListAsync();

                return Ok(obj);

            }
            catch (Exception ex)
            {
                return BadRequest(ex);
            }
        }
    }

I can use this controller as a base for each of my models by manually creating a controller per model like this:

[ApiController]
public class ModelController : GenericController<MyModel>
{
    public ActiviteitenObjectsController(Dbcontext context) : base(context)
    {
    }
}

And it works fine with OData filters and everything. But the problem is I have way too many tables to be able to manually create the controllers for every single one of them. I know you can use app.MapGet("/", () => "Hello World!") to map the endpoints to a delegate or even use HTTPContext inside of it, but I can't figure out how to use it in my case, so that it would work with OData as well. Are there any approaches I can use to solve my problem?


Solution

  • With reference from few links.

    Add Helper, GenericControllerNameAttribute & GenericControllerFeatureProvider.

    Then decorate your controller with annotation [GenericControllerName].

    Modify ConfigureServices from Startup and append .ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new Controllers.GenericControllerFeatureProvider())); after services.AddMvc() or services.AddControllers() or services.AddControllersWithViews() whatever you have used.

    Helper will initialize all the DBSet from your DbContext and create dictionary with key as name of Model and/or name of property.

    Helper.cs

    public static class Helper
    {
        public static Dictionary<string, System.Type> modelDictionary = new Dictionary<string, System.Type>(System.StringComparer.OrdinalIgnoreCase);
    
        static Helper()
        {
            var properties = typeof(DbContext).GetProperties();
    
            foreach (var property in properties)
            {
                var setType = property.PropertyType;
                var isDbSet = setType.IsGenericType && (typeof(DbSet<>).IsAssignableFrom(setType.GetGenericTypeDefinition()));
    
                if (isDbSet)
                {
                    // suppose you have DbSet as below
                    // public virtual DbSet<Activity> Activities { get; set; }
    
                    // genericType will be typeof(Activity)
                    var genericType = setType.GetGenericArguments()[0];
    
                    // Use genericType.Name if you want to use route as class name, i.e. /OData/Activity
                    if (!modelDictionary.ContainsKey(genericType.Name))
                    {
                        modelDictionary.Add(genericType.Name, genericType);
                    }
                }
            }
        }
    }
    

    GenericControllerNameAttribute.cs

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class GenericControllerNameAttribute : Attribute, IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            if (controller.ControllerType.GetGenericTypeDefinition() == typeof(Generic2Controller<>))
            {
                var entityType = controller.ControllerType.GenericTypeArguments[0]; 
                controller.ControllerName = entityType.Name;
            }
        }
    }
    

    GenericControllerFeatureProvider

    public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
    {
        public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
        {
            // Get the list of entities that we want to support for the generic controller       
            foreach (var entityType in Helper.modelDictionary.Values.Distinct())
            {
                var typeName = entityType.Name + "Controller";
                // Check to see if there is a "real" controller for this class            
                if (!feature.Controllers.Any(t => t.Name == typeName))
                {
                    // Create a generic controller for this type               
                    var controllerType = typeof(Generic2Controller<>).MakeGenericType(entityType).GetTypeInfo(); 
                    feature.Controllers.Add(controllerType);
                }
            }
        }
    }
    

    GenericController.cs

    [GenericControllerName]
    public class GenericController<TEntity> : ODataController
        where TEntity : class, new()
    {
        // Your code
    }
    

    Modification in Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            // Add .ConfigureApplicationPartManager(...); with AddMvc or AddControllers or AddControllersWithViews based on what you already have used.
            services.AddMvc()
                    .ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new GenericControllerFeatureProvider()));
        
            //services.AddControllers()
            //        .ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new GenericControllerFeatureProvider()));
        
            //services.AddControllersWithViews()
            //        .ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new GenericControllerFeatureProvider()));
        
        }