Preventing/getting around circular dependency problems

From OpenPetra Wiki
Revision as of 14:15, 19 June 2012 by Christiankatict (talk | contribs) (→‎Setting up Delegates)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Introduction

This page that describes the recommended way of preventing/getting around circular dependency problems in OpenPetra's C# program code.


The General Problem: Circular Dependencies

A Method resides in Assembly 'A' and it needs to call a Method that resides in Assembly 'B'. This is fine as long as there is no code at all in Assembly 'B' that requires a Reference to be added to Assembly 'B' that references Assembly 'A' - this would create a 'circular dependency', i.e. Assembly 'A' would reference Assembly 'B' and Assembly 'B' would in turn reference Assembly 'A'. Such referencing creates a problem for the C# compiler as it cannot compile Assembly 'A' without Assembly 'B' to be built before and it can't build Assembly 'B' without Assembly 'A' to be built before; and therefore neither Assembly can be built (the 'chicken and egg problem').

Note: A circular dependency can also be much trickier as the dependency circle can involve several Assemblies that have Assembly References pointing only one way (and that is fine) but the last Assembly then has a Reference back to the first Assembly, therefore closing the circle and introducing a circular dependency. The C# compiler will find that out and will refuse the compilation of any of the Assemblies involved.

The Particular Problem: Circular Dependencies between Module Assemblies of OpenPetra

Since we structure the program code of OpenPetra into Assemblies that go along with the OpenPetra Modules, the need arises from time to time to make calls to program code that resides in a different Module (=Assembly) than the Module (=Assembly) the current program code is defined in. As such calls are spread through the client side and the server side code of OpenPetra and we are writing more and more program code in different Assemblies, circular dependencies between the OpenPetra Module Assemblies are arising more and more - on the client side as well as on the server side.


The General Solution to Circular References

While there are several solutions to solving circular dependency, some are more preferred than others for reasons that I can't describe here in full (bad performance and missing compile-time checks are some of the reasons that make certain solutions less preferred).

A solution that provides non-degraded performance and that allows for compile-time checks is the use of C# Delegates. The set-up of a Delegate that points to a Method in an arbitrary OpenPetra Server DLL and that will not cause a circular dependency is described in the following sections.

Creating a Delegate for a Method

Intro to Delegates

(You can skip this paragraph if you know about Delegates!)

A 'Delegate' is the type-safe C# way of creating a pointer to a function that is early bound and not late bound (for a discussion on early and late binding see this article). The function that the Delegate is pointing to is set up only at runtime, but since a Delegate is early bound there is no danger of it not working at runtime or of accidentally pointing it to a Method with a different signature (i.e. Return Value and Arguments), which would indeed have devastating consequences at run time. A Delegate is therefore perfectly safe to use.

Declaration, Means of Setting-up and Means of Invocation of a Delegate

An OpenPetra file that has all in place that is needed for the declaration, the means of setting a Delegate up and the means of invoking a Delegate safely is this file:

\csharp\ICT\Petra\Shared\lib\MCommon\validation\Helper.cs

Explanation:

  • The Delegate TSharedGetData declares the signature of a Method that can be called at run time by invoking the Delegate. It does not point to any specific Method in the program code, though!
  • Some code outside of the TSharedValidationHelper Class needs to pass in a reference to a Method that matches the exact signature of Delegate TSharedGetData. It does this by assigning the Property 'SharedGetData'. This is how the Delegate is set up at run time.
  • Some other code outside of the TSharedValidationHelper Class can call Method 'GetData' to invoke the Method that got passed in from other outside code (by assigning the Property 'SharedGetData'). This is how the Delegate is invoked at run time. The Method throws an Exception if it gets called and the Delegate wasn't set up before.

You will notice that everything in this file is strongly typed and can therefore be checked by the compiler, i.e. everything is early bound, although nothing in this file specifies which Method will be called at run time.

Setting up the Delegate

Program code that resides in an Assembly which has a Reference to the Assembly in which the Method is declared that is to be executed at the point in time when the Method for the invocation of the Delegate is called (in this example it is the 'GetData' Method) needs to 'set up' the Delegate once, i.e. it needs to establish what Method should be called once the Delegate gets invoked.

In the example of the TSharedGetData Delegate this happens in Method 'InitialiseClasses' in file \csharp\ICT\Petra\Client\app\MainWindow\PetraClientMain.cs for the client side, and in the Constructor of the 'TCallForwarding' Class in file \csharp\ICT\Petra\Server\lib\CallForwarding\CallForwarding.cs for the server side. You see there that the notation for setting up a Method that a Delegate should execute is '= @MyMethod' (this code effectively establishes a type safe function pointer at run time).

Invoking the Delegate

Program code that wants to execute the Method that the Delegate is pointing to needs to call the Delegate. In doing so, it will effectively perform a call to the Method that the Delegate was set up to point to.

The thing to watch out for when doing this is that the Delegate 'was set up to point to some matching Method' before the Delegate is executed. Code that does this check and informs the caller (by throwing an Exception) in case the Delegate wasn't set up can be found in Method 'GetData' in file csharp\ICT\Petra\Shared\lib\MCommon\validation\Helper.cs.


The Particular Solution: Circular Dependencies between Module Assemblies of OpenPetra

The following server-side approach of setting up Delegates using a special Assembly that is loaded using .NET Remoting would work for the client side as well - it is just not implemented yet for the client side. (Delegates are already used on the Client side for other reasons, though.)

Forward-facing Call Forwarding using a Dynamically Loaded Assembly

The key to this concept is that a special Assembly, Ict.Petra.Server.lib.CallForwarding.dll, was introduced and that this Assembly isn't referenced from any other Assembly. This Assembly can reference any Assembly on the server side of OpenPetra and can therefore set up Delegates to any of the Methods in those Assemblies - without causing circular dependencies as the special Assembly itself is not referenced from any other Assembly (therefore I use the term 'forward facing').

You might ask 'How can an Assembly be part of OpenPetra if it isn't referenced at compile time'? Well, by using .NET Reflection to load that special Assembly at run time. This Assembly is loaded automatically once into each Client AppDomain by code in Method 'LoadCallForwardingAssembly' of file \csharp\ICT\Common\Remoting\Server\ClientAppDomainConnector.cs and an Instance of Class 'TCallForwarding' is created. You don't need to know how it works, just that it is done.

Setting up Delegates

The Class 'TCallForwarding' in file \csharp\ICT\Petra\Server\lib\CallForwarding\CallForwarding.cs has got a static parameterless Constructor. In this Constructor the setting up of arbitrary server-side Delegates can happen.

Add your Delegate setup code like this:

  • Add (a) code line(s) for setting up the Delegate at the end of the existing Delegate set-up code lines.
    • Only one code line is needed for setting up Delegates that point to static Methods, an example is the Delegate that points to @TCommonDataReader.GetData
    • Several code lines are needed for setting up Delegates that point to Methods that aren't static
      • Create a Field that holds a reference to the Class where the Method is defined
      • Create an instance of that Class and assign it to the Field
      • Point the Delegate to the Method of the Instance of the Class
        • Example:
private static Ict.Petra.Server.MCommon.Cacheable.TCacheable CachePopulatorCommon;
CachePopulatorCommon = new Ict.Petra.Server.MCommon.Cacheable.TCacheable();
TSharedDataCache.TMCommon.GetCacheableCommonTableDelegate = @CachePopulatorCommon.GetCacheableTable;
  • You might need to add one or two 'using' clauses to the C# file so that the code that accepts the reference to the Delegate Method and the Delegate Method itself are both reachable by the C# compiler.
  • Add References to the Assemblies as needed.
  • You can now invoke the Delegate (and thus execute the Method that it is pointing to) from server-side code without needing a reference to the Assembly in which the Method is contained that the Delegate is pointing to!!!