Dealing With Circular Dependency Injection References

While mentoring a junior developer and trying to help them understand dependency injection, I came across a topic that I didn’t find a really great explanation out there on the internet. That is, when you end up with circular references while using dependency injection.

I found that there was some great resources at understanding circular references in general, and obviously great articles on the .NET Core dependency injection system, but not so many options when trying to jam them together. These were my explanations and example classes on how things work, so that hopefully they help someone else!

The Problem

Imagine a “simple” piece of code like below.

public class UserService
{
    private readonly PhoneService _phoneService;

    public UserService(PhoneService phoneService)
    {
        _phoneService = phoneService;
    }

    public string GetUserPhoneNumber()
    {
        var userId = 123;
        return _phoneService.GetPhoneNumberById(userId);
    }
}

public class PhoneService
{
    private readonly UserService _userService;

    public PhoneService(UserService userService)
    {
        _userService = userService;
    }

    public string GetPhoneNumberById(int phoneNumberId)
    {
        return "+64123123123";
    }

    public string GetUserPhoneNumber()
    {
        return _userService.GetUserPhoneNumber();
    }
}

Now the circular reference in this case is rather.. idiotic and I don’t expect anyone to realistically run into this sort of code in production, but it demonstrates the issue quite well.

You see, first someone created a UserService. This dealt with everything about a user. Next came a PhoneService, that dealt with everything to do with phone numbers. A smart developer then said, well given I have a UserService, and I want to get the phone number of a user, I’ll inject the PhoneService into the UserService, and then get the current users phone number.

But then along comes another developer. They say, well, if I have the phone number service, since I am dealing only with phone numbers, then I want to get the current users phone number. They noticed that there is already a method called “GetUserPhoneNumber” on the UserService, so they just inject that and call it. Remember, they may not even know that the way that UserService actually finds the phone number is by calling the PhoneService itself, but alas, it does.

And now we have a problem. For the UserService to be constructed, we need a PhoneService. But for a PhoneService to be constructed, we need a UserService. This is a circular reference. Luckily for us, .NET Core is pretty good at picking these issues up at the earliest possible runtime (But not compile time), and we usually end up with an exception.

A circular dependency was detected for the service of type 'UserService'.
UserService -> PhoneService -> UserService

It even directly points out the exact services which depend on each other. Note that in some cases, the circle could be much larger. For example

UserService -> PhoneService -> AnotherService -> UserService

But as long as the circle is enclosed, then you will get an exception.

The “Lazy” Fix / Service Locator

This is a terrible bandaid and I never recommend using this. Ever. It’s simply a bandaid to a problem that is very very easy to get wrong. Previously in .NET, you could do something like :

public class UserService
{
    private readonly Lazy<PhoneService> _phoneService;

    public UserService(Lazy<PhoneService> phoneService)
    {
        _phoneService = phoneService;
    }


    public string GetUserPhoneNumber()
    {
        var userId = 123;
        return _phoneService.Value.GetPhoneNumberById(userId);
    }
}

All the Lazy modifier did was not try and request the service until it was actually required. This meant that the UserService could be created without actually needing the PhoneService until the actual execution path was called, and so theoretically, while the constructors were running, they did not enter an infinite loop.

.NET Core now picks this up however and if you try any sort of the above with Lazy, it will stop you from doing so.

A circular dependency was detected for the service of type 'UserService'.
UserService -> System.Lazy -> PhoneService -> UserService

So then, people would just inject some sort of ServiceLocator. For example :

public class UserService
{
    private readonly ServiceLocator _serviceLocator;

    public UserService(ServiceLocator serviceLocator)
    {
        _serviceLocator = serviceLocator;
    }


    public string GetUserPhoneNumber()
    {
        var userId = 123;
        return _serviceLocator.Get<PhoneService>().GetPhoneNumberById(userId);
    }
}

This does compile and actually run, but again is simply a band aid to the problem. You still have a circular reference, it’s just that at some point you are killing the circle by using a ServiceLocator. Ugh.

I would put the use of “Factories” in here as well. I’ve seen people get around the problem by using a “PhoneServiceFactory” that itself uses a ServiceLocator or similar to construct the PhoneService for the UserService, so again you are breaking the circle. But again, you still have a circular reference you are just hacking your way out of it.

Kill The Reference

Far and away the best option for digging your way out of this hole is to not reference each other at all and if at all possible, have a one way reference between the services. So we *could* change our code to simple remove the GetUserPhoneNumber method from the PhoneService, and if you want a users phone number, you have to use UserService to get it.

public class UserService
{
    private readonly PhoneService _phoneService;

    public UserService(PhoneService phoneService)
    {
        _phoneService = phoneService;
    }

    public string GetUserPhoneNumber()
    {
        var userId = 123;
        return _phoneService.GetPhoneNumberById(userId);
    }
}

public class PhoneService
{
    public string GetPhoneNumberById(int phoneNumberId)
    {
        return "+64123123123";
    }
}

In our example, we really only had a circular reference out of convenience and not because we actually needed the method on both services. In some cases simply having a one way reference doesn’t work in which case…

Create A “Joining” Service

While not a great working example, what you’ll often have to do is create a “JoiningService”, for example a “UserPhoneNumberService”, where either the UserService and PhoneService both reference this service, or vice versa.

Often you can put all the shared functionality that both services need into this service. Or you can have this as an entry point to then call both the UserService and the PhoneService to get data instead of them calling each other.

Runtime Stack Overflows

If you have a circular reference that you attempt to get around using Factories, Lazy’s, ServiceLocators, or some other bandaid. What you might find is that you end up with one of the following errors.

System.InsufficientExecutionStackException
The program '[4176] iisexpress.exe' has exited with code -1073741819 (0xc0000005) 'Access violation'.

The most common reason is because you have registered a service as a factory in .NET Core DI :

services.AddTransient(s => new UserService(s.GetService()));

This is essentially a form of Lazy that the .NET Core DI container cannot pickup early on at runtime. What you are basically saying is “When you create the UserService, run this method”. Now “This method” is constructoring a new UserService, and trying to pull the PhoneService from the ServiceCollection. But because PhoneService references the UserService, we loop back on ourselves and execute this method again.

There aren’t really any safeguards against this because it’s hard to know at compile time exactly how things will be executed, but it’s a circular reference none the less.

2 thoughts on “Dealing With Circular Dependency Injection References”

  1. in my opinion, to better solve the problem to create one dependence:

    public UserService(PhoneService phoneService)
    {
        _phoneService = phoneService.WithUserService(this);
    }
    
    Reply
    • Your main problem there is you are breaking dependency injection by manually injecting dependencies. It does solve the DI blowing up, but IMO you still have a circular reference (Just not as obvious). But still, a solution is a solution!

      Reply

Leave a Comment