Giter VIP home page Giter VIP logo

Comments (11)

brud avatar brud commented on May 25, 2024

UPD.:
It looks like my logical mistake

            foreach (var property in type.GetProperties())
            {
                property.SetValue(entity, property.GetValue(delta));
            }

here I'm iterating over entity properties, but not over related entities properties

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

Hi @brud, thanks for trying the lib and for the feedback. Let me try to add some extra information.

This is your entity (without some fields):

public class User : IHasId
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual int Age { get; set; }
}

PatchTypeFactory generates a new type in runtime that looks like this:

// inherits from your type, and implements IPatch
public class UserPatch0f8fad5bd9cb469fa16570867728950e : User, IPatch
{
     HashSet<string> isSet = new HashSet<string>();

      public virtual int Id 
     {  
          get
          {
                return base.Id; // return the actual value
          }
         set
         {
              base.Id = value; // set the actual vlaue
              isSet.Add("Id"); // mark the prop as dirty
         }
        // the rest of the props are omitted.

        IPatch.IsSet(string propName) => _isSet.Contains(propName);
 }

Let's say that your receive this json in your controller (Id + Name, but no Age):

    { "id": 1, "name": "myUser" }

It will be deserialized into a new User class:

new User { 
    Id = 1,
    Name = "myUser",
    Age = 0
}

After that, you run your code to copy the properties:

foreach (var property in type.GetProperties())
{
    property.SetValue(entity, property.GetValue(delta));
}

Before running that, .IsSet("id") and IsSet("name") and IsSet("age") are all false.
On the 1st iteration, 'Id' prop value is copied and, IsSet("id") is true;
2nd iteration, 'Name' prop value is copied and, IsSet("name") is true;
3rd iteration, 'Age' prop value is copied and, IsSet("age") is true;

So that, Age will be maped and you won't know if that 0 is an intentional value or a default value.

Json can handle "undefined" (the property is not there), null, and default value, C# can't do that.

That's why I think the best place to do the patching is in the conversion from Json to C#, i.e.: the deserialization.

You can do that by adding PatchJsonConverterFactory to the MVC serialize, in Startup.cs:

services.AddControllers().AddJsonOptions(j =>
{
    j.JsonSerializerOptions.Converters.Add(new PatchTypes.PatchJsonConverterFactory());
});

I will add a sample and dig in some options to enable/disable the types that you want to patch.

Sorry for the lack of documentation! I'm looking for some help to improve this, but for now it is only me.

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

Added more docs and a sample here if you are still interested...

from detached-mapper.

brud avatar brud commented on May 25, 2024

@leonardoporro thanks a lot for your time :)
I will try to configure JsonOptions in my solution

from detached-mapper.

brud avatar brud commented on May 25, 2024

If I understand correctly, I need to do:

  1. Add to Startup.cs
services.AddControllers().AddJsonOptions(o =>
            {
                o.JsonSerializerOptions.Converters.Add(new PatchJsonConverterFactory(new AnnotationPatchTypeInfoProvider()));
            });
  1. Mark all needed entities with an attribute [UsePatch] (for now I marked all of it)
  2. And my PATCH method should be like:
        [HttpPatch("/odata/[controller]({id:int})")]
        public virtual async Task<ActionResult<TEntity>> Patch(int id, [FromBody]TEntity delta, CancellationToken ctx)
        {
            await _db.MapAsync<TEntity>(delta as IPatch);
            await _db.SaveChangesAsync(ctx);
            return Updated(entity);
        }

After this changes I make request:

PATCH https://localhost:5001/odata/users(8)
Accept: application/json
OData-Version: 4.0
Content-Type: application/json;odata.metadata=full;odata.streaming=true;IEEE754Compatible=false;charset=utf-8

{
  "Id": 8,
  "Name": "another user",
  "Role": {
    "Id": 8,
    "Name": "RENAMED Role 8",
    "Permissions": [
      {
        "Id": 14,
        "Name": "RENAMED Permission 14"
      },
      {
        "Id": 15,
        "Name": "NEW Permission 15"
      }
    ]
  }
}

and see that entity and all subentities mapped correctly, all User properties marked as isSet instead of Role.

I can't call entity.Role.Name.IsSet() - I'm get an 'IPatch' does not contain a definition for 'Role' and no accessible extension method 'Role' accepting a first argument of type 'IPatch' could be found (are you missing a using directive or an assembly reference?) error.

User.Name and User.RoleId updated correctly in DB, but all changes for Role and Permissions (renaming and adding relations) are not applied to DB, and response code is 204.

Maybe I'm missing something?

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

No problem, in the controller action, delta doesn't need to be casted to IPatch, and MapAsync returns the attached entity, so you may replace by:

TEntity result = await _db.MapAsync<TEntity>(delta);
await _db.SaveChangesAsync(ctx);
return result;

Please try replacing [Aggregation] by [Composition] in the Roles property of the User entity.
Aggregation asociations are linked to the root entity, but not modified. Composition asociations are saved with the root entity.

For example, lets say that you have an entity called UserType with the following data.

[
    { "id": 1,  "name": "System" },
    { "id": 2,  "name": "Human" }
]

And you map a User:

{
   "id": 1
   "name": "myUser",
   "type": { "id": 1 }
}

In this example UserType.Name being mapped is null, but nobody wants name "System" to be replaced by null, that is why it should be marked as [Aggregation].
But if you want UserType.Name and other properties to be saved along with User, then it should be marked as [Composition],

About calling IsSet, each class should be casted to IPatch individually, once you converted it to IPatch, the properties won't be available. Collection items should also be casted individually.

User myUser...;
((IPatch)myUser).IsSet("Name");
((IPatch)myUser.Role).IsSet("Name");
((IPatch)myUser.Role.Permissions[0]).IsSet("Name");

Anyway, you don't have to mess with IPatch if using mapper, it will take care of IPatch internally.
Please update to 5.0.21, as I fixed an error about PatchTypes not executing setter for null values.

Good luck and please let me know how it goes...

from detached-mapper.

brud avatar brud commented on May 25, 2024

I updated to 5.0.21, remove IPatch casting and mark property Role as [Aggregation] in User entity.

Then I send the same request, and it tries to insert new permission with Id = 14 instead of updating it's name (Permission 14 to RENAMED Permission 14).

Is this behavior by design?

UPD.: If I marked property Permissions in Role entity as [Aggregation], then I get exception MySqlConnector.MySqlException (0x80004005): Cannot add or update a child row: a foreign key constraint fails (`odata`.`permissionrole`, CONSTRAINT `FK_PermissionRole_Permissions_PermissionsId` FOREIGN KEY (`PermissionsId`) REFERENCES `permissions` (`id`) ON DELETE CASCADE)

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

Sorry, I don't know the structure of your business logic.
You are saving a User with a Role, but permissions are fixed and already exist?
If permission already exists, then it is an Aggregation.

public class User : IHasId
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual int Age { get; set; }
        public virtual int? RoleId { get; set; }
        [Composition]
        public virtual Role? Role { get; set; }
    }

    public class Role : IHasId
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }

        // this is a back reference, if you are going to save Roles you need to add [Composition] otherwise
       // try by leaving it empty.
        public virtual List<User>? Users { get; set; }

        [Aggregation]
        public virtual List<Permission>? Permissions { get; set; }
    }

    public class Permission : IHasId
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
     
        // same thing, if you are not going to save a Permission with the list of Roles in the controller
       // leave it without annotation for now
        public virtual List<Role>? Roles { get; set; }
    }

Update about Update:
If you mark Permission as Aggregate you need to add the permission manually.
Aggregation is linking, as in the previous example, you link a User to an existing UserType, assuming that the type exists and is a kind of autonomous entity.
There is a parameter that allows you to force the aggregations to be created if they do not exist.
Here

 _db.MapAsync<TEntity>(delta, new MapperParameters { AggregationAction =  AggregationAction.Map );

Attach: Creates an entity, assuming that it exists, and mark it as Unmodified.
Map: Queries the db and adds it if not found.

This option is used when importing Json without any specific order.. but I don't recommend it, as it is slower and may cause some problems.
The best way would be to have the permissions in the db before trying to link them to a user.
Let me know how it goes...

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

UPD.: If I marked property Permissions in Role entity as [Aggregation], then I get exception MySqlConnector.MySqlException (0x80004005): Cannot add or update a child row: a foreign key constraint fails (odata.permissionrole, CONSTRAINT FK_PermissionRole_Permissions_PermissionsId FOREIGN KEY (PermissionsId) REFERENCES permissions (id) ON DELETE CASCADE)

Forgot to mention, the exception means exactly that,

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

Effectively, this exception is happenning because the Permission does not exist:

MySqlConnector.MySqlException (0x80004005): Cannot add or update a child row: a foreign key constraint fails (odata.permissionrole, CONSTRAINT FK_PermissionRole_Permissions_PermissionsId FOREIGN KEY (PermissionsId) REFERENCES permissions (id) ON DELETE CASCADE)

So that, one of these fixes may help:

  • Map permissions in a separate call, before saving the User, or seed them in the DB or add them manually.
  • Mark everything as Composition (but be careful as next calls will overwrite all Permission properties again).
  • Pass MapperParameters argument { AggregationAction = AggregationAction.Map } to the MapAsync call, but that will load associations one by one to check if they exist, which is not very performant.

When you call MapAsync, an initial query is run to get the root entity by key, including all compositions. Aggregations are not included, as they are considered independent entities and expected to be there before the root entity.
I might modify this to load everything when you use that option, but I'm not sure... it may end loading big graphs!

from detached-mapper.

leonardoporro avatar leonardoporro commented on May 25, 2024

Reopen if you have more questions. Thanks.

from detached-mapper.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.