使用 OData 的 ASP.Net Core Web API 实现对于单个实体 URI 失败

Posted

技术标签:

【中文标题】使用 OData 的 ASP.Net Core Web API 实现对于单个实体 URI 失败【英文标题】:ASP.Net Core Web API implementation with OData fails for a single entity URI 【发布时间】:2020-08-23 12:52:51 【问题描述】:

无论我尝试什么,我都无法让 OData 7.3.0 使用像 https://localhost:44316/odata/Widget(5) 这样的简单 URL 返回单个资源...

重现步骤:

    我创建了一个名为“WidgetDB”的数据库是 SQL Server。

    我使用以下 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
    在 Visual Studio 2019 中,我使用 ASP.Net Core 3.1 创建了一个名为“WidgetWebAPI”的新 Web API 解决方案。

    我为以下内容添加了 Nuget 包:

    Microsoft.EntityFrameworkCore 3.1.3 Microsoft.EntityFrameworkCore.SqlServer 3.1.3 Microsoft.EntityFrameworkCore.Tools 3.1.3 Microsoft.Extensions.DependencyInjection 3.1.3 Microsoft.AspNetCore.OData 7.4.0

    我从 Visual Studio 创建的默认脚手架项目中删除了 Weatherforecast.cs 类和 WeatherforecastController.cs 类。

    我转到包管理器控制台并输入以下行来为实体框架核心构建一个 DbContext:

    PM> Scaffold-DbContext -Connection "Server=.;Database=WidgetDB;Trusted_Connection=True;" -Provider Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
    Build started...
    Build succeeded.
    PM>
    我打开 appsettings.json 文件并添加了ConnectionStrings 部分:

 "ConnectionStrings": 
   "Default": "Server=.;Database=WidgetDB;Trusted_Connection=True;"
 ,
 "Logging": 
   "LogLevel": 
     "Default": "Information",
     "Microsoft": "Warning",
     "Microsoft.Hosting.Lifetime": "Information"
   
 ,
 "AllowedHosts": "*"

    我打开第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);
   

    我打开了 Startup.cs 文件,并拼凑了我能找到的完整示例,我将代码清理为以下内容:
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 通过右键单击 Controllers 文件夹并选择 Add Controller... 创建了类 Controllers\WidgetsController.cs,然后在向导对话框中选择 API Controller with actions, using Entity Framework 选项:

    这是我的第一个错误。参见第 13 步。

    我将[EnableQuery]属性添加到脚手架创建的Controller类的GetWidget()方法中,并将类继承从ControllerBase更改为ODataController。除了确保我的命名空间得到正确解析外,我没有对现有文件做任何其他事情。

    我更改了调试设置,将 URL 设置为 odata/Widgets 而不是 weatherforecast,然后运行了应用程序。

没有任何效果! 经过数小时的诅咒、困惑和反复试验,我终于发现默认情况下 OD​​ata 讨厌复数命名对象和控制器。

    (或 9 次修订)我回到我的 Startup.cs 类并将这行代码更改为使用单数形式:
builder.EntitySet<Widget>("Widget").EntityType

    (或 10 次修订)我再次运行 添加控制器... 向导,这一次,我将控制器名称设置为 WidgetController,然后重新应用步骤中提到的更改11.

    我将项目属性中的 Launch Browser Debug 设置更新为 odata/Widget 并再次运行应用程序:

所有小部件都返回了,所以我们取得了进展!

但是,任何使用格式良好的 OData Url(例如 https://localhost:44316/odata/Widget(4))获取单个实体的尝试都只会返回整个数据集,而不是 Id 为 4 的单个实体。事实上,SQL Profiler 跟踪显示构造的 SQL 查询除了从整个表中选择之外不包含任何内容:

SELECT [w].[widget_id], [w].[widget_name]
FROM [widget] AS [w]

我浏览了整个互联网,但我的 Google Fu 让我失望了。我找不到这不起作用的原因,也找不到一个当前示例来说明它在哪里工作以及我缺少什么!我可以找到许多演示 $filter、$expand 等的示例,但没有一个仅从集合中返回单个实体的示例。

我尝试过更改方法签名之类的方法。这也没有效果:

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

我知道终点是能够返回单个实体。我可以通过输入 URL:https://localhost:44316/odata/Widget?$filter=WidgetId eq 5 来实现它,它工作正常,并且可以适当地导致针对数据库生成正确的 SQL。

【问题讨论】:

我也在github.com/mitselplik/widget-web-api分享了演示项目。 【参考方案1】:

经过三天的挫折后,我偶然发现了问题的解决方案——一个简单到令人抓狂的解决方案,令人愤怒的是,在我能找到的任何示例中,它似乎都没有在任何地方作为关键必需品进行记录。

当涉及到单个实体的方法签名时,此方法签名不起作用。路由中间件永远不会匹配到它,所以该方法永远不会被调用:

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

以下任一选项都可以正常工作:

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

解开谜团的关键(双关语)是使用单词key作为id...

为什么这不是用大粗体字写的?太愚蠢了...#fumes #aggravation

【讨论】:

我很高兴你知道了。我克隆了你的回购,但也无法让它工作。既然你已经发布了这个,我注意到这个参数在这个文档中也被命名为key:devblogs.microsoft.com/odata/asp-net-core-odata-now-available【参考方案2】:

以下是一些建议:

在你的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());
);

在控制器的顶部,添加:

[ODataRoutePrefix("Widget")]

如果要检索单个实体,请删除 [EnableQuery] 属性。相反,使用:

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

您也不需要[HttpGet("id")] 属性。

【讨论】:

感谢您尝试提供帮助 :-) 但是以上都不起作用。我已将解决方案添加到 github @github.com/mitselplik/widget-web-api,以防您认为它可能会有所帮助。 另外,我不知道它在管道中是如何工作的,但是属性路由的 MVC 路由机制有可能取代 OData 路由,因此它永远没有机会做任何事情?我尝试跟踪 UseMVC(...) 调用,因为我注意到调试变量中的顺序。看来属性路由是故意插入到RouteBuilder.Routes开头的。 @Mitselplik 好的,我将克隆您的存储库并检查一下。同时,如果您想查看我是如何设置它的,我已经部署了一个工作版本:github.com/crgolden/Inventory

以上是关于使用 OData 的 ASP.Net Core Web API 实现对于单个实体 URI 失败的主要内容,如果未能解决你的问题,请参考以下文章

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

使用 OData 的 ASP.Net Core Web API 实现对于单个实体 URI 失败

带有 EF Core / ASP.NET Core 的 OData - 好还是坏?

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

ASP.NET.Core允许在设置RequireAuthenticatedUser()时匿名访问OData $ metadata

使用asp.net webapi实现oData inlinecount [重复]