简体   繁体   中英

How can I POST with navigation properties using Microsoft.OData.Client and Web Api

I have two classes, Vehicle and OwnershipRecord and neither can be persisted to the database without the other. A Vehicle must have at least one OwnershipRecord and an OwnershipRecord must be associated with a Vehicle . It doesn't make sense otherwise.

Using Web Api and OData v4 Client Code Generator I haven't figured out a way to serialize both objects and POST them together. It seems I need to POST a Vehicle and then add an OwnershipRecord or post an OwnershipRecord and then add a Vehicle, which isn't possible.

DataServiceContext.AddObject provides the following:

The object is put into the tracking set of the DataServiceContext in the Added state. The DataServiceContext will try to insert the object by HTTP POST on the next call to SaveChanges. This method does not add objects related to the specified entity to the DataServiceContext. Each object must be added through a separate call to AddObject.

The method does not validate that the entity set specified is in the data service associated with the DataServiceContext or that the added object has the required properties needed to be added to the specified entity set..

Therefore, all navigation properties are null when passed. So when I add OwnershipRecord s to newVehicle and then call Container.AddToVehicles(newVehicle) , the POST method on the VehiclesController renders ModelState.IsValid as false saying Vehicle must have an owner! .

How can I use the client code to send the vehicle with it's navigation property and add the two together? I've tried to use AddLink and AddRelatedObject on the Container but it won't work with relative url's because the items don't exist yet.

public class Vehicle : IValidatableObject
{
    public const string MissingOwnerMessage = "Vehicle must have an owner!";

    public Vehicle()
    {
        OwnershipRecords = new HashSet<OwnershipRecord>();
    }

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

    public virtual ICollection<OwnershipRecord> OwnershipRecords { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (OwnershipRecords.Count == 0)
        {
            yield return new ValidationResult(MissingOwnerMessage);
        }
    }
}

public class OwnershipRecord : IValidatableObject
{
    public const string MissingOwnerMessage = "Owner is required when creating Ownership-Record!";
    public const string MissingVehicleMessage = "Vehicle is required when creating Ownership-Record!";

    public OwnershipRecord()
    {
        Owners = new HashSet<Entity>();
    }

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

    [Required]
    public int VehicleId { get; set; }

    [ForeignKey("VehicleId")]
    public virtual Vehicle Vehicle { get; set; }

    public virtual ICollection<Entity> Owners { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Owners.Count == 0)
        {
            yield return new ValidationResult(MissingOwnerMessage);
        }

        if (Vehicle == null)
        {
            yield return new ValidationResult(MissingVehicleMessage);
        }
    }
}

Here is my WebApiConfig.cs and my ODataControllers.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.EnableEnumPrefixFree(true);
        config.MapODataServiceRoute("nms", "nms", GetImplicitEdmModel(), new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));

        config.EnsureInitialized();
    }

    private static IEdmModel GetImplicitEdmModel()
    {
        ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Entity>("Entities");
        builder.EntitySet<Vehicle>("Vehicles");
        builder.EntitySet<OwnershipRecord>("OwnershipRecords");

        builder.Namespace = "LocationService";

        return builder.GetEdmModel();
    }

[ODataRoutePrefix("OwnershipRecords")]
public class OwnershipRecordsController : ODataController
{
    private NirvcModelV2 db = new NirvcModelV2();

    // GET: odata/OwnershipRecords
    [EnableQuery]
    public IQueryable<OwnershipRecord> GetOwnershipRecords()
    {
        return db.OwnershipRecords;
    }

    // GET: odata/OwnershipRecords(5)
    [EnableQuery]
    public SingleResult<OwnershipRecord> GetOwnershipRecord([FromODataUri] int key)
    {
        return SingleResult.Create(db.OwnershipRecords.Where(ownershipRecord => ownershipRecord.Id == key));
    }

    // POST: odata/OwnershipRecords
    public IHttpActionResult Post(OwnershipRecord ownershipRecord)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        db.OwnershipRecords.Add(ownershipRecord);

        return Created(ownershipRecord);
    }

    // GET: odata/OwnershipRecords(5)/Vehicle
    [EnableQuery]
    public SingleResult<Vehicle> GetVehicle([FromODataUri] int key)
    {
        return SingleResult.Create(db.OwnershipRecords.Where(m => m.Id == key).Select(m => m.Vehicle));
    }

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

}

[ODataRoutePrefix("Vehicles")]
public class VehiclesController : ODataController
{
    private NirvcModelV2 db = new NirvcModelV2();

    // GET: odata/Vehicles
    [EnableQuery(MaxExpansionDepth = 0)]
    public IQueryable<Vehicle> GetVehicles()
    {
        return db.Vehicles;
    }

    // GET: odata/Vehicles(5)
    [EnableQuery(MaxExpansionDepth = 0)]
    public SingleResult<Vehicle> GetVehicle([FromODataUri] int key)
    {
        return SingleResult.Create(db.Vehicles.Where(vehicle => vehicle.Id == key));
    }

    // POST: odata/Vehicles
    public IHttpActionResult Post(Vehicle vehicle)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        db.Vehicles.Add(vehicle);
        db.SaveChanges();

        return Created(vehicle);
    }

    // PATCH: odata/Vehicles(5)
    [AcceptVerbs("PATCH", "MERGE")]
    public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Vehicle> patch)
    {
        Vehicle vehicle = await db.Vehicles.FindAsync(key);
        if (vehicle == null)
        {
            return NotFound();
        }

        patch.Patch(vehicle);

        try
        {
            await db.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!VehicleExists(key))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return Updated(vehicle);
    }

    // GET: odata/Vehicles(5)/OwnershipRecords
    [EnableQuery]
    public IQueryable<OwnershipRecord> GetOwnershipRecords([FromODataUri] int key)
    {
        return db.Vehicles.Where(m => m.Id == key).SelectMany(m => m.OwnershipRecords);
    }

    [HttpPost]
    [ODataRoute("({key})/OwnershipRecords")]
    public async Task<IHttpActionResult> PostOwnershipRecord([FromODataUri] int key, OwnershipRecord ownershipRecord)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        db.OwnershipRecords.Add(ownershipRecord);
        try
        {
            await db.SaveChangesAsync();
        }
        catch (DBConcurrencyException)
        {
            throw;
        }

        return Created(ownershipRecord);
    }

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

    private bool VehicleExists(int key)
    {
        return db.Vehicles.Count(e => e.Id == key) > 0;
    }
}

==Update==

For the time being, I am serializing the payload with JsonConvert and sending it myself using WebClient . I have removed all ModelState logic from the server. It seems there is not current support for including Navigation Properties in client code. I may not fully understand intercepting the batch command, because it seems if I can use expand with GET, I should be able to use something similar to expand for POST

Use $batch request to send two post in the same changeset.

Refer to this sample project on how $batch is supported, https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataBatchSample

So I realized to send two parameters together I could use an Action in WebApiConfig.cs

var newVehicleFunction = builder.EntityType<Entity>().Action("AddVehicle").ReturnsFromEntitySet<OwnershipRecord>("OwnershipRecords");
        newVehicleFunction.Parameter<Vehicle>("Vehicle");
        newVehicleFunction.Parameter<OwnershipRecord>("OwnershipRecord");

The Controller

public IHttpActionResult PostVehicle([FromODataUri] int key, ODataActionParameters parameters)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest();
        }

        Vehicle vehicle = (Vehicle)parameters["Vehicle"];
        OwnershipRecord ownRecord = (OwnershipRecord)parameters["OwnershipRecord"];

        var owner = db.Entities.Find(key);
        if (owner == null)
        {
            return NotFound();
        }

        ownRecord.Vehicle = vehicle;
        owner.OwnershipRecords.Add(ownRecord);

        try
        {
            db.SaveChanges();
        }
        catch (DbUpdateException e)
        {
            throw;
        }

        return Created(ownRecord); 
    }

The Client

        if (newVehicle)
        {
            nmsContainer.Entities.ByKey(ownerId).AddVehicle(vehicle, ownRecord).GetValue();
        }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM