Search code examples
c#asp.net-coredomain-driven-designentity-framework-coreasp.net-core-webapi

How to use AsNoTracking in a service layer


I usually use AsNoTracking when I'm not intending to write anything. How should I handle this in my service layer where dbContext is hidden behind it? (I treat EF core as repository because it is repository)

public class SomeService
{
    //...

    public SomeEntity GetById(int id)
    {
        return _dbContext.Find(id);
    }

    public SomeEntity GetReadonlyById(int id)
    {
        return _dbContext.SomeEntitities.AsNoTracking().SingleOrDefault(e => e.Id == id);
    }

    public SomeEntity Update(SomeEntity someEntity)
    {
        _dbContext.Update(someEntity);
        _dbContext.SaveChanges();
    }
}

public class SomeController
{
    private readonly SomeService _someService;

    //....

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var someEntity = _someService.GetReadonlyById(id);
        if (someEntity == null)
        {
            return NotFound();
        }
        return someEntity;
    }

    [HttpPut("{id}")]
    public IActionResult Modify(int id, SomeEntity modified)
    {
        var someEntity = _someService.GetById(id);
        if (someEntity == null)
        {
            return NotFound();
        }
        someEntity.Someproperty = modified.Someproperty;
        _someService.Update(someEntity);
        return Ok(someEntity);
    }
}

Is there any better way to do this?

I can also define my service as follows:

public class SomeService
{
    //...

    public SomeEntity GetById(int id)
    {
        return _dbContext.AsNoTracking.SingleOrDefault(e => e.Id == id);
    }

    public SomeEntity Update(int id, SomeEntity someEntity)
    {
        var entity = _dbContext.SomeEntities.Find(id);
        if (entity == null)
        {
            return null;
        }
        entity.Someproperty = someEntity.Someproperty;
        _dbContext.Update(entity);
        _dbContext.SaveChanges();
        return entity;
    }
}

public class SomeController
{
    private readonly SomeService _someService;

    //....

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var someEntity = _someService.GetById(id);
        if (someEntity == null)
        {
            return NotFound();
        }
        return someEntity;
    }

    [HttpPut("{id}")]
    public IActionResult Modify(int id, SomeEntity modified)
    {
        var someEntity = _someService.Update(id, modified);
        if (someEntity == null)
        {
            return NotFound();
        }
        return Ok(someEntity);
    }
}

What is the better way?


Solution

  • Basically, it is more common problem.

    It is often happens that optimized reading methods are not convenient for updating scenarios and convenient reading methods for updating scenarios have unnecessary overhead for reading only scenarios. I see 3 options here:

    1. Ignoring all problems with performance and just use universal GetById from your first approach for reads and updates. Obviously, it is applicable for simple applications and may not be applicable for high-load applications.
    2. Using CQRS. It means you will have completely separate data model for reads and updates. Since reads usually don't require the complex domain logic it allows you to use any optimizations like AsNoTracking method or even use a plain sql in repositories. It is applicable for complex apps and requires more code.
    3. Trying to find some compromise between these two options according to your particular needs.

    As noted in comments your SomeService looks like repository. Ideally, domain service should contain only business logic and shouldn't mix it with infrastructure features like AsNoTracking. Whereas repositories can and should contain infrastructure features like AsNoTracking, Include and etc.