Skip to main content
added 2366 characters in body
Source Link
Flater
  • 59.5k
  • 8
  • 112
  • 171

Long-lived scopes

But there is a problem when working with scopes that are long lasting, like Blazor (server) components. We would not save data here, until the components are disposed.

Because the services live in a shared project, which both an API and Blazor project access. It's a bad idea in those instances, because I would need to build an exception for Blazor projects. But I do still find this idea intriguing and I think there is more to it.

I've struggled with this issue in the past, in a different context. Our business logic was written to be consumed by a web api, but was later also implemented in a long-running Windows service, breaking a lot of the repositories' functionality due to contexts living much longer than they should ever have. This led to all sorts of inexplicable behavior and took weeks to troubleshoot.

The solution here is to design your business logic so that it works with a factory.

When you inject a repository (or unit of work, same story) into your service, and your service lives a long life, then your repository (or unit of work) inherently lives for the same amount of time.

public class MyService
{
    private readonly UnitOfWork uow;

    public MyService(UnitOfWork uow)
    {
        this.uow = uow;
    }

    public DoStuff()
    {
        // logic
    }
}

However, this changes when you inject a factory, not a direct instance.

public class MyService
{
    private readonly UnitOfWorkFactory uowFactory;

    public MyService(UnitOfWorkFactory uowFactory)
    {
        this.uowFactory = uowFactory;
    }

    public DoStuff()
    {
        using(var uow = uowFactory.Create())
        {
            // logic
        }
    }
}

public class UnitOfWorkFactory
{
     public UnitOfWork Create() => new UnitOfWork();
}

No matter how long your MyService instance lives, every time MyService.DoStuff is called, you will be provided with a new, fresh unit of work.

In a web context, it's not harmful. Your factory will just fire once. It's not very useful, but it doesn't harm your workflow either.
In a long-lived context, you now avoid the issue with long lived repositories (or units of work).


Long-lived scopes

But there is a problem when working with scopes that are long lasting, like Blazor (server) components. We would not save data here, until the components are disposed.

Because the services live in a shared project, which both an API and Blazor project access. It's a bad idea in those instances, because I would need to build an exception for Blazor projects. But I do still find this idea intriguing and I think there is more to it.

I've struggled with this issue in the past, in a different context. Our business logic was written to be consumed by a web api, but was later also implemented in a long-running Windows service, breaking a lot of the repositories' functionality due to contexts living much longer than they should ever have. This led to all sorts of inexplicable behavior and took weeks to troubleshoot.

The solution here is to design your business logic so that it works with a factory.

When you inject a repository (or unit of work, same story) into your service, and your service lives a long life, then your repository (or unit of work) inherently lives for the same amount of time.

public class MyService
{
    private readonly UnitOfWork uow;

    public MyService(UnitOfWork uow)
    {
        this.uow = uow;
    }

    public DoStuff()
    {
        // logic
    }
}

However, this changes when you inject a factory, not a direct instance.

public class MyService
{
    private readonly UnitOfWorkFactory uowFactory;

    public MyService(UnitOfWorkFactory uowFactory)
    {
        this.uowFactory = uowFactory;
    }

    public DoStuff()
    {
        using(var uow = uowFactory.Create())
        {
            // logic
        }
    }
}

public class UnitOfWorkFactory
{
     public UnitOfWork Create() => new UnitOfWork();
}

No matter how long your MyService instance lives, every time MyService.DoStuff is called, you will be provided with a new, fresh unit of work.

In a web context, it's not harmful. Your factory will just fire once. It's not very useful, but it doesn't harm your workflow either.
In a long-lived context, you now avoid the issue with long lived repositories (or units of work).

Source Link
Flater
  • 59.5k
  • 8
  • 112
  • 171

Repositories as collections?

Recently I read a blog post about the repository pattern saying that is should act like a collection. It should have a Find(), GetList() and Add() methods, and varieties on those. But it shouldn't save the changes itself. And it should never have an Update() method because changes to the entities in the collection are references. We should let a Unit of Work save the changes.

This advice is allround bad. Or, at the very least, it applies to a very specific and very niche subset of cases and should have been given a disclaimer to point this out. You didn't link the article in question so I can't verify whether there is no disclaimer or whether you missed this.

The crux of the issue here is what the repository represents. If it represents a networked data storage, the advice is really, really bad.
If, however, the repository represents an in-memory collection of reference types, then the advice is reasonable, though I still wouldn't particularly state that such a repository must act like a collection.

Let's rewind back to the past. Repositories have a very neat "each type to its own storage" idea. You get people from the PersonRepository, you get orders from the OrderRepository, and so on. Organisationally speaking, and from a code perspective, this is a very clean way to organize your in-memory data sources.

But when we started using networked data sources, commonly database servers, we gained the ability to store more data than we can reasonably have in-memory during runtime. That's great, but some repositories banked on the idea of having in-memory access to every entity of given type (all people, all orders, ...).

Once you reach this point, it is no longer feasible to load the entire data set into your memory. It's a waste of both memory and network-bandwidth. It also completely cuts out the performance boosts database servers can offer (indexing, query caching, ...).

This is where the advice you've been given goes off the rails. It only makes sense for in-memory collections. Nowadays, we almost always store our data on a networked resource, and thus the advice is almost always wrong.


Unit of work

The example you use here is logic that only stores a Book. The important point I'm trying to get at here is that you're only impacting one repository. In such a case, a unit of work is not really very useful.

But I would still advocate for its usage in general.

Units of work start making a lot more sense when you want transactional safety, especially when dealing with multiple entity types at once. For example:

  • You want to store multiple books. If any of the books cannot be stored (e.g. validation error, network error, ...), then none of the books should be stored.
    • Arguably, this could still be solved without a unit of work, but it effectively requires you to implement extra logic in your repository which you otherwise would've implemented into your unit of work. If you have more than one repository which should provide transactional safety, it already starts making sense to use a unit of work.
  • You want to store a new author and their books. If one of these entities (author or any of the books) cannot be stored, then you want none of them to be stored.

As I said, repositories have an "each type to its own storage" design principle. This works well for operations scoped to a single entity type, but it is incapable of dealing with any transaction that involves more than one entity type.

This is why a unit of work is implemented. It is an "overseer" of the repositories. It keeps them in sync, and it coordinates a transaction that involves several repositories.

Some REST APIs always focus on a single entity type. In pure REST, your API effectively mirrors the CRUD functionality of your repository. In such cases, I can agree that a unit of work doesn't really add much value and can be skipped.
However, these kinds of APIs are not very common. Often, you end up resorting to some operations that require multiple entity types, and at this point, a unit of work is needed to coordinate between the repositories.

You can follow YAGNI here and avoid using units of work until the day comes where your codebase performs multi-entity-type operations; but this will require a refactoring of your code when it happens. Personally, I would opt for the unit of work by default here, simply because pure REST APIs that remain pure across their lifetime are exceedingly rare in my experience.