我已经寻找了类似问题的解决方案,并且取得了一些进展,但我陷入了无法取得进展的地步。 我正在开展一个实践项目,我可以在其中更新应用程序详细信息的成员。 我使用 EntityFramework 写入 sql 数据库,但更新 memebr 的 ICollection 属性时遇到问题 会员对象:
{
Some properties: string.....
ICollection<Interest> Interest
}
当我从前端发送和反对时,我会发送整个 Mmeber。我试图保存的更改是新添加到兴趣集合中的项目。 我的终点:
[HttpPut]
public async Task<ActionResult> UpdateMember(MemberDto member)
{
if (this.HttpContext.Request.Cookies.ContainsKey("access-token"))
{
var cookieValue = this.HttpContext.Request.Cookies["access-token"];
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.ReadJwtToken(cookieValue);
var userName = token
.Claims.FirstOrDefault(claim =>
claim.Type == JwtRegisteredClaimNames.UniqueName
).Value;
var user = await this.userRepository.GetUserByUsernameAsync(userName.ToString());
if (user == null) return NotFound();
this.context.Entry(user).State = EntityState.Detached;
var updatedUser = this.mapper.Map(member, user);
this.context.Entry(user).State = EntityState.Modified;
this.userRepository.Update(user, updatedUser);
if (await this.userRepository.SaveAllAsync()) return NoContent();
return BadRequest("Failed to update user");
}
return BadRequest("User nor recognised");
}
我的存储库处理更新和保存:
public async Task<bool> SaveAllAsync()
{
return await context.SaveChangesAsync() > 0;
}
public void Update(AppUser user, AppUser updatedUser)
{
context.Entry(user).CurrentValues.SetValues(updatedUser);
UpdateCollection<Interest>(user.Interests, updatedUser.Interests);
}
private void UpdateCollection<T>(ICollection<T> existingCollection, ICollection<T> updatedCollection) where T : class
{
var existingItems = existingCollection.ToList();
var updatedItems = updatedCollection.ToList();
foreach(var updatedItem in updatedItems)
{
if(!existingItems.Contains(updatedItem))
{
existingCollection.Add(updatedItem);
context.Entry(updatedItem).State = EntityState.Added;
}
}
foreach(var existingItem in existingItems)
{
if(!updatedItems.Contains(existingItem))
{
existingCollection.Remove(existingItem);
context.Entry(existingItem).State = EntityState.Deleted;
}
}
}
我正在使用范围服务作为上下文。 我也在使用 AutoMapper:
CreateMap<MemberDto, AppUser>()
.ForMember(dest => dest.Interests, opt => opt.MapFrom(src => src.Interests))
.ForMember(dest => dest.Photos, opt => opt.MapFrom(src => src.Photos))
.ForMember(dest => dest.Languages, opt => opt.MapFrom(src => src.Languages))
.ForMember(dest => dest.Settings, opt => opt.MapFrom(src => src.Settings));
CreateMap<PhotoDto, Photo>();
CreateMap<LanguageDto, Language>();
CreateMap<InterestDto, Interest>();
CreateMap<UserSettingDto, UserSetting>();
我还在学习,我已经尝试调试它。我可以看到,当 memberDTO 在兴趣属性中有 5 个对象并且从上下文检索的用户有 4 个对象时,但是当我循环现有属性时,用户兴趣已经有 5 个对象。我没有收到任何错误,但是当我检索回同一用户时,数据兴趣属性有 4 个对象。看来这些改变并没有拯救自己。
感谢任何帮助
完成上述所有代码后,我希望 ICollection 能够随着新的兴趣而更新。
好吧,一开始你就做了太多太多的工作...读到你从 EF 中窃取的内容然后试图交回是令人困惑的。首先,像设置
EntityState
这样的代码并不是一件正常的事情,简单的更新场景不应该涉及它。使用 Update()
也是如此。这是为您与独立实体合作的情况提供的。您没有理由需要使用分离的实体,因为您是在现场加载实体。
第一步是大大简化您的代码。忽略在这里引入存储库的冲动,您不需要它。 EF 已经通过
DbSet<T>
提供了一个存储库,从这里开始。
因此,您有一个针对更新后的用户的 DTO,我假设它也有兴趣信息。如果没有看到 DTO,我建议 DTO 需要包含的所有兴趣信息都是 InterestId 的集合。 我们不需要 Automapper 将 DTO 和兴趣转换为实体对象。这对于插入场景很有用,但我会在更新时避免使用它。我在更新时避免使用它的原因是 DTO 应该只传递我们希望允许更改的详细信息。这可能意味着我们没有足够的信息来准确地组成完整的用户,我们也不应该尝试从客户端传递足够的信息来组成完整的用户。该信息可能会被篡改,导致我们覆盖客户没有业务变化的详细信息。如果客户端可以更新 3 列,则 DTO 应提供 UserId 和这 3 列。 (我们的服务器调用应该验证 UserId 是要更新的客户端会话的有效用户)
接下来,执行更新。更新具有相关数据集合的实体时,获取相关数据非常重要。这意味着急切地加载用户的兴趣:
[HttpPut]
public async Task<ActionResult> UpdateMember(MemberDto member)
{
try
{
var userName = getCurrentUserName();
var user = await _context.Users
.Include(user => user.Interests)
.SingleOrDefaultAsync(user => user.UserName == userName);
if (user == null)
return NotFound();
// TODO: Check that current user can alter user passed in member?
// (either that member is same user, or an authorization check?)
// TODO: copy values from DTO to user....
// Update the Interest references.
var existingInterestIds = user.Interests.Select(i => i.InterestId).ToList();
var updatedInterestIds = member.InterestIds;
var interestIdsToAdd = updatedInterestIds.Except(existingInterestIds);
var interestIdsToRemove = existingInterestIds.Except(updatedInterestIds);
if(interestIdsToRemove.Any())
{
var interestsToRemove = user.Interests.Where(i => interestIdsToRemove.Contains(i.InterestId));
foreach(var interest in interestsToRemove)
user.Interests.Remove(interest);
}
if(interestIdsToAdd.Any())
{
var interestsToAdd = await _context.Interests
.Where(i => interestIdsToAdd.Contains(i.InterestId))
.ToListAsync();
foreach(var interest in interestsToAdd)
user.Interests.Add(interest);
}
await _context.SaveChangesAsync();
return NoContent();
}
catch(UnauthorizedAccessException)
{
return Unauthorized();
}
catch(Exception ex)
{
// TODO: Log exception
return BadRequest();
}
}
private string getCurrentUserName()
{
if (!HttpContext.Request.Cookies.ContainsKey("access-token"))
throw new UnauthorizedAccessException("No user session.");
var cookieValue = this.HttpContext.Request.Cookies["access-token"];
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.ReadJwtToken(cookieValue);
var userName = token.Claims
.FirstOrDefault(claim => claim.Type == JwtRegisteredClaimNames.UniqueName)
.Value;
if (string.IsNullOrEmpty(userName))
throw new UnauthorizedAccessException("No user session.");
return userName;
}
在这里,我提取了获取当前用户会话的逻辑,这可能是需要做的事情,以便可以将其向下移动到基类控制器方法等。当我们获取要更新的用户时,我们使用
.Include(user => user.Interests)
预先加载兴趣。从这里开始,因为我假设我们正在处理用户和兴趣的关联,所以我们发现可能已添加或删除了哪些兴趣。然后,我们删除更新列表中没有的任何兴趣,对于要添加的任何兴趣,我们从 DbContext 中获取这些兴趣并将它们与用户关联。完成后,我们只需调用 SaveChanges
,因为 EF 默认情况下会跟踪用户和关联集合的所有更改。
要复制可以更改的用户详细信息,您可以逐个字段手动执行此操作,也可以设置 Automapper 将值从 DTO 复制到用户实体,并使用您正在使用的
Mapper.Map(src, dest)
方法。我通常建议使用手动复制或参数化 DDD 风格的方法,因为它很容易阅读和理解应该复制的内容,特别是当您正在调查问题并在属性设置器上查找用法时。映射器很有用,但可能有点像隐藏错误的黑匣子。