web-dev-qa-db-ja.com

OData $ expand、DTO、およびEntity Framework

データベースの最初のEFDataModelをセットアップした基本的なWebApiサービスのセットアップがあります。 WebApi、EF6、およびWebApiODataパッケージのナイトリービルドを実行しています。 (WebApi:5.1.0-alpha1、EF:6.1.0-alpha1、WebApi OData:5.1.0-alpha1)

データベースには、ProductとSupplierの2つのテーブルがあります。製品は1つのサプライヤーを持つことができます。サプライヤーは複数の製品を持つことができます。

また、2つのDTOクラスを作成しました。

public class Supplier
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public virtual IQueryable<Product> Products { get; set; }
}

public class Product
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }
}

WebApiConfigを次のように設定しました。

public static void Register(HttpConfiguration config)
{
    ODataConventionModelBuilder oDataModelBuilder = new ODataConventionModelBuilder();

    oDataModelBuilder.EntitySet<Product>("product");
    oDataModelBuilder.EntitySet<Supplier>("supplier");

    config.Routes.MapODataRoute(routeName: "oData",
        routePrefix: "odata",
        model: oDataModelBuilder.GetEdmModel());
}

2つのコントローラーを次のようにセットアップしました。

public class ProductController : ODataController
{
    [HttpGet]
    [Queryable]
    public IQueryable<Product> Get()
    {
        var context = new ExampleContext();

        var results = context.EF_Products
            .Select(x => new Product() { Id = x.ProductId, Name = x.ProductName});

        return results as IQueryable<Product>;
    }
}

public class SupplierController : ODataController
{
    [HttpGet]
    [Queryable]
    public IQueryable<Supplier> Get()
    {
        var context = new ExampleContext();

        var results = context.EF_Suppliers
            .Select(x => new Supplier() { Id = x.SupplierId, Name = x.SupplierName });

        return results as IQueryable<Supplier>;
    }
}

返されるメタデータは次のとおりです。ご覧のとおり、ナビゲーションプロパティは正しく設定されています。

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="1.0" xmlns:edmx="http://schemas.Microsoft.com/ado/2007/06/edmx">
 <edmx:DataServices m:DataServiceVersion="3.0" m:MaxDataServiceVersion="3.0" xmlns:m="http://schemas.Microsoft.com/ado/2007/08/dataservices/metadata">
  <Schema Namespace="StackOverflowExample.Models" xmlns="http://schemas.Microsoft.com/ado/2009/11/edm">
   <EntityType Name="Product">
    <Key>
     <PropertyRef Name="Id" />
    </Key>
    <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    <Property Name="Name" Type="Edm.String" />
   </EntityType>
   <EntityType Name="Supplier">
    <Key>
     <PropertyRef Name="Id" />
    </Key>
    <Property Name="Id" Type="Edm.Int32" Nullable="false" />
    <Property Name="Name" Type="Edm.String" />
    <NavigationProperty Name="Products" Relationship="StackOverflowExample.Models.StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartner" ToRole="Products" FromRole="ProductsPartner" />
   </EntityType>
   <Association Name="StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartner">
    <End Type="StackOverflowExample.Models.Product" Role="Products" Multiplicity="*" />
    <End Type="StackOverflowExample.Models.Supplier" Role="ProductsPartner" Multiplicity="0..1" />
   </Association>
  </Schema>
  <Schema Namespace="Default" xmlns="http://schemas.Microsoft.com/ado/2009/11/edm">
   <EntityContainer Name="Container" m:IsDefaultEntityContainer="true">
    <EntitySet Name="product" EntityType="StackOverflowExample.Models.Product" />
    <EntitySet Name="supplier" EntityType="StackOverflowExample.Models.Supplier" />
     <AssociationSet Name="StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartnerSet" Association="StackOverflowExample.Models.StackOverflowExample_Models_Supplier_Products_StackOverflowExample_Models_Product_ProductsPartner">
      <End Role="ProductsPartner" EntitySet="supplier" />
      <End Role="Products" EntitySet="product" />
     </AssociationSet>
    </EntityContainer>
   </Schema>
  </edmx:DataServices>
</edmx:Edmx>

したがって、odataクエリの通常の配列は正常に機能します。たとえば、/ odata/product?$ filter = Name + eq + 'Product1'および/ odata/suplier?$ select = Idはすべて正常に機能します。

問題は、$ expandを操作しようとしたときです。/odata/supportlier?$ expand = Productsを実行すると、もちろんエラーが発生します。

「指定されたタイプメンバー「Products」は、LINQ to Entitiesではサポートされていません。初期化子、エンティティメンバー、およびエンティティナビゲーションプロパティのみがサポートされています。」

更新:同じ質問が続くので、さらに情報を追加しています。はい、上記のメタデータ情報に見られるように、ナビゲーションプロパティは正しく設定されています。

これは、コントローラーに欠落しているメソッドとは関係ありません。 IODataRoutingConventionを実装するクラスを作成する場合、/ odata/suplier(1)/ productは「〜/ entityset/key/navigation」として適切に解析されます。

DTOを完全にバイパスし、EFで生成されたクラスを返すだけの場合、$ expandはそのままで機能します。

更新2: Productクラスを次のように変更した場合:

public class Product
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public virtual Supplier Supplier { get; set; }
}

次に、ProductControllerを次のように変更します。

public class ProductController : ODataController
{
    [HttpGet]
    [Queryable]
    public IQueryable<Product> Get()
    {
        var context = new ExampleContext();

        return context.EF_Products
            .Select(x => new Product() 
            { 
                Id = x.ProductId, 
                Name = x.ProductName, 
                Supplier = new Supplier() 
                {
                    Id = x.EF_Supplier.SupplierId, 
                    Name = x.EF_Supplier.SupplierName 
                } 
            });
    }
}

/ odata/productに電話すると、期待どおりの結果が得られます。応答で返されなかったSupplierフィールドを持つProductsの配列。生成されたSQLクエリは、Suppliersテーブルから結合して選択します。これは、次のクエリ結果がなければ意味があります。

/ odata/product?$ select = Idを呼び出すと、期待どおりの結果が返されます。ただし、$ selectは、suppliersテーブルに結合しないSQLクエリに変換されます。

/ odata/product?$ expand = Productは別のエラーで失敗します:

「DbIsNullExpressionの引数は、プリミティブ、列挙型、または参照型を参照する必要があります。」

製品コントローラーを次のように変更した場合:

public class ProductController : ODataController
{
    [HttpGet]
    [Queryable]
    public IQueryable<Product> Get()
    {
        var context = new ExampleContext();

        return context.EF_Products
            .Select(x => new Product() 
            { 
                Id = x.ProductId, 
                Name = x.ProductName, 
                Supplier = new Supplier() 
                {
                    Id = x.EF_Supplier.SupplierId, 
                    Name = x.EF_Supplier.SupplierName 
                } 
            })
            .ToList()
            .AsQueryable();
    }
}

/ odata/product、/ odata/product?$ select = Id、および/ odata/product?$ expand = Supplierは正しい結果を返しますが、明らかに.ToList()は目的を少し損ないます。

次のように、$ expandクエリが渡されたときにのみ.ToList()を呼び出すようにProductControllerを変更してみることができます。

    [HttpGet]
    public IQueryable<Product> Get(ODataQueryOptions queryOptions)
    {
        var context = new ExampleContext();

        if (queryOptions.SelectExpand == null)
        {
            var results = context.EF_Products
                .Select(x => new Product()
                {
                    Id = x.ProductId,
                    Name = x.ProductName,
                    Supplier = new Supplier()
                    {
                        Id = x.EF_Supplier.SupplierId,
                        Name = x.EF_Supplier.SupplierName
                    }
                });

            IQueryable returnValue = queryOptions.ApplyTo(results);

            return returnValue as IQueryable<Product>;
        }
        else
        {
            var results = context.EF_Products
                .Select(x => new Product()
                {
                    Id = x.ProductId,
                    Name = x.ProductName,
                    Supplier = new Supplier()
                    {
                        Id = x.EF_Supplier.SupplierId,
                        Name = x.EF_Supplier.SupplierName
                    }
                })
                .ToList()
                .AsQueryable();

            IQueryable returnValue = queryOptions.ApplyTo(results);

            return returnValue as IQueryable<Product>;
        }
    }
}

残念ながら、/ odata/product?$ select = Idまたは/ odata/product?$ expand = Supplierを呼び出すと、returnValueをIQueryableにキャストできないため、シリアル化エラーがスローされます。/odata/productを呼び出すと、キャストできます。

このあたりの回避策は何ですか?自分のDTOを使用することをスキップする必要がありますか、それとも$ expandと$ selectの独自の実装をロールすることができますか/すべきですか?

17
Schandlich

根本的な問題はEF6.1.0で修正されました。 https://entityframework.codeplex.com/workitem/826 を参照してください。

1
divega

WebAPIでエンティティ関係を設定していません。コントローラにメソッドを追加する必要があります。

次のURLも機能しないと思います。/odata/product(1)/Supplierこれは、関係が設定されていないためです。

次のメソッドをコントローラーに追加すると、問題が解決するはずです。

// GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
    var context = new ExampleContext();
    Product product = context.EF_Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return product.Supplier;
}

それはあなたの名前と一致したと思います。必要に応じて修正してください。詳細については、 http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/working-with-entity-relations を参照してください。モデルの構造は非常に似ています。

0
Rafi

ICollectionの代わりにIQueryableナビゲーションプロパティを使用する必要があります。これらのタイプは非常に異なります。それがあなたの問題かどうかはわかりませんが、修正する価値があります。

0
tne

$ expandコマンドは、コントローラーアクションのQueryable属性に0より大きいMaxExpansionDepth引数が追加されている場合にのみ機能します。

[Queryable(MaxExpansionDepth = 1)]
0
Scott Corbett