Share via


Entitätsbeziehungen in OData v4 mit ASP.NET-Web-API 2.2

von Mike Wasson

Die meisten Datasets definieren Beziehungen zwischen Entitäten: Kunden haben Aufträge; Bücher haben Autoren; Produkte haben Lieferanten. Mithilfe von OData können Clients über Entitätsbeziehungen navigieren. Bei einem Produkt können Sie den Lieferanten finden. Sie können auch Beziehungen erstellen oder entfernen. Beispielsweise können Sie den Lieferanten für ein Produkt festlegen.

In diesem Tutorial erfahren Sie, wie Sie diese Vorgänge in OData v4 mithilfe von ASP.NET-Web-API unterstützen. Das Tutorial baut auf dem Tutorial Erstellen eines OData v4-Endpunkts mit ASP.NET-Web-API 2 auf.

Im Tutorial verwendete Softwareversionen

  • Web-API 2.1
  • OData v4
  • Visual Studio 2017 (Visual Studio 2017 hier herunterladen)
  • Entity Framework 6
  • .NET 4.5

Tutorialversionen

Informationen zu OData Version 3 finden Sie unter Unterstützen von Entitätsbeziehungen in OData v3.

Hinzufügen einer Lieferantenentität

Hinweis

Das Tutorial baut auf dem Tutorial Erstellen eines OData v4-Endpunkts mit ASP.NET-Web-API 2 auf.

Zunächst benötigen wir eine zugehörige Entität. Fügen Sie im Ordner Models eine Klasse mit dem Namen Supplier hinzu.

using System.Collections.Generic;

namespace ProductService.Models
{
    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }
}

Fügen Sie der Product -Klasse eine Navigationseigenschaft hinzu:

using System.ComponentModel.DataAnnotations.Schema;

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }

        // New code:    
        [ForeignKey("Supplier")]
        public int? SupplierId { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}

Fügen Sie der ProductsContext Klasse ein neues DbSet hinzu, damit Entity Framework die Tabelle Supplier in die Datenbank einschließt.

public class ProductsContext : DbContext
{
    static ProductsContext()
    {
        Database.SetInitializer(new ProductInitializer());
    }

    public DbSet<Product> Products { get; set; }
    // New code:
    public DbSet<Supplier> Suppliers { get; set; }
}

Fügen Sie in WebApiConfig.cs dem Entitätsdatenmodell eine Entität "Suppliers" hinzu, die festgelegt ist:

public static void Register(HttpConfiguration config)
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Product>("Products");
    // New code:
    builder.EntitySet<Supplier>("Suppliers");
    config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}

Hinzufügen eines Lieferantencontrollers

Fügen Sie dem Ordner Controller eine SuppliersController Klasse hinzu.

using ProductService.Models;
using System.Linq;
using System.Web.OData;

namespace ProductService.Controllers
{
    public class SuppliersController : ODataController
    {
        ProductsContext db = new ProductsContext();

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

Ich zeige nicht, wie CRUD-Vorgänge für diesen Controller hinzugefügt werden. Die Schritte sind identisch mit denen für den Products-Controller (siehe Erstellen eines OData v4-Endpunkts).

Um den Lieferanten für ein Produkt zu erhalten, sendet der Client eine GET-Anforderung:

GET /Products(1)/Supplier

Fügen Sie der -Klasse die folgende Methode hinzu, um diese Anforderung zu ProductsController unterstützen:

public class ProductsController : ODataController
{
    // GET /Products(1)/Supplier
    [EnableQuery]
    public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
    {
        var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
        return SingleResult.Create(result);
    }
 
   // Other controller methods not shown.
}

Diese Methode verwendet eine Standardbenennungskonvention.

  • Methodenname: GetX, wobei X die Navigationseigenschaft ist.
  • Parametername: Schlüssel

Wenn Sie diese Namenskonvention befolgen, ordnet die Web-API die HTTP-Anforderung automatisch der Controllermethode zu.

Http-Beispielanforderung:

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

Beispiel für eine HTTP-Antwort:

HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}

Im vorherigen Beispiel hat ein Produkt einen Lieferanten. Eine Navigationseigenschaft kann auch eine Auflistung zurückgeben. Der folgende Code ruft die Produkte für einen Lieferanten ab:

public class SuppliersController : ODataController
{
    // GET /Suppliers(1)/Products
    [EnableQuery]
    public IQueryable<Product> GetProducts([FromODataUri] int key)
    {
        return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
    }

    // Other controller methods not shown.
}

In diesem Fall gibt die Methode ein IQueryable anstelle eines SingleResult<T> zurück.

Http-Beispielanforderung:

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

Beispiel für eine HTTP-Antwort:

HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
    {
      "Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
    }
  ]
}

Erstellen einer Beziehung zwischen Entitäten

OData unterstützt das Erstellen oder Entfernen von Beziehungen zwischen zwei vorhandenen Entitäten. In der OData v4-Terminologie ist die Beziehung ein "Verweis". (In OData v3 wurde die Beziehung als Link bezeichnet. Die Protokollunterschiede spielen in diesem Tutorial keine Rolle.)

Ein Verweis verfügt über einen eigenen URI mit dem Format /Entity/NavigationProperty/$ref. Hier ist beispielsweise der URI, der den Verweis zwischen einem Produkt und seinem Lieferanten adressiert:

http:/host/Products(1)/Supplier/$ref

Um eine Beziehung hinzuzufügen, sendet der Client eine POST- oder PUT-Anforderung an diese Adresse.

  • PUT, wenn die Navigationseigenschaft eine einzelne Entität ist, z Product.Supplier. B. .
  • POST, wenn die Navigationseigenschaft eine Auflistung ist, z Supplier.Products. B. .

Der Text der Anforderung enthält den URI der anderen Entität in der Beziehung. Hier folgt eine Beispielanforderung:

PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue

{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}

In diesem Beispiel sendet der Client eine PUT-Anforderung an , wobei es sich um /Products(6)/Supplier/$refden $ref URI für den Supplier des Produkts mit der ID = 6 handelt. Wenn die Anforderung erfolgreich ist, sendet der Server die Antwort 204 (Kein Inhalt):

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

Hier ist die Controllermethode zum Hinzufügen einer Beziehung zu :Product

public class ProductsController : ODataController
{
    [AcceptVerbs("POST", "PUT")]
    public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }
        switch (navigationProperty)
        {
            case "Supplier":
                // Note: The code for GetKeyFromUri is shown later in this topic.
                var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
                var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
                if (supplier == null)
                {
                    return NotFound();
                }

                product.Supplier = supplier;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();
        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

Der parameter navigationProperty gibt an, welche Beziehung festgelegt werden soll. (Wenn mehr als eine Navigationseigenschaft für die Entität vorhanden ist, können Sie weitere case Anweisungen hinzufügen.)

Der Linkparameter enthält den URI des Lieferanten. Die Web-API analysiert den Anforderungstext automatisch, um den Wert für diesen Parameter abzurufen.

Um den Lieferanten nachzuschlagen, benötigen wir die ID (oder den Schlüssel), die Teil des Linkparameters ist. Verwenden Sie dazu die folgende Hilfsmethode:

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;

namespace ProductService
{
    public static class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName, 
                request.ODataProperties().PathHandler, new List<ODataPathSegment>());
            var odataPath = request.ODataProperties().PathHandler.Parse(
                request.ODataProperties().Model, 
                serviceRoot, uri.LocalPath);

            var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
            return (TKey)value;
        }

    }
}

Im Grunde verwendet diese Methode die OData-Bibliothek, um den URI-Pfad in Segmente aufzuteilen, das Segment zu finden, das den Schlüssel enthält, und den Schlüssel in den richtigen Typ zu konvertieren.

Löschen einer Beziehung zwischen Entitäten

Um eine Beziehung zu löschen, sendet der Client eine HTTP DELETE-Anforderung an den $ref URI:

DELETE http://host/Products(1)/Supplier/$ref

Hier ist die Controllermethode zum Löschen der Beziehung zwischen einem Produkt und einem Lieferanten:

public class ProductsController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = db.Products.SingleOrDefault(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }

        switch (navigationProperty)
        {
            case "Supplier":
                product.Supplier = null;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }        

    // Other controller methods not shown.
}

In diesem Fall Product.Supplier ist das "1"-Ende einer 1:n-Beziehung, sodass Sie die Beziehung entfernen können, indem Sie auf nullfestlegenProduct.Supplier.

Am "n"-Ende einer Beziehung muss der Client angeben, welche verknüpfte Entität entfernt werden soll. Dazu sendet der Client den URI der verknüpften Entität in der Abfragezeichenfolge der Anforderung. So entfernen Sie beispielsweise "Product 1" aus "Supplier 1":

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

Um dies in der Web-API zu unterstützen, müssen wir einen zusätzlichen Parameter in die DeleteRef -Methode einschließen. Hier ist die Controllermethode, um ein Produkt aus der Supplier.Products Beziehung zu entfernen.

public class SuppliersController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        [FromODataUri] string relatedKey, string navigationProperty)
    {
        var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
        if (supplier == null)
        {
            return StatusCode(HttpStatusCode.NotFound);
        }

        switch (navigationProperty)
        {
            case "Products":
                var productId = Convert.ToInt32(relatedKey);
                var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);

                if (product == null)
                {
                    return NotFound();
                }
                product.Supplier = null;
                break;
            default:
                return StatusCode(HttpStatusCode.NotImplemented);

        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

Der Schlüsselparameter ist der Schlüssel für den Lieferanten, und der relatedKey-Parameter ist der Schlüssel für das Produkt, das aus der Products Beziehung entfernt werden soll. Beachten Sie, dass die Web-API den Schlüssel automatisch aus der Abfragezeichenfolge abruft.