OData и WebAPI: свойство навигации отсутствует на модели

Я пытаюсь собрать простой игрушечный проект, используя Entity Framework, WebAPI, OData и клиент Angular. Все работает нормально, за исключением того, что свойство навигации, которое я наложил на одну из моих моделей, похоже, не работает. Когда я вызываю свой API с помощью $expand, возвращаемые объекты не имеют своих навигационных свойств.

Мои классы - Собака и Владелец, и выглядят так:

    public class Dog
{
    // Properties
    [Key]
    public Guid Id { get; set; }
    public String Name { get; set; }
    [Required]
    public DogBreed Breed { get; set; }
    public int Age { get; set; }
    public int Weight { get; set; }


    // Foreign Keys
    [ForeignKey("Owner")]
    public Guid OwnerId { get; set; }

    // Navigation
    public virtual Owner Owner { get; set; }
}

    public class Owner
{
    // Properties
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string Phone { get; set; }
    public DateTime SignupDate { get; set; }

    // Navigation
    public virtual ICollection<Dog> Dogs { get; set; } 
}

У меня также есть контроллер Dog для обработки запросов:

public class DogsController : ODataController
{
    DogHotelAPIContext db = new DogHotelAPIContext();
    #region Public methods 

    [Queryable(AllowedQueryOptions = System.Web.Http.OData.Query.AllowedQueryOptions.All)]
    public IQueryable<Dog> Get()
    {
        var result =  db.Dogs.AsQueryable();
        return result;
    }

    [Queryable(AllowedQueryOptions = System.Web.Http.OData.Query.AllowedQueryOptions.All)]
    public SingleResult<Dog> Get([FromODataUri] Guid key)
    {
        IQueryable<Dog> result = db.Dogs.Where(d => d.Id == key).AsQueryable().Include("Owner");
        return SingleResult.Create(result);
    }

    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }

}

Я засеял базу данных небольшим количеством выборочных данных. Все записи собаки имеют OwnerId, который соответствует идентификатору владельца в таблице Owners.

Запрос для списка собак, использующих это, отлично работает:

http://localhost:49382/odata/Dogs

Я получаю список объектов Dog без свойства навигации Owner.

Запрос для собак с их владельцами с использованием OData $expand НЕ работает:

http://localhost:49382/odata/Dogs?$expand=Owner

Мой ответ - 200 со всеми сущностями Dog, но ни один из них не обладает свойством Owner в JSON.

Если я запрошу свои метаданные, я обнаружил, что OData, похоже, знает об этом:

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
  <edmx:DataServices>
    <Schema Namespace="DogHotelAPI.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityType Name="Dog">
        <Key>
          <PropertyRef Name="id" />
        </Key>
        <Property Name="id" Type="Edm.Guid" Nullable="false" />
        <Property Name="name" Type="Edm.String" />
        <Property Name="breed" Type="DogHotelAPI.Models.Enums.DogBreed" Nullable="false" />
        <Property Name="age" Type="Edm.Int32" Nullable="false" />
        <Property Name="weight" Type="Edm.Int32" Nullable="false" />
        <Property Name="ownerId" Type="Edm.Guid" />
        <NavigationProperty Name="owner" Type="DogHotelAPI.Models.Owner">
          <ReferentialConstraint Property="ownerId" ReferencedProperty="id" />
        </NavigationProperty>
      </EntityType>
      <EntityType Name="Owner">
        <Key>
          <PropertyRef Name="id" />
        </Key>
        <Property Name="id" Type="Edm.Guid" Nullable="false" />
        <Property Name="name" Type="Edm.String" />
        <Property Name="address" Type="Edm.String" />
        <Property Name="phone" Type="Edm.String" />
        <Property Name="signupDate" Type="Edm.DateTimeOffset" Nullable="false" />
        <NavigationProperty Name="dogs" Type="Collection(DogHotelAPI.Models.Dog)" />
      </EntityType>
    </Schema>
    <Schema Namespace="DogHotelAPI.Models.Enums" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EnumType Name="DogBreed">
        <Member Name="AfghanHound" Value="0" />
        <Member Name="AmericanStaffordshireTerrier" Value="1" />
        <Member Name="Boxer" Value="2" />
        <Member Name="Chihuahua" Value="3" />
        <Member Name="Dachsund" Value="4" />
        <Member Name="GermanShepherd" Value="5" />
        <Member Name="GoldenRetriever" Value="6" />
        <Member Name="Greyhound" Value="7" />
        <Member Name="ItalianGreyhound" Value="8" />
        <Member Name="Labrador" Value="9" />
        <Member Name="Pomeranian" Value="10" />
        <Member Name="Poodle" Value="11" />
        <Member Name="ToyPoodle" Value="12" />
        <Member Name="ShihTzu" Value="13" />
        <Member Name="YorkshireTerrier" Value="14" />
      </EnumType>
    </Schema>
    <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityContainer Name="Container">
        <EntitySet Name="Dogs" EntityType="DogHotelAPI.Models.Dog">
          <NavigationPropertyBinding Path="owner" Target="Owners" />
        </EntitySet>
        <EntitySet Name="Owners" EntityType="DogHotelAPI.Models.Owner">
          <NavigationPropertyBinding Path="dogs" Target="Dogs" />
        </EntitySet>
      </EntityContainer>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>

Что я могу потерять, что мешает моей навигации preoprty вернуться к остальной части моей модели?

ИЗМЕНИТЬ

Чтобы дополнительно изолировать проблему, которую я пробовал, включая владельцев на С# на стороне сервера. Я добавил эту строку в метод Get моего контроллера Dog:

var test = db.Dogs.Include("Owner").ToList();

С этим я могу отлаживать и видеть, что связанные владельцы включены. У каждой собаки есть владелец, связанный с ним в этом списке.

Использование .Include( "Владелец" ) в том, что действительно возвращается, не устраняет проблему - свойства все еще не доходят до клиента.

Это означает, что свойства навигации работают, но не отправляются обратно клиенту. Кажется, что это рана указывает на проблему с OData или WebAPI, я бы предположил, но я не уверен, что.

Кроме того, я добавил следующие строки в Application_Start в файле Global.asax, чтобы обрабатывать свойства круговой навигации:

            var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
        json.SerializerSettings.PreserveReferencesHandling =
            Newtonsoft.Json.PreserveReferencesHandling.All;

Я сделал это, если круговая ссылка была каким-то образом виновником, но это ничего не меняет.

UPDATE

Я заметил, что вызов

http://localhost:49382/odata/Dogs(abfd26a5-14d8-4b14-adbe-0a0c0ef392a7)/owner

работает. Это возвращает владельца, связанного с этой собакой. Это еще раз иллюстрирует, что мои свойства навигации настроены правильно, они просто не включаются в ответы на вызовы с использованием $expand.

ОБНОВЛЕНИЕ 2

Вот метод регистрации моего файла WebApiConfig:

        public static void Register(HttpConfiguration config)
    {
        //config.Routes.MapHttpRoute(
        //    name: "DefaultApi",
        //    routeTemplate: "api/{controller}/{id}",
        //    defaults: new { id = RouteParameter.Optional }
        //);

        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EnableLowerCamelCase();
        builder.EntitySet<Dog>("Dogs");
        builder.EntitySet<Owner>("Owners");

        config.EnableQuerySupport();

        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: "odata",
            model: builder.GetEdmModel());


        // Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
        // To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
        // For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
        //config.EnableQuerySupport();

        // To disable tracing in your application, please comment out or remove the following line of code
        // For more information, refer to: http://www.asp.net/web-api
        config.EnableSystemDiagnosticsTracing();
    }

Ответ 1

Я нашел решение своей проблемы, что в конечном итоге было вызвано тремя вещами:

1.) Я использовал атрибут [Queryable] в моих методах контроллера, которые устарели. Мне нужно было использовать новые атрибуты [EnableQuery].

2.) В моем файле WebApiConfig.cs я включил запрос, используя конфигурацию по умолчанию config.EnableQuerySupport(). Это устарело и было удалено.

3). Мой требуемый разворот был в форме $expand = Owner, но должен был быть в форме $expand = owner, так как я включаю нижний случай верблюда в свой ODataConventionModelBuilder. Большое спасибо Марку Беннетту, чей ответ указал на это!

После внесения всех этих изменений связанные объекты Owner возвращаются с объектами Dog.

Ответ 2

Это потому, что вы используете

builder.EnableLowerCamelCase();

в настройке ODataConventionModelBuilder.

Не распознает "Владелец" в ваших вариантах запроса $expand, потому что этот путь действительно не существует в модели OData, поскольку он чувствителен к регистру.

Если вы попробуете запросить это /Dogs? $expand = owner, я уверен, что это сработает, и вы получите как Собаки, так и их владельцев, возвращенные в ответ JSON.

Ответ 3

У меня была очень похожая проблема, которая, как мне кажется, вызвана одной и той же проблемой.

Я пытался создать некоторые связанные функции OData, которые возвратили бы целые графики сущностей, чтобы сделать работу с клиентами немного легче в определенных ситуациях, вместо того, чтобы указывать $expand clauses для всего.

Я указал Include в выражениях linq-интерфейсов сущности и проверил, что возвращаемые данные действительно были полностью заполнены при отладке, но, как и вы, я только когда-либо получал объект верхнего уровня, возвращенный ни с чем другим.

Проблема заключается в сериализации, используемой для OData​​p >

Что вы обнаружите, так это то, что если вы удалите первичный ключ из вашего класса Owner, чтобы он по существу стал сложным объектом, он будет включен в результат последовательного JSON, основанный на OData, иначе он не будет, если только запрос OData uri не содержит предложение $expand, которое включает его.

Я попытался найти способ вставить предложения $expand в код, чтобы он работал, но, к сожалению, был пустым.

Надеюсь, что это поможет

Ответ 4

Посмотрите, может ли ниже работать для вас. Я тестирую в OData v4, поэтому вам может потребоваться настроить [EnableQuery] на [Queryable]. Контекст db должен возвращать результат IQueryable, так что .AsQueryable() может не понадобиться.

// GET: odata/Dogs
[EnableQuery]
public IQueryable<Dog> Get()
{
    return db.Dogs;
}

// GET: odata/Dogs(5)/Owner
[EnableQuery]
public IQueryable<Owner> GetOwner([FromODataUri] int key)
{
    return db.Dogs.Where(m => m.ID == key).SelectMany(m => m.Owner);
}

Я сравниваю то, что у вас есть, с небольшим проектом, над которым я сейчас работаю. Вероятно, это не так, но моя ассоциация FK настроена немного по-другому, и, возможно, какой-то случайностью является вопрос о заказе FK. Мои внешние ключи, кажется, украшены поверх свойств навигатора.

public int PublicImageID { get; set; }
[ForeignKey("PublicImageID")]
public PublicImage PublicImage { get; set; }

// Foreign Keys    
public Guid OwnerId { get; set; }
[ForeignKey("OwnerId")]
public virtual Owner Owner { get; set; }