Wait for all tasks without throwing exceptions - c#

If I do await Task.WhenAll(tasks) it throws exception if one or more tasks failed. I would like to examine each task objects manually.
Usually it is not good idea to catch(Exception). Is it reasonable to do it in this case? Or maybe there is another way to wait for all tasks? I cannot catch AggregateException, because if there is just one it will be unwrapped.
How to do it correctly in async method?

The AwaitCompletion extention method below does what you're looking for, with a more generic API. It returns a Task, so you'll still have to await that, either via await or Task.Wait() or something else.
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public static class TaskExtensions
{
public static Task AwaitCompletion<T>(this ICollection<Task<T>> unfinishedTasks)
{
int remainingCount = unfinishedTasks.Count;
var promise = new TaskCompletionSource<ICollection<Task<T>>>();
var finishers = unfinishedTasks.Select(x => x.ContinueWith((task, state) =>
{
int postCount = Interlocked.Decrement(ref remainingCount);
if (postCount == 0)
{
promise.SetResult(unfinishedTasks);
}
}, remainingCount));
// force evaluation
var _ = finishers.ToList();
return promise.Task;
}
/// <summary>
/// Unlike Task.Value, this doesn't wait for completion. Hence, it also doesn't
/// throw exceptions.
/// </summary>
/// <returns>Value if completed, otherwise null (or default value if a value type)</returns>
public static T GetResultOrDefault<T>(this Task<T> self)
{
if (self.IsCompletedSuccessfully)
{
return self.Result;
}
else
{
return default(T);
}
}
}
After completion of the returned Task value, all tasks in unfinishedTasks will either be completed or error'd (or still going if you use Task.Wait() with a timeout). AwaitCompletion does not throw exceptions, and awaiting it will only throw exceptions if a timeout is reached. To retrieve exceptions, look at the tasks you passed in.
Usage:
Task[] tasksToWaitFor = ...
await tasksToWaitFor.AwaitCompletion()
foreach (var task in tasksToWaitFor)
{
Console.WriteLine("result: {}", task.GetResultOrDefault());
}
The GetResultOrDefault extension method is important. If you hit Value directly, it'll throw AggregateException when the task fails, OTOH GetResultOrDefault will return null.

Swallow the exceptions momentarily with
await Task.WhenAll(tasks).ContinueWith(delegate {} )
but remember to then check all tasks individually and properly surface errors.

Related

Exception handling using tasks and Task.WaitAny

Task.WaitAll method can throw an AggregateException, but Task.WaitAny method does not.
Questions:
I do not understand for what purpose the developers of the framework did this?
How to catch an exception from a task using Task.WaitAny method?
Example:
using System;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
void MyMethodForDivideByZeroException()
{
int a = 1;
a = a / 0;
}
void MyMethodForIndexOutOfRangeException()
{
int[] MyArrayForInt = new int[2] { 1, 2 };
Console.WriteLine(MyArrayForInt[2]);
}
Task MyTask22 = new Task(MyMethodForDivideByZeroException);
Task MyTask23 = new Task(MyMethodForIndexOutOfRangeException);
MyTask22.Start();
MyTask23.Start();
Task.WaitAny(MyTask22, MyTask23); //No exceptions
//Task.WaitAll(MyTask22, MyTask23); //AggregateException
Console.WriteLine(MyTask22.Status);
Console.WriteLine(MyTask23.Status);
}
}
}
WaitAny() waits for any one of the tasks to complete (successfuly or otherwise).
When one does, with an exception, the exception is not propagated (thrown).
WaitAll() waits for all of the tasks to complete.
As they both throw an exception, these are aggregated into an AggregateException.
You can check for errors like this:
var tasks = new[] { MyTask22, MyTask23 };
int taskIndex = Task.WaitAny(tasks); //No exceptions
if (taskIndex >= 0)
{
throw tasks[taskIndex].Exception;
}
More useful information here:
https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
https://blog.stephencleary.com/2014/10/a-tour-of-task-part-5-wait.html
The job of the Task.WaitAny method is to inform you that a task has completed, not that a task has completed successfully.
I'll answer your questions in reverse order:
How to catch an exception from a task using Task.WaitAny method?
You probably ask how to throw the exception of a completed task that has failed. The simplest way is probably just to Wait the task:
var tasks = new Task[] { MyTask22, MyTask23 };
int completedTaskIndex = Task.WaitAny(tasks);
tasks[completedTaskIndex].Wait();
I do not understand for what purpose the developers of the framework did this?
Because otherwise the method would not be able to do its intended job. You won't be able to use this method in order to be informed about the completion of a task. Instead of getting the index of the completed task, you would get a useless and expensive exception. You could catch this exception, but you would still don't know which of the tasks is the one that caused the exception.

Async method that returns result and Task

I'm trying to design a public method that returns a quick result and, if needed, kicks off a long-running background task.
The result is retrieved within the method pretty quickly, and once it is retrieved it needs to be immediately available to the caller, without waiting for the potential background task to complete.
The background task is either not needed at all, or it must run for a significant amount of time - longer than the caller and all other logic in the program would take to complete without it.
Looking through the MS docs, the best design option I can come up with is to return two things from this method: the result, as well as a Task for the background task.
I don't see a better way to do this, but there are some downsides: first, the responsibility for making sure the background task completes falls on the caller of the method, even though the caller really just wants to consume the immediate result and not be concerned with the behind-the-scene stuff. Second, it is awkward to return null if the background task isn't needed to begin with: now the caller must ensure the task isn't null, in addition to making sure it completes if it is null.
What other options are available for this sort of situation?
Here's an example of what my current design looks like:
public async Task<Tuple<string, Task>> GetAndSave() {
var networkResult = await GetFromNetworkAsync();
if (NeedsToSave(networkResult)) {
var saveTask = SaveToDiskAsync(networkResult);
return new Tuple<string, Task>(networkResult, saveTask);
}
else {
return new Tuple<string, Task>(networkResult, null);
}
}
first, the responsibility for making sure the background task completes falls on the caller of the method, even though the caller really just wants to consume the immediate result and not be concerned with the behind-the-scene stuff.
If it's important to make sure the background task completes then instead of returning the Task you could hand it off to another object (that has been injected into the class that has your GetAndSave method). For example:
public class Foo
{
readonly Action<Task> _ensureCompletion;
public Foo(Action<Task> ensureCompletion)
{
_ensureCompletion = ensureCompletion;
}
public async Task<string> GetAndSaveAsync() // Your method
{
string networkResult = await GetFromNetworkAsync();
if (NeedsToSave(networkResult))
{
Task saveTask = SaveToDiskAsync(networkResult);
_ensureCompletion(saveTask); // This is a synchronous call... no await keyword here. And it returns `void`
}
return networkResult;
}
Task<string> GetFromNetworkAsync() {...}
bool NeedsToSave(string x) {...}
Task SaveToDiskAsync(string x) {...}
}
Now you can inject whatever follow-up behavior you desire for the saveTask. For example, you could write stuff out to the console depending on how it goes:
async Task DoStuff()
{
var foo = new Foo(async task =>
// ^^^^^ More on this in a moment
{
try
{
await task;
Console.Writeline("It worked!");
}
catch (Exception e)
{
Console.Writeline(e.ToString());
}
});
var immediateResult = await foo.GetAndSaveAsync();
// do stuff with `immediateResult`
}
Now, the thing that might be confusing about this (and the power behind this type of solution) is how you can have a synchronous call on the one hand:
_ensureCompletion(saveTask); // This is a synchronous call... no await keyword here
...that does asynchronous things:
var foo = new Foo(async task => ...);
// ^^^^^ This statement lambda is asynchronous
(The injected delegate might even write out to the console on a different thread than whatever thread called GetAndSaveAsync()!)
There's no magic here. It all comes down to SynchronizationContext and the inner workings of async/await.
When the compiler encounters the await keyword (in an async context) then it will do a few things:
Turn everything after the await into a continuation (basically a delegate with some state)
Replace the await keyword with something like SynchronizationContext.Current.Post(continuation)
In other words, this:
async void EnsureCompletion(Task task)
{
try
{
await task;
Console.Writeline("It worked!");
}
catch (Exception e)
{
Console.Writeline(e.ToString());
}
}
...gets turned into something like this:
void EnsureCompletion(Task task)
{
try
{
task.ContinueWith(t => SynchronizationContext.Current.Post(_ =>
{
if (t.IsCompletedSuccessfully)
{
Console.Writeline("It worked!");
}
else
{
Console.Writeline(task.Exception.ToString());
}
});
}
catch (Exception e)
{
Console.Writeline(e.ToString());
}
}
As you can see, EnsureCompletion is an entirely synchronous function that does (almost) the exact same thing as its asynchronous form. Notice how it returns void and everything. This is how you can jam an asynchronous statement lambda into a delegate parameter that has a synchronous signature.
I hinted that the console writing might happen on a totally different thread. That's because async/await is orthogonal to threading. It depends on whatever the currently assigned implementation of SynchronizationContext is programmed to do. Unless you're using WPF or WinForms then by default there will be no SynchronizationContext and continuations (the things that get passed to SynchronizationContext.Post) will just be tossed over to whatever thread happens to be free in the default thread pool.
Second, it is awkward to return null if the background task isn't needed to begin with: now the caller must ensure the task isn't null, in addition to making sure it completes if it is null.
I'm of the opinion that null was a design mistake in C#. (Ask me what some other ones are :) ). If you return null from this method:
public Task DoSomethingAsync()
{
return null; // Compiles just fine
}
...then anyone who uses it will encounter a NullReferenceException when they await it.
async Task Blah()
{
await DoSomethingAsync(); // Wham! NullReferenceException :(
}
(the reason why comes back to how the compiler desugurs the await keyword)
...and it's awkward to have to check for nulls everywhere.
Much better would be to just return Task.CompletedTask as #juharr said.
But if you just hand off the background task like I showed above then you don't have to worry about this.

How can I await an array of tasks and stop waiting on first exception?

I have an array of tasks and I am awaiting them with Task.WhenAll. My tasks are failing frequently, in which case I inform the user with a message box so that she can try again. My problem is that reporting the error is delayed until all tasks are completed. Instead I would like to inform the user as soon as the first task has thrown an exception. In other words I want a version of Task.WhenAll that fails fast. Since no such build-in method exists I tried to make my own, but my implementation does not behave the way I want. Here is what I came up with:
public static async Task<TResult[]> WhenAllFailFast<TResult>(
params Task<TResult>[] tasks)
{
foreach (var task in tasks)
{
await task.ConfigureAwait(false);
}
return await Task.WhenAll(tasks).ConfigureAwait(false);
}
This generally throws faster than the native Task.WhenAll, but usually not fast enough. A faulted task #2 will not be observed before the completion of task #1. How can I improve it so that it fails as fast as possible?
Update: Regarding cancellation, it is not in my requirements right now, but lets say that for consistency the first cancelled task should stop the awaiting immediately. In this case the combining task returned from WhenAllFailFast should have Status == TaskStatus.Canceled.
Clarification: Τhe cancellation scenario is about the user clicking a Cancel button to stop the tasks from completing. It is not about cancelling automatically the incomplete tasks in case of an exception.
Your best bet is to build your WhenAllFailFast method using TaskCompletionSource. You can .ContinueWith() every input task with a synchronous continuation that errors the TCS when the tasks end in the Faulted state (using the same exception object).
Perhaps something like (not fully tested):
using System;
using System.Threading;
using System.Threading.Tasks;
namespace stackoverflow
{
class Program
{
static async Task Main(string[] args)
{
var cts = new CancellationTokenSource();
cts.Cancel();
var arr = await WhenAllFastFail(
Task.FromResult(42),
Task.Delay(2000).ContinueWith<int>(t => throw new Exception("ouch")),
Task.FromCanceled<int>(cts.Token));
Console.WriteLine("Hello World!");
}
public static Task<TResult[]> WhenAllFastFail<TResult>(params Task<TResult>[] tasks)
{
if (tasks is null || tasks.Length == 0) return Task.FromResult(Array.Empty<TResult>());
// defensive copy.
var defensive = tasks.Clone() as Task<TResult>[];
var tcs = new TaskCompletionSource<TResult[]>();
var remaining = defensive.Length;
Action<Task> check = t =>
{
switch (t.Status)
{
case TaskStatus.Faulted:
// we 'try' as some other task may beat us to the punch.
tcs.TrySetException(t.Exception.InnerException);
break;
case TaskStatus.Canceled:
// we 'try' as some other task may beat us to the punch.
tcs.TrySetCanceled();
break;
default:
// we can safely set here as no other task remains to run.
if (Interlocked.Decrement(ref remaining) == 0)
{
// get the results into an array.
var results = new TResult[defensive.Length];
for (var i = 0; i < tasks.Length; ++i) results[i] = defensive[i].Result;
tcs.SetResult(results);
}
break;
}
};
foreach (var task in defensive)
{
task.ContinueWith(check, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
return tcs.Task;
}
}
}
Edit: Unwraps AggregateException, Cancellation support, return array of results. Defend against array mutation, null and empty. Explicit TaskScheduler.
I recently needed once again the WhenAllFailFast method, and I revised #ZaldronGG's excellent solution to make it a bit more performant (and more in line with Stephen Cleary's recommendations). The implementation below handles around 3,500,000 tasks per second in my PC.
public static Task<TResult[]> WhenAllFailFast<TResult>(params Task<TResult>[] tasks)
{
if (tasks is null) throw new ArgumentNullException(nameof(tasks));
if (tasks.Length == 0) return Task.FromResult(new TResult[0]);
var results = new TResult[tasks.Length];
var remaining = tasks.Length;
var tcs = new TaskCompletionSource<TResult[]>(
TaskCreationOptions.RunContinuationsAsynchronously);
for (int i = 0; i < tasks.Length; i++)
{
var task = tasks[i];
if (task == null) throw new ArgumentException(
$"The {nameof(tasks)} argument included a null value.", nameof(tasks));
HandleCompletion(task, i);
}
return tcs.Task;
async void HandleCompletion(Task<TResult> task, int index)
{
try
{
var result = await task.ConfigureAwait(false);
results[index] = result;
if (Interlocked.Decrement(ref remaining) == 0)
{
tcs.TrySetResult(results);
}
}
catch (OperationCanceledException)
{
tcs.TrySetCanceled();
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}
}
Your loop waits for each of the tasks in pseudo-serial, so that's why it waits for task1 to complete before checking if task2 failed.
You might find this article helpful on a pattern for aborting after the first failure: http://gigi.nullneuron.net/gigilabs/patterns-for-asynchronous-composite-tasks-in-c/
public static async Task<TResult[]> WhenAllFailFast<TResult>(
params Task<TResult>[] tasks)
{
var taskList = tasks.ToList();
while (taskList.Count > 0)
{
var task = await Task.WhenAny(taskList).ConfigureAwait(false);
if(task.Exception != null)
{
// Left as an exercise for the reader:
// properly unwrap the AggregateException;
// handle the exception(s);
// cancel the other running tasks.
throw task.Exception.InnerException;
}
taskList.Remove(task);
}
return await Task.WhenAll(tasks).ConfigureAwait(false);
}
I'm adding one more answer to this problem, not because I've found a faster solution, but because I am now a bit skeptical about starting multiple async void operations on an unknown SynchronizationContext. The solution I am proposing here is significantly slower. It's about 3 times slower than #ZaldronGG's excellent solution, and about 10 times slower than my previous async void-based implementation. It has though the advantage that after the completion of the returned Task<TResult[]>, it doesn't leak fire-and-forget continuations attached on the observed tasks. When this task is completed, all the continuations created internally by the WhenAllFailFast method have been cleaned up. Which is a desirable behavior for APIs is general, but in many scenarios it might not be important.
public static Task<TResult[]> WhenAllFailFast<TResult>(params Task<TResult>[] tasks)
{
ArgumentNullException.ThrowIfNull(tasks);
CancellationTokenSource cts = new();
Task<TResult> failedTask = null;
TaskContinuationOptions flags = TaskContinuationOptions.DenyChildAttach |
TaskContinuationOptions.ExecuteSynchronously;
Action<Task<TResult>> continuationAction = new(task =>
{
if (!task.IsCompletedSuccessfully)
if (Interlocked.CompareExchange(ref failedTask, task, null) is null)
cts.Cancel();
});
IEnumerable<Task> continuations = tasks.Select(task => task
.ContinueWith(continuationAction, cts.Token, flags, TaskScheduler.Default));
return Task.WhenAll(continuations).ContinueWith(allContinuations =>
{
cts.Dispose();
var localFailedTask = Volatile.Read(ref failedTask);
if (localFailedTask is not null)
return Task.WhenAll(localFailedTask);
// At this point all the tasks are completed successfully
Debug.Assert(tasks.All(t => t.IsCompletedSuccessfully));
Debug.Assert(allContinuations.IsCompletedSuccessfully);
return Task.WhenAll(tasks);
}, default, flags, TaskScheduler.Default).Unwrap();
}
This implementation is similar to ZaldronGG's in that it attaches one continuation on each task, with the difference being that these continuations are cancelable, and they are canceled en masse when the first non-successful task is observed. It also uses the Unwrap technique that I've discovered recently, which eliminates the need for the manual completion of a TaskCompletionSource<TResult[]> instance, and usually makes for a concise implementation.

Task WhenAll exception handling

I am looking to use Task.WhenAll in a situation where some tasks may fail, but I still need the result data from the rest of the completed tasks.
According to MSDN,
If any of the supplied tasks completes in a faulted state, the
returned task will also complete in a Faulted state, where its
exceptions will contain the aggregation of the set of unwrapped
exceptions from each of the supplied tasks.
However, what it doesn't say, is whether or not Task.WhenAll will still wait for the rest of the Tasks to complete in that instance. Can anyone provide any clarification on this issue?
As the documentation says:
Creates a task that will complete when all of the Task objects in an enumerable collection have completed.
So it will wait for all tasks to complete, regardless of whether any have thrown an exception already or been cancelled. Then it will aggregate the possible cancellations and exceptions and define its state. The results of the given tasks will be in the original tasks, as well as the exceptions and cancellations.
You can easily handle your requirement by wrapping the body of all the tasks in a try catch block and use a proper data structure as the result type of your task, so that you can understand if the task was failed or not.
An example could be useful to better understand what i am trying to tell you.
public async static void Main(string[] args)
{
var task1 = GetIntegerAsync();
var task2 = GetAnotherIntegerAsync();
var result = await Task.WhenAll(new[] {task1, task2});
foreach(var res in results)
{
// show a different message depending on res.isFailed
}
}
private static async Task<(int result, bool isFailed)> GetIntegerAsync()
{
try
{
await Task.Delay(1000).ConfigureAwait(false);
return (10, false);
}
catch(Exception)
{
return (default(int), true);
}
}
private static async Task<(int result, bool isFailed)> GetAnotherIntegerAsync()
{
try
{
await Task.Delay(600).ConfigureAwait(false);
throw new Exception("Something bad happened...");
}
catch(Exception)
{
return (default(int), true);
}
}

Creating an async webservice method

I've tried to read up on async methods and am now trying to create my own async method. The method is a webservice call that returns a list of error logs. I'm not sure that I've understood correctly so I thought I'd share my code to see if I should do anything different.
All I want the code to do is return a list of errorlogs by calling a method GetAllErrorLogs(), that is a synchronized method. Since it can take a second to fetch all the error logs I want to have the opportunity to do other stuff once I called the GetAllErrorLogs() method. Here is the code.
[WebMethod]
public async Task<List<ErrorLog>> GetAllErrorLogs()
{
List<ErrorLog> errorLogs = new List<ErrorLog>();
await System.Threading.Tasks.Task.Run(() => {
errorLogs = ErrorLogRepository.GetAllErrorLogs();
});
if (errorLogs == null)
return new List<ErrorLog>();
return errorLogs;
}
Thanks!
I recently gave a talk at ThatConference on async on the server side, and I address this issue in the slides.
On the server side, you want to avoid the use of Task.Run and other constructs that queue work to the thread pool. As much as possible, keep thread pool threads available for handling requests.
So, ideally your repository would have an asynchronous method GetAllErrorLogsAsync, which would itself be asynchronous. If GetAllErrorLogs cannot be asynchronous, then you may as well just call it directly (removing the await Task.Run).
Since it can take a second to fetch all the error logs I want to have the opportunity to do other stuff once I called the GetAllErrorLogs() method.
If you have a GetAllErrorLogsAsync available, then this can easily be done using Task.WhenAll. However, if GetAllErrorLogs is synchronous, then you can only do this by doing parallel work in your request (e.g., multiple calls to Task.Run followed by Task.WhenAll).
Parallel code on the server must be approached with great trepidation. It is only acceptable in a very limited set of scenarios. The entire point of async on the server side is to use fewer threads per request, and when you start parallelizing, you're doing the opposite: multiple threads per request. This is only appropriate if you know your user base is very small; otherwise, you'll kill your server scalability.
I found this great codeproject detailed article about how to achieve that
http://www.codeproject.com/Articles/600926/Asynchronous-web-services-call-in-ASP-NET
**This is potentially wrong, read comments or spinoff question at HttpContext.Current after an await
If ErrorLogRepository.GetAllErrorLogs() is not thread-safe, it will cause weird bugs and potentially exception out. Make sure your code is ready for multi-threaded operation before switching to async methods, this is obviously very trivial advice but often overlooked. For example, if you reference HttpContext.Current in your methods, your code will die in the async method, and sometimes even AFTER the await. The reason is that the code within the async block will potentially be run on a separate thread, which will not have access to the same HttpContext.Current thread-static property, and await gets compiled into two methods. All code before an await gets run on one thread, and then calls the code after an await keyword as a continuation, but potentially on yet another thread. So sometimes your code will even work in an async block, only to choke unexpectedly after it gets "out" of the async back to what you think is a synchronous part of your code (but in reality everything after an await keyword is already not guaranteed to be the original thread).
Here is some production code...
using System.Web.Http;
using AysncTask = System.Threading.Tasks.Task;
public class myController : ApiControllerBase
{
[HttpPut]
[Route("api/cleardata/{id}/{requestId}/")]
public async AysncTask ClearData(Guid id, Guid requestId)
{
try
{
await AysncTask.Run(() => DoClearData(id, requestId));
}
catch (Exception ex)
{
throw new Exception("Exception in myController.ClearData", ex);
}
}
}
Handling Async exceptions is also VERY VERY important.. although this is for a windows console app, the same principles should apply.
source: https://blogs.msdn.microsoft.com/ptorr/2014/12/10/async-exceptions-in-c/
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncAndExceptions
{
class Program
{
static void Main(string[] args)
{
AppDomain.CurrentDomain.UnhandledException += (s, e) => Log("*** Crash! ***", "UnhandledException");
TaskScheduler.UnobservedTaskException += (s, e) => Log("*** Crash! ***", "UnobservedTaskException");
RunTests();
// Let async tasks complete...
Thread.Sleep(500);
GC.Collect(3, GCCollectionMode.Forced, true);
}
private static async Task RunTests()
{
try
{
// crash
// _1_VoidNoWait();
// crash
// _2_AsyncVoidAwait();
// OK
// _3_AsyncVoidAwaitWithTry();
// crash - no await
// _4_TaskNoWait();
// crash - no await
// _5_TaskAwait();
// OK
// await _4_TaskNoWait();
// OK
// await _5_TaskAwait();
}
catch (Exception ex) { Log("Exception handled OK"); }
// crash - no try
// await _4_TaskNoWait();
// crash - no try
// await _5_TaskAwait();
}
// Unsafe
static void _1_VoidNoWait()
{
ThrowAsync();
}
// Unsafe
static async void _2_AsyncVoidAwait()
{
await ThrowAsync();
}
// Safe
static async void _3_AsyncVoidAwaitWithTry()
{
try { await ThrowAsync(); }
catch (Exception ex) { Log("Exception handled OK"); }
}
// Safe only if caller uses await (or Result) inside a try
static Task _4_TaskNoWait()
{
return ThrowAsync();
}
// Safe only if caller uses await (or Result) inside a try
static async Task _5_TaskAwait()
{
await ThrowAsync();
}
// Helper that sets an exception asnychronously
static Task ThrowAsync()
{
TaskCompletionSource tcs = new TaskCompletionSource();
ThreadPool.QueueUserWorkItem(_ => tcs.SetException(new Exception("ThrowAsync")));
return tcs.Task;
}
internal static void Log(string message, [CallerMemberName] string caller = "")
{
Console.WriteLine("{0}: {1}", caller, message);
}
}
}

Categories

Resources