[英]ASP.Net Core Web API implementation with OData fails for a single entity URI
No matter what I try, I cannot get OData 7.3.0 to return a single resource using a simple URL like https://localhost:44316/odata/Widget(5)
...无论我尝试什么,我都无法让 OData 7.3.0 使用简单的 URL(如https://localhost:44316/odata/Widget(5)
...
Steps to reproduce:重现步骤:
I created a database called 'WidgetDB' is SQL Server.我创建了一个名为“WidgetDB”的数据库是 SQL 服务器。
I used the following SQL script to add a single table with some data:我使用以下 SQL 脚本来添加包含一些数据的单个表:
create table widget
(
widget_id int identity(1, 1) not null,
widget_name varchar(100) not null,
constraint PK_widget primary key clustered (widget_id)
)
GO
insert into widget (widget_name)
values
('Thingamabob'), ('Thingamajig'), ('Thingy'),
('Doomaflotchie'), ('Doohickey'), ('Doojigger'), ('Doodad'),
('Whatchamacallit'), ('Whatnot'), ('Whatsit'),
('Gizmo'), ('Nicknack')
GO
I added Nuget Packages for the following:我为以下内容添加了 Nuget 包:
I removed the Weatherforecast.cs class and WeatherforecastController.cs classes from the default scaffold project that is created by Visual Studio.我从 Visual Studio 创建的默认脚手架项目中删除了 Weatherforecast.cs class 和 WeatherforecastController.cs 类。
I went to the package manager console and typed in the following line to scaffold a DbContext for Entity Framework core:我去了 package 管理器控制台并输入以下行来为实体框架核心搭建一个 DbContext:
PM> Scaffold-DbContext -Connection "Server=.;Database=WidgetDB;Trusted_Connection=True;" -Provider Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models Build started... Build succeeded. PM>
ConnectionStrings
section:我打开了appsettings.json文件并添加了ConnectionStrings
部分:{
"ConnectionStrings": {
"Default": "Server=.;Database=WidgetDB;Trusted_Connection=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
OnConfiguring
method:我打开了第 6 步创建的 Models\WidgetDBContext.cs 文件,并取出了OnConfiguring
方法:using Microsoft.EntityFrameworkCore;
namespace WidgetWebAPI.Models
{
public partial class WidgetDBContext : DbContext
{
public WidgetDBContext() { }
public WidgetDBContext(DbContextOptions<WidgetDBContext> options) : base(options) { }
public virtual DbSet<Widget> Widget { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Widget>(entity =>
{
entity.ToTable("widget");
entity.Property(e => e.WidgetId).HasColumnName("widget_id");
entity.Property(e => e.WidgetName)
.IsRequired()
.HasColumnName("widget_name")
.HasMaxLength(100)
.IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
}
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OData.Edm;
using WidgetWebAPI.Models;
namespace WidgetWebAPI
{
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)
{
// See note on https://devblogs.microsoft.com/odata/experimenting-with-odata-in-asp-net-core-3-1/
// Disabling end-point routing isn't ideal, but is required for the current implementation of OData
// (7.4.0 as of this comment). As OData is further updated, this will change.
//services.AddControllers();
services.AddControllers(mvcOoptions => mvcOoptions.EnableEndpointRouting = false);
services.AddDbContext<Models.WidgetDBContext>(optionsBuilder =>
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(Configuration.GetConnectionString("Default"));
}
});
services.AddOData();
}
// 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();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
// Again, this is temporary due to current OData implementation. See note above.
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapControllers();
//});
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
}
private IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.Namespace = "WidgetData"; // Hide Model Schema from $metadata
builder.EntitySet<Widget>("Widgets").EntityType
.HasKey(r => r.WidgetId)
.Filter() // Allow for the $filter Command
.Count() // Allow for the $count Command
.Expand() // Allow for the $expand Command
.OrderBy() // Allow for the $orderby Command
.Page() // Allow for the $top and $skip Commands
.Select(); // Allow for the $select Command;
return builder.GetEdmModel();
}
}
}
A created the class Controllers\WidgetsController.cs by right-clicking on the Controllers folder, selecting Add Controller... , and choosing the API Controller with actions, using Entity Framework option in the wizard dialog: A created the class Controllers\WidgetsController.cs by right-clicking on the Controllers folder, selecting Add Controller... , and choosing the API Controller with actions, using Entity Framework option in the wizard dialog:
This was my first mistake.这是我的第一个错误。 See step 13.请参见第 13 步。
I added the [EnableQuery]
attribute to the GetWidget()
method of the Controller class that the scaffold created, and changed the class inheritance from ControllerBase
to ODataController
. I added the [EnableQuery]
attribute to the GetWidget()
method of the Controller class that the scaffold created, and changed the class inheritance from ControllerBase
to ODataController
. Other than ensuring my namespaces were properly resolved, I did nothing else to the existing file.除了确保我的命名空间得到正确解析之外,我对现有文件没有做任何其他事情。
I changed the debug settings to set the URL to odata/Widgets rather than weatherforecast and ran the application.我更改了调试设置以将URL设置为odata/Widgets而不是天气预报并运行应用程序。
NOTHING WORKED!没有任何效果! After hours of cursing and puzzlement and trial and error, I finally figured out that by default OData hates plural named objects and controllers.经过数小时的诅咒、困惑和反复试验,我终于发现默认情况下 OData 讨厌复数命名对象和控制器。
builder.EntitySet<Widget>("Widget").EntityType
(Or 10 revisted) I ran the Add Controller... wizard again and this time, I set the Controller Name to WidgetController
, and then reapplied the changes mentioned in step 11. (或 10 次修订)我再次运行Add Controller...向导,这一次,我将 Controller 名称设置为WidgetController
,然后重新应用步骤 11 中提到的更改。
I updated the Launch Browser Debug setting in the project properties to odata/Widget and ran the application again:我将项目属性中的 Launch Browser Debug 设置更新为odata/Widget并再次运行应用程序:
All the widgets are returned, so we've made progress!所有的小部件都返回了,所以我们取得了进展!
However, any attempt to get a single entity using a well-formed OData Url such as https://localhost:44316/odata/Widget(4)
simply returns the whole data set rather than the single entity whose Id is 4. In fact, a SQL Profiler trace shows that the constructed SQL query doesn't contain anything other than a select from the entire table:但是,使用格式良好的 OData Url(例如https://localhost:44316/odata/Widget(4)
获取单个实体的任何尝试都只会返回整个数据集,而不是返回 Id 为 4 的单个实体。事实上,SQL Profiler 跟踪显示,构造的 SQL 查询不包含除整个表中的 select 之外的任何内容:
SELECT [w].[widget_id], [w].[widget_name]
FROM [widget] AS [w]
I have looked all over the Internet and my Google Fu is failing me.我浏览了整个互联网,我的 Google Fu 让我失望了。 I cannot find a reason this isn't working, nor a current example demonstrating where it is working and what I am missing, I can find many examples demonstrating $filter, $expand.我找不到这不起作用的原因,也找不到演示它在哪里工作以及我缺少什么的当前示例,我可以找到许多演示 $filter、$expand 的示例。 etc. but not a single example of just returning a single entity from a set.等等,但不是仅从集合中返回单个实体的单个示例。
I've tried things like changing the method signatures.我尝试过更改方法签名之类的方法。 This has no effect either:这也没有效果:
[HttpGet]
[EnableQuery]
public IQueryable<Widget> GetWidget() => _context.Widget.AsQueryable();
[HttpGet("{id}")]
[EnableQuery]
public IQueryable<Widget> GetWidget([FromODataUri] int id) => _context.Widget.Where(r => r.WidgetId == id);
I know the end-point is capable of returning a single entity.我知道端点能够返回单个实体。 I can get it to do so by entering the URL: https://localhost:44316/odata/Widget?$filter=WidgetId eq 5
, which works fine and appropriately causes the correct SQL to be generated against the database. I can get it to do so by entering the URL: https://localhost:44316/odata/Widget?$filter=WidgetId eq 5
, which works fine and appropriately causes the correct SQL to be generated against the database.
After three days of frustration, I stumbled across the solution to the problem - a solution that is maddening in its simplicity, and infuriating that it doesn't appear to be documented anywhere as a critical necessity in any examples I can find.经过三天的挫折后,我偶然发现了该问题的解决方案——这个解决方案因其简单性而令人抓狂,并且令人愤怒的是,在我能找到的任何示例中,它似乎都没有作为关键必需品被记录在任何地方。
When it comes to the method signature for a single entity, this method signature doesn't work.当涉及到单个实体的方法签名时,此方法签名不起作用。 The routing middleware never matches to it, so the method never gets called:路由中间件永远不会匹配到它,所以该方法永远不会被调用:
[EnableQuery]
[ODataRoute("({id})", RouteName = nameof(GetWidget))]
public async Task<IActionResult> GetWidget([FromODataUri] int id)
{
var widget = await _context.Widget.FindAsync(id);
if (widget == null) return NotFound();
return Ok(widget);
}
Either of the following options works fine however:但是,以下任一选项都可以正常工作:
[EnableQuery]
public async Task<IActionResult> GetWidget([FromODataUri] int key)
{
var widget = await _context.Widget.FindAsync(key);
if (widget == null) return NotFound();
return Ok(widget);
}
[EnableQuery]
public async Task<IActionResult> GetWidget([FromODataUri] int keyWidgetId)
{
var widget = await _context.Widget.FindAsync(keyWidgetId);
if (widget == null) return NotFound();
return Ok(widget);
}
The key to the mystery (pun intended) is using the word key
for the id...谜团的关键(双关语)是使用单词key
来表示 id...
Why isn't this written somewhere in giant bold type?为什么这不是用大粗体字写的? So stupid... #fumes #aggravation太愚蠢了... #fumes #aggravation
Here are some suggestions:以下是一些建议:
In your Startup.cs
:在您的Startup.cs
中:
app.UseMvc(routeBuilder =>
{
// the following will not work as expected
// BUG: https://github.com/OData/WebApi/issues/1837
// routeBuilder.SetDefaultODataOptions(new ODataOptions { UrlKeyDelimiter = Microsoft.OData.ODataUrlKeyDelimiter.Parentheses });
var options = routeBuilder.ServiceProvider.GetRequiredService<ODataOptions>();
options.UrlKeyDelimiter = Microsoft.OData.ODataUrlKeyDelimiter.Parentheses;
routeBuilder.MapODataServiceRoute("odata", "odata", GetEdmModel());
});
At the top of your controller, add:在 controller 的顶部,添加:
[ODataRoutePrefix("Widget")]
Remove the [EnableQuery]
attribute if you want to retrieve a single entity.如果要检索单个实体,请删除[EnableQuery]
属性。 Instead, use:相反,使用:
[ODataRoute("({id})", RouteName = nameof(GetWidget))]
public async Task<IActionResult> GetWidget([FromODataUri] int id)
{
var widget = await _context.Widget.SingleOrDefaultAsync(x => x.WidgetId == id);
return Ok(widget);
}
You also don't need the [HttpGet("{id}")]
attribute.您也不需要[HttpGet("{id}")]
属性。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.