如何正确地将 OData 与 ASP.net Core 集成

Posted

技术标签:

【中文标题】如何正确地将 OData 与 ASP.net Core 集成【英文标题】:How to properly integrate OData with ASP.net Core 【发布时间】:2017-02-13 17:57:30 【问题描述】:

我正在尝试使用 OData 和 EntityFramework 创建一个带有“简单”Web API 的新 ASP.NET Core 项目。我以前在旧版本的 ASP.NET 中使用过 OData。

我已经设置了一个只有一个简单的 get 函数的控制器。我已经设法让它使用基本的 OData 命令作为过滤器和顶部,但我无法让扩展命令工作。我认为这是因为我不太清楚如何在 Startup.cs 中进行设置。我尝试了很多东西,包括关注 Github 的一些 odata 示例:

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

在我的启动文件中,我尝试从服务类中排除一些根本没有效果的属性。所以问题可能出在我使用 IDataService 接口的方式上。 (ApplicationContext 像示例中一样实现它)

需要明确的是,我正在创建一个具有完整 .NET Framework 而不仅仅是 .Core 框架的 ASP.NET Core Web api。我当前的代码混合了两个示例的最佳/最差,并且在某种意义上可以过滤 WebAPI,但无法扩展或隐藏属性。

任何人都可以看到我缺少什么以及有一个有效的 ASP.NET Odata 示例。我对 startup.cs 中的整个设置不熟悉?我想我正在寻找能够完成这项工作的人。

控制器

[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;
    

启动

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": 
    "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"

【问题讨论】:

@l--''''''---------'''''''''''',尝试使用github.com/voronov-maxim/OdataToEntity女巫有容器作为客户端、展开、选择等 【参考方案1】:

从 WEB API 服务器端:

最简单的使用方法是直接 [EnableQuery] 属性。现在使用最近的 7.x 包,它运行良好。

你也可以很容易地拥有通用的实现,如下所示。想法是有一个通用的方法,并根据您需要的实体名称消除歧义。使用 Linq2RestANC 进行客户端消费,您也可以轻松传递自定义查询参数。如下例所示,如果您有 2 个表 Movies1 和 Movies2,那么当您在其中执行 $expand 和 sub-filter/sub-process 条件时,查询将仅直接应用于您的数据库。

[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));
 

对于客户端消费- --> 不要使用 ODATA 服务代理。它是错误的并且会引发很多错误。 --> Simple.OData.Client 很好。但是对 expand 中的嵌套查询的支持滞后。 例如。 /Products?$expand=供应商($select=SupplierName;$top=1;) 对于这种内部扩展,它不支持进一步过滤。这被跟踪为错误 #200

--> Linq2RestANC 是一个不错的选择。那也原生不支持嵌套展开,但它是通过继承原生的IQueryProvider实现的,所以修改和测试完成的嵌套——深层次展开场景只需要3-4个小时。您需要在 Expressionprocessor.cs "Expand" 和 ParameterBuilder.cs GetFullUri() 处稍作更改以使其正常工作。

【讨论】:

【参考方案2】:

看起来这目前在 OData 团队中处于 alpha 阶段。 according to this issue

【讨论】:

【参考方案3】:

我也让Microsoft.AspNetCore.OData.vNextversion 6.0.2-alpha-rtm 工作,但我使用以下代码将Edm 模型映射到路由:

services.AddOData();
// ...
app.UseMvc(routes =>

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

还有services.AddOData()

这很奇怪,但它似乎适用于 .Net Core 1.1

【讨论】:

这看起来是一种有趣的方法,如果您只想在特定查询中应用 OData,它可能会有所帮助。 支持 461 所以是的。【参考方案4】:

我设法让它工作,但我没有使用提供的 OData 路由,因为我需要更多粒度。使用此解决方案,您可以创建自己的 Web API,同时仍允许使用 OData 查询参数。

注意事项:

我使用了Nuget包Microsoft.AspNetCore.OData.vNext,版本6.0.2-alpha-rtm,需要.NET 4.6.1 据我所知,OData vNext 仅支持 OData v4(所以不支持 v3) OData vNext 似乎很匆忙,并且充满了错误。例如,$orderby 查询参数已损坏

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>();
        
    

如何测试

您可以使用以下 URI:/api/Entity/all?$filter=contains(SomeText,'Test')。如果它正常工作,您应该只会看到前两个实体。

【讨论】:

确认这适用于核心 2.0,当我们有多个实体之间存在关系时会发生什么?我们能够进行扩展和所有这些操作吗? 此解决方案允许您更好地控制如何通过 OData 公开实体,但缺点是它不会自动生成路由。所以不,您将无法展开子实体。 很遗憾,这不适用于 SELECT/EXPAND 那么你会选择上面的选项吗? 使用核心 2.2 和 OData 7.1.0 时出现以下错误:找不到非 OData 路由的服务容器。在非 OData 路由上使用 OData 组件时可能会发生这种情况,通常是配置问题。调用 EnableDependencyInjection() 以在非 OData 路由上启用 OData 组件。当请求被 ASP.NET Core 路由层而不是 OData 路由层错误地处理时,也可能发生这种情况,例如 URL 不包含通过调用 MapODataServiceRoute() 配置的 OData 路由前缀。【参考方案5】:

我有一个 github 存储库,它使用 T4 从代码优先 EF 模型自动生成 ASP.NET Core OData v4 控制器。它使用 Microsoft.AspNetCore.OData.vNext 6.0.2-alpha-rtm。可能会感兴趣。

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

【讨论】:

我尝试了好几个小时才让它工作。但是由于某种原因,我无法对其进行转换,并且遇到了一些非常奇怪的错误,我无法通过 Google 搜索。你能有时间帮忙吗?【参考方案6】:

你需要从 ODataController 继承控制器

【讨论】:

以上是关于如何正确地将 OData 与 ASP.net Core 集成的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ASP Net Core Web API 上正确地将列表转换为 IQueryable

如何在 ASP.NET Web API 实现中将数组传递给 OData 函数?

不能将“Microsoft.AspNet.OData.Routing.ODataRoute”与端点路由一起使用。 ASP Net Core 2.2 的异常

如何正确 URL 重写图像? C# ASP.NET

使用 ASP.NET Core OData 8.0 映射动态 OData 路由

在 ASP.NET Core WebAPI 中获取 OData 计数