1

Assume we create a sample app, let's call it MyOdataApp: I am unable to dynamically create controllers for these two models which you see in the Program.cs, I also have no errors to tell my where I have gone awry. What's in these models is irrelavant, let's assume they are: Order & LineItem

namespace MyOdataApp.Models
    {
        public class Order
        {
            public string OrderId { get; set; } = null!;
            public string? CustomerName { get; set; }
            public string? BillingAddress { get; set; }
            public string? ShippingAddress { get; set; }
            public decimal? SubTotal { get; set; }
            public string? Currency { get; set; }
            public double? ExchangeRate { get; set; }
            public string? Status { get; set; }
            public DateTimeOffset CreateTs { get; set; }
    
            public virtual ICollection <LineItem> LineItems { get; set; }
        }
    
        public class LineItem
        {
            public string LineItemId { get; set; } = null!;
            public string? OrderId { get; set; }
            public int? LineIndex { get; set; }
            public string? Name { get; set; }
            public string? Description { get; set; }
            public int? Quantity { get; set; }
            public decimal? UnitPrice { get; set; }
            public double? Discount { get; set; }
            public DateTimeOffset CreateTs { get; set; }

            public virtual Order? Order { get; set; }
        }
    }

Here's my code: Program.cs

static IEdmModel GetEdmModel()
    {
        Microsoft.OData.ModelBuilder.ODataConventionModelBuilder builder = new();
        builder.EntitySet(nameof(MyOdataApp.Models.Order));
        builder.EntitySet(nameof(MyOdataApp.Models.LineItem));
        return builder.GetEdmModel();
    }
    
    builder.Services.AddControllers()
        .AddOData(opt => opt.EnableQueryFeatures().AddRouteComponents("odata", GetEdmModel()).Expand())
        .ConfigureApplicationPartManager(p => p.FeatureProviders.Add(new Controllers.GenericControllerFeatureProvider()));

OdataTemplateController.cs

using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.OData.Query;
    using Microsoft.EntityFrameworkCore;
    
    namespace Controllers;
    
    [GenericControllerName]
    [Route("odata/[Controller]")]
    public abstract class OdataTemplateController<T> : Microsoft.AspNetCore.OData.Routing.Controllers.ODataController
    {
        public readonly DbContext _context;
        public readonly ILogger<T> _logger;
    
        public OdataTemplateController(ILogger<T> logger, DbContext context)
        {
            _context = context;
            _logger = logger;
        }
    
        [EnableQuery]
        public virtual IQueryable Get() where T : class => _context.Set().AsQueryable();
    }

GenericControllerFeature.cs

using Microsoft.AspNetCore.Mvc.ApplicationParts;
    using Microsoft.AspNetCore.Mvc.Controllers;
    using System.Reflection;
    
    namespace Controllers;
    
    public class GenericControllerFeatureProvider : IApplicationFeatureProvider
    {
        public void PopulateFeature(IEnumerable parts, ControllerFeature feature)
        {
            var sysParts = parts.First() as dynamic;
            foreach (TypeInfo entityType in sysParts.Types)
            {
                if (entityType.FullName!.StartsWith("MyOdataApp.Models") && !feature.Controllers.Any(t => t.Name == entityType.Name))
                {
                    feature.Controllers.Add(typeof(OdataTemplateController<>)
                            .MakeGenericType(entityType.AsType())
                            .GetTypeInfo());
    
                }
            }
        }
    }

GenericControllerNameAttribute.cs

using Microsoft.AspNetCore.Mvc.ApplicationModels;
    
    namespace Controllers;
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class GenericControllerNameAttribute : Attribute, IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            if (controller.ControllerType.Name == "OdataTemplateController`1")
                controller.ControllerName = controller.ControllerType.GenericTypeArguments[0].Name;
    
        }
    }

Now, aside from some very poor choices like using the (literal) Name ("OdataTemplateController`1") of the ControllerType and taking shortcuts like StartsWith "MyOdataApp.Models" which you can beat/ whip me for later... ...What am I doing wrong?

I noticed the Edm relationships are perfect at /$metadata but the (ControllerFeature) features is only injecting the MetadataController and nothing else. Could this be the problem?

Please help :)

Jawid Hassim
  • 357
  • 3
  • 9

1 Answers1

1

The main issue here is that you are trying to resolve the Entity Types from the parts, but I cannot see where you have registered a resolver for these types. It might be easier to enumerate the DbSet<> definitions on the DbContext (if you have registered them) or you could enumerate all types in the MyOdataApp.Models namespace;

Looking at this implementation on SO, you can see that they have separately constructed an EntityTypes provider singleton, there are various other techniques, but it is important that those controller instance get created.

I noticed the Edm relationships are perfect at /$metadata

The Edm is provided by the IEdmModel, it will show the correct schema even if you have no controllers defined. By explicityly adding the builder.EntitySet() definitions the convention model builder does not resolve from the available routes, that takes too long at runtime anyway, but if the routes are not available then the requests will result in 404s, or if the builder and routes do not match correctly you may also experience 503s.


I have had more long-term success in creating concrete stubs that match the expected names of the controllers that inherit from the base controller. So bypassing the GenericControllerFeatureProvider. Without whipping you for all the other crimes against C#, this gives you a good hybrid approach that allows you to quickly implement customizations on specific controllers when you need them, otherwise I would recommend modifying the GenericControllerFeatureProvider to only create controllers for the types that do not already have a controller implementation.

  • I use T4 templates to generate the controller stubs and extend them through partial classes when or if I need to.
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
  • Thanks, I was getting the entity types from parts but I could've just as easily did: var types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes().Where(t => t.Namespace != null && t.Namespace.StartsWith("MyOdataApp.Models")); That part wasn't the issue. I upped your answer for anyone else who might try it but I ended up also creating concrete controllers, using System.IO.File, StringBuilder and string.Replace For Entity Names. Seeing that I was doing that anyway I just bypassed inheritance altogether. For me this was a failed exercise on the whole. – Jawid Hassim Feb 14 '22 at 15:47
  • There's a lot of value in getting it right if you do this in more than 1 project. Part of the reason concrete controllers works well is the speed, most attempts to resolve types at runtime will just slow down the API, a good generic solution can help you with rapid development if you need it, but inheritance is still very useful, my base controller looks similar to yours but has a lot more in it, I immediately recognized where you were coming from because I've been there too. Don't give up, that's when you fail. – Chris Schaller Feb 14 '22 at 22:21