简体   繁体   中英

AutoMapper.Collections, EFCore - using short living DbContext

I try to use AutoMapper.Collection.EntityFrameworkCore to map my objects. All is working fine if I use the same DbContext all the time.

The problem is that there is no possibility to reject all cached objects in DbContext. Yes I did a search and found this post but it doesn't work. I don't realy understand the problem but I bet it is because I only detach the container object. There is no way without complex algorithms to traverse all objects to detach them all.

This is the currently working code (very simplyfied):

using var ctx = this.DbContextFactory.CreateDbContext();
var dtoProject = await ctx.Set<DtoProject>().Include(item => item.Jobs).FirstAsync();

var p = this.Mapper.Map<Project>(dtoProject);
var j = new Job(Guid.NewGuid().ToString("B").ToUpperInvariant(), $"Job {p.Jobs.Count + 1}");

p.Jobs.Add(j);

await ctx.Set<DtoProject>().Persist(this.Mapper).InsertOrUpdateAsync(p);
await ctx.SaveChangesAsync();

This code reuses ctx at SaveChangesAsync() which is working as expected.

But this leads to a DbContext instance that is very long living because it has to be alive as long as the business objects are in use. This doesn't sound like a real problem but I'm not able to invalidate objects in DbContext to force a reload if needed.

It seams that the way to go is to have a short living DbContext instance. Sounds good. I changed the code above so that a separate method is loading the business object and a new context is used to save the changes.

This simplyfied code shows the changes:

using var ctx = this.DbContextFactory.CreateDbContext();
var dtoProject = await ctx.Set<DtoProject>().Include(item => item.Jobs).FirstAsync();

var p = this.Mapper.Map<Project>(dtoProject);
var j = new Job(Guid.NewGuid().ToString("B").ToUpperInvariant(), $"Job {p.Jobs.Count + 1}");

p.Jobs.Add(j);

using var tmpCtx = this.DbContextFactory.CreateDbContext();
await tmpCtx.Set<DtoProject>().Persist(this.Mapper).InsertOrUpdateAsync(p);
await tmpCtx.SaveChangesAsync();

The only change is a new DbContext called tmpCtx used to store the changed value.

But this code throws a DbUpdateException that told me a UNIQUE constraint violation for jobs.id . The 'container' instance p seams to be accepted but the contained job instances seam to fail.

How to fix that?

The following code shows the automapper configuration and object declarations:

private IMapper CreateMapper()
{
    var mapperCfg = new MapperConfiguration(cfg =>
    {
        cfg.AddExpressionMapping();
        cfg.AddCollectionMappers();

        cfg.CreateMap<Job, DtoJob>()
            .EqualityComparison((blo, dto) => blo.Id == dto.Id)
            .ForMember(dst => dst.ParentId, opt => opt.Ignore())
            .ForMember(dst => dst.Parent, opt => opt.Ignore())
            .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
            .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name));
        cfg.CreateMap<DtoJob, Job>()
            .EqualityComparison((dto, blo) => dto.Id == blo.Id)
            .ForCtorParam("id", opt => opt.MapFrom(src => src.Id))
            .ForCtorParam("name", opt => opt.MapFrom(src => src.Name))
            .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
            .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name))
            .ForSourceMember(src => src.ParentId, opt => opt.DoNotValidate())
            .ForSourceMember(src => src.Parent, opt => opt.DoNotValidate());

        cfg.CreateMap<Project, DtoProject>()
            .EqualityComparison((blo, dto) => blo.Id == dto.Id)
            .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
            .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name))
            .ForMember(dst => dst.Jobs, opt => opt.MapFrom(src => src.Jobs));
        cfg.CreateMap<DtoProject, Project>()
            .EqualityComparison((dto, blo) => dto.Id == blo.Id)
            .ForCtorParam("id", opt => opt.MapFrom(src => src.Id))
            .ForCtorParam("name", opt => opt.MapFrom(src => src.Name))
            .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
            .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name))
            .ForMember(dst => dst.Jobs, opt => opt.MapFrom(src => src.Jobs));
    });
    mapperCfg.AssertConfigurationIsValid();

    return mapperCfg.CreateMapper();
}

public class Job
{
    public Job(string id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public string Id { get; }

    public string Name { get; }
}

public class Project
{
    public Project(string id, string name)
    {
        this.Id = id;
        this.Name = name;
    }

    public string Id { get; }

    public string Name { get; }

    public List<Job> Jobs { get; set; }
}

[Table("jobs")]
public class DtoJob
{
    [Key]
    [Column("id")]
    public string Id { get; set; }

    [Column("parent_id")]
    [ForeignKey(nameof(Parent))]
    [Required]
    public string ParentId { get; set; }

    public DtoProject Parent { get; set; }

    [Column("name")]
    [Required]
    public string Name { get; set; }
}

[Table("projects")]
public class DtoProject
{
    [Key]
    [Column("id")]
    [Required]
    public string Id { get; set; }

    [Column("name")]
    [Required]
    public string Name { get; set; }

    public List<DtoJob> Jobs { get; set; }
}

This all is a very simplyfied test code to isolate the problem.

I found a solution that seems to work. The steps are the following:

  • Load:
    • Create a DbContext instance
    • Load the DTO and map it to business logic instance
    • Reject the DbContext instance
  • Modify the business logic instance (add/modify/remove children)
  • Save:
    • Create a DbContext instance
    • Load the DTO that corresponds to the business object by key into DbSet . The return value doesn't matter. We only need to know about this object.
    • Save the changed business logic instance using InsertOrUpdate .

This performs a load operation that shouldn't be needed but I didn't find an other solution.

The simplyfied code is:

var p = (await this.LoadAsync()).FirstOrDefault();
// null handling omitted
var j = new Job(Guid.NewGuid().ToString("B").ToUpperInvariant(), $"Job {p.Jobs.Count + 1}");

p.Jobs.Add(j);
p.Jobs.RemoveAt(0);

await this.SaveAsync(p);

This uses the following methods:

private async Task<IList<Project>> LoadAsync(Expression<Func<Project, bool>> filter = null)
{
    using var ctx = this.DbContextFactory.CreateDbContext();
    IQueryable<DtoProject> query = ctx.Set<DtoProject>().Include(item => item.Jobs);
    if (!(filter is null))
    {
        query = query.Where(this.Mapper.MapExpression<Expression<Func<DtoProject, bool>>>(filter));
    }

    var resultDtos = await query.ToListAsync();
    var result = resultDtos.Select(this.Mapper.Map<Project>).ToList();
    return result;
}

private async Task SaveAsync(Project project)
{
    using var ctx = this.DbContextFactory.CreateDbContext();
    await ctx.Set<DtoProject>().FindAsync(p.Id == project.Id);
    await ctx.Set<DtoProject>().Persist(this.Mapper).InsertOrUpdateAsync(projectToStore);
    await ctx.SaveChangesAsync();
}

The main advantage of this solution is that the DbContext instances are very short living. They are only used to load and save the objects. The lifetime of the business logic objects doesn't have any effect to the DbContext instances.

That's the approach I'll try now, hoping it will solve all the problems.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM