๐Ÿ—๏ธ Friend Assemblies compromise for Software Engineering

A practical way in .NET to encapsulate modules' internals, helping you drive a good-enough software architecture

๐Ÿ—๏ธ Friend Assemblies compromise for Software Engineering
Image source: https://devan.codes/blog/2019/1/31/the-problems-of-selling-a-modular-software-system

If you're a software engineer ๐Ÿ‘ท, you most definitely use some kind of architectural design pattern like Domain-Driven-Design principles (๐Ÿšซ not DDD) or IDesign principles or something else; doesn't matter that much for the sake of this article.

Now, with any kind of architectural pattern you need some kind of modularization ๐Ÿ“ฆ and therefore some means of encapsulating the internal workings of these modules.

For instance, in DDD, you don't want all the internals of <<say>> the accounting domain to be exposed to <<say>> the manufacturing domain.

Or in IDesign, you don't want to expose engines and resource accessors to consuming clients.

Or in general, you don't want to expose the internal business logic and models to the consuming client.


In an ideal world ๐ŸŒŸ, with unlimited time, effort and money, you'd achieve this by completely separating the contracts (aka interfaces and exposed models) from their concrete implementations AND truly decouple them! Meaning no compile time reference to the concrete csproj! Such binding would need to happen only at runtime based on some conventions or dependency config files and factories. And this would need to be done for each and every module, with no exception. And make no mistake, I totally ๐Ÿ’ฏ recommend this approach if you have the luxury to do so!

Ideally, only the shells of other modules are directly referenced

Unfortunately, in our real world, more often than not, โœŒ๏ธthe software engineersโœŒ๏ธ go full JavaScript style ๐Ÿคข and reference everything from everywhere, most of the time without even realizing it, through ignored reference chains.

And just like ๐ŸŒฎ Seรฑor JavaScript/FE ๐Ÿคข developers ๐ŸŒฎ, when corners are cut and ๐Ÿ’ฉ is produced, they leave it there, call it technical debt, and comfort themselves that they'll compensate it with self discipline. And that discipline lasts for a day or two until they need to switch focus on something else.

In many cases, they even do put interfaces on the concrete types but blindly, just for the fun of it, without any practical value.

"Just because that's how good software looks like ๐Ÿง ... and it has something to do with the I in SOLID..." ๐Ÿคฆ ๐Ÿ˜”.


The fully decoupled approach, is clean, sexy, beautiful, sublime, extensible, solid, maintainable but hard to sell to most of nowadays โœŒ๏ธsoftware engineersโœŒ๏ธ and time consuming and effort intensive compared to the latter ๐Ÿ’ฉ, which is very handy and fast to write but it's painful if not impossible to upkeep for the long run. Trade-offs, as usual.


๐ŸคHello, Friend Assemblies!

So, a good-enough compromise to mitigate the effort of encapsulating the concrete internals of modules, in .NET, is to use friend assemblies.

With this approach you tell the compiler that all internal artifacts of a given .NET assembly act like public artifacts only for other specific friend assemblies. And that's how you achieve module encapsulation!

This is done via the InternalsVisibleTo attribute.


As a usage example, say our DataAggregatorA module exposes itself only to BusinessLogicA and BusinessLogicB modules. Therefore, everything in DataAggregatorA needs to be marked internal, and then it has to explicitly declare the two friend assemblies. Like below.

In DataAggregatorA csproj, we add a new root file called FriendAssemblies.cs with the following content.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("BusinessLogicA")]
[assembly: InternalsVisibleTo("BusinessLogicB")]

Friend Assemblies syntax

And that's it. Module encapsulated. Any assembly other than BusinessLogicA or BusinessLogicB that references DataAggregatorA, directly or indirectly (via a reference chain that contains BusinessLogicA or BusinessLogicB), will not have any visibility inside DataAggregatorA.

The effort for doing this is minimal, and the benefit is huge, as you mimic a great deal of that utopian encapsulation scenario. So there's no real excuse for not doing it!


๐ŸŽ“Encapsulation Example

Encapsulation via Friend Assemblies

Let's analyze the depicted example.

  1. We have a consumer module that needs to run a given business operation for a specific input model and get back a specific result model.
    1. As it should, the consumer only knows about the business operation and the exchange models. Nothing more, nothing less.
  2. The business operation module needs to orchestrate various internal business logic modules to achieve the end result. Therefore it needs to know about:
    1. The business logic modules
    2. The internal data models to interact with the business logic modules
    3. The public data model to project the end result
  3. Each business logic module needs to know about some internal resource modules to be able to do their thing.
  4. The resource modules don't need to know about any other module.
Each piece can be a separate module, or grouped together per layer, like ResourcesLayerModule, BusinessLogicsLayerModule, BusinessOperationsLayerModule, PublicModels, PrivateModels, etc.

This architecture would translate into the following friend assemblies:

  1. Internal DataModel and Operations are friends with and referenced by:
    1. Resources
    2. Business Logic
    3. Business Operations
  2. Resources are friends with and referenced by:
    1. Business Logic
  3. Business Logic are friends with and referenced by:
    1. Business Operations
  4. Business Operations are not friends with anyone, they expose public contracts and models for consumers.

In IDesign.

Friend Assemblies in IDesign
  1. Internal Model is friend with and referenced by:
    1. Resource Accessor
    2. Engine
    3. Manager
  2. Resource Accessor is friend with and referenced by:
    1. Engine
    2. Manager
  3. Engine is friend with and referenced by:
    1. Manager
  4. Manager has no friend assemblies, but references both internal model and public model and handles the mapping between the two.
  5. Client references manager and public model but has no access to internal engine, resource or model, even though they're part of the reference chain.

A further reading that adds to this topic and presents a neat way to hide the concrete implementations between friend assemblies themselves:

https://hintea.com/internal-modules-concrete-encapsulation/