So, most console applications seemingly terminate instantly when they receive a CTRL+C, but occasionally you may notice that some have a termination message, or take an unusually long time to terminate. This is probably due to the application winding itself up cleanly without corrupting anything that was in-progress.

This becomes especially important you're manipulating the file-system, talking to an external system, or dealing with threads that need to finish before you can just arbitrarily exit your process.

So, first of all, how do you detect a CTRL+C?
Using a delegate function to attach a handler to the Console.CancelKeyPress event. You can do this in a few ways, but the two most common are inline or defining an actual function.

Inline:

class Program
{
    // cancelled: Used for determining if a cancel has been requested
    // "volatile" ensures concurrent threads get the latest value of an object
    private static volatile bool cancelled = false;

    public static void Main()
    {
        Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs args) {
            args.Cancel = true;
            Program.cancelled = true;
            Console.Log("CANCEL command received! Cleaning up. please wait...");
        };
    }
}

or, as a function:

class Program
{
    // cancelled: Used for determining if a cancel has been requested
    private static volatile bool cancelled = false;

    public static void Main()
    {
        Console.CancelKeyPress += new ConsoleCancelEventHandler(myHandler);
    }

    protected static void myHandler(object sender, ConsoleCancelEventArgs args)
    {
        args.Cancel = true;
        Program.cancelled = true;
        Console.Log("CANCEL command received! Cleaning up. please wait...");
    }
}

That's it. That's the handler that set's you up for gracefully handling program termination.

Once the handler has been defined, you can read the user defined Program.cancelled boolean property at any time to check if the user has requested termination of the running process.

For example, in your user-defined "Run" function you might do this:

private static void Run()
{
    var data = ReadSomeDataData(); // takes a few seconds to complete
    
    if (Program.cancelled) return; // Check if we received CRTL+C
    
    ProcessSomeData(data); // Might take several hours to complete.
}

The Run function simply loads some data, and processes it, however if the user tries to terminate using CTRL+C the process is halted before the second half of the process begins.

The ReadSomeData() function might check for termination as well, however we'll get into the ProcessSomeData() function as it gets more complicated once we begin updating things.

/**
 * A long running process that spawns multiple threads.
 */
private static void ProcessSomeData(List<MyDataObject> data)
{
    var maxThreadCount = Math.Max(1, Math.Min(Environment.ProcessorCount, 4)));
    var opts = new ParallelOptions {
        MaxDegreeOfParallelism = maxThreadCount
    };
    
    // For each item in the list, process as many as we can in parallel threads.
    Parallel.ForEach(data, ProcessDataItem);
}

Interestingly, this function does not check for process termination. Instead, it's simply responsible for spawnign a Parallel threaded loop of data processing functions.

The reason it can't check for temination, is that the only function here that really does anything is the "ForEach" and it doesn't have a callback. Interestingly, a Parallel.ForEach cant be "broken" (using break/`continue` etc.) you need to exit the current iteration fo the function using return and prevent further iterations of the ForEach loop by setting the stopped state of the Parallel.

You could inline the "Func<>" but for the sake of simplcity I'm using a regular function.

So, inside the ProcessDataItem() function, which may be being called for different data items simultaneously in multiple threads, we need to check

private static void ProcessDataItem( MyDataObject item, ParallelLoopState state)
{
    // ...do some small calculations or data manipulation
    var data = PreProcessItemData(item);
    
    // check for cancellation
    if (Program.cancelled)
    {
    	state.Stop();
    	return; // nothing has been written, so we can safely return
    }
    
    // Do some actual manipulation of a DB or the filesystem.
    var result = WriteData(data);
    
    // Check for cancellation here, but DO NOT return.
    // The second half of this process needs to complete
    // or the data will be out of sync.
    if (Program.cancelled)
    {
    	state.Stop();
    }
    
    // Another arbitrary data processing function
    var journalData = PostProcessResult(result);
    var success = WriteJournalData(journalData);
    
    // We're at the end of the function,
    // but, if the user has requested termination,
    // we still set the stopped state, so a new
    // loop of the ForEach does not begin after this one.
    if (Program.cancelled)
    {
    	state.Stop();
        // "return;" is not required here, but we may
        // add more code after this check later.
        return;
	}
}

Terminating individual loops of this function will in turn end the ForEach parallel processing. This will in turn eventually return control to the Main() function and your program will end normally, safely, and hopefully without any corruption of half-processed events..

See also:

Console.CancelKeyPress Event (System)
Occurs when the Control modifier key (Ctrl) and either the C console key (C) or the Break key are pressed simultaneously (Ctrl+C or Ctrl+Break).
Write a simple parallel program using Parallel.ForEach
In this article, learn how to enable data parallelism in .NET. Write a Parallel.ForEach loop over any IEnumerable or IEnumerable data source.