entity-framework-core - 如何使用 Automapper 和 Entity Framework 仅映射更改的属性

标签 entity-framework-core automapper

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

目标很简单:

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

我有什么:

实体

Client 与数据库中的表相关。

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

数据库表

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 Controller

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

关于entity-framework-core - 如何使用 Automapper 和 Entity Framework 仅映射更改的属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71009981/

相关文章:

datetime - 可为空的日期时间到日期时间转换器自动映射器

c# - 如何获取 OData 可查询 Web API 端点过滤器并将其映射到 DTO 对象?

c# - 使用 AutoMapper 将对象的属性映射到字符串

c# - 无法识别 Visual Studio Code Entity Framework 核心添加迁移

c# - 如何使用 ForNpgsqlUseXminAsConcurrencyToken 创建的 EF Core 并发 token

c# - options.UseOracle() 在 EF Core 中不可用

asp.net-core-mvc - MVC 6 EF7 RC1 创建多个 dbcontext

c# - Entity Framework 核心 : Eager loading navigation properties of derived types

c# - 使用 AutoMapper 映射未知类型

automapper - 使用 AutoMapper 从列表映射到对象