1

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?

2 Answers2

1

You can do something like below. Not using Generic controller as you have but its similar to that.

  • Add Helper & GenericController as below and modify Startup.cs.

Details

  1. Helper will get all the DBSet from your DbContext and create dictionary with key as name of Model and/or name of property.
  2. Modify your Startup.cs and add some code in Configure. Explanation is there in comment.

References

Helper.cs

public static class Helper
{
    // Dictionary used to generate url pattern in Startup.cs & getting Entity for DbSet in GenericController.
    public static Dictionary<string, System.Type> modelDictionary = new Dictionary<string, System.Type>(System.StringComparer.OrdinalIgnoreCase);
   
    // Initialize dictionary and it will be use to generate URL pattern
    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);
                }

                // Use property.Name if you want to use route as property name, i.e. /OData/Activities
                if (!modelDictionary.ContainsKey(property.Name))
                {
                    modelDictionary.Add(property.Name, genericType);
                }
            }
        }
    }
}

GenericController

public class GenericController : ODataController
{
    private readonly DbContext _context;

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

    // GET: OData/{modelName}
    public async Task<ActionResult<IEnumerable<Object>>> Get(string modelName)
    {

        // Using reflection get required DbSet and return the list
        var entities = (IQueryable<object>)_context.GetType()
                                        .GetMethod("Set", types: Type.EmptyTypes)
                                        .MakeGenericMethod(Helper.modelDictionary[modelName])
                                        .Invoke(_context, null);

        var obj = await entities.ToListAsync();

        return Ok(obj);
    }
}

Modification in Startup.cs

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // your code

        // if you do not have app.UseEndpoints then add after app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            // For reference for pattern matching and route binding check below link
            // https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0#regular-expressions-in-constraints
            // Build Regex pattern for your generic controller and Map route accordingly for GenericController & action that we have added.
            var pattern = "OData/{modelName:regex(" + string.Join('|', Controllers.Helper.modelDictionary.Keys) + ")}";

            // Map controller action with pattern defined as "OData/{modelName:regex(Table1|Table2|...)}"
            endpoints.MapControllerRoute(
                name: "OData",
                pattern: pattern,
                defaults: new { controller = "Generic", action = "Get" });

            
        });

         // rest of your code
    }
Karan
  • 12,059
  • 3
  • 24
  • 40
  • Controller mapping works great, although the `OData` won't recognize the returned type and throws this exception: `Could not find a property named '{PropertyName}' on type 'System.Object'.` Any ideas on how to fix this? – Edvinas Deksnys Aug 19 '22 at 16:08
  • Check this link. It might be helpful. https://stackoverflow.com/questions/57096991/generic-o-data-controller-does-not-return-expected-result – Karan Aug 22 '22 at 05:35
1

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()));
    
    }
Karan
  • 12,059
  • 3
  • 24
  • 40