Entity Framework Core - 基于通过 Web API 传入的实体的 JSON 表示来更新具有子级的实体的有效方法

Jam*_*son 4 c# postgresql entity-framework entity-framework-core .net-core

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

客户 -< 地址 -< 联系信息

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

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; 
    }
Run Code Online (Sandbox Code Playgroud)

我希望能够通过 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(); 
Run Code Online (Sandbox Code Playgroud)

但随后能够使用客户或其相关实体中包含的任何属性的任何不同值来整齐地更新客户和关联表。所以,用伪代码来说:

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.
Run Code Online (Sandbox Code Playgroud)

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

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

L.V*_*let 5

您正在处理断开连接的实体的图表。以下是您可能感兴趣的文档部分。

例子:


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

Run Code Online (Sandbox Code Playgroud)