Search code examples
c#routesasp.net-core-mvcodata

Odata .Net core v4 mvc routing not working


When learning Odata, I've tried a test project with books and then tried making my own with a database connected. Both projects has edmx version 4 and is similar configured.

However, when calling my methods with parameters in the original project, it doesn't work, but it does in the Book example. etc. odata/v4/Resources(1) then the books(1). I can fetch data from Companies and resources controllers, but not from AccountController even it's similar setup method wised. It returns 404 not found. I can do the ?$select filtering on the resources controller, but not on the companiesController. Every other odata function works as it should. Changing to odataprefix and odataroute, every controller works, but then the method calling with parameters doesn't work.

I'm unaware of what I'm missing or what is giving me this issue and are now searching for help.

The guides I've been following and reading about is linked below. I'm aware that prefix routing is differently, but it should work like the book test to begin with:

Start.cs - My project - Github => original project

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }


    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {

        services.AddControllers();
        //-- Own data - This is entity framework core without
        services.AddDbContext<CompanyBrokerEntities>(options => options.UseSqlServer(Configuration.GetConnectionString("CompanyBrokerEntities")));
        //--- Adding odata to asp.net core's dependency injection system
        services.AddOData();
        //--- ODATA CONTENT ROUTE disabling - Odata does not support end point routing
        services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        //--- ODATA CONTENT ROUTE for each controller with 'odata' infront of it
        app.UseMvc(routebuilder =>
        {
            // EnableDependencyInjection is required if we want to have OData routes and custom routes together in a controller
            routebuilder.EnableDependencyInjection();
            //- The route etc. localhost:50359/odata/v4/Accounts
            //- sets the route name, prefix and Odata data model
            routebuilder.MapODataServiceRoute("ODataRoute", "odata/v4", GetEdmModel());
            //-- enables all OData query options, for example $filter, $orderby, $expand, etc.
            routebuilder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
        });
    }


    ////--- ODATA CONTENT - Create the OData IEdmModel as required:
    private IEdmModel GetEdmModel()
    {
        //-- Creates the builder 
        var odataBuilder = new ODataConventionModelBuilder();
        //-- Eks odataBuilder.EntitySet<ResourceDescription>("Resource Descriptions").EntityType.HasKey(p => p.DescriptionId);
        //-- Use Annotations with [Key] field on the primary key fields in the model instead of above one liner
        odataBuilder.EntitySet<CompanyAccount>("Accounts");
        odataBuilder.EntitySet<Company>("Companies");
        odataBuilder.EntitySet<CompanyResource>("Resources");
        odataBuilder.EntitySet<ResourceDescription>("Descriptions");

        //var getAccountF = odataBuilder.EntityType<CompanyAccount>().Function("GetAccount");
        //    getAccountF.Returns<AccountResponse>();
        //    getAccountF.Parameter<string>("username");

        //var GetResourcesByIdF = odataBuilder.EntityType<CompanyResource>().Function("GetResourcesByCompanyId");
        //     GetResourcesByIdF.ReturnsCollection<IList<CompanyResource>>();
        //     GetResourcesByIdF.Parameter<int>("companyId");

        //-- returns the IEdmModel
        return odataBuilder.GetEdmModel();
    }
}

Account controller

//[ODataRoutePrefix("Accounts")]
public class AccountController : ODataController
{
    #region constructor and DBS data
    //-- database context 
    private readonly CompanyBrokerEntities db;

    public AccountController(CompanyBrokerEntities context)
    {
        db = context;
    }

    #endregion

        #region Get Methods

    /// <summary>
    /// Fetches all accounts, through a model to not contain sensitive data like passwords.
    /// </summary>
    /// <returns></returns>
    [EnableQuery]
    //[ODataRoute]
    public async Task<IActionResult> GetAccounts()
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        //-- Uses the CompanyBrokeraccountEntity to access the database
        //-- Filtered by AccountResponse for sensitive data
        var responseList = await db.CompanyAccounts.AsQueryable().Select(a => new AccountResponse(a)).ToListAsync();

        if (responseList != null)
        {
            return Ok(responseList);
        }
        else
        {
            return NotFound();
        }
    }

The context - DBS datasets created with EF

public partial class CompanyBrokerEntities : DbContext
{
    public virtual DbSet<Company> Companies { get; set; }
    public virtual DbSet<CompanyAccount> CompanyAccounts { get; set; }
    public virtual DbSet<CompanyResource> CompanyResources { get; set; }
    public virtual DbSet<ResourceDescription> ResourceDescriptions { get; set; }

    //public CompanyBrokerEntities() : base("name=CompanyBrokerEntities")
    //{
    //}

    //protected override void OnModelCreating(DbModelBuilder modelBuilder)
    //{
    //    throw new UnintentionalCodeFirstException();
    //}

    public CompanyBrokerEntities(DbContextOptions<CompanyBrokerEntities> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //throw new UnintentionalCodeFirstException();
    }

}

The book test : Github => test project

start.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        //---- ODATA CONTENT 
        services.AddOData();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
        services.AddDbContext<BookStoreContext>(opt => opt.UseInMemoryDatabase("BookLists"));
        services.AddMvc(options => options.EnableEndpointRouting = false);
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        //--- ODATA CONTENT ROUTE
        app.UseMvc(b =>
        { 
            //- The route
            b.MapODataServiceRoute("odata", "odata", GetEdmModel());
            //-- Adding the following line of code in Startup.cs enables all OData query options, for example $filter, $orderby, $expand, etc.
            b.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
        });
    }

    //--- ODATA CONTENT
    private static IEdmModel GetEdmModel()
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Book>("Books");
        builder.EntitySet<Press>("Presses");
        return builder.GetEdmModel();
    }

BookController

  public class BooksController : ODataController
{
    private BookStoreContext _db;

    public BooksController(BookStoreContext context)
    {
        _db = context;
        if (context.Books.Count() == 0)
        {
            foreach (var b in DataSource.GetBooks())
            {
                context.Books.Add(b);
                context.Presses.Add(b.Press);
            }
            context.SaveChanges();
        }
    }

    // ...
    [EnableQuery]
    public IActionResult Get()
    {
        return Ok(_db.Books);
    }

    [EnableQuery]
    public IActionResult Get([FromODataUri] int key)
    {
        return Ok(_db.Books.FirstOrDefault(c => c.Id == key));
    }

    // ...
    [EnableQuery]
    public IActionResult Post([FromBody] Book book)
    {
        _db.Books.Add(book);
        _db.SaveChanges();
        return Created(book);
    }
}

Solution

    1. The 404 when calling odata/v4/Accounts is because while your entity set is named Accounts your controller is named AccountCoutroller. Make it AccountsController and that will take care of the 404
    2. With regard to the query options ($filter, $select, etc) working on /odata/v4/Resources but not working on /odata/v4/Companies, note that GetCompanies action returns CompanyResponse (and not Company) objects. CompanyResponse is not represented in the service metadata - it's not in the EDM. The response you're actually seeing is because your API is falling back on ASP.NET Web API serialization. OData will have a challenge constructing the $filter or $select expressions since it "knows" nothing about CompanyResponse. Returning Company objects would fix the issue.