OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
454 stars 160 forks source link

Delta<TModel> returns null #942

Open AndriiLesiuk opened 1 year ago

AndriiLesiuk commented 1 year ago

Hi Microsoft.AspNetCore.OData 8.2.0 .NET 7.0

I'm trying to configure $delta functionality so that I can handle queries like this: PATCH: https://localhost:44394/odata/Dictionary Body:

{
    "value": [
        {
            "Row_Id": 228,
            "Display_name": "Test Record 1",
            "Source_code": "Test Record 2"
        },
        {
            "Row_Id": 229,
            "Display_name": "Test Record 3",
            "Source_code": "Test Record 4"
        }
    ]
}

My controller:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.OData.Deltas;
    using Microsoft.AspNetCore.OData.Formatter;
    using Microsoft.AspNetCore.OData.Query;
    using Microsoft.AspNetCore.OData.Routing.Attributes;
    using Microsoft.AspNetCore.OData.Routing.Controllers;
    using Newtonsoft.Json;
    using System.Text.Json.Nodes;

    public class DictionaryController : ODataController
    {
        private readonly IDictionaryService _service;

        [HttpPatch]
        public async Task<IActionResult> Patch([FromBody] Delta<Dictionary> delta)
        {
            return Ok();
        }    ...

Program class OData configuration part:

var builder = WebApplication.CreateBuilder(args);
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Dictionary>("Dictionary").HasCountRestrictions().IsCountable(true);

var batchHandler = new DefaultODataBatchHandler();
batchHandler.MessageQuotas.MaxNestingDepth = 2;
batchHandler.MessageQuotas.MaxOperationsPerChangeset = 10;
batchHandler.MessageQuotas.MaxReceivedMessageSize = 100;

builder.Services.AddControllers().AddOData(opt =>
{
    opt.EnableQueryFeatures(3000);
    opt.AddRouteComponents(routePrefix: "odata", model: modelBuilder.GetEdmModel(), services => services.AddSingleton<ISearchBinder, SearchBinder>());
    opt.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
    opt.EnableAttributeRouting = true;
});

Model:

[Table("dictionary")]
    public class Dictionary
    {
        [Key]
        [Column("row_id")]
        public int? Row_Id { get; set; }

        [Column("document_id")]
        public string? Document_id { get; set; }

        [Column("name")]
        public string? Name { get; set; }

        [Column("display_name")]
        public string? Display_name { get; set; }

        [Column("unique_name")]
        public string? Unique_name { get; set; }

        [Column("source_code")]
        public string? Source_code { get; set; }

        [Column("document_timestamp")]
        public DateTime? Document_timestamp { get; set; }

        [Column("capacity")]
        public ICollection<Capacity>? Capacity { get; set; }
    }

When I send the request, the delta object in the controller returns null to me. But when I comment out this line of code in the Program file:

//modelBuilder.EntitySet<Dictionary>("Dictionary").HasCountRestrictions().IsCountable(true);

delta starts to contain the value I sent. Please tell me what I'm doing wrong? And are there any examples of how to correctly implement the $delta functionality as described in the official OData documentation? Any advice would be greatly appreciated.

xuzhg commented 1 year ago

@AndriiLesiuk Can it work if you change 'Patch([FromBody] Delta< Dictionary > delta)' to 'Patch([FromBody] DeltaSet< Dictionary > delta)'?

Patch with Delta< T > should contain the key to update a certain entity, and the payload is an updated entity value and the request Uri should contain the key segment.

AndriiLesiuk commented 1 year ago

@xuzhg Thanks! It really worked. Are there any informative articles on how to implement this functionality according to the OData documentation? Do I need to do any additional configuration when working with delta to get this kind of response, or is it already out of the box?

{

  "@context":"http://host/service/$metadata#Customers/$delta",

  "@count":5,

  "value":

  [

    {

      "@id":"Customers('BOTTM')",

      "ContactName":"Susan Halvenstern"

    },

    {

      "@context":"#Customers/$deletedLink",

      "source":"Customers('ALFKI')",

      "relationship":"Orders",

      "target":"Orders(10643)"

    },

    {

      "@context":"#Customers/$link",

      "source":"Customers('BOTTM')",

      "relationship":"Orders",

      "target":"Orders(10645)"

    },

    {

      "@context":"#Orders/$entity",

      "@id":"Orders(10643)",

      "ShippingAddress":{

        "Street":"23 Tsawassen Blvd.",

        "City":"Tsawassen",

        "Region":"BC",

        "PostalCode":"T2F 8M4"

      },

    },

    {

      "@context":"#Customers/$deletedEntity",

      "@removed": {

        "reason":"deleted"

      },

      "@id":"Customers('ANTON')"

    }

  ],

  "@deltaLink": "Customers?$expand=Orders&$deltatoken=8015"

}
xuzhg commented 1 year ago

@AndriiLesiuk It seems you are looking for this fix: https://github.com/OData/AspNetCoreOData/pull/915.

If yes, It's included in 8.2.0.

AndriiLesiuk commented 1 year ago

I looked at the information you shared for me above. Therefore, I will try to describe in more detail. 1. I try to repeat the logic written in your unit tests, but for some reason I get an error: link

"A resource of type 'Edm.Untyped' was found in a resource set that otherwise has entries of type 'PostgresCRUDRelational.Controllers.Employee'. In OData, all entries in a resource set must have a common base type."

image

image

Request PATCH url: https://localhost:44394/odata/Employees Request body:

{
    "@context": "http://host/service/$metadata#Employees/$delta",
    "value": [
        {
            "ID": 1,
            "Name": "Employee1",
            "Friends@delta": [
                {
                    "Id": 1,
                    "Name": "Friend1",
                    "Orders@delta": [
                        {
                            "Id": 1,
                            "Price": 10
                        },
                        {
                            "Id": 2,
                            "Price": 20
                        }
                    ]
                },
                {
                    "Id": 2,
                    "Name": "Friend2"
                }
            ]
        },
        {
            "ID": 2,
            "Name": "Employee2",
            "Friends@delta": [
                {
                    "Id": 3,
                    "Name": "Friend3",
                    "Orders@delta": [
                        {
                            "Id": 3,
                            "Price": 30
                        },
                        {
                            "Id": 4,
                            "Price": 40
                        }
                    ]
                },
                {
                    "Id": 4,
                    "Name": "Friend4"
                }
            ]
        }
    ]
}

Class:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using System.ComponentModel.DataAnnotations;

namespace PostgresCRUDRelational.Controllers
{
    public class Employee
    {
        [Key]
        public int ID { get; set; }
        public String Name { get; set; }
        public List<Friend> Friends { get; set; }
    }

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

        public string Name { get; set; }

        public List<Order> Orders { get; set; }
    }

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

        public int Price { get; set; }
    }

    public class EmployeesController : ODataController
    {
        public EmployeesController()
        {
            if (null == Employees)
            {
                InitEmployees();
            }
        }

        /// <summary>
        /// static so that the data is shared among requests.
        /// </summary>
        public static IList<Employee> Employees = null;

        private List<Friend> Friends = null;

        private void InitEmployees()
        {
            Friends = new List<Friend>
            {
                new Friend
                {
                    Id = 1,
                    Name = "Test0"
                },
                new Friend
                {
                    Id = 2,
                    Name = "Test1",
                    Orders = new List<Order>()
                    {
                        new Order
                        {
                            Id = 1,
                            Price = 2
                        }
                    }
                },
                new Friend
                {
                    Id = 3,
                    Name = "Test3"
                },
                new Friend
                {
                    Id = 4,
                    Name = "Test4"
                }
            };
            Employees = new List<Employee>
            {
                new Employee()
                {
                    ID=1,
                    Name="Name1",
                    Friends = this.Friends.Where(x=>x.Id ==1 || x.Id==2).ToList()
                },
                new Employee()
                {
                    ID=2,Name="Name2",
                    Friends =  this.Friends.Where(x=>x.Id ==3 || x.Id==4).ToList()
                },
                new Employee()
                {
                    ID=3,
                    Name="Name3"
                },
            };
        }

        [HttpPatch]
        public IActionResult PatchEmployees([FromBody] DeltaSet<Employee> coll)
        {            
            return Ok(coll);
        }
    }
}

Edm part:

var modelBuilder = new ODataConventionModelBuilder();

modelBuilder.EntitySet<Employee>("Employees");
modelBuilder.EntitySet<Friend>("Friends");
modelBuilder.EntitySet<Order>("Orders");
modelBuilder.Namespace = typeof(Employee).Namespace;
modelBuilder.MaxDataServiceVersion = EdmConstants.EdmVersion401;
modelBuilder.DataServiceVersion = EdmConstants.EdmVersion401;

var batchHandler = new DefaultODataBatchHandler();
batchHandler.MessageQuotas.MaxNestingDepth = 2;
batchHandler.MessageQuotas.MaxOperationsPerChangeset = 10;
batchHandler.MessageQuotas.MaxReceivedMessageSize = 100;

Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
builder.WebHost.UseSerilog();

builder.Services.AddControllers().AddOData(opt =>
{
    opt.EnableQueryFeatures(3000);
    opt.AddRouteComponents(routePrefix: "odata", model: modelBuilder.GetEdmModel(), services => services.AddSingleton<ISearchBinder, SearchBinder>());
    opt.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
    opt.EnableAttributeRouting = true;
});
  1. Now I can already see what is in the delta object in my controller: image

I am interested in whether I need to write some mechanism myself that will look inside this object, and depending on what is there, execute a command to the database:

        [HttpPatch]
        public IActionResult PatchEmployees([FromBody] DeltaSet<Employee> coll)
        {    
            //for exmpl:
            foreach (var op in coll)
            {
                service.PartialUpdate(op);
            }   
            return Ok(coll);
        }

, or is this process somehow automated? Thanks.

habbes commented 1 year ago

For more information, check out the docs at: https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/entityset-routing?tabs=net60%2Cvisual-studio#patching-a-collection-of-entities

mikepizzo commented 1 year ago

1) @xuzhg -- the initial issue was that the controller method had the wrong signature; a patch to a collection should take a deltaset, rather than a delta. Is there any way that we can provide a compile-time check for that? 2) @xuzhg, @ElizabethOkerio -- The second part of the issue seems to be asking how to apply the delta set to the collection. In 7.x we use an ODataApiHandler to automate this, but we have not ported to 8 because we want to get more feedback on the design. Perhaps we can work with @AndriiLesiuk for feedback on that (or alternate) design.

AndriiLesiuk commented 1 year ago

For more information, check out the docs at: https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/entityset-routing?tabs=net60%2Cvisual-studio#patching-a-collection-of-entities

Okay, I realized that I need to implement this myself, but it's good that there is an example of how to do it better. Thanks. The second question seems to me to be closed for now. But I still don't understand why the error occurs.

AndriiLesiuk commented 1 year ago

@xuzhg , @habbes Sorry to bother you, but could you clarify why this kind of error might occur?

"A resource of type 'Edm.Untyped' was found in a resource set that otherwise has entries of type 'PostgresCRUDRelational.Controllers.Employee'. In OData, all entries in a resource set must have a common base type."

https://github.com/OData/AspNetCoreOData/issues/942#issuecomment-1568043328

Thanks

ElizabethOkerio commented 1 year ago

@AndriiLesiuk I think the changes in this PR: https://github.com/OData/AspNetCoreOData/pull/915 were not included in the 8.2.0 release. Are you in a position to try out with the changes in the main branch as we plan on another release.

AndriiLesiuk commented 1 year ago

@ElizabethOkerio, ok, thanks, will try.

ElizabethOkerio commented 1 year ago

@AndriiLesiuk The other question on whether you have to write your own logic for applying the DeltaSet to the collection; In 8.x you have to write your own logic but in v7.x we added the ODataApiHandlers that apply the DeltaSet for you. We haven't ported this to 8.x as we need to get customers' feedback on this approach and whether it is something that will be beneficial to them. You can also look at this doc on what these ODataApiHandlers are and how they can be used. https://devblogs.microsoft.com/odata/bulk-operations-support-in-odata-web-api/. We will appreciate any feedback on this. Thanks.

AndriiLesiuk commented 1 year ago

@AndriiLesiuk The other question on whether you have to write your own logic for traversing the DeltaSet, In 8.x you have to write your own logic but in v7.x we added the ODataApiHandlers that traverses the DeltaSet for you. We haven't ported this to 8.x as we need to get customers' feedback on this approach and whether it is something that will be beneficial to them. You can also look at this doc on what these ODataApiHandlers are and how they can be used. https://devblogs.microsoft.com/odata/bulk-operations-support-in-odata-web-api/. We will appreciate any feedback on this. Thanks.

@ElizabethOkerio Thanks, but I already implemented my own logic for this functionality.

ElizabethOkerio commented 1 year ago

@AndriiLesiuk The fix for this issue was released in v8.2.1. Could you try out and let's know if it works for you.