New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with resolving OData Delete calls with ETags on property type of byte array #491

Closed
mbryantII opened this Issue Aug 16, 2016 · 6 comments

Comments

Projects
None yet
3 participants
@mbryantII

mbryantII commented Aug 16, 2016

I am unable to successfully delete an entity using If-Match header with the ETag provided. I always retrieve an 412 no matter the format I try. I am preforming a spike and the only developer hitting the end point, thus no concurrency issues. This may not be an Actual Bug, but I can not seem to find documentation to help in my use case.

Assemblies affected

  <package id="Microsoft.Restier" version="0.6.0" targetFramework="net452" />
  <package id="Microsoft.Restier.Core" version="0.6.0" targetFramework="net452" />
  <package id="Microsoft.Restier.Providers.EntityFramework" version="0.6.0" targetFramework="net452" />
  <package id="Microsoft.Restier.Publishers.OData" version="0.6.0" targetFramework="net452" />

Reproduce steps

On a simple EF model done from "Code First from existing database" Create an Object of

Expected result

Retrieve a 204 No Data, and the entity is removed from the target database.

Actual result

Must provide If-Match Header or a 412.

Additional details

Here is an example of the get request to a specific entity, and then the attempted delete call with the appropriate etag.

I HAVE MASKED THE URL ENDPOINTS due the public view, willing to work one on one to help resolve by unmasking

Request

GET http://xxxxxxxxxx-api-semservices.azurewebsites.net/api/SEM/UserPreferences(26) HTTP/1.1
cache-control: no-cache
Postman-Token: 9efd3d12-c63e-4b9f-86ef-481efb7a34b6
User-Agent: PostmanRuntime/2.4.1
Accept: */*
Host: xxxxxxxxxx-api-semservices.azurewebsites.net
cookie: ARRAffinity=790352ef715d16f81dd6bc3ff119435eeb849431c185591cb7908964661c1506
accept-encoding: gzip, deflate
Connection: keep-alive

RESPONSE

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 447
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Expires: -1
Server: Microsoft-IIS/8.0
OData-Version: 4.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=790352ef715d16f81dd6bc3ff119435eeb849431c185591cb7908964661c1506;Path=/;Domain=xxxxxxxxxx-api-semservices.azurewebsites.net
Date: Tue, 16 Aug 2016 18:03:32 GMT

{
  "@odata.context":"http://xxxxxxxxxx-api-semservices.azurewebsites.net/api/SEM/$metadata#UserPreferences/$entity","@odata.etag":"W/\"YmluYXJ5J0FBQUFBQWhWR2NvPSc=\"","preferenceId":26,"createDateTime":"2016-08-16T17:46:48.643Z","createdBy":"test_user4","updateDateTime":null,"updatedBy":null,"dataSourceId":null,"dataSourceSystemKey":"0b3476b4-a7cf-4803-b88d-50bd9659e820","rowVersion":"AAAAAAhVGco=","userId":4,"key":"timeZone","value":"-7"
}

DELETE REQUEST

DELETE http://xxxxxxxxxx-api-semservices.azurewebsites.net/api/SEM/UserPreferences(26) HTTP/1.1
cache-control: no-cache
Postman-Token: eda5eda0-6642-473b-8d66-efab4a292261
If-Match: W/"YmluYXJ5J0FBQUFBQWhWR2NvPSc="
User-Agent: PostmanRuntime/2.4.1
Accept: */*
Host: xxxxxxxxxx-api-semservices.azurewebsites.net
cookie: ARRAffinity=790352ef715d16f81dd6bc3ff119435eeb849431c185591cb7908964661c1506
accept-encoding: gzip, deflate
content-length: 0
Connection: keep-alive

DELETE RESPONSE

HTTP/1.1 412 Precondition Failed
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 175
Content-Type: application/json; odata.metadata=minimal
Expires: -1
Server: Microsoft-IIS/8.0
OData-Version: 4.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Tue, 16 Aug 2016 18:06:33 GMT

{
  "error":{
    "code":"","message":"The precondition check for request Remove on resource Stryker.LINK.BDC.SmartEquipmentManager.Models.UserPreference is failed."
  }
}
@mirsking

This comment has been minimized.

Contributor

mirsking commented Aug 17, 2016

Can you post the defination of entity UserPreferences? @mbryantII I'd like to know where you use ConcurrencyCheck.
For example

   public class Flight
    {
        [ConcurrencyCheck]
        public int FlightId { get; set; }

        [ConcurrencyCheck]
        public string ConfirmationCode { get; set; }

        [ConcurrencyCheck]
        public DateTimeOffset StartsAt { get; set; }

        [ConcurrencyCheck]
        public DateTimeOffset EndsAt { get; set; }
    }
@mbryantII

This comment has been minimized.

mbryantII commented Aug 17, 2016

I am currently not using that attribute on any items.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;

namespace Stryker.LINK.BDC.SmartEquipmentManager.Models
{
    /// <summary>
    /// User Preference Entity model
    /// </summary>
    [Table("user.UserPreference")]
    public partial class UserPreference
    {
        /// <summary>
        /// Gets or sets the preference identifier.
        /// </summary>
        /// <value>
        /// The preference identifier.
        /// </value>
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int preferenceId { get; set; }

        /// <summary>
        /// Gets or sets the create date time.
        /// </summary>
        /// <value>
        /// The create date time.
        /// </value>
        public DateTime? createDateTime { get; set; }

        /// <summary>
        /// Gets or sets the created by.
        /// </summary>
        /// <value>
        /// The created by.
        /// </value>
        [Required]
        [StringLength(64)]
        public string createdBy { get; set; }

        /// <summary>
        /// Gets or sets the update date time.
        /// </summary>
        /// <value>
        /// The update date time.
        /// </value>
        public DateTime? updateDateTime { get; set; }

        /// <summary>
        /// Gets or sets the updated by.
        /// </summary>
        /// <value>
        /// The updated by.
        /// </value>
        [StringLength(128)]
        public string updatedBy { get; set; }

        /// <summary>
        /// Gets or sets the data source identifier.
        /// </summary>
        /// <value>
        /// The data source identifier.
        /// </value>
        public int? dataSourceId { get; set; }

        /// <summary>
        /// Gets or sets the data source system key.
        /// </summary>
        /// <value>
        /// The data source system key.
        /// </value>
        [StringLength(64)]
        public string dataSourceSystemKey { get; set; }

        /// <summary>
        /// Gets or sets the row version.
        /// </summary>
        /// <value>
        /// The row version.
        /// </value>
        [Column(TypeName = "timestamp")]
        [MaxLength(8)]
        [Timestamp]
        public byte[] rowVersion { get; set; }

        /// <summary>
        /// Gets or sets the user identifier.
        /// </summary>
        /// <value>
        /// The user identifier.
        /// </value>
        public int? userId { get; set; }

        /// <summary>
        /// Gets or sets the key.
        /// </summary>
        /// <value>
        /// The key.
        /// </value>
        [Required]
        [StringLength(64)]
        public string key { get; set; }

        /// <summary>
        /// Gets or sets the value.
        /// </summary>
        /// <value>
        /// The value.
        /// </value>
        [Required]
        [StringLength(1000)]
        public string value { get; set; }
    }
}

@mirsking

This comment has been minimized.

Contributor

mirsking commented Aug 17, 2016

Hi mbryantII,
I'd like to explain you that how ETag works in Restier firstly.
For a entity, we calculate the ETag only with properties with [ConcurrencyCheck] and [TimeStamp] attributes, NOT all of the properties, i.e. only the properties with those attributes need concurrency checking.

For your scenario, you put a TimeStamp attribute on public byte[] rowVersion { get; set; }. I'd like to ask whether you really want to use this property for concurrency checking. If not, my advice is that remove this attribute and add ConcurrencyCheck on other properties which need concurrency checking. Then you will find the ETag works like a charm.

If you really want to use rowVersion for concurrency check, yes, there is a known issue on byte[] concurrency check. And we will fix it in the next version of release.

@mbryantII

This comment has been minimized.

mbryantII commented Aug 17, 2016

Thanks for your reply, in reality that is a database generated column that the user does not place in a post call.

here is what that call looks like.

POST http://xxxxxxxxxxxxx-api-semservices.azurewebsites.net/api/SEM/UserPreferences HTTP/1.1
cache-control: no-cache
Postman-Token: 1acf03a5-b1bd-4713-900c-becd5013ec96
Content-Type: application/json
User-Agent: PostmanRuntime/2.4.1
Accept: */*
Host: xxxxxxxxxxxxx-api-semservices.azurewebsites.net
cookie: ARRAffinity=790352ef715d16f81dd6bc3ff119435eeb849431c185591cb7908964661c1506
accept-encoding: gzip, deflate
content-length: 328
Connection: keep-alive

{
      "createDateTime" : "2016-08-17T13:05:23.626Z",
      "createdBy" : "test_user5",
      "updateDateTime" : null,
      "updatedBy" : null,
      "dataSourceId" : null,
      "dataSourceSystemKey" : "2f8e143b-2be8-4ad9-b5ff-84e9e7f4e741",
      "userId" : 5,
      "key" : "dateRangeWeeks",
      "value" : "4"
}

and the response

HTTP/1.1 201 Created
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 452
Content-Type: application/json; odata.metadata=minimal
Expires: -1
Location: http://xxxxxxxxxxxxx-api-semservices.azurewebsites.net/api/SEM/UserPreferences(28)
Server: Microsoft-IIS/8.0
OData-Version: 4.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Wed, 17 Aug 2016 13:05:19 GMT

{
  "@odata.context":"http://xxxxxxxxxxxxx-api-semservices.azurewebsites.net/api/SEM/$metadata#UserPreferences/$entity","@odata.etag":"W/\"YmluYXJ5J0FBQUFBQWhWR2ZNPSc=\"","preferenceId":28,"createDateTime":"2016-08-17T13:05:23.626Z","createdBy":"test_user5","updateDateTime":null,"updatedBy":null,"dataSourceId":null,"dataSourceSystemKey":"2f8e143b-2be8-4ad9-b5ff-84e9e7f4e741","rowVersion":"AAAAAAhVGfM=","userId":5,"key":"dateRangeWeeks","value":"4"
}

i just don't want to interfere from a model perspective by removing that attribute, and placing concurrency check on it if its going to cause issues on the back end with EF. Some general advice here would be great!

mirsking added a commit to mirsking/RESTier that referenced this issue Aug 18, 2016

mirsking added a commit to mirsking/RESTier that referenced this issue Aug 18, 2016

@mirsking mirsking referenced this issue Aug 18, 2016

Merged

Fix issue #491 #495

@chinadragon0515 chinadragon0515 changed the title from Issue with resolving OData Delete calls and ETags to Issue with resolving OData Delete calls with ETags on property type of byte array Aug 19, 2016

@chinadragon0515

This comment has been minimized.

Contributor

chinadragon0515 commented Aug 19, 2016

@mbryantII If etag support is not needed for UserPreferences entity set in your case, then there is one workaround to remove the concurrency properties now, it is a little ugly as it need reflection call.

                var flights = model.EntityContainer.FindEntitySet("UserPreferences");
                var annotations = model.FindDeclaredVocabularyAnnotations(flights);
                foreach (var annotation in annotations)
                {
                    if(annotation.Term.Name == "OptimisticConcurrency" )
                    {
                        var valueField = typeof(EdmVocabularyAnnotation).GetField("value", BindingFlags.Instance | BindingFlags.NonPublic);
                        if (valueField != null)
                        {
                            valueField.SetValue(annotation, new EdmCollectionExpression());
                        }
                    }
                }

Let me know if this does not work for you.

As to where to put these code, you will need to define your own APi class, and create a model builder, you can refer to class https://github.com/OData/RESTier/blob/master/test/ODataEndToEnd/Microsoft.OData.Service.Sample.Trippin/Api/TrippinApi.cs, create a class like TrippinModelCustomizer , and then register in ConfigureApi method.

@chinadragon0515 chinadragon0515 added the bug label Aug 19, 2016

@chinadragon0515 chinadragon0515 added this to the 1.0 milestone Aug 19, 2016

@chinadragon0515

This comment has been minimized.

Contributor

chinadragon0515 commented Aug 24, 2016

I close the issue now, if you have more questions or comments, let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment