如何使用 Automapper 和 Entity Framework 仅映射已更改的属性

How to map only changed properties using Automapper and Entity Framework

我已经知道这个问题 had been asked,但这些答案对我不起作用。所以,我请求不要关闭这个问题!

目标很简单:

  1. 使用 Entity Framework 从数据库中获取实体。
  2. 将此实体传递给 MVC 视图。
  3. 编辑页面上的实体。
  4. 保存实体。

我有什么:

实体

Client 与数据库中的 table 相关。

ClientDtoClient 相同,但没有 Comment 属性(模拟用户不得看到此字段的情况)。

public class Client
{
  public string TaskNumber { get; set; }
  public DateTime CrmRegDate { get; set; }
  public string Comment { get; set; }
}

public class ClientDto
{
  public string TaskNumber { get; set; }
  public DateTime CrmRegDate { get; set; }
}

数据库table

CREATE TABLE dbo.client
(
  task_number  varchar(50)   NOT NULL,
  crm_reg_date date          NOT NULL,
  comment      varchar(3000)     NULL
);

-- Seeding
INSERT INTO dbo.client VALUES
('1001246', '2010-09-14', 'comment 1'),
('1001364', '2010-09-14', 'comment 2'),
('1002489', '2010-09-22', 'comment 3');

MVC 控制器

public class ClientsController : Controller
{
  private readonly ILogger logger;
  private readonly TestContext db;
  private IMapper mapper;

  public ClientsController(TestContext db, IMapper mapper, ILogger<ClientsController> logger)
  {
    this.db = db;
    this.logger = logger;
    this.mapper = mapper;
  }

  public IActionResult Index()
  {
    var clients = db.Clients;
    var clientsDto = mapper.ProjectTo<ClientDto>(clients);
    return View(model: clientsDto);
  }

  public async Task<IActionResult> Edit(string taskNumber)
  {
    var client = await db.Clients.FindAsync(taskNumber);
    return View(model: client);
  }

  [HttpPost]
  public async Task<IActionResult> Edit(ClientDto clientDto)
  {
    // For now it's empty - it'll be used for saving entity
  }
}

映射:

builder.Services
  .AddAutoMapper(config =>
  {
    config.CreateMap<Client, ClientDto>();
    config.CreateMap<ClientDto, Client>();
  }

助手(输出实体条目为JSON)

internal static class EntititesExtensions
{
  internal static string ToJson<TEntity>(this EntityEntry<TEntity> entry) where TEntity: class
  {
    var states = from member in entry.Members
                 select new
                 {
                   State = Enum.GetName(member.EntityEntry.State),
                   Name = member.Metadata.Name,
                   Value = member.CurrentValue
                 };
    var json = JsonSerializer.SerializeToNode(states);
    return json.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
  }
}

问题

我只需要在 Edit(ClientDto clientDto) 中将更改的属性从 ClientDto 映射到 Client

使用了哪些解决方案

解决方案 1

只需将 ClientDto 映射到 Client:

[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
  var client = mapper.Map<Client>(clientDto);
  logger.LogInformation(db.Entry(client).ToJson());
  db.Update(client);
  await db.SaveChangesAsync();
  return View(viewName: "Success");
}

此代码的问题是创建了绝对新的 Client 实体,其属性将由 ClientDto 填充。这意味着 Comment 属性 将是 NULL,这将使 NULL 成为数据库中的列 comment。在一般情况下,所有隐藏属性(即 DTO 中不存在的属性)都将具有其默认值。 不好。

输出:

[
  {
    "State": "Detached",
    "Name": "TaskNumber",
    "Value": "1001246"
  },
  {
    "State": "Detached",
    "Name": "Comment",
    "Value": null
  },
  {
    "State": "Detached",
    "Name": "CrmRegDate",
    "Value": "2010-09-15T00:00:00"
  }
]

解决方案 2

我尝试使用我上面提到的答案中的solution

public static IMappingExpression<TSource, TDestination> MapOnlyIfChanged<TSource, TDestination>(this IMappingExpression<TSource, TDestination> map)
{
  map.ForAllMembers(source =>
  {
    source.Condition((sourceObject, destObject, sourceProperty, destProperty) =>
    {
      if (sourceProperty == null)
        return !(destProperty == null);
      return !sourceProperty.Equals(destProperty);
    });
  });
  return map;
}

配置中:

builder.Services
  .AddAutoMapper(config =>
  {
    config.CreateMap<Client, ClientDto>();
    config.CreateMap<ClientDto, Client>().MapOnlyIfChanged();
  });

运行 与解决方案 1 相同的代码,我们得到相同的输出(Commentnull):

[
  {
    "State": "Detached",
    "Name": "TaskNumber",
    "Value": "1001246"
  },
  {
    "State": "Detached",
    "Name": "Comment",
    "Value": null
  },
  {
    "State": "Detached",
    "Name": "CrmRegDate",
    "Value": "2010-09-15T00:00:00"
  }
]

不好.

解决方案 3

我们走另一条路:

  1. 从数据库中获取实体。
  2. 使用非泛型 Map 以覆盖数据库中从 ClientDtoClient 的值。
  3. 调整映射配置以跳过值相等的属性。
builder.Services
  .AddAutoMapper(config =>
  {
    config.CreateMap<Client, ClientDto>();
    config.CreateMap<ClientDto, Client>()
      .ForMember(
        dest => dest.TaskNumber,
        opt => opt.Condition((src, dest) => src.TaskNumber != dest.TaskNumber))
      .ForMember(
        dest => dest.TaskNumber,
        opt => opt.Condition((src, dest) => src.CrmRegDate != dest.CrmRegDate));
  });
[HttpPost]
public async Task<IActionResult> Edit(ClientDto clientDto)
{
  var dbClient = await db.Clients.FindAsync(clientDto.TaskNumber);
  logger.LogInformation(db.Entry(dbClient).ToJson());
  mapper.Map(
    source: clientDto,
    destination: dbClient,
    sourceType: typeof(ClientDto),
    destinationType: typeof(Client)
  );
  logger.LogInformation(db.Entry(dbClient).ToJson());
  db.Update(dbClient);
  await db.SaveChangesAsync();
  return View(viewName: "Success");
}

这个 终于可以工作了 ,但是它有问题 - 它仍然修改 所有 属性。这是输出:

[
  {
    "State": "Modified",
    "Name": "TaskNumber",
    "Value": "1001246"
  },
  {
    "State": "Modified",
    "Name": "Comment",
    "Value": "comment 1"
  },
  {
    "State": "Modified",
    "Name": "CrmRegDate",
    "Value": "2010-09-15T00:00:00"
  }
]

那么,如何让Automapper只更新修改过的属性呢?

您不需要显式调用 context.Update()

加载实体时,EF 会记住每个映射的每个原始值 属性。

然后,当您更改 属性 时,EF 会将当前属性与原始属性进行比较,并仅为更改后的属性创建适当的更新 SQL。

进一步阅读:Change Tracking in EF Core