How to properly integrate OData with ASP.net Core
Asked Answered
F

6

28

I'm trying to create a new ASP.NET Core project with a "simple" web api using OData and EntityFramework. I have previously used OData with older versions of ASP.NET.

I have set up a controller with only a simple get function. I've managed to get it working with basic OData commands as filter and top, but I can't get the expand command working. I think it's because I can't quite figure out how to set it up in Startup.cs. I have tried a lot of things including following some odata samples from Github:

https://github.com/OData/WebApi/tree/vNext/vNext/samples/ODataSample.Web https://github.com/bigfont/WebApi/tree/master/vNext/samples/ODataSample.Web

In my startup file I try to exclude some properties from the Service class which has no effect at all. So the problem may lie in the way I'm using the IDataService interface. (The ApplicationContext implements it like in the samples)

To be clear I'm creating a ASP.NET Core web api with the full .NET Framework and not only .Core framework. My current code is a mix of the best/worst of both samples and work in the sense that I can filter the WebAPI but can't get it to expand or hide properties.

Can anyone see what I'm missing og have a working ASP.NET Odata sample. I'm new to the whole setup in startup.cs? Guess I'm looking for someone who have made this work.

Controller

[EnableQuery]
[Route("odata/Services")]
public class ServicesController : Controller
{
    private IGenericRepository<Service> _serviceRepo;
    private IUnitOfWork _unitOfWork;

    public ServicesController(IGenericRepository<Service> serviceRepo, IUnitOfWork unitOfWork)
    {
        _serviceRepo = serviceRepo;
        _unitOfWork = unitOfWork;
    }

    [HttpGet]
    public IQueryable<Service> Get()
    {
        var services = _serviceRepo.AsQueryable();
        return services;
    }
}

startup

using Core.DomainModel;
using Core.DomainServices;
using Infrastructure.DataAccess;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.OData.Extensions;

namespace Web
{
public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();

        if (env.IsDevelopment())
        {
            // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
            builder.AddApplicationInsightsSettings(developerMode: true);
        }
        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddApplicationInsightsTelemetry(Configuration);
        services.AddMvc().AddWebApiConventions();

        services.AddSingleton<ApplicationContext>(_ => ApplicationContext.Create());

        services.AddSingleton<IDataService, ApplicationContext>();

        services.AddOData<IDataService>(builder =>
        {
            //builder.EnableLowerCamelCase();
            var service = builder.EntitySet<Service>("Services");
            service.EntityType.RemoveProperty(x => x.CategoryId);
            service.EntityType.RemoveProperty(x => x.PreRequisiteses);
        });


        services.AddSingleton<IGenericRepository<Service>, GenericRepository<Service>>();
        services.AddSingleton<IUnitOfWork, UnitOfWork>();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();

        //ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

        app.UseApplicationInsightsRequestTelemetry();

        //var builder = new ODataConventionModelBuilder(app.ApplicationServices.GetRequiredService<AssembliesResolver>());
        //var serviceCtrl = nameof(ServicesController).Replace("Controller", string.Empty);
        //var service = builder.EntitySet<Service>(serviceCtrl);
        //service.EntityType.RemoveProperty(x => x.CategoryId);

        app.UseOData("odata");

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseApplicationInsightsExceptionTelemetry();

        app.UseStaticFiles();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Project.json dependencies

  "dependencies": {
    "Microsoft.ApplicationInsights.AspNetCore": "1.0.2",
    "Microsoft.AspNet.Identity.EntityFramework": "2.2.1",
    "Microsoft.AspNetCore.Diagnostics": "1.0.0",
    "Microsoft.AspNetCore.Identity": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.1",
    "Microsoft.AspNetCore.Razor.Tools": {
      "version": "1.0.0-preview2-final",
      "type": "build"
    },
    "Microsoft.AspNetCore.Routing": "1.0.1",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Microsoft.AspNetCore.StaticFiles": "1.0.0",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0",
    "Microsoft.AspNetCore.OData": "1.0.0-rtm-00015",
    "dnx-clr-win-x86": "1.0.0-rc1-update2",
    "Microsoft.OData.Core": "7.0.0",
    "Microsoft.OData.Edm": "7.0.0",
    "Microsoft.Spatial": "7.0.0"
}
Flournoy answered 5/10, 2016 at 10:22 Comment(1)
@l--''''''---------'''''''''''', Try to use github.com/voronov-maxim/OdataToEntity witch has container as client,expand, select and otherGrinnell
G
6

I managed to make it work, but I didn't use the provided OData routing because I needed more granularity. With this solution, you can create your own web API, while still allowing the use of OData query parameters.

Notes:

  • I used Nuget package Microsoft.AspNetCore.OData.vNext, version 6.0.2-alpha-rtm, which requires .NET 4.6.1
  • As fas as I can tell, OData vNext only support OData v4 (so no v3)
  • OData vNext seems to have been rushed, and is packed with bugs. For example, the $orderby query parameter is broken

MyEntity.cs

namespace WebApplication1
{
    public class MyEntity
    {
        // you'll need a key 
        public int EntityID { get; set; }
        public string SomeText { get; set; }
    }
}

Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Builder;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace WebApplication1
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            /* ODATA part */
            services.AddOData();
            // the line below is used so that we the EdmModel is computed only once
            // we're not using the ODataOptions.ModelManager because it doesn't seemed plugged in
            services.AddSingleton<IODataModelManger, ODataModelManager>(DefineEdmModel);
        }

        private static ODataModelManager DefineEdmModel(IServiceProvider services)
        {
            var modelManager = new ODataModelManager();

            // you can add all the entities you need
            var builder = new ODataConventionModelBuilder();
            builder.EntitySet<MyEntity>(nameof(MyEntity));
            builder.EntityType<MyEntity>().HasKey(ai => ai.EntityID); // the call to HasKey is mandatory
            modelManager.AddModel(nameof(WebApplication1), builder.GetEdmModel());

            return modelManager;
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Controller.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Query;
using System.Linq;

namespace WebApplication1.Controllers
{
    [Produces("application/json")]
    [Route("api/Entity")]
    public class ApiController : Controller
    {
        // note how you can use whatever endpoint
        [HttpGet("all")]
        public IQueryable<MyEntity> Get()
        {
            // plug your entities source (database or whatever)
            var entities = new[] {
                new MyEntity{ EntityID = 1, SomeText = "Test 1" },
                new MyEntity{ EntityID = 2, SomeText = "Test 2" },
                new MyEntity{ EntityID = 3, SomeText = "Another texts" },
            }.AsQueryable();

            var modelManager = (IODataModelManger)HttpContext.RequestServices.GetService(typeof(IODataModelManger));
            var model = modelManager.GetModel(nameof(WebApplication1));
            var queryContext = new ODataQueryContext(model, typeof(MyEntity), null);
            var queryOptions = new ODataQueryOptions(queryContext, HttpContext.Request);

            return queryOptions
                .ApplyTo(entities, new ODataQuerySettings
                {
                    HandleNullPropagation = HandleNullPropagationOption.True
                })
                .Cast<MyEntity>();
        }
    }
}

How to test

You can use the following URI : /api/Entity/all?$filter=contains(SomeText,'Test'). If it works correctly, you should only see the first two entities.

Guimpe answered 4/8, 2017 at 9:27 Comment(6)
confirmed that this works with core 2.0, also what happens when we have multiple entities with a relationship between them? will we be able to do expand and all of those operations?Ton
This solution allows you to have more control about how you expose your entities via OData, but the downside is that it doesn't automatically generates routes. So no, you won't be able to expand children entities.Outland
this is unfortunately not working with SELECT/EXPANDTon
so would you go with the option above instead?Ton
Got the following error using core 2.2 and OData 7.1.0: Cannot find the services container for the non-OData route. This can occur when using OData components on the non-OData route and is usually a configuration issue. Call EnableDependencyInjection() to enable OData components on non-OData routes. This may also occur when a request was mistakenly handled by the ASP.NET Core routing layer instead of the OData routing layer, for instance the URL does not include the OData route prefix configured via a call to MapODataServiceRoute().Formaldehyde
@Formaldehyde : OData conflicts with the new ASP.NET Core routing. You need to either use services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1) or services.AddMvc(options => options.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Version_2_2). See github.com/Microsoft/aspnet-api-versioning/issues/361.Outland
E
6

I also got Microsoft.AspNetCore.OData.vNext, version 6.0.2-alpha-rtm to work, but I used the following code to map Edm model to routes:

services.AddOData();
// ...
app.UseMvc(routes =>
{
  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  modelBuilder.EntitySet<Product>("Products");
  IEdmModel model = modelBuilder.GetEdmModel();
  routes.MapODataRoute(
    prefix: "odata",
      model: model
  );

along with services.AddOData()

It's strange but it seems to work with .Net Core 1.1

Equate answered 8/8, 2017 at 9:34 Comment(2)
This looks like an interesting approach that could help if you just want to apply OData in specific queries.Locule
Supports 461 so yes.Absence
B
2

I have a github repo which auto generates ASP.NET Core OData v4 controllers from a code first EF model, using T4. It uses Microsoft.AspNetCore.OData.vNext 6.0.2-alpha-rtm. Might be of interest.

https://github.com/afgbeveridge/AutoODataEF.Core

Branca answered 26/4, 2017 at 2:28 Comment(1)
I tried for many hours to get this to work. But for some reason I could not get it to transform and I was getting some very weird error that I was unable to Google. Would you happen to have a few moments to help?Ton
L
0

Looks like this is currently in alpha with the OData team. according to this issue

Locule answered 12/12, 2017 at 23:45 Comment(0)
O
0

From WEB API Server side:

Simplest way to use is the direct [EnableQuery] attribute. Now with recent 7.x pacakge, it is working great.

You can also easily have generic impl., as below. idea is have a common method, and disambiguate based on entity name you require. With Linq2RestANC for client side consumption, you can easily pass your custom query parameters too. As in example below, if you have 2 tables Movies1 and Movies2, then the queries will be applied directly on your db only, when you do an $expand and sub-filter/sub-process conditions within them.

[EnableQuery]
public IActionResult Get([FromQuery] string name)
{
        switch (name)
        {
            case "Movie2":
                return Ok(new List<ViewModel>{new ViewModel(Movies2=_db.Movies2)});
        }
        return Ok(new List<ViewModel>{new ViewModel(Movies1=_db.Movies1)});
 }

For client side consumption- --> Don't use ODATA Service proxy. It is buggy and throws lot of errors. --> Simple.OData.Client is good. But lags support for nested queries within expand . for eg. /Products?$expand=Suppliers($select=SupplierName;$top=1;) For such inner expand it doesn't support further filtering. That is tracked as bug #200

--> Linq2RestANC is a beautiful choice. That too natively doesn't support nested expands, but it is implemented by inheriting native IQueryProvider, so it just took 3-4 hours to modify and test the completed nested - deep level expand scenarios. You will need to change at Expressionprocessor.cs "Expand" And ParameterBuilder.cs GetFullUri() a little to get it working.

Outrigger answered 22/7, 2018 at 0:22 Comment(0)
A
-7

You need inherit controller from the ODataController

Auberon answered 4/2, 2017 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.