【问题标题】:How to properly integrate OData with ASP.net Core如何正确地将 OData 与 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"
}

【问题讨论】:

标签: c# asp.net-core odata asp.net-web-api-odata asp.net-core-webapi


【解决方案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.vNext、version 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 继承控制器

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-01-22
        • 1970-01-01
        • 1970-01-01
        • 2019-08-25
        • 1970-01-01
        • 2021-12-28
        • 2017-10-18
        • 2021-11-22
        相关资源
        最近更新 更多