2

I'm new to ASP.NET CORE and I wanted to know which of these is the best for performance and scalability and design wise:

Case 1:

[HttpPost]
public async Task CreateBook([FromBody] Book book)
{
    _context.Add(book);
    await _context.SaveChangesAsync();
}

Case 2:

[HttpPost]
public async Task CreateBook([FromBody] Book book)
{
    await _booksRepository.AddBookAsync(book);
}
//Repository class:
public async Task AddBookAsync(Book book)
{
    _context.Add(book);
    await _context.SaveChangesAsync();
}

Case 3:

  [HttpPost]
  public async Task CreateBook([FromBody] Book book)
    {
        await _booksRepository.AddBook(book);
        await _context.SaveChangesAsync();
    }
    //Repository class:
    public async Task AddBook(Book book)
    {

        _context.Add(book);
    }
David Specht
  • 7,784
  • 1
  • 22
  • 30
George
  • 75
  • 1
  • 2
  • 5
    You could use a [performance profiler](https://www.google.com/search?q=.net+performance+profiler) and find it out by yourself. – Uwe Keim Feb 04 '20 at 08:20
  • 1
    "The First Rule of Program Optimization: Don't do it. The Second Rule of Program Optimization (for experts only!): Don't do it yet." — Michael A. Jackson – ShamPooSham Feb 04 '20 at 08:27
  • On a more serious note, I'd advice you not to care too much about micro optimizations that this would be, but it's a good idea to care about scalability – ShamPooSham Feb 04 '20 at 08:29
  • 4
    Forget performance, that `AddBookAsync` in Case 2 is incorrect, as in not doing what you expect it to do. A DbContext is essentially a Unit-of-Work caching all changes. `SaveChanges` will store *all* those changes to the database, not just the latest one. `AddBookAsync` can easily end up executing 2 DELETEs and 3 UPDATEs – Panagiotis Kanavos Feb 04 '20 at 08:30
  • 6
    "Generic" repositories are an antipattern - the `DbSet` itself does what a generic repository tries to do. DbContext itself offers the Unit-Of-Work and *domain transaction*. By themselves, generic repositories don't justify their cost (or problems). Extra query methods can easily be added to the DbContext. *Specialized* repositories make sense when they abstract and simplify complex business scenarios. One case would be to map business entities to storage DTOs if they are different. Another case is to hide complex reporting queries. – Panagiotis Kanavos Feb 04 '20 at 08:36
  • 2
    Check Gunar Peipman's [No need for repositories and unit of work with Entity Framework Core](https://gunnarpeipman.com/ef-core-repository-unit-of-work/) – Panagiotis Kanavos Feb 04 '20 at 08:54
  • I think question must be renamed. Because it's not about perfomance. – Alexbogs Feb 04 '20 at 08:58
  • @PanagiotisKanavos where all database code should be put? – StepUp Feb 04 '20 at 09:05
  • @PanagiotisKanavos you are saying that `Repository` and `UoW` are incorrect. Could you show what the correct variant is? I would be really great to see and try to implement your solution. It would be really great to see some code or link to read how it should be done. – StepUp Feb 04 '20 at 09:22
  • I did, 3 comments above – Panagiotis Kanavos Feb 04 '20 at 09:25

1 Answers1

0

I would use the third approach with some slight modifications. I would use Unit of Work pattern:

[HttpPost]
public async Task CreateBook([FromBody] Book book)
{
    await _booksRepository.AddBook(book);
    await _uow.Commit();
}

There are some reasons:

  • repository should be just like collection of objects such as List<T>. We do not have Save method in List<T>
  • using UnitOfWork allows to decouple our code to have better unit testing and we will have loose coupling
  • it is simplier to write unit tests

It is not important from performance point as creating new objects is not highly cost operation. However, editing, supporting, modifications of this code will be easier and faster.

UPDATE:

_uow is a variable which will store the following method:

public class UnitOfWork : IUnitOfWork
{
    private readonly MyDbContext _dbContext;

    public async Task Commit()
    {
        await _dbContext.SaveChangesAsync();
    }
}

You can read more about Entity Framework SaveChanges() vs. SaveChangesAsync().

Reasons to use Repository and UoW pattern:

  • it allows to write unit tests just by replacing result by IEnumerable<T>. So it becomes so simple to write unit tests for services without calling database code
  • When we have separate business logic from repository code, then code becomes cleaner and clearer. In this case, business logic will not have big queries. So we paid attention to separation of concerns.
  • cleaner and clearer architecture
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • What _uow variable in your example is? – Alexbogs Feb 04 '20 at 08:31
  • Some people think this is an anti pattern as a dbcontext essentially *is* a unit of work. See [this post](https://gunnarpeipman.com/ef-core-repository-unit-of-work/) for example. – Peter Bons Feb 04 '20 at 08:37
  • 1
    @Alexbogs please, see my updated answer – StepUp Feb 04 '20 at 08:38
  • How many `DELETE`s is that `Commit` going to do? Why rename `SaveChangesAsync` likte this anyway? – Panagiotis Kanavos Feb 04 '20 at 08:45
  • @PeterBons please, see my updated answer – StepUp Feb 04 '20 at 08:46
  • @StepUp Thank you! Could you please explain one moment for me: this uow scoped-service must be injected every time with my repository service? Why i can't just inherit my repositories from it and use Commit() method? – Alexbogs Feb 04 '20 at 08:46
  • 2
    DbSet is already a repository, DbContext is already a UoW. The typical generic repository is a *lower* level abstraction that breaks when applied to higher level abstractions like these – Panagiotis Kanavos Feb 04 '20 at 08:47
  • You mean UoW not UoF right? – Pablo Recalde Feb 04 '20 at 08:48
  • @Alexbogs because repositories should not have `Save` method. `List` does not have `Save` method – StepUp Feb 04 '20 at 08:49
  • 1
    @Alexbogs read Gunar Peipman's [No need for repositories and unit of work with Entity Framework Core](https://gunnarpeipman.com/ef-core-repository-unit-of-work/) before you consider using an extra Repository or UoW. He explains why the typical repository implementation is wrong, as in "resulting in unexpected executions". The other side of the coin is the scalability issues caused by adding database transactions to bring back UoW semantics – Panagiotis Kanavos Feb 04 '20 at 08:49
  • @PanagiotisKanavos but what best practice to work with DbSet if i need to operate with high-level entity abstraction? Use extension methods and inject my DbContext without any wrappers? – Alexbogs Feb 04 '20 at 08:50
  • @PanagiotisKanavos it depends on how many `DELETEs` you've written – StepUp Feb 04 '20 at 08:53
  • 1
    @Alexbogs the DbSet *is* the high-level abstraction - it's higher than the generic repo. Gunar Peipman's article is an excellent starting point on building even higher-level abstractions. His subsequent articles explain how to eg [abstract query methods](https://gunnarpeipman.com/ef-core-dbcontext-repository/) or raw SQL commands and reporting queries – Panagiotis Kanavos Feb 04 '20 at 08:53
  • 1
    @Alexbogs for meaningful unit testing you have to use *specialized* interfaces anyway. Instead of trying to mock `IRepository.GetAll().Where(c=>c.Name=...)` you should mock `ICustomerRepo.GetByName` or `ICustomerRepo.GetByMemberCardNo()`. Otherwise you'd have to spill the data access implementation details into the unit test itself – Panagiotis Kanavos Feb 04 '20 at 08:59
  • @PanagiotisKanavos you are saying that `Repository` and `UoW` are incorrect. Could you show what the correct variant is? I would be really great to see and try to implement your solution. It would be really great to see some code or link to read how it should be done. – StepUp Feb 04 '20 at 09:21
  • I did post the link. Gunar Peipman's [No need for repositories and unit of work with Entity Framework Core](https://gunnarpeipman.com/ef-core-repository-unit-of-work/) – Panagiotis Kanavos Feb 04 '20 at 09:25
  • @PanagiotisKanavos so the article says `Simple idea – use what you already have.`. So as alternative we can create a Database layer with all logic and custom transactions. So is this a way to go? Are you agree with this approach. If no, I would be really glad to see a link to github or somethinh else to see what the best way. – StepUp Feb 04 '20 at 09:32
  • You didn't read it. It doesn't say *anything* like that. In fact it warns *against* the extra transactions needed when `SaveChanges` is used to store individual modifications, instead of using its disconnected model – Panagiotis Kanavos Feb 04 '20 at 09:37
  • @PanagiotisKanavos you can read these words at the botton [in the article](https://gunnarpeipman.com/ef-core-repository-unit-of-work/) *There’s no actual need to implement your own unit of work and repositories if they just wrap DbContext functionalities without adding any new value. Simple idea – use what you already have.* – StepUp Feb 04 '20 at 10:04
  • Read the article, don't just scan it for reasons to dismiss it. The problems introduced by generic repos aren't new, they go back to 1996 and the introduction of disconnected recordsets. That's how the initial .NET demos like PetStore were 100x times faster than the equivalent Java demos - they were disconnected, didn't use per-record (or Row-By-Agonizing-Row) operations, used optimistic concurrency – Panagiotis Kanavos Feb 04 '20 at 10:05
  • And yes, you *shouldn't* write code unless it adds new value. New code always has a cost which must be justified. – Panagiotis Kanavos Feb 04 '20 at 10:08
  • Yoiu first argument is bad. If you use IEnumerable in a unit test you test possibly nothing - besides performance EfCore has a lot of limitations in what it can do and evaluate, so you may end up writing code that tests well and is totally broken in production. I gnerally never UNIT test code that touches the db - i always integration test it. Also your cleaner architecture i a fallacy - you end up with 3-5 times the classes and methods just s they look clean. I just finish a one year project making a codebase someone wrote like that mainteanable again. – TomTom Feb 04 '20 at 15:42
  • @TomTom IEnumerable allows you to mock result set to use it in unit test. So it is a way to write clean unit tests without depending on database. Repository can be tested by integration tests. Clean architecture does not require to abstract every class in your system. As you said it is fallacy. It is hard to say why there were so many classes in that project, however I don’t think that clean architecture is a thing that should be avoided – StepUp Feb 05 '20 at 17:59
  • THe problem is that anything BELOW IEnumerable is not really mockable. Particularly not if you have an API that llows you to send in query parameters and dynamically generates the query graph, which we do as standard. It is pretty much impossible to reproduce the peciularities of the generated SQL. Tried it, always had to give up. – TomTom Feb 05 '20 at 20:04