【问题标题】:Entity Framework Core - efficient way to update Entity that has children based on JSON representation of entity being passed in via Web APIEntity Framework Core - 基于通过 Web API 传入的实体的 JSON 表示来更新具有子实体的有效方法
【发布时间】:2020-10-13 02:00:54
【问题描述】:

我正在编写一个 .NET Core Web API,它由 Entity Framework Core 支持,并在下面有一个 PostgreSQL 数据库(AWS Aurora)。我已经能够学习并成功使用 EF Core 进行插入和查询数据,这很棒,但是开始研究现有实体的更新,我不清楚实现我所追求的最有效的方法。作为我的 Database First 脚手架练习的结果,我有了我的数据库实体。我有一个类似于下面的实体(简化)。

客户 -

所以一个客户,它可能有多个地址和/或多个 ContactInformation 记录。我期望我的 Web API 传递一个 JSON 有效负载,该有效负载可以转换为具有所有相关信息的客户。我有一个方法可以将我的 CustomerPayload 转换为可以添加到数据库中的客户:

public class CustomerPayload : Payload, ITransform<CustomerPayload, Customer>
{
    [JsonProperty("customer")]
    public RequestCustomer RequestCustomer { get; set; }

    public Customer Convert(CustomerPayload source)
    {
        Console.WriteLine("Creating new customer");
        Customer customer = new Customer
        {
            McaId = source.RequestCustomer.Identification.MembershipNumber,
            BusinessPartnerId = source.RequestCustomer.Identification.BusinessPartnerId,
            LoyaltyDbId = source.RequestCustomer.Identification.LoyaltyDbId,
            Title = source.RequestCustomer.Name.Title,
            FirstName = source.RequestCustomer.Name.FirstName,
            LastName = source.RequestCustomer.Name.Surname,
            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 = DateTime.UtcNow,
            UpdatedDate = DateTime.UtcNow,
            UpdatedBy = Functions.DbUser
        };

        Console.WriteLine("Creating address");
        if (source.RequestCustomer.Address != null)
        {
            customer.Address.Add(new Address
            {
                AddressType = "Home",
                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 = DateTime.UtcNow,
                UpdatedDate = DateTime.UtcNow,
                UpdatedBy = Functions.DbUser,
                UpdatingStore = null, // Not passed by API at present
                AddressValidated = false, // Not passed by API
                AddressUndeliverable = false, // Not passed by API
            });
        }

        Console.WriteLine("Creating marketing preferences");
        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 = DateTime.UtcNow,
                    UpdatedDate = DateTime.UtcNow,
                    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 // Should inherit parent marketing preference channel
                        }).ToList(),
                    UpdatingStore = null // Not passed by API
                })
                .ToList();
        }

        Console.WriteLine("Creating contact information");
        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);

            if (!emails.Any()) throw new Exception("At least 1 email address must be provided for a customer registration.");

            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 = DateTime.UtcNow,
                    UpdatedBy = Functions.DbUser,
                    UpdatedDate = DateTime.UtcNow,
                    Validated = x.Validated,
                    UpdatingStore = x.UpdatingStore

                })
                .ToList();
        }
        else
        {
            throw new Exception("Minimum required elements not present in POST request");
        }
        Console.WriteLine("Creating external cards");
        if (source.RequestCustomer.ExternalCards != null)
        {
            customer.ExternalCards = source.RequestCustomer.ExternalCards
                .Select(x => new ExternalCards()
                {
                    CardNumber = x.CardNumber,
                    CardStatus = x.Status,
                    CardDesign = x.CardDesign,
                    CardType = x.CardType,
                    UpdatingStore = x.UpdatingStore,
                    UpdatedBy = Functions.DbUser
                })
                .ToList();
        }

        Console.WriteLine($"Converted customer object --> {JsonConvert.SerializeObject(customer)}");
        return customer; 
    }

我希望能够通过 McaId 查找现有客户(这很好,我可以通过)

            var customer = await loyalty.Customer
                .Include(c => c.ContactInformation)
                .Include(c => c.Address)
                .Include(c => c.MarketingPreferences)
                .Include(c => c.ContentTypePreferences)
                .Include(c => c.ExternalCards)
                .Where(c => c.McaId == updateCustomer.McaId).FirstAsync(); 

但随后能够使用 Customer 中包含的任何属性的任何不同值 - 或者 - 其相关实体,巧妙地更新该 Customer 和关联的表。所以,在伪代码中:

CustomerPayload (cust: 1234) Comes in. 
Convert CustomerPayload to Customer(1234)
Get Customer(1234) current entity and related data from Database. 
Check changed values for any properties of Customer(1234) compared to Customer(1234) that's come in. 
Generate the update statement: 
UPDATE Customer(1234)
Set thing = value, thing = value, thing = value. 
UPDATE Address where Customer = Customer(1234) 
Set thing = value

Save to Database.

任何人都可以帮助了解实现这一目标的最佳方法吗?

编辑:尝试更新。代码如下:

public static async void UpdateCustomerRecord(CustomerPayload customerPayload)
{
    try
    {
        var updateCustomer = customerPayload.Convert(customerPayload);

        using (var loyalty = new loyaltyContext())
        {
            var customer = await loyalty.Customer
                .Include(c => c.ContactInformation)
                .Include(c => c.Address)
                .Include(c => c.MarketingPreferences)
                .Include(c => c.ContentTypePreferences)
                .Include(c => c.ExternalCards)
                .Where(c => c.McaId == updateCustomer.McaId).FirstAsync();

            loyalty.Entry(customer).CurrentValues.SetValues(updateCustomer);
            await loyalty.SaveChangesAsync();
            //TODO expand code to cover scenarios such as an additional address on an udpate
        }
    }
    catch (ArgumentNullException e)
    {
        Console.WriteLine(e);
        throw new CustomerNotFoundException();
    }
}

我只更改了客户的姓氏。没有错误发生,但是数据库中的记录没有更新。我有以下设置,所以希望在我的日志中看到生成的 SQL 语句,但没有生成语句:

Entity Framework Core 3.1.4 使用提供者“Npgsql.EntityFrameworkCore.PostgreSQL”初始化“loyaltyContext”,选项:SensitiveDataLoggingEnabled

这是我在日志中列出的唯一条目。

【问题讨论】:

    标签: c# postgresql entity-framework .net-core entity-framework-core


    【解决方案1】:

    您正在处理断开连接的实体图。这是您可能感兴趣的documentation section

    例子:

    
    var existingCustomer = await loyalty.Customer
        .Include(c => c.ContactInformation)
        .Include(c => c.Address)
        .Include(c => c.MarketingPreferences)
        .Include(c => c.ContentTypePreferences)
        .Include(c => c.ExternalCards)
        .FirstOrDefault(c => c.McaId == customer.McaId);
    
    if (existingCustomer == null)
    {
        // Customer does not exist, insert new one
        loyalty.Add(customer);
    }
    else
    {
        // Customer exists, replace its property values
        loyalty.Entry(existingCustomer).CurrentValues.SetValues(customer);
    
        // Insert or update customer addresses
        foreach (var address in customer.Address)
        {
            var existingAddress = existingCustomer.Address.FirstOrDefault(a => a.AddressId == address.AddressId);
    
            if (existingAddress == null)
            {
                // Address does not exist, insert new one
                existingCustomer.Address.Add(address);
            }
            else
            {
                // Address exists, replace its property values
                loyalty.Entry(existingAddress).CurrentValues.SetValues(address);
            }
        }
    
        // Remove addresses not present in the updated customer
        foreach (var address in existingCustomer.Address)
        {
            if (!customer.Address.Any(a => a.AddressId == address.AddressId))
            {
                loyalty.Remove(address);
            }
        }
    }
    
    loyalty.SaveChanges();
    
    

    【讨论】:

    • 谢谢,你能在这条线上稍微扩展一下吗?其余的都是有道理的(尽管让我看到我必须对此非常冗长,并且仅更新更改的属性没有简洁的快捷方式)但是这条线在做什么?忠诚度.Entry(existingCustomer).CurrentValues.SetValues(customer);
    • loyalty.Entry(existingCustomer).CurrentValues.SetValues(customer); 此行获取 EF DbContext 跟踪的现有实体,并将其属性值替换为断开连接的实体属性值。被跟踪实体的状态将更改为“已修改”。因此它会在调用 SaveChanges() 时被保存。
    • 啊,好吧,我想我现在明白了。因此,之后的额外代码只是处理不是直接更新现有属性的情况(例如,它是对客户的更新,但具有以前不存在的附加地址)。对吗?
    • 我在示例中添加了 cmets。
    • 当然,只要给我时间来实施和测试,以确保它确实提供了我正在寻找的东西,我一定会把它标记为已回答。再次感谢。
    猜你喜欢
    • 2018-12-10
    • 2020-04-22
    • 2020-11-16
    • 2020-02-06
    • 2017-05-05
    • 1970-01-01
    • 2018-10-12
    • 2023-03-29
    • 2012-07-10
    相关资源
    最近更新 更多