Using .NET Access Modifiers
Access modifiers are an often overlooked super power in programming languages. I’m often guilty of making all classes ‘public’. I’m beginning to see the error of my ways.
I was watching a great talk by Simon Brown where he states:
“If all types are public, Java packages are about organization of code rather than encapsulation” Modular monoliths by Simon Brown https://www.youtube.com/watch?v=kbKxmEeuvc4
That hit me hard. It made me realize exactly how my approach (of using public for all classes) has been a pretty silly approach to follow. Let’s explore how to make things better.
Example
Let’s create an assembly called “Domain” and have it contain an interface and a class that implements that interface.
namespace Arch101.Domain;
public interface IFoo
{
void Bar(DateTime now);
}
public class Foo : IFoo
{
private readonly ILogger<Foo> _logger;
public Foo(ILogger<Foo> logger)
{
_logger = logger;
}
public void Bar(DateTime now)
{
_logger.LogInformation("Bar called at {now}", now);
}
}
Having the Foo
class be publicly accessible is the concern here. It breaks the encapsulation because anything can instantiate an instance of Foo
without having to use the IFoo
interface.
In .NET, I would use the built-in dependency injection (DI) framework to register the service to the interface:
builder.Services.AddTransient<IFoo, Foo>();
And then create instances using the service provider:
var app = builder.Build();
var foo = app.Services.GetRequiredService<IFoo>()
foo.Bar(DateTime.Now);
Now we have a second break in the encapsulation. The root of the DI container now has to know how to map the IFoo
interface to the Foo
implementation.
Modular Approach
To address these encapsulation concerns, C# provides access modifiers to limit which parts of the code can access a type.
Access Modifier | Description |
---|---|
public | The type or member can be accessed by any other code in the same assembly or another assembly that references it. |
private | The type or member can be accessed only by code in the same class or struct. |
protected | The type or member can be accessed only by code in the same class, or in a class that is derived from that class. |
internal | The type or member can be accessed by any code in the same assembly, but not from another assembly. |
protected internal | The type or member can be accessed by any code in the assembly in which it’s declared, or from within a derived class in another assembly. |
private protected | The type or member can be accessed by types derived from the class that are declared within its containing assembly. |
Types with a public
access modifier can be created by anything in or outside of your assembly.
If we look back a Simon Brown’s quote, this translates to:
“If all types are public, .NET assemblies are about organization of code rather than encapsulation”
Why create a “Domain” project? If all the classes are defined as public
then it is not for encapsulation. The only thing it helps with is code organization.
Let’s fix this in a simple way by changing the access modifier of the Foo
class to internal
. This will cause the registration of the IFoo
service to fail:
builder.Services.AddTransient<IFoo, Foo>();
// error: CS0122: 'Foo' is inaccessible due to its protection level
To fix this, you need to move the code that registers the services to the same assembly. Creating a new extension method in the Domain project and copy the registration there:
namespace Microsoft.Extensions.DependencyInjection;
public static class RegisterServices
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
services.AddTransient<IFoo, Foo>();
return services;
}
}
Notice the namespace is Microsoft.Extensions.DependencyInjection
. This is a recommended convention. It reduces the number of using
directives needing to added to the Program.cs
file and makes the extension method more discoverable.
Then the DI root uses the new extension method:
builder.Services.AddDomainServices(builder.Configuration);
The built-in DI framework is capable of creating internal classes, so there is no issue with this approach from that perspective. This takes care of two problems. The Foo
class is now encapsulated within the Domain assembly as is the registration that maps the IFoo
interface to an implementation. From the perspective of a use of the Domain assembly, the only thing they are aware of is the IFoo
interface. How it is implemented is of no concern.
What about Testing?
In .NET, the de-facto approach to organizing your unit tests is to places them in a separate assembly. Once you make the Foo
class “internal”, the unit tests won’t be able to access the class to create test cases. You are left with 2 options. First, you can add your tests to the same assembly where the classes under test exist. This is a popular approach in other programming languages (TypeScript/JavaScript for example). But it has never been popular in .NET projects. However, it is possible. You can add your tests to the same assembly as the classes being tested.
The second approach is to add an assembly attribute that tells the compiler to allow another assembly to access internal types in another assembly. In the Domain project, adding the following will make the internal classes, like Foo
to be visible to the project where the unite tests are coded:
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("UnitTests.Arch101.Domain")]
I plan to explorer these 2 options a bit more in a future blog post.
Modular Monolith
This approach to encapsulation has become popular in recent years due to the rise of the ‘Modular Monolith’ architecture style. I hope to spend more time exploring this style of architecture to see what its benefits and drawbacks are.