2

I have a library that provides access to our content management. It consist of a public ContentService and an internal ContentRepository. The ContentService is public and Dependency injected through out our solution. The ContentRepository is only used by the ContentService.

My issue is that I am passing 3-4 arguments to ContentService, that are only used to new a ContentRepository in the ContentService ctor.

I do not like this because,

  1. I am passing many arguments to ContentService.
  2. ContentService has low cohesion because I am passing in 3-4 arguments that are not used in the other methods, and only in the ctor.
  3. I am creating a dependency in ContentService by using the new keyword to create a ContentRepository.

I would like to solve 1-3 by dependency injecting ConentRepository into ContentService. My only issue is that ContentRepository is internal, rightfully so because it should only be used by ContentService ( do not want it exposed, I only want my consumers of this APi to use ContentService)

So how can I properly solve 1-3 and not expose ContentRepository?

4 Answers4

1

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.

Flater
  • 49,580
0

Yes, you should remove these 3-4 arguments from the ContentService constructor and just pass the already constructed repository, preferably behind a public interface.

If you made the class internal, you should have a very good reason. The internal modifier makes your life harder because it increases the complexity of your code. You have private, meaning "noone else knows about it", public meaning "everyone else knows about it", and internal meaning, "only a few know about it" (but not the few you choose, only your own few).

You want the consumers of your API to use ContentService only and not ContentRepository. This is not enough of a reason to make a class internal (you can always make the constructor private if you never want them to instantiate it).

While there are various "tricks" you can use to simply "hide" the ContentRepository, I will just say the first thing that comes right off the top of my head:

public class ContentServiceFactory
{
    public ContentService CreateContentService(/* the 3 parameters */)
    {
        ContentRepository repo = new ContentRepository(/* the 3 parameters */);
        return new ContentService(repo);
    }
}

If this class is inside the ContentRepository-containing assembly, the ContentRepository will be visible as internal. The DI project only sees the ContentServiceFactory and no internal ContentRepository class. But a ContentService instance can easily be created.

Does this look similar to your initial solution? Yes, because it is. What you are asking to achieve is:

So how can I properly solve 1-3 and not expose ContentRepository?

1-3 means you want to create and inject an instance of a class. Unless you hide it behind another class (but why would you get into that trouble?), there is no way to directly create an instance of a class that is internal to another assembly (with reflection being obviously out of the question). Registering the type in your DI container has the obvious prerequisite that the type is visible, which brings us to...

Workaround

If, for some reason, you have made the ContentRepository class internal so that it would not be visible to any other assembly, but want to make an exception, you can always use the InternalsVisibleTo attribute.

Vector Zita
  • 2,442
0

Having internal dependencies is a good practice, this will reduce affected scope in case you will made a change.
Because class is internal you can freely make changes without worrying that change will brake something you are not aware of.

You didn't mention what dependency injection container you are using, but with built-in ASP.NET Core implementation you can create an extension method for service collection in the project where you register service and repository class.
Extension method will have access to internal dependencies and you will be able to register any internal implementation without making it public or using Microsoft's hack as InternalsVisibleTo

public static IServiceCollection AddContentService(this IServiceCollection services)
{
    // Conditionally you can register others 3-4 dependencies here
    services.AddTransient<ConentRepository>();

    services.AddTransient<ContentService>();

    return services;
}

Than in root project where you are registering application dependencies you can simple call

services.AddContentService();
Fabio
  • 3,126
0

In the constructor of the ContentService you will accept an ContentRepository object of type ContentRepositoryInterface. You can keep this parameter optional.

If this parameter is not passed, you should have an internal function that would create the repository for you. This function can be mocked in case of your UnitTesting. So your new up of object won't be an concern.

No body external should know how to create a repository for your service, but if somebody wants to replace your ContentRepository, you are giving them the option through constructor injection. They can inject this dependency.

  1. I am passing many arguments to ContentService.

You can replace them with "introduce parameter object refactoring"

  1. ContentService has low cohesion because I am passing in 3-4 arguments that are not used in the other methods, and only in the ctor.

Not if you use a parameter object instead of 3-4 arguments.

  1. I am creating a dependency on ContentService by using the new keyword to create a ContentRepository.

Though you are new-ing up inside your service, you are giving a provision to inject it. So I don't think it would be a concern.

Another option

You can create a repository factory inside your domain, next to the service. Pass the 3-4 arguments to this factory and it would give you a Repository object. This is still internal to your package / service / domain. Inject this dependency in your service constructor.