简体   繁体   English

通过 .NET Core Web API 在 Entity Framework Core 中 PATCH 实体和相关实体

[英]PATCH entity and related entities in Entity Framework Core via .NET Core Web API

I have a Web API written in .NET Core which uses EF Core to manage inserts and queries to a postgresql database.我有一个 Web API 写在 .NET Core 中,它使用 EF Core 来管理对 ZE4728F4044B242839ADFZF 数据库的插入和查询。 The API works great for inserts and queries of existing entities, but I am having trouble working out how to do partial 'patch' updates. API 非常适合现有实体的插入和查询,但我无法弄清楚如何进行部分“补丁”更新。 The client wants to be able to pass only the attributes they wish to update.客户端希望能够只传递他们希望更新的属性。 So a full customer JSON payload may look like this:因此,完整的客户 JSON 有效载荷可能如下所示:

{
    "customer": {
        "identification": {
            "membership_number": "2701138910268@priceline.com.au",
            "loyalty_db_id": "4638092"
        },
        "name": {
            "title": "Ms",
            "first_name": "tx2bxtqoa",
            "surname": "oe6qoto"
        },
        "date_of_birth": "1980-12-24T00:00:00",
        "gender": "F",
        "customer_type": "3",
        "home_store_id": "777",
        "home_store_updated": "1980-12-24T00:00:00",
        "store_joined_id": "274",
        "store_joined_date": "1980-12-24T00:00:00",
        "status_reason": null,
        "status": "50",
        "contact_information": [
            {
                "contact_type": "EMAIL",
                "contact_value": "2yupelxqui@hotmails.com",
                "validated": true,
                "updating_store": null
            },
            {
                "contact_type": "MOBILE",
                "contact_value": "xxxxxxxxx",
                "validated": true,
                "updating_store": null
            }
        ],
        "marketing_preferences": [],
        "address": {
            "address_line_1": "something stree",
            "address_line_2": "Snyder",
            "postcode": "3030"
        },
        "external_cards": [
            {
                "updating_store": null,
                "card_type": "PY",
                "card_design": null,
                "card_number": "2701138910268",
                "status": "ACTIVE",
                "physical_print": false
            }
        ]
    }
}

But the client wants to pass in a payload like:但是客户端想要传递一个有效载荷,例如:

{
    "customer": {
        "identification": {
            "membership_number": "2701138910268@priceline.com.au"
        },
        "address": {
            "address_line_1": "something stree"
        },
    }
}

And have only the address_line_1 property updated.并且只更新了address_line_1属性。 The rest of the fields are to remain as is.字段的 rest 将保持原样。 Unfortunately, because I convert the JSON into a CustomerPayload object, then the CustomerPayload object into a Customer object (and related entities), if a property is not passed, then it is set to NULL . Unfortunately, because I convert the JSON into a CustomerPayload object, then the CustomerPayload object into a Customer object (and related entities), if a property is not passed, then it is set to NULL .

This means when I use SetValues in EF Core to copy properties across, those not provided are set to NULL, then updated in the database as NULL.这意味着当我在 EF Core 中使用SetValues复制属性时,未提供的属性设置为 NULL,然后在数据库中更新为 NULL。 Short of asking the client to pass all properties through and just pass existing values for the properties to be left unchanged, I am unsure how to deal with this.没有要求客户端传递所有属性并且只传递属性的现有值保持不变,我不确定如何处理这个问题。

So once the incoming JSON is converted to CustomerPayload (and attributes are validated) I use the below to convert CustomerPayload to Customer :因此,一旦传入的 JSON 转换为CustomerPayload (并验证属性),我使用以下将CustomerPayload转换为Customer

public Customer Convert(CustomerPayload source)
{
    Customer customer = new Customer
            {
                McaId = source.RequestCustomer.Identification.MembershipNumber,
                BusinessPartnerId = source.RequestCustomer.Identification.BusinessPartnerId,
                Status = source.RequestCustomer.Status,
                StatusReason = source.RequestCustomer.StatusReason, 
                LoyaltyDbId = source.RequestCustomer.Identification.LoyaltyDbId,
                Gender = source.RequestCustomer.Gender,
                DateOfBirth = source.RequestCustomer.DateOfBirth,
                CustomerType = source.RequestCustomer.CustomerType,
                HomeStoreId = source.RequestCustomer.HomeStoreId,
                HomeStoreUpdated = source.RequestCustomer.HomeStoreUpdated,
                StoreJoined = source.RequestCustomer.StoreJoinedId,
                CreatedDate = Functions.GenerateDateTimeByLocale(),
                UpdatedBy = Functions.DbUser
            };

    if (source.RequestCustomer.Name != null)
    {
        customer.Title = source.RequestCustomer.Name.Title;
        customer.FirstName = source.RequestCustomer.Name.FirstName;
        customer.LastName = source.RequestCustomer.Name.Surname;
    }

    if (source.RequestCustomer.Address != null)
    {
        customer.Address.Add(new Address
                {
                    AddressType = source.RequestCustomer.Address.AddressType,
                    AddressLine1 = source.RequestCustomer.Address.AddressLine1,
                    AddressLine2 = source.RequestCustomer.Address.AddressLine2,
                    Suburb = source.RequestCustomer.Address.Suburb,
                    Postcode = source.RequestCustomer.Address.Postcode,
                    Region = source.RequestCustomer.Address.State, 
                    Country = source.RequestCustomer.Address.Country,
                    CreatedDate = Functions.GenerateDateTimeByLocale(),
                    UpdatedBy = Functions.DbUser,
                    UpdatingStore = source.RequestCustomer.Address.UpdatingStore,
                    AddressValidated = source.RequestCustomer.Address.AddressValidated,
                    AddressUndeliverable = source.RequestCustomer.Address.AddressUndeliverable
                });
    }

    if (source.RequestCustomer.MarketingPreferences != null)
    {
        customer.MarketingPreferences = source.RequestCustomer.MarketingPreferences
                    .Select(x => new MarketingPreferences()
                    {
                        ChannelId = x.Channel,
                        OptIn = x.OptIn,
                        ValidFromDate = x.ValidFromDate,
                        UpdatedBy = Functions.DbUser,
                        CreatedDate = Functions.GenerateDateTimeByLocale(),
                        UpdatingStore = x.UpdatingStore,
                        ContentTypePreferences = (from c in x.ContentTypePreferences
                            where x.ContentTypePreferences != null
                            select new ContentTypePreferences
                            {
                                TypeId = c.Type,
                                OptIn = c.OptIn,
                                ValidFromDate = c.ValidFromDate,
                                ChannelId = x.Channel //TODO: Check if this will just naturally be passed in JSON so can use c. instead of x.)
                            }).ToList(),
                    })
                    .ToList();
    }

    if (source.RequestCustomer.ContactInformation != null)
    {
        // Validate email if present
        var emails = (from e in source.RequestCustomer.ContactInformation
                      where e.ContactType.ToUpper() == ContactInformation.ContactTypes.Email && e.ContactValue != null
                    select e.ContactValue);

        foreach (var email in emails)
        {
            Console.WriteLine($"Validating email {email}");

            if (!IsValidEmail(email))
            {
                throw new Exception($"Email address {email} is not valid.");
            }
        }

        customer.ContactInformation = source.RequestCustomer.ContactInformation
                    .Select(x => new ContactInformation()
                    {
                        ContactType = x.ContactType,
                        ContactValue = x.ContactValue,
                        CreatedDate = Functions.GenerateDateTimeByLocale(),
                        UpdatedBy = Functions.DbUser,
                        Validated = x.Validated,
                        UpdatingStore = x.UpdatingStore

                    })
                    .ToList();
        }

        if (source.RequestCustomer.ExternalCards != null)
        {
            customer.ExternalCards = source.RequestCustomer.ExternalCards
                    .Select(x => new ExternalCards()
                    {
                        CardNumber = x.CardNumber,
                        CardStatus = x.Status.ToUpper(),
                        CardDesign = x.CardDesign,
                        CardType = x.CardType,
                        UpdatingStore = x.UpdatingStore,
                        UpdatedBy = Functions.DbUser
                    })
                    .ToList();
        }

        Console.WriteLine($"{customer.ToJson()}");
        return customer; 
   }

Then I use the below method to update.然后我使用下面的方法进行更新。 The best compromise I have right now, is that they can omit certain sections (like Address, or anything inside Contact_information etc) and nothing will be updated, but they want full flexibility to pass individual properties, and I want to provide it.我现在最好的折衷方案是他们可以省略某些部分(如地址或 Contact_information 中的任何内容等)并且不会更新任何内容,但他们希望完全灵活地传递各个属性,我想提供它。 How can I restructure this so that if they don't pass specific properties for the Customer or related entities (Address etc) they are simply ignored in the SetValues or update statement generated by EF Core?我该如何重新构建它,以便如果它们不传递客户或相关实体(地址等)的特定属性,它们会在 EF Core 生成的 SetValues 或更新语句中被忽略?

public static CustomerPayload UpdateCustomerRecord(CustomerPayload customerPayload)
    {
        try
        {
            var updateCustomer = customerPayload.Convert(customerPayload);
            var customer = GetCustomerByCardNumber(updateCustomer.ExternalCards.First().CardNumber);

            Console.WriteLine($"Existing customer {customer.McaId} will be updated from incoming customer {updateCustomer.McaId}");

            using (var loyalty = new loyaltyContext())
            {
                loyalty.Attach(customer);
               
                // If any address is provided
                if (updateCustomer.Address.Any())
                {
                    Console.WriteLine($"Update customer has an address");
                    foreach (Address a in updateCustomer.Address)
                    {
                        Console.WriteLine($"Address of type {a.AddressType}");
                        if (customer.Address.Any(x => x.AddressType == a.AddressType))
                        {
                            Console.WriteLine($"Customer already has an address of this type, so it is updated.");
                            a.AddressInternalId = customer.Address.First(x => x.AddressType == a.AddressType).AddressInternalId;
                            a.CustomerInternalId = customer.Address.First(x => x.AddressType == a.AddressType).CustomerInternalId;
                            a.CreatedDate = customer.Address.First(x => x.AddressType == a.AddressType).CreatedDate;
                            a.UpdatedDate = Functions.GenerateDateTimeByLocale();
                            a.UpdatedBy = Functions.DbUser;
                            loyalty.Entry(customer.Address.First(x => x.AddressType == a.AddressType)).CurrentValues.SetValues(a);
                        }
                        else
                        {
                            Console.WriteLine($"Customer does not have an address of this type, so it is inserted.");
                            customer.AddAddressToCustomer(a);
                        }
                    }
                }
                // We want to update contact information 
                if (updateCustomer.ContactInformation.Any())
                {
                    Console.WriteLine($"Some contact information has been provided to update");
                    foreach (var c in updateCustomer.ContactInformation)
                    {
                        Console.WriteLine($"Assessing contact information {c.ContactValue} of type {c.ContactType}");
                        if (customer.ContactInformation.Any(ci => ci.ContactType == c.ContactType))
                        {
                            Console.WriteLine($"The customer already has a contact type of {c.ContactType}");
                            // we have an existing contact of this type so update
                            var existContact = (from cn in customer.ContactInformation
                                                where cn.ContactType == c.ContactType
                                                select cn).Single();

                            Console.WriteLine($"Existing contact id is {existContact.ContactInternalId} with value {existContact.ContactValue} from customer id {existContact.CustomerInternalId} which should match db customer {customer.CustomerInternalId}");
                            // Link the incoming contact to the existing contact by Id 
                            c.CustomerInternalId = existContact.CustomerInternalId;
                            c.ContactInternalId = existContact.ContactInternalId;

                            // Set the update date time to now
                            c.UpdatedDate = Functions.GenerateDateTimeByLocale();
                            c.UpdatedBy = Functions.DbUser;
                            c.CreatedDate = existContact.CreatedDate;
                            loyalty.Entry(existContact).CurrentValues.SetValues(c);
                        }
                        else
                        {
                            Console.WriteLine($"There is no existing type of {c.ContactType} so creating a new entry");
                            // we have no existing contact of this type so create
                            customer.AddContactInformationToCustomer(c);
                        }
                    }
                }

                updateCustomer.CustomerInternalId = customer.CustomerInternalId;
                updateCustomer.CreatedDate = customer.CreatedDate;
                updateCustomer.UpdatedDate = Functions.GenerateDateTimeByLocale();

                loyalty.Entry(customer).CurrentValues.SetValues(updateCustomer);
                loyalty.Entry(customer).State = EntityState.Modified;

                if (updateCustomer.BusinessPartnerId == null)
                {
                    Console.WriteLine($"BPID not specified or NULL. Do not update.");
                    loyalty.Entry(customer).Property(x => x.BusinessPartnerId).IsModified = false;
                }

                // CustomerPayload used to check name, as Customer has no outer references/element for name details. 
                if (customerPayload.RequestCustomer.Name == null)
                {
                    loyalty.Entry(customer).Property(x => x.FirstName).IsModified = false;
                    loyalty.Entry(customer).Property(x => x.LastName).IsModified = false;
                    loyalty.Entry(customer).Property(x => x.Title).IsModified = false;
                }

                loyalty.SaveChanges();
                customerPayload = customer.Convert(customer);

                // Return customer so we can access mcaid, bpid etc. 
                return customerPayload; 
            }
        }
        catch (ArgumentNullException e)
        {
            Console.WriteLine(e);
            throw new CustomerNotFoundException();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex}");
            throw ex; 
        }
    }

Example of mapping Identification section:映射标识部分的示例:

public class Identification
{
    [DisplayName("business_partner_id")]
    [Description("A business_partner_id is required")]
    [StringLength(10)]
    [DataType(DataType.Text)]
    [JsonProperty("business_partner_id", Required = Required.Default)]
    public string BusinessPartnerId { get; set; } 

    [DisplayName("membership_number")]
    [Description("A membership_number is required")]
    [StringLength(50)]
    [DataType(DataType.Text)]
    [JsonProperty("membership_number", Required = Required.Default)]
    public string MembershipNumber { get; set; }

    [DisplayName("loyalty_db_id")]
    [Description("A loyalty_db_id is required")]
    [StringLength(50)]
    [DataType(DataType.Text)]
    [JsonProperty("loyalty_db_id", Required = Required.Default)]
    public string LoyaltyDbId { get; set; }
}

Okay, so i am sure i am missing something as this is absolutely bare-bones, but the basic idea is follows.好的,所以我确定我遗漏了一些东西,因为这绝对是简单的,但基本思想如下。

Given your DTO classes that look something like this:鉴于您的 DTO 类看起来像这样:

    public class CustomerPayload
    {
        public Identification Identification { get; set; }

        [JsonProperty("contact_information")]
        public ContactInfo[] ContactInformation { get; set; }
    }

    public class ContactInfo
    {
        public bool Validated { get; set; }
    }

    public class Identification
    {
        [JsonProperty("membership_number")]
        public string MembershipNumber { get; set; }

        public string SomePropertyNotInPayload { get; set; }
    }

We need to declare one crutches thingy (coz for some reason your sample has a top level 'customer' property, looks like this:我们需要声明一个拐杖的东西(因为由于某种原因,您的样本具有顶级“客户”属性,如下所示:

    public class PartialCustomerPayloadWrapper
    {
        public JObject Customer { get; set; }
    }

Then we can have a method that does all the voodoo:然后我们可以有一个方法来完成所有的巫术:

    private void SetThings(object target, JObject jObj)
    {
        var properties = target.GetType()
            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Select(x =>
        {
            var attr = x
            .GetCustomAttributes(typeof(JsonPropertyAttribute), false)
            .FirstOrDefault();

            string jPropName = null;
            if (attr != null)
            {
                jPropName = ((JsonPropertyAttribute)attr).PropertyName;
            }

            return (Property: x, Name: x.Name, JsonName: jPropName);
        });

        foreach (var val in jObj)
        {
            var key = val.Key.ToLowerInvariant();
            var property = properties
                .FirstOrDefault(x => x.Name.ToLowerInvariant() == key ||
                x.JsonName?.ToLowerInvariant() == key);

            if (property == default)
            {
                continue;
            }

            if (val.Value.Type == JTokenType.Object)
            {
                var newTarget = property.Property.GetValue(target);
                if (newTarget == null)
                {
                    newTarget = Activator.CreateInstance(property.Property.PropertyType);
                    property.Property.SetValue(target, newTarget);
                }

                SetThings(property.Property.GetValue(target), (JObject)val.Value);
            }
            else
            {
                property.Property.SetValue(target, val.Value.ToObject(property.Property.PropertyType));
            }
        }
    }

And finally our API action:最后是我们的 API 操作:

    [HttpPost]
    public string Post([FromBody] PartialCustomerPayloadWrapper wrapper)
    {
    // So  here i expect you to get data from DB 
    // and then pass through the method that converts the db object to `CustomerPayload`
    // Since i do not have that, this is just a POCO with some properties initialized.
        var dbCustomer = new CustomerPayload { Identification = new Identification { SomePropertyNotInPayload = "banana" } };

        var customer = wrapper.Customer;

        SetThings(dbCustomer, customer);
     // at this point our SomePropertyNotInPayload is still banana, but contact info and MembershipNumber are set
        return "OK";
    }

I used this payload for testing:我使用这个有效载荷进行测试:


{
    "customer": {
        "identification": {
            "membership_number": "2701138910268@priceline.com.au"
        },
        "address": {
            "address_line_1": "something stree"
        },
        "contact_information": [
            {
                "contact_type": "EMAIL",
                "contact_value": "2yupelxqui@hotmails.com",
                "validated": true,
                "updating_store": null
            },
            {
                "contact_type": "MOBILE",
                "contact_value": "xxxxxxxxx",
                "validated": false,
                "updating_store": null
            }
        ]
    }
}

Note: The biggest downfall of this approach is that you can't really marry up the 'contact_info' because you need some kind of primary key (which i am assuming is already in the route for your customer).注意:这种方法的最大缺点是您不能真正结合“contact_info”,因为您需要某种主键(我假设它已经在您的客户的路线中)。 If you had that, you can extend the voodoo part by checking for JTokenType.Array and then processing individual items through the similar set up.如果你有这个,你可以通过检查JTokenType.Array来扩展巫术部分,然后通过类似的设置处理单个项目。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 .NET Web API(非核心)-在Entity Framework控制器中创建修补程序操作 - .NET Web API (not Core) - Creating a Patch operation in an Entity Framework controller Audit.NET Entity Framework Core - 相关实体管理 - Audit.NET Entity Framework Core - Related Entities management 如何使用Entity Framework Core加载相关实体 - How to load related entities with the Entity Framework Core Entity Framework Core 检索相关实体 - Entity Framework Core retrieving related entities Entity Framework Core 添加到 .Net Core Web Api - IRelationalTypeMappingSource 问题 - Entity Framework Core adding to .Net Core Web Api - IRelationalTypeMappingSource problem ASP.NET 核心 Web API 使用实体框架核心 - ASP.NET Core Web API using Entity Framework Core 实体框架核心获取没有相关实体的实体 - Entity Framework Core get entity without related entities Entity Framework Core GroupBy - 相关实体上的聚合函数 - Entity Framework Core GroupBy - aggregate functions on related entities Entity Framework Core 2.0.1 急切加载所有嵌套的相关实体 - Entity Framework Core 2.0.1 Eager Loading on all nested related entities 在 Entity Framework Core 和 ASP.NET Core 中加载相关数据 - Loading related data in Entity Framework Core and ASP.NET Core
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM