如何正确地将 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.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 继承控制器
【讨论】:
以上是关于如何正确地将 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 的异常