繁体   English   中英

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

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

我有一个 Web API 写在 .NET Core 中,它使用 EF Core 来管理对 ZE4728F4044B242839ADFZF 数据库的插入和查询。 API 非常适合现有实体的插入和查询,但我无法弄清楚如何进行部分“补丁”更新。 客户端希望能够只传递他们希望更新的属性。 因此,完整的客户 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
            }
        ]
    }
}

但是客户端想要传递一个有效载荷,例如:

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

并且只更新了address_line_1属性。 字段的 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 .

这意味着当我在 EF Core 中使用SetValues复制属性时,未提供的属性设置为 NULL,然后在数据库中更新为 NULL。 没有要求客户端传递所有属性并且只传递属性的现有值保持不变,我不确定如何处理这个问题。

因此,一旦传入的 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; 
   }

然后我使用下面的方法进行更新。 我现在最好的折衷方案是他们可以省略某些部分(如地址或 Contact_information 中的任何内容等)并且不会更新任何内容,但他们希望完全灵活地传递各个属性,我想提供它。 我该如何重新构建它,以便如果它们不传递客户或相关实体(地址等)的特定属性,它们会在 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; 
        }
    }

映射标识部分的示例:

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; }
}

好的,所以我确定我遗漏了一些东西,因为这绝对是简单的,但基本思想如下。

鉴于您的 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; }
    }

我们需要声明一个拐杖的东西(因为由于某种原因,您的样本具有顶级“客户”属性,如下所示:

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

然后我们可以有一个方法来完成所有的巫术:

    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));
            }
        }
    }

最后是我们的 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";
    }

我使用这个有效载荷进行测试:


{
    "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
            }
        ]
    }
}

注意:这种方法的最大缺点是您不能真正结合“contact_info”,因为您需要某种主键(我假设它已经在您的客户的路线中)。 如果你有这个,你可以通过检查JTokenType.Array来扩展巫术部分,然后通过类似的设置处理单个项目。

暂无
暂无

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

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