There are such situations when you want to provide something in background, but only as long as it's needed. This could be a resource or anything you want to keep togheter. The goal can be to ensure, that it is only alive as long as you need it and/or to have a maximum one single instance to provide over complete program live cycle.
Explained in other words: an outer method call could be the first place in code when the resource or connector is used, but cascaded called methods in that outer method want's also access to that resource.
One solution to solve such problem is the use of an ambient scope. For example, the TransactionScope is a good example for ambient scopes. But it does not give you possibility to open one scope over the other with the goal to access the same transaction, if a transaction scope was opened before. For transaction scope might this have its reasons, but in this article I want to show you how you can create your action scope which supports cascaded calls.
You might argue, that you can pass such a resource from one method to the other. Yes, that is true, but there are situations where you place your code where you cannot or don't want to change the method profile (in methods such as in interfaces). And that specific ressource might become that central, that you don't want to reply code to check and eventually re-fabric this resource over and over again.
Below you find an example how such a cascaded ambient scope type could look like. As an example you could imagine, that we want to share a DB connection through some program scopes. As you see, the DB connection construction is quite flawly, it's only to give you some idea about possiblities. We will see later on, how this cascaded ambient scope could be used with another example code.
using System;
using
System.Collections.Generic;
using
System.Data.Common;
using
System.Linq;
using
System.Text;
using
System.Threading.Tasks;
namespace
CascadedAmbientScopes
{
public sealed class DbConnectionScope :
IDisposable
{
#region Internal diagnosys helpers
public static event
EventHandler<DbConnectionScope> ScopeStarted;
public static event
EventHandler<DbConnectionScope> ScopeEnded;
public static event
EventHandler<DbConnectionScope> CascadeStarted;
public static event
EventHandler<DbConnectionScope> CascadeEnded;
public static int CascadeLevel => stack.Count -
1;
public int StackLevel { get; private set; }
#endregion
static DbConnectionScope Current =>
stack.Any() ? stack.Peek() : null;
#region Cascade lifetime stuff
[ThreadStatic]
static DbConnection dbConnection;
public DbConnection DbConnection =>
dbConnection;
static void CascadeStart()
{
/*
* Build the DbConnection
*/
dbConnection =
DbProviderFactories.GetFactory("f.ex. retrieved from configuration or use
dependency injection or whatever").CreateConnection();
}
static void CascadeEnd()
{
/*
* Destroy the DbConnection
*/
dbConnection.Dispose();
}
#endregion
#region Scope stack
[ThreadStatic]
static readonly Stack<DbConnectionScope>
stack = new Stack<DbConnectionScope>();
static void Add(DbConnectionScope scope)
{
if (!stack.Any())
{
CascadeStart();
CascadeStarted?.Invoke(null, scope);
}
stack.Push(scope);
ScopeStarted?.Invoke(null, scope);
}
static void Remove(DbConnectionScope scope)
{
#region Optional extra check for lousy
programmers
if (Current != scope)
{
throw new Exception("How
you dare? It's not your turn: probably you have forgotten to dispose a previous
scope or you might dispose an object in a different thread than the thread when
it was created.");
}
#endregion
ScopeEnded?.Invoke(null, scope);
_ = stack.Pop();
if (!stack.Any())
{
CascadeEnd();
CascadeEnded?.Invoke(null, scope);
}
}
#endregion
public DbConnectionScope()
{
StackLevel = stack.Count;
Add(this);
}
bool disposedValue = false;
public void Dispose()
{
if (!disposedValue)
{
Remove(this);
}
disposedValue = true;
}
}
}
Some take-outs for this type:
- It must be disposed correctly, because the disposing of a scope level is the trigger to give back control to parent level. Otherwise the most parent level is never disposed - and the resource is not freed.
- The cascaded ambient scope works isolated in it's thread. That's a limitation because otherwise, we would not be able to find out, when a scope really ends since different threads will dispose and create the ambient type concurrent.
Now the example, how we can use the ambient type. Let's say we have a program which run's a report on sales revenue. It can either run for a complete week or for single day. We have two separate methods, to fulfill our functional requirements: DailyReport and WeeklyReport. As part of functionality, WeeklyReport is calling DailyReport, but DailyReport can be called isolated as well (yes, I know, you would not design a program like that, neither would I - but it's only for demonstration). Both methods needs same DB connection and we want to provide the DB connection as a single resource in order to increase performance.
using System;
namespace
CascadedAmbientScopes
{
class Program
{
static void Main(string[] args)
{
if (args[0] == "FullAction")
{
WeeklyReport();
}
else
{
DailyReport(DateTime.Now.DayOfWeek);
}
}
static void DailyReport(DayOfWeek dayOfWeek)
{
using(var dbConnectinScope = new
DbConnectionScope())
{
/*
* do something with the
dbConnectionScope
*/
}
}
static void WeeklyReport()
{
using (var dbConnectinScope = new
DbConnectionScope())
{
for (var day = DayOfWeek.Sunday; day
<= DayOfWeek.Sunday; day++)
{
DailyReport(day);
}
/*
* do something
additional with the dbConnectionScope
*/
}
}
}
}
Look now the using directives with DbConnectionScope: In case of "FullAction", it's called in WeeklyReport once, and seven times implicitly with the call of DailyReport. However: The DB connection is exactly created once, namely in the second line of WeeklyReport (using (... new DbConnectionScope())) and the DB connection will be still the same for all code in DailyReport because there will be no new DB connection created as long as the using directive in WeeklyReport is open.
See the idea behind? Again, the business case with DB connection is just an example to demonstrate the mechanics and the benefits of an cascaded ambient scope.
An other Idea is the use for the possibility of prefixed logging: Each scope could add a prefix in background, so wen something should be logged, the prefixes are included.
When we take again our sales revenue example, our code could look like this:
using System;
using
System.Diagnostics;
namespace
CascadedAmbientScopes
{
class Program
{
static void Main(string[] args)
{
if (args[0] == "FullAction")
{
WeeklyReport();
}
else
{
DailyReport(DateTime.Now.DayOfWeek);
}
}
static decimal DailyReport(DayOfWeek dayOfWeek)
{
var moneyIndicator = default(decimal);
using(var logging = new
PrefixedLoggingScope(dayOfWeek.ToString()))
{
/*
* calculate the money
indicator
*/
logging.Log($"Turnover
at {moneyIndicator}");
}
return moneyIndicator;
}
static void WeeklyReport()
{
using (var logging = new
PrefixedLoggingScope("Weekly report"))
{
var moneyIndicator = default(decimal);
for (var day = DayOfWeek.Sunday; day
<= DayOfWeek.Sunday; day++)
{
moneyIndicator +=
DailyReport(day);
}
logging.Log($"Turnover
at {moneyIndicator}");
}
}
}
}
In case of "FullAction" the output could look like this:
Weekly Report/Monday/Turnover at 6000
Weekly Report/Tuesday/Turnover at 5400
Weekly Report/Wednesday/Turnover at 4400
....
Weekly Report/Turnover at 48800
And in case of not "FullAction" like this:
Monday/Turnover at 6000
The token "Weekly Report" would be taken from most outer scope, created with the using directive in WeeklyReport method. The Monday, Tuesday, a.s.o. is then taken from the level created by the DailyReport method (either as most outer scope or as sub level of its parent scope created by WeeklyReport).