The scope of a repository (IoC scope) in Spring Data is singleton, but in .NET, it is not. Why there's a difference?
What is a repository?
Repositories are just classes that implement data access logic. It is generally implemented in the data access layer and provides data access service to the application layer. A repository represents a data entity (a table on the database), including common data access logic (CRUD) and other special logic. — EnLab Software
There is a different way to implement a repository between Spring / .NET
- Spring: we can use an amnotation @Repository, and extend the JpaRepository. Spring will auto-generate the implementation
- .NET: Declare a repository interface, then implement it by utilizing DbContext (If you use the Entity Framework)
When the implementation is ready, we inject the repository into our controllers/business services through DI (Dependency Injection). The repository must have a lifetime. The following is a recommendation for the scope of the repository.
- Spring: The repository bean is registered as “singleton” (Spring Data uses a default scope which is singleton for the repository bean)
- .NET: The repository service is registered as “scoped” (The repository instance lifetime in your IoC container)
For folks who don’t know about Spring IoC or .NET IoC, you can find references here: Service lifetime — .NET and Bean Scopes — Spring.
An equivalent table is shown below:
So, why is the repository’s scope recommended in Spring Singleton but scoped in .NET?
How is a repository service implemented in .NET (with Entity Framework)?
There are many discussions regarding whether we should use a repository pattern with Entity Framework (Why shouldn’t I use the repository pattern with Entity Framework? — Software Engineering Stack Exchange). The DbContext itself also implement a repository pattern (From Microsoft Docs).
So, in the context of this post, I assume we’re talking about a repository where we utilize a DbContext to perform database operations.
You can find the simplified implementation below:
A DbContext is defined as a “Scoped” lifetime by default. The repository service can be registered as Scoped or Transient. It’s recommended to use Scoped regarding memory optimization. Besides, we shouldn’t use singleton scope for the repository service since it will cause concurrency problems. (the repository instance lifetime).
Even if you register your DbContext as singleton, and then our repository service can be added as a singleton one as well, you will soon face the following issues:
- DbContext is not thread-safe. If it’s a singleton service, then this will be an issue.
- Performance inefficiency: DbContext is designed to have a very short lifetime. As I’ve just mentioned, it will go against the original intention about their lifetime if it’s a singleton service. Then, it will affect other EF’s features, such as Change Tracking (Instead of tracking changes per HTTP Request, it will need to follow per application lifetime) — DbContext Lifetime, Configuration, and Initialization
How does the repository work as a singleton bean (service) in Spring Data?
As I stated earlier, registering a repository service as a singleton can lead to many issues. So, what makes it work in Spring?
In Spring Data JPA, a repository instance itself doesn’t hold any data. It acts as a mediator and merely provides a set of operations that can be performed on the underlying data without exposing details about the data source. When we call a method on the repository, it uses an EntityManager to interact with the database and returns the result.
One crucial thing to mention here is that EntityManager is not thread-safe, which means it can’t be declared a singleton.
This behaviour is like a repository in .NET, where we use a DbContext to interact with the database, and DbContext is also not thread-safe.
How can a singleton repository access a service/bean with a shorter lifetime?
Spring injects a proxy for a singleton bean that gets an EntityManager injected. This proxy is tied to the current transaction context. Whenever a method is invoked on the proxy, it delegates the call to the EntityManager linked to the ongoing transaction. This EntityManager is created at the start of the transaction and closed when the transaction ends.
This design allows a singleton bean, like a repository, to work with transaction-scoped EntityManager instances, ensuring correct data access and manipulation across multiple threads.
A key point here is that the EntityManager injected in the repository is a thread-safe proxy that forwards calls to the actual EntityManager bound to the current transaction.
In summary
In Spring, we can safely access EntityManager in a singleton repository without enabling or implementing anything.
In contrast, .NET doesn’t have a built-in mechanism for accessing a shorter lifetime service in a singleton service.
But this doesn’t mean a repository in Spring runs more efficiently than the one in .NET. Even though the repository is singleton, it still creates its owner instance EntityManager for each thread.