In object oriented programming, a class is held responsible for declaring how it can be created. This means that the accessibility of a class and its construction are both under the purview of the class access modifier. The access modifier determines exactly who can use, reference and create instances of this class.
What you want, is to separate these two responsibilities. You want your DI framework to be able to construct the class, but not use it in any other way. You essentially want to override the class' access modifier only for the constructor.
This can't be done directly, but there is a design pattern that exists specifically to outsource the construction of a class: the factory pattern. Essentially, the factory's responsibility is to wrap itself around your class' constructor(s) to act as its public interface.
This means that you are able to hide your class internally, as long as its interface is publically known and its factory is publically accessible.
public interface IProduct
{
string Name { get; }
}
internal class Product : IProduct
{
public string Name { get; set; }
}
public interface IProductFactory
{
IProduct Create(string name);
}
public class ProductFactory : IProductFactory
{
public IProduct Create(string name)
{
return new Product { Name = name };
}
}
Note that I'm using "product" and "factory" to easily distinguish one from the other for the sake of example. "Repository" is a more appropriate name for your specific context.
In your DI setup, you reference the factory, not the product.
serviceCollection.AddScoped<IProductFactory, ProductFactory>();
If you prefer to not have your services rely on product factories and instead want them to keep using the product directly, you could still register the product interface but rely on the factory to instantiate it:
serviceCollection.AddScoped<IProductFactory, ProductFactory>();
serviceCollection.AddScoped<IProduct>(
serviceProvider => serviceProvider.GetService<IProductFactory>().Create("foo")
);
You can vary this approach based on your preference/circumstances. You mentioned using AddSingleton
, which means you could opt to not register your factory in your DI, instead instantiating it yourself during the DI setup and using it to create your products (repositories):
var productFactory = new ProductFactory();
serviceCollection.AddSingleton<IProduct>(
serviceProvider => productFactory.Create("foo")
);
You can tailor this to what seems most appropriate to you.
The core of the solution is that the concrete Product
type does not actually get used outside of its own assembly. Only the ProductFactory
references it, and therefore it can be kept internal.
Notice how in all of the above examples, you never have to reference the concrete Product
type, because the factory acts as the middle man, shielding the Product
from the consumer (i.e. the project with the DI registration)
As a small aside:
I am passing 3-4 arguments to ContentService
, that are only used to new a ContentRepository
in the ContentService
ctor.
You already were using a factory pattern, but you had pushed that responsibility onto the ContentService
class. You should split that off into its own resposibility, i.e. the factory, to avoid violating SRP.
However, this is a good guideline. You already know exactly how to implement it.