dotnet package visibility
Suppose we are building new project.
The simplest starting point will be just to create an web project and put everything inside.
But sooner or later we will realize that we also need some kafka consumers, cron jobs etc so we gonna need to have some kind of shared library.
In other words appearance of packages is just a matter of time.
Packages has few additional offside effects.
Package may be used to hide actual implementations and give only interfaces to outside users by marking implementations as internal
, and because of that we are enforcing usage of interfaces, which dramatically simplifies creating of unit tests and future changes of implementations.
And if we are started to use packages why not split them even more, to hide some low level stuff from high level components, e.g. imagine you have OrdersRepository
and OrdersService
in such case we do not want OrdersController
to know about low level repository stuff, thats why we may want to split them even further.
Probably the simples and more famous way will be something like:
- Consumer
- Web
- Services
- Repositories
In reality if we will change Services to Domain, and Repositroies to Infrastructure nothing really wont be changed it is more about starting point and naming preferences.
In example I am using Services and Repositories by intent, because in dotnet we have ServiceProvider
which responsibility is to provide services to us and that sound pretty clear and straight forward to me.
The next step and question is how to organize everything to be "pretty"
Here are sample classes:
- Repositories
- public record Order
- public interface IOrdersRepository
- internal class SqlOrdersRepository
- Services
- public record Order
- public interface IOrdersService
- internal class OrdersService
- Web
- public class OrdersController
So our dependency graph is: Web <- Services <- Repositories
Sounds really cool on paper
From Web project I have only IOrdersService
and do not know nothing about actual implementations and can not inject it directly.
From a Services project I do not know nothing about Web, and in the same way know only about IOrdersRepository
and not about its implementations.
With such setup following code wont even compile:
public class OrdersController {
private readonly OrdersService _service;
public OrdersController(OrdersService service) {
_service = service;
}
}
Because actual implementation is marked as internal
and I can not use it outside, so I am forced to use interface instead.
But there is still one more problem, technically I still may do something like this:
public class OrdersController {
private readonly IOrdersRepository _repository;
public OrdersController(IOrdersRepository repository) {
_repository = repository;
}
}
Which is exactly the problem we wish to get rid off, imagine that in few years some new comer will decide that it is much easier for him to just inject orders repository and write something to storage, but he will bypass all rules in corresponding service which will lead to possible errors and makes mess.
To do this impossible in our Servicess project we should mark repositories as an private assets by adding PrivateAssets="All"
attribute, so they wont be visible to anyone outside
<ItemGroup>
<ProjectReference Include="..\Repositories\Repositories.csproj" PrivateAssets="all" />
</ItemGroup>
Now from Web layer, I technically can not talk directly to Repositories bypassing Services - profit
Note about service provider
Projects are using Microsoft.Extensions.DependencyInjection.Abstractions
and extension class to register them selves in service provider
From Web we have:
builder.Services.AddOrdersService();
From Services:
public static class ServiceCollectionExtensions {
public static void AddOrdersService(this IServiceCollection services) {
services.AddOrdersRepository();
services.AddSingleton<IOrdersService, OrderService>();
}
}
And from Repositories:
public static class ServiceCollectionExtensions {
public static void AddOrdersRepository(this IServiceCollection services) {
services.AddSingleton<IOrdersRepository, SqlOrdersRepository>();
}
}
Thanks for that in Web we have:
IOrdersRepository
- invisibleIOrdersService
- visiblenew SqlOrdersRepository()
- Can not resolve symbolnew OrderServiceImpl()
- Can not access internal class here
If you will try to visualize your current projects graph you may see something like: Web <- Infrastructure <- Domain, which looks weird and seems like it is not only has wrong direction but shuffled
Note about unit tests: to make internal classes withible to test projects you may want to add following in your project <ItemGroup><InternalsVisibleTo Include="Repositories.Tests" /></ItemGroup>
Prevent csproj reference
For now we have desired: Web <- Services <- Repositories
But nothing wont prevent me to reference Repositories in Web and use it directly
To avoid that we may add something like this in Web csproj:
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Error Text="'Repositories' are not allowed to be referenced by 'Web'" Condition="$([System.IO.File]::ReadAllText('Web.csproj').Contains('<ProjectReference Include="..\Repositories\Repositories.csproj"'))" />
</Target>
And from now on if someone by mistake will decide to reference it - project wont even build and hopefully it will be a signal for engineer that he is doing something wrong.
Custom rules or avoid warnings
Adding <WarningsAsErrors>true</WarningsAsErrors>
to PropertyGroup
will broke build for all possible warning (aka it will be like golang which does compile if you have unused variable)
More gentle way might be creating custom rulesets
analysis.ruleset
<RuleSet Name="Rules for Web" Description="Code analysis rules for Web.csproj." ToolsVersion="15.0">
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CS0219" Action="Error" /> <!-- disallow unused variables -->
</Rules>
</RuleSet>
And adding it as <CodeAnalysisRuleSet>analysis.ruleset</CodeAnalysisRuleSet>
in PropertyGroup
Dotnet CLI has build it editorconfig dotnet new editorconfig
which creates some configuration with default values
So I did tried to create dedicated editorconfig for repositories with contents like this:
dotnet_naming_symbols.types_and_namespaces.applicable_kinds = class
dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = internal
dotnet_naming_symbols.types_and_namespaces.required_modifiers =
But have no luck
TODO: is there an rule or another way to enforce internal classes in project?
Project Resistance (stability)
Ask yourself: If I will modify this class in this project - how many changes in other projects it will require?
If the answer is 0 - then this project is unstable, you are free to change it as much as you want
If the answer is quadrillion - then this project is stable, so probably no one want to touch it because it will require to rewrite the world
Project is resistant (stable) if it has no dependencies, but other projects depends on him (aka: it is hard to make changes inside, they will propagate)
Project is non resistant (unstable) if it depends on other projects but no one is depend on him (aka: it is easy to make changes inside, nothing will be propagated)
Usually projects will be somewhere in between (flexible), except the edges of project (storage, web, ...)
The rule of thumb is that stable project should not depend on unstable
In our case:
- Repositories is stable - it has no dependencies, but Services depends on it
- Services is flexible - it depends on Repositories, and Web depends on it
- Web is unstable - it depends on Services, and no one depends on it
So we are in rule: less stable projects are depending on more stable ones.
Note that neither stable and/or unstable are not good not bad, at the very end we want everything to be flexible enough to evolve
CCP: Common Closure Principle
Classes that are changed together at same time should live inside dedicated project. Classes that are changed independently or in different times should live in separate projects.
Is is something like single responsibility principle but on upper level for projects.
Having that the answer to question: Should Sql/Elastic/Redis implementations live inside Repositories project should be obvious.
Stable Abstractions Principle
How can we add some flexibility to a very stable project? The answer is to use abstraction classes.
Stable project should be also abstract (consist of interface and/or abstract classes), so its stability does not prevent extensions and evolution.
Unstable project should be concrete, so it is easy to make changes.
AKA: it is fine to have single project consist of single interface, it is like inversion of dependencies, imagine that you have situation when for some reason your stable project becomes dependant on unstable, how to fix that? Just create yet another project with single interface - it will be very very stable, so from now on, your stable project depends on even more stable which is good, and your unstable project just adds implementation and continues to be as unstable as he want.
Also it is related to Common Reuse Principle (CRP) - which asks you to not make more publicly available stuff to outside users than really needed
Package Resistance and Abstraction Metrics
SDP: I should decrease from top to bottom
Web (I = 1) <- Services (I = 0.5) <- Repositories (I = 0)
- Repositories (stable, pain):
- I = 0, I = Fan-out / (Fan-in + Fan-out), Fan-in = 2 (OrdersService depends on IOrdersRepository, Orders service registration depends on Repository), Fan-out = 0 (Repositories does not depend on anything)
- A = 0.25, A = Na / Nc, Na = 1 (only one interface IOrdersRepository), Nc = 4 (four classes in total)
- Services (flexible):
- I = 0.5, Fan-in = 2 (OrdersController depends on IOrdersService, service registration), Fan-out = 2 (OrdersService implementation depends on IOrdersRepository, service registration)
- A = 0.25
- Web (unstable):
- I = 1, Fan-in = 0 (No one depends on Web), Fan-out = 1 (OrdersController depends on IOrdersService)
- A = 0
Notes:
- I = 1 - unstable, can be changed often
- I = 0 - stable, changes are hard
- I = 0, A = 0 - pain, everything is concrete rather than abstract, and changes will propagate
- I = 1, A = 1 - useless, everything is abstract and not used, dead code
- stdvar(D) > X - requires attention something wrong
- projectA (I = 0, stable) depends on projectB (I = 1, unstable) - broken SDP principle
NDepend in its reports for assemblies has all this and may be used in CI pipeline
with trial I was able to analyse all this
dotnet ./net6.0/NDepend.Console.MultiOS.dll RegEval
dotnet ./net6.0/NDepend.Console.MultiOS.dll -cp solution.ndproj /path/to/solution.sln
dotnet ./net6.0/NDepend.Console.MultiOS.dll solution.ndproj
Note: NDepend calculates internal classes as well that's why numbers are different but overall the shape is same