ASP. Net Реализация Core Web API с OData не выполняется для URI одного объекта - PullRequest
2 голосов
/ 08 мая 2020

Независимо от того, что я пытаюсь, я не могу заставить OData 7.3.0 возвращать один ресурс, используя простой URL-адрес, например https://localhost:44316/odata/Widget(5) ...

Шаги для воспроизведения:

  1. Я создал базу данных под названием «WidgetDB» - это SQL Server.

  2. Я использовал следующий сценарий 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 я создал новое решение веб-API, используя ASP. Net Core 3.1 под названием «WidgetWebAPI».

Я добавил пакеты 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

Я удалил класс Weatherforecast.cs и классы WeatherforecastController.cs из проекта скаффолда по умолчанию который создается Visual Studio.

Я зашел в консоль диспетчера пакетов и набрал следующую строку, чтобы сформировать DbContext для ядра Entity Framework:

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": "*"
}
Я открыл файл Models \ WidgetDBContext.cs, созданный на этапе 6, и вынул метод 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 \ WidgetsController.cs , щелкнув правой кнопкой мыши папку Controllers , выбрав Добавить контроллер ... и выбор API-контроллера с действиями, используя опцию Entity Framework в диалоговом окне мастера:

Add Controller Dialog

Это была моей первой ошибкой. См. шаг 13.

Я добавил атрибут [EnableQuery] в метод GetWidget() класса Controller, созданный скаффолдом, и изменил наследование классов с ControllerBase на ODataController. Помимо обеспечения правильного разрешения моих пространств имен, я ничего не сделал с существующим файлом.

Я изменил настройки отладки, чтобы установить URL-адрес odata / Widgets , а чем прогноз погоды и запустил приложение.

НИЧЕГО НЕ РАБОТАЕТ! После нескольких часов проклятий, недоумений, проб и ошибок , Я наконец понял, что по умолчанию OData ненавидит объекты и контроллеры с множественными именами.

(или изменено 9) Я вернулся в свой класс Startup.cs и изменил эту строку кода, чтобы использовать единственную форму:
builder.EntitySet<Widget>("Widget").EntityType

(или 10 изменено) Я снова запустил мастер Добавить контроллер ... , и на этот раз я установил имя контроллера на WidgetController, а затем повторно применил изменения, упомянутые в шаге 11 .

Я обновил параметр Launch Browser Debug в свойствах проекта до odata / Widget и снова запустил приложение:

Все виджеты возвращены, поэтому мы добились прогресса!

Однако любая попытка получить отдельную сущность с использованием правильно сформированного URL-адреса OData, такого как поскольку https://localhost:44316/odata/Widget(4) просто возвращает весь набор данных, а не отдельную сущность, идентификатор которой равен 4. Фактически, трассировка SQL Profiler показывает, что построенный запрос SQL не содержит ничего, кроме выбора из всей таблицы :

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

Я просмотрел весь Inte rnet, и мой Google Fu меня подводит. Я не могу найти причину, по которой это не работает, или текущий пример, демонстрирующий, где он работает и чего мне не хватает! Я могу найти множество примеров, демонстрирующих $ filter, $ expand и т. Д. c. но ни одного примера простого возврата одной сущности из набора.

Я пробовал менять сигнатуры методов. Это тоже не имеет никакого эффекта:

        [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 для базы данных.

Ответы [ 2 ]

2 голосов
/ 10 мая 2020

После трех дней разочарования я наткнулся на решение проблемы - решение, которое сводит с ума своей простотой и приводит в ярость, что оно нигде не задокументировано как критическая необходимость ни в каких примерах, которые я могу найти.

Когда дело доходит до сигнатуры метода для отдельного объекта, эта сигнатура метода не работает. Промежуточное ПО маршрутизации никогда не совпадает с ним, поэтому метод никогда не вызывается:

 [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 для идентификатора ...

Почему это не написано где-то гигантским жирным шрифтом? Так глупо ... # испарения # обострение

0 голосов
/ 08 мая 2020

Вот несколько предложений:

В вашем 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}")].

...