0

I'm implementing 3-tier architecture, and my question is: Where should I call SaveChanges(); (if applicable)? Where should I create and commit transaction? Please read the whole question.

In the code below I have only "read" operation, but you'll get the idea of the architecture. I'm struggling with the decision, but I came out with two options:

  1. Reference EFCore in the Core project. Inject DbContext into Service class and via method inject it to the Data layer. Like this:

    public class ItemService
    {
        private MyContext _ctx;
        private readonly IItemData _itemData;   
    
        public ItemService(IItemData itemData)
        {
            _itemData = itemData;
        }   
    
        public void InjectCtx(MyContext ctx)
        {
            _ctx = ctx;
            _itemData.InjectCtx(_ctx);
        }   
    
        public void Operation(int itemId)
        {
            using (var transaction = _ctx.Database.BeginTransaction())
            {
                //Do something  
    
                _ctx.SaveChanges();
                transaction.Commit();
            }
        }
    }
    
  2. Create an interface in the Core for Unit of work object. Which will have SaveChanges and Transaction, Commit methods. Then implement them in the Data layer and call accordingly in the Service class.

Here is the code. I have 3 projects:

  • Infrstructure (Data) - references Core and EFCore
  • Core (Business logic) - no dependencies
  • Web API (MVC project) - references Core, Data and EFCore

- Core (no dependencies, not even a reference to EFCore):

public class ItemDto
{
    public int ItemId { get; set; }
    public string Name { get; set; }
}

public interface IItemData
{
    Task<ItemDto> GetItem(int itemId);
}

public class ItemService
{
    private readonly IItemData _itemData;

    public ItemService(IItemData itemData)
    {
        _itemData = itemData;
    }

    public async Task<ItemDto> GetItem(int itemId)
    {
        return await _itemData.GetItem(itemId);
    }
}

- Data (reference to Core project and EFCore)

   public class ItemData : IItemData
    {
        private readonly MyContext _ctx;

        public ItemData(MyContext ctx)
        {
            _ctx = ctx;
        }

        public async Task<ItemDto> GetItem(int itemId)
        {
            var item = await _ctx.Items
                .Where(i => i.ItemId == itemId)
                .Select(row => new ItemDto()
                {
                    ItemId = row.ItemId,
                    Name = row.ItemName
                })
                .FirstOrDefaultAsync();
            return item;
        }
    }

- Web API:

[Route("api/[controller]")]
[ApiController]
public class ItemsController : ControllerBase
{
    private readonly ItemService _itemService;

    public ItemsController(ItemService itemService)
    {
        _itemService = itemService;
    }

    [HttpGet("{itemId}")]
    public async Task<ItemDto> Get(int itemId)
    {
        return await _itemService.GetItem(itemId);
    }
}
Ish Thomas
  • 2,270
  • 2
  • 27
  • 57
  • Show us where and how you're bootstrapping the Context. The DI Container should be managing the life cycle of the context. Also how long of a lifetime do you want the context to live? Per request? – Train Oct 01 '19 at 21:26
  • @Train thx, DI container is built in ASP.NET Core Web api. It Startup class you configure it. There is a new instance per request. – Ish Thomas Oct 02 '19 at 02:22
  • I would vote for your second solution. But I would move the interface and its implementation to the infrastructure project. Persistence is no responsibility of the business logic – bdongus Oct 02 '19 at 04:10

1 Answers1

0

If you really feel you need transactions, I would try to handle them 'above' your services, so that those ones are not responsible for handling them.

A way of doing this can be:

  • Use a scoped DbContext.

  • When a request starts or a service methods is invoked, create a transaction with Database.BeginTransaction().

  • Your service layer invokes your data layer and handles the business logic.

  • Your data layer applies .SaveChanges() wherever it's needed.

  • When a request ends or a service method invocation end, either run transaction.Commit() or transaction.Rollback().

A way of achieving those transactions creations/commits/rollbacks without making your services responsble of it can be using filters, middlewares or interceptors.

eduherminio
  • 1,514
  • 1
  • 15
  • 31