1

I have 2 strategies named CompanySyncStrategy and ProjectSyncStrategy.

CompanySyncStrategy.cs

public class CompanySyncStrategy : ISyncStrategy
    {
        private readonly ICompanyService companyService;

        public CompanySyncStrategy(ICompanyService companyService)
        {
            this.companyService = companyService;
        }

        public void Sync()
        {
            //sync code
        }
    }

ProjectSyncStrategy

public class ProjectSyncStrategy : ISyncStrategy
{
    private readonly IProjectService projectService;

    public ProjectSyncStrategy(IProjectService projectService)
    {
        this.projectService = projectService;
    }

    public void Sync()
    {
        //sync code
    }
}

program.cs file looks like this

builder.Services.AddScoped<ICompanyService, CompanyService>();
builder.Services.AddScoped<IProjectService, ProjectService>();

builder.Services.AddScoped<ISyncContext, SyncContext>();
builder.Services.AddScoped<ISyncStrategy, CompanySyncStrategy>();
builder.Services.AddScoped<ISyncStrategy, ProjectSyncStrategy>();
builder.Services.AddSingleton<ISyncStrategyFactory, SyncStrategyFactory>();

when trying to get a strategy in runtime I get the following error from SyncStrategyFactory

System.InvalidOperationException: 'No service for type 'Strategy.Services.CompanySyncStrategy' has been registered.'

SyncStrategyFactory class looks like this

public class SyncStrategyFactory : ISyncStrategyFactory
    {
        private readonly IServiceProvider serviceProvider;

        public SyncStrategyFactory(IServiceProvider serviceProvider)
        {
            this.serviceProvider = serviceProvider;
        }

        public ISyncStrategy GetSyncStrategy(string strategyType)
        {
            switch (strategyType)
            {
                case "company":
                    return serviceProvider.GetRequiredService<CompanySyncStrategy>();
                case "project":
                    return serviceProvider.GetRequiredService<ProjectSyncStrategy>();
                default:
                    throw new InvalidOperationException();
            }
        }
    }

Can't I use dependency injection here? What am I doing wrong?

Ayesh Silva
  • 88
  • 1
  • 6
  • 1
    Instead of letting `SyncStrategyFactory` depend on `IServiceProvider`, why don't you inject `CompanySyncStrategy` and `ProjectSyncStrategy` directly into the constructor. Or, alternatively, inject the `IEnumerable` in the constructor, which basically results in the solution that @johnmoarr suggests. – Steven Aug 23 '23 at 15:25
  • I didn't thought of this too. I will try this as well. Thank you. – Ayesh Silva Aug 25 '23 at 05:39

2 Answers2

2

There are two issues with your code:

1.: the services registered with an interface cannot be resolved by their implementation type.

2.: a service being registered with scoped-lifetime can normally not be resolved within a singleton-lifetime service please check this answer. Consider registering them with a different lifetime or create a scope when resolving them.

If you don't wanna change your registrations though, this might work:

// service registrations
builder.Services.AddScoped<ISyncStrategy, CompanySyncStrategy>();
builder.Services.AddScoped<ISyncStrategy, ProjectSyncStrategy>();
builder.Services.AddSingleton<ISyncStrategyFactory, SyncStrategyFactory>();

// SyncStrategyFactory
public ISyncStrategy GetSyncStrategy(string strategyType)
{
    switch (strategyType)
    {
        case "company":
            using (var scope = serviceProvider.CreateScope())
            {
                return scope.ServiceProvider.GetServices<ISyncStrategy>().First(s => s is CompanySyncStrategy);
            }
        case "project":
            ...
        default:
            throw new InvalidOperationException();
    }
}
johnmoarr
  • 430
  • 3
  • 7
  • Your solution worked. Thanks a lot. – Ayesh Silva Aug 23 '23 at 10:29
  • Wrapping the `GetSyncStrategy` call in a scope is a *very bad* idea, because when the scope is disposed, so will all its created disposable dependencies. The returned `ISyncStrategy` implementation (or one of its dependencies) might very well implement `IDisposable`, in which case the consumer of the factory will break. And if it works today, it might suddenly (and confusingly) break in a month (when a dependency becomes disposable). In other words, the only solution is to lower the factory's lifestyle to scoped as well. – Steven Aug 23 '23 at 15:24
  • That is why I linked Chris Pratts answer when I also suggested to consider changing the lifetime. It might be best to re-think the whole design approach. – johnmoarr Aug 24 '23 at 08:56
1

Your issue lies within the way you register your strategies. You're registering strategies with the interface ISyncStrategy and not CompanySyncStrategy. So when you ask for a CompanySyncStrategy it does not know it because it is only the resolved type for a ISyncStrategy.

In order to get what you want you need to register CompanyStrategy & other directly like: builder.Services.AddScoped<CompanyStrategy>();

Another way that I like more and offers better options of adapting to later strategies would be to make the Interface generic ISyncStrategy<T> and then have the CompanySyncStrategy : ISyncStrategy<Company> (or whatever offers itself as the generic discriminator) Registering like builder.Services.AddScoped<ISyncStrategy<Company>, CompanySyncStrategy>();

And finally using the factory like serviceProvider.GetRequiredService<ISyncStrategy<Company>>();

Pio
  • 513
  • 4
  • 19