0

I'm having an issue with dependencies in a C# app that I'm creating.

I have an assembly for my authentication process, and a separate assembly for starting up the main program once authentication is complete.

My issue is where to put the interfaces between these two systems. The main program assembly doesn't need to know anything about authentication other than its state (signed in/not). The authentication assembly doesn't need to know anything about the main program other than when to start authenticating (sign in/ sign out events).

Now the issue I'm facing is how to link up these two assemblies. I currently have a 3rd assembly called "AuthenticationLinker" that references both assemblies. This 3rd assembly forwards its own signing in state and events between these two assemblies using their public interfaces, so they don't need to reference each other.

But this 3rd assembly seems to be required no matter where I use my authentication assembly. This seems like a smell to me, since it's always required, why not just put those scripts in the authentication assembly?

I think I'm missing something here that would more elegantly link up these assemblies. I'm hoping to reuse my authentication assembly in other projects.

I see 3 options:

  1. Make sign-in state and events part of authentication assembly, reference that assembly from the main assembly. This means that it's more difficult to swap the authentication assembly in the future in this program.

  2. Keep my go-between assembly. Any future changes in authentication will only mean changing this assembly, but then I have an extra assembly to keep track of.

  3. Make the authentication assembly reference the main assembly. I think this is the least good option since then I can't use the authentication code anywhere else.

Although here I'm talking about authentication, I've run into similar issues with other situations, where I have two relatively independent assemblies that somehow need to communicate with each other.

Are there any established ways to deal with this scenario? I'm sure it's a common thing people need to do.

2
  • It’s OK to put the interfaces in a separate assembly. – John Wu Apr 14 at 19:14
  • @JohnWu is what you mean similar to what Flater is talking about with his B.Interfaces project? (In the answer below) – Adam B Apr 16 at 3:42
2

You've touched on the main options for project (or assembly) dependency graphs.

There is no one-size-fits-all answer here. It depends on many different environmental circumstances. Instead, I'm going to offer you an insight into where the strong points of each approach are, or common real-world use cases I've encountered.

The main program assembly doesn't need to know anything about authentication other than its state (signed in/not). The authentication assembly doesn't need to know anything about the main program other than when to start authenticating (sign in/ sign out events).

The main issue I have with your approach is that you describe these two assemblies as needing each other.

If these are the only two relevant assemblies (forget your third joined assembly for now), then we'd be discussing a parent-child-like relationship, i.e.:

┌────────┐
│   A    │
└─▲────┬─┘
  │    │
  │ OR │
  │    │
┌─┴────▼─┐
│   B    │
└────────┘

Which means that the dependency itself is one way. A can depend on B, but not the other way around at the same time.

I suspect there is a third player in the game who you haven't mentioned (and I don't specifically mean your proposed "AB" go-between assembly). An assembly which, in the grand scheme of things, consumes both of the other assemblies.

┌────────┐ ┌────────┐
│   A    │ │   B    │
└───────┬┘ └┬───────┘
        │   │
        │   │
      ┌─▼───▼─┐
      │   C   │
      └───────┘

This means that A and B are no longer parent-child, but rather siblings. Note that I'm not going to differentiate these familial relationships any further. I'm not interested in labeling aunts, grandchildren, or third cousins twice removed. You're either a sibling (= not each other's ancestor) or parent-child (= one is the ancestor of the other - this includes considering a grandparent as a parent).

This is why I prefer speaking about projects instead of assemblies. "Assembly" very much stresses independence, whereas "project" inherently imply that there's a bunch of them with a particular relationship between them. I'm aware that this is a semantic argument.

What matters most is what I call the TLA (top level application), i.e. the project/assembly that you build and release and who (directly or indirectly) depends on all of your codebase's other projects.
In absence of concrete information, I'm going to refer to this as the TLA. Think of it like a web project with REST controllers, for the sake of example, but it can be any kind of assembly, as long as it's the one you build and which starts the runtime.


Before we get any further, I want to point out that when I say A=>B, I mean that B is the library and A is the consumer of B. Some people instinctively prefer the arrows the other way, so I'd rather be clear.


Parent-child dependency

In a parent-child dependency structure, there is a very simple chain of dependency whereby the consumer depends on the library. For naming's sake, let's assume A is the library and B is the consumer.

┌───────┐   ┌───────┐
│   A   ◄───┤   B   │
└───────┘   └───────┘

Since B consumes A, let's say that there is an AService with an IAService interface, and a BService which wants to have an IAService dependency.

If it helps to have a concrete example, thinks of A as the data layer (repositories) and B as the business logic (services). Regardless of the approaches we discuss in this answer, conceptually we would say that B makes voluntary use of A, i.e. the business logic chooses to store data (or not).

As an aside, in this example B would be the TLA. This is not relevant now but will be brought back up later.

The core question is where do we put IAService, in A or B?. Well, annoyingly, there are two options here: classic and inverted.

Classic dependencies

In a classic dependency structure, the interface and its implementations belong to the library. It's technically possible to separate A into A.Interfaces and A.Library projects, but since B would always have to depend on both, the distinction between them is meaningless (for this specific example - not other examples I may get into).

┌───────────┐  ┌───────────┐
│     A     ◄──┤     B     │
├───────────┐  ├───────────┤
│ IAService │  │ BService  │
│ AService  │  │           │
└───────────┘  └───────────┘

When do we use this?

Most commonly, this is the approach you use when A is an independent library whose designed is driven by its own personal considerations. For example, if you think of A as "the database team" and B as "the business logic team", if the database team has designed their own database system without input from the business logic team.

Inverted dependencies

With inverted dependencies, we unsurprisingly do it the other way around, where IAService lives in B, not A, and then we make A depend on B instead (= inverting the dependency)

┌───────────┐  ┌───────────┐
│     A     ├──►     B     │
├───────────┐  ├───────────┤
│ AService  │  │ IAService │
│           │  │ BService  │
└───────────┘  └───────────┘

This may come across as a useless complication, but there is a very subtle reason as to why you want to do this. Now, B has control over the contract that A has to adhere to. B suddenly has a lot more power in this relationship.

Going back to the example of thinking of A as "the database team" and B as "the business logic team", suddenly the database team is no longer completely free to do as they please. The business logic team have defined a very strict contract that the database team must follow (IAService).
The database team is still free to choose their own implementation, but they are bound by the requirements set forth by the business logic team.

When do we use this?

As the example above makes clear, when B is the important one of the two, who gets to make demands of A. Instead of being subjected to whatever A chooses to make available, B gets to demand what is made available.

This is very common in Onion and DDD-driven architectures. The domain sits at the core of the onion, it has no dependencies (by design, making it strong to define the logic as it sees fit), but it does get to dictate what the outer layers should do for the domain, because it defines the interfaces for everyone. The outer layers exist purely to serve the domain, even though technically the outer layers have a dependency on the domain, and not the other way around.

However, there is a problem here. As A now depends on B, B can no longer be the TLA, since it doesn't depend on A and therefore cannot include it in its build process.

Inverted dependencies simply do not work in a pure parent-child relationship without involving at least a third project. This third project is either B's interface project:

                ┌──────────────┐
       ┌────────► B.Interfaces │
       │        └──────▲───────┘
       │               │
┌──────┴─────┐  ┌──────┴───────┐
│      A     ◄──┤      B       │
└────────────┘  └──────────────┘

But this is fairly uncommon in the real world. Either B.Interfaces is no longer considered to be part of B but rather as an independent "Interface" project, or instead of adding this B.Interfaces project, we add an encompassing third project (which is what the next section is about), and in that case the B.Interfaces project is also somewhat irrelevant.


Sibling dependency

Here, we inherently need a third project to exist. C depends on both A and B, and orchestrates any logic that depends on both A and B.

Note that C can be the TLA here, but it is not inherently required. C itself might be a library to the actual TLA (= D, a consumer of C).

┌───────────┐  ┌───────────┐
│     A     │  │     B     │
└─────▲─────┘  └─────▲─────┘
      │              │
┌─────┴──────────────┴─────┐
│            C             │
└──────────────────────────┘

Notice how there is no relation between A and B right now. This is a classic dependency. C simply uses A and B, and orchestrates any action between them as it sees fit. A and B don't know of each other's existence, C is responsible for all of that.

Just like before, in a classic dependency, everyone defines their own interface:

┌───────────┐  ┌───────────┐
│     A     │  │     B     │
├───────────┤  ├───────────┤
│ IAService │  │ IBService │
│ AService  │  │ BService  │
└─────▲─────┘  └─────▲─────┘
      │              │
      │              │
┌─────┴──────────────┴─────┐
│            C             │
├──────────────────────────┤
│        ICService         │
│        CService          │
└──────────────────────────┘

You would expect here that CService has both an IAService and IBService dependency and somehow uses them during its lifetime. This C is what you mean by your "go-between" assembly, if I understood you correctly?

When do we use this?

Compared to doing a classic A <- B <- C structure, in this case A and B are slightly looser coupled, because B no longer depends on A. Any logic that you would have had in B which makes use of A, now lives in C instead. In a way, you've split B into the A-agnostic part (still B) and the A-dependent part (now called C).

If you look at it, this is just two classic parent-child dependencies side by side. The use case is exactly the same. A and B get the freedom to define themselves however they want to, and then C consumes them as it sees fit.
The only difference here is because there's more than one child, the parent is able to orchestrate any interaction that depends on both of them.

But we really want to get at the inverted dependency approach here, because it adds an interesting opportunity which I think you're going to want. Let's revert back to the inverted parent-child dependency, but with C being involved now:

┌───────────┐  ┌───────────┐
│     A     │  │     B     │
├───────────┤  ├───────────┤
│           │  │ IAService │
│ AService  ├──► IBService │
│           │  │ BService  │
└─────▲─────┘  └─────▲─────┘
      │              │
┌─────┴──────────────┴─────┐
│            C             │
├──────────────────────────┤
│        ICService         │
│        CService          │
└──────────────────────────┘

Since C is our TLA (or any consumer of C), we no longer have the build process issue. This allows for everything we want:

  • CService can depend on both IAService and IBService
  • B can freely define what A must fulfill in the IAService contract
  • The build order logic is preserved.

When do we use this?

The interesting difference, when compared to the classic sibling dependency, is that in this case B knows of A's existence (through the IAService interface). Unlike before, B is not able to write logic that depends on A, because A chose to fulfill the IAService contract that B defined.

In other words, B defined a contract of its own design (IAService), is unaware of anyone who may or may not have implemented it, but B is able to write code that blindly assumes that someone has created a valid IAService implementation.

If no one ever implemented IAService, then C will be unable to instantiate any BService that depends on an IAService dependency (since there is no concrete implementation of that contract to pass to the BService instance.
But as long as some concrete type exists, it can be passed and this IAService-dependency BService can be instantiated and used.

So, in short, B gets to define a contract and consume it, without needing to know about it being implemented. It can blindly trust that at least one party (possibly more) will create a valid implementation of its contract.


Interface assemblies

When using inverted dependencies, it's fairly common for one project (B in our case) to start defining many interfaces for many of its siblings (A1, A2, ...), at which point it's fairly common for B to separate its non-B interfaces into a project of its own.

                ┌──────────────┐
      ┌─────────►              ◄──────────┐
      │         │ B.Interfaces │          │
      │       ┌─►              │          │
      │       │ └──────▲───────┘          │
      │       │        │                  │
┌─────┴─────┐ │ ┌──────┴───────┐   ┌──────┴──────┐
│     A1    │ │ │      B       │   │     A2      │
└─────▲─────┘ │ └──────▲───────┘   └──────▲──────┘
      │       │        │                  │
      │       │        │                  │
      │   ┌───┴────────┴─────────────┐    │
      └───┤            C             ├────┘
          └──────────────────────────┘

But you can start wondering here if B.Interfaces is really still a B project, or whether it's just a shared interface project that shouldn't be particularly named after B anymore.

When do we use this?

The main difference here is that B has lost its "command" over the design. Instead, the Interfaces project now decides the design, and A1, B and A2 merely implement what the Interfaces project dictates. Afterwards, C will collect all these implementations and let them interact with one another through their interfaces.

Compared to the previous example this allows a two-way street in the dependency graph. You could write a SomeA1Service which has an IBService dependency, while at the same time also having a SomeBService which has an IA1Service dependency. This gives you a lot of freedom in terms of defining the interaction between projects that are technically separated.

HOWEVER:

  • Beware cyclic references. These will usually not be caught until you're at runtime.
  • More often than not, having projects that interact in both directions is a first indication of bad layer design. This approach enables that two-way communication, but that can lead to enabling a bad architecture to be persisted when it should really be redesigned. Be very careful about putting all your eggs in this basket.
1
  • That’s so much for taking the time for this detailed discussion. I think this answers my question by giving me a more concrete way to consider the alternatives. In my scenario it sounds like your classic approach with C consuming A and B makes the most sense. In my head I had assumed that my go between would be some D library residing in its own land to connect A and B. But it makes sense that it would be part of the TLA, and the TLA can orchestrate actually creating these objects and passing them back and forth. Perhaps as the project grows I will expand to an interface project. – Adam B Apr 16 at 3:35
2

The core issue I see is your AuthenticationLinker having two responsibilities:

  • managing the authentication state

  • providing the "glue" between the "main assembly" and the authentication assembly.

Instead, there could be an AuthenticationState assembly which manages the actual state and references Authentication, and another assembly for connecting the parts together. The latter would probably not be named AuthenticationLinker any more, but something like "AggregateRoot" or "Root". The latter could be responsible for connecting all components of your system together, which may be more than just Main, AuthenticationState and Authentication.

This will have the following consequences:

  • AuthenticationState will be reusable for other projects which require stateful authentication (but projects which don't need stateful authentication have still the option not to use it). "Root", however, will contain the project-specific code, and so not interfer with those other projects.

  • Authentication can still be swapped out easily in case you need a different one in future

  • there will be another assembly to manage, of course, but hey, you cannot have the cake and eat it.

Note also when you can have "Root" being responsible exclusively for wiring up the application, you may be able to replace most of its functionality by using a DI container. Even if you don't go that route, "Root" will probably need no unit tests, only integration tests, whilst AuthenticationState will be more easily adressable by unit tests.

2
  • I agree with your analysis. I’m going to proceed along this route. I think this is similar to what @Flater said regarding the C project consuming both A and B and orchestrating how they interact. Thanks for making it a bit more applied to my actual project. It helped me understand that explanation a bit better. Perhaps in the future I may need this extra state project you mention. I think for now I can do without it until the package is used in more widely in different situations. – Adam B Apr 16 at 3:40
  • @AdamB: thank's for the response. What you observed is actually my critics to Flater's (surely correct) answer and the reason I wrote it - it appears to me more like a general essay about dependencies instead of an answer to the original question. – Doc Brown Apr 16 at 4:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.