Sunday, August 24, 2008
Code: Test For Service Existence
bool serviceExists = false;
foreach ( System.ServiceProcess.ServiceController serviceController in System.ServiceProcess.ServiceController.GetServices())
{
if (serviceController.ServiceName == "TwentyTwentyLiveService")
{
serviceExists = true;
}
}
Saturday, August 23, 2008
Code: Determining If A User Is An Admin
WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent();
WindowsPrincipal currentPrincipal = newWindowsPrincipal(currentIdentity);
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
if (currentPrincipal.IsInRole(WindowsBuiltInRole.Administrator))
{
//Administator code
}
// User code
}
// Guest or anonymous code
}
Wednesday, August 20, 2008
Reflecting On Browser Properties
Wednesday, July 23, 2008
Testing for Endianess
One of the questions is to write code to test for Endian.
The answer Microsoft likes involves bit twiddling, but one wonders why they would not prefer calling their own framework:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("BitConverter.IsLittleEndian " + BitConverter.IsLittleEndian.ToString());
Console.ReadLine();
}
}
}
Wednesday, July 9, 2008
Obsolete's Key Role In Agile Software Development
Here are four examples applications to the same function 'Funky'
1:
[Obsolete()]
void Funky(){...}
2:
[Obsolete("Use Freaky instead")]
void Funky(){...}
3:
[Obsolete("Use Freaky instead", false)]
void Funky(){...}
4:
[Obsolete("Use Freaky instead", true)]
void Funky(){...}
Starting with 1 you can use this attribute without any of the parameters. In this default mode it will generate a warning indicating the method is obsolete. Following the rule of "Making the Right thing Easy and the Wrong thing Hard" this should be avoided in favor of 2; Use the message string to explain what should be done instead.
Example 3 is equivalent to 2, the second parameter determines if the Obsolete attribute results in a warning (false, the default) or an error (true). It is rare that you will actually have this set to true (4) and checked in to source control, since that would break the build. But it does happen and it is a more elegant way of making "The Wrong thing Hard" than commenting out the obsolete code.
How you will use 4 is to explore the level of effort involved in changing code. In a typical "Drain A Swamp" effort; the code might not even compile from source control without error. After considerable effort you will get the errors out but be left with numerous warnings. This morass of warnings will hide the ones generated by Obsolete. So working with generate error set to true will provide a count of what is left and a road map of what to fix.
In Green Field efforts Obsolete provides a effective means of communication of intent between teams working on separate modules. It raises the flag that one team wants to change something but does not force another team to change immediately. Perhaps the second team needs to transform and obsolete some code as a result. On balance it allows for teams of programmers to behave in an agile manner.
But perhaps the best benefit of Obsolete is the knowledge that it is there and provides a way for a programmer to take back his mistakes. What is not stressed enough about Agile methodologies is that it is a safe way for programmers to make mistakes. This frees them to discover things that can never be known beforehand in a Command and Control Central Planning mode of development. By fearless experimentation and ruthless obsoleting programmers will flow past problems, discovering the easy way much like water finds the downward path to the sea.
Tuesday, July 1, 2008
Regular Expression Groups
===========
Sample Code
===========
string testIpString = "192.168.110.112";
string regularExpressionString = @"^(?<octet1>[01]?\d\d2[0-4]\d25[0-5])(\.)(?<octet2>[01]?\d\d2[0-4]\d25[0-5])(\.)(?<octet3>[01]?\d\d2[0-4]\d25[0-5])(\.)(?<octet4>[01]?\d\d2[0-4]\d25[0-5])$";
//Option 1//Match matches = Regex.Match(testIpString, regularExpressionString);
//Option 2//Match matches = Regex.Match(testIpString, regularExpressionString,RegexOptions.ExplicitCapture);
Console.WriteLine("Groups");
foreach (Group group in matches.Groups)
{
Console.WriteLine(group.ToString());
}
Console.WriteLine("Named Groups");
Console.WriteLine( matches.Groups["octet1"].ToString());
Console.WriteLine( matches.Groups["octet2"].ToString());
Console.WriteLine( matches.Groups["octet3"].ToString());
Console.WriteLine( matches.Groups["octet4"].ToString());
Console.ReadLine();
===============
Option 1 output
===============
Groups
192.168.110.112
.
.
.
192
168
110
112
Named Groups
192
168
110
112
===============
Option 2 output
===============
Groups
192.168.110.112
192
168
110
112
Named Groups
192
168
110
112
===============
Commentary
===============
Most of the power is in the regular expression:
@"^(?<octet1>[01]?\d\d2[0-4]\d25[0-5])(\.)(?<octet2>[01]?\d\d2[0-4]\d25[0-5])(\.)(?<octet3>[01]?\d\d2[0-4]\d25[0-5])(\.)(?<octet4>[01]?\d\d2[0-4]\d25[0-5])$";
Friday, June 27, 2008
Regular Expressions: Special Characters And Escape Sequences
Special Characters
"^" Beginning of text
"$" End of text
"." Any single character but (\n) the newline
"*" Previous expression 0 or more times
"+" Preceding character 1 or more times
"?" Preceding character 0 or 1 times
"" Alteration used in Groups
"(" Used in Groups
")" Used in Groups
"[" Used in Ranges
"]" Used in Ranges
"-" Used in Groups and Ranges
"{" Used in Quantifiers
"}" Used in Quantifiers
Escape Sequences
"\b" Word boundary
"\B" Any non Word boundary
"\d" Any digit
"\D" Any non digit
"\s" Any whitespace character; space, tab, newline
"\S" Any character that is not whitespace
Thursday, June 26, 2008
Last Day of Current Month
//One liner
DateTime lastday = DateTime.Today.AddMonths(1).AddDays(-DateTime.Today.Day);
// Extra line to make it more readable
DateTime today = DateTime.Today;
DateTime lastday = today.AddMonths(1).AddDays(-today.Day);
Sunday, June 15, 2008
-0X80000000 is not -2147483648
The code that follows illustrates the problem but is not the code where the problem was
found.
We were expecting that the hex integer literal 0X80000000 would correspond to the integer value of (-1)(2^31) since this is a legal value. We tried this several ways (2) (3) (4) and they all resulted in an error. This is because the code which parses hex integer literals does not recognize the 0X80000000 as valid int value as such it then tries to create it as uint. If you remove comment (1) you will see these (2) works.
What is a little disturbing is (5) the decimal integer literal for (-1)(2^31) is a valid value.
===Code Follows===
enum Range //(1)// : uint
{
flag1 = 0X1,
flag2 = -0X2,
flag3 = 0X4,
flag4 = 0X8,
flag5 = 0X10,
flag6 = 0X20,
flag7 = 0X40,
flag8 = 0X80,
flag9 = 0X100,
flag10 = 0X200,
flag11 = 0X400,
flag12 = 0X800,
flag13 = 0X1000,
flag14 = 0X2000,
flag15 = 0X4000,
flag16 = 0X8000,
flag17 = 0X10000,
flag18 = 0X20000,
flag19 = 0X40000,
flag20 = 0X80000,
flag21 = 0X100000,
flag22 = 0X200000,
flag23 = 0X400000,
flag24 = 0X800000,
flag25 = 0X1000000,
flag26 = 0X2000000,
flag27 = 0X4000000,
flag28 = 0X8000000,
flag29 = 0X10000000,
flag30 = 0X20000000,
flag31 = 0X40000000,
//2// flag32 = 0X80000000 // Cannot implicitly convert 'uint' to 'int'
//3// flag32 = -0X80000000 // Cannot implicitly convert 'uint' to 'int'
//4// flag32 = 0X80000000u // Cannot implicitly convert 'uint' to 'int'
//5// flag32 = -2147483648 // Works
}
Friday, June 13, 2008
Stacking Exceptions
Which solves the age old delimma "I caught an exception, now what do I do with it?"
By adding context to where the exception was caught you can assist the troubleshooting that will occur in production. This is because the "ToString" method will faithfully walk down the list of all the exceptions and report their contents as the following code shows.
====Code Starts=======
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplicationNestedException
{
class Program
{
static void Main(string[] args)
{
Exception exception = OuterException();
Console.WriteLine(exception.ToString());
Console.ReadLine();
}
static public Exception OuterException()
{
Exception twoDeep = new Exception("Two Levels Deep");
Exception innerException = new Exception("Inner Exception",twoDeep);
return new Exception("Outer Exception",innerException);
}
}
}
===End Code, Output Follows====
System.Exception: Outer Exception --->
System.Exception: Inner Exception --->
System.Exception: Two Levels Deep
--- End of inner exception stack trace ---
--- End of inner exception stack trace ---
Saturday, June 7, 2008
Passing Parameters to Threads in C#
This misunderstands what the Threadstart needs. It need the location of the function to pass control to when the thread is started. But you could create an object and use it to identify a method as the following code illustrates.
===Code Follows===
using System;
using System.Threading;
namespace ConsoleApplicationThread
{
public class ThreadClass
{
private int _Count = 0;
public ThreadClass(int count)
{
_Count = count;
}
public void ThreadMethod()
{
for (int i = 0; i < _Count; i++)
{
Console.WriteLine("ThreadClass.ThreadMethod {1}: {0}", i, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
}
}
}
public class MultiThreadStart
{
public static void Main()
{
Console.WriteLine("Main thread: Create multiple threads");
ThreadClass threadClass1 = new ThreadClass(10);
Thread thread1 = new Thread(new ThreadStart(threadClass1.ThreadMethod));
ThreadClass threadClass2 = new ThreadClass(20);
Thread thread2 = new Thread(new ThreadStart(threadClass2.ThreadMethod));
ThreadClass threadClass3 = new ThreadClass(30);
Thread thread3 = new Thread(new ThreadStart(threadClass3.ThreadMethod));
ThreadClass threadClass4 = new ThreadClass(40);
Thread thread4 = new Thread(new ThreadStart(threadClass4.ThreadMethod));
Console.WriteLine("Main thread: Start threads");
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
Console.WriteLine("Main: Call Join() on each thread to wait for thread to end.");
thread1.Join();
thread2.Join();
thread3.Join();
thread4.Join();
Console.WriteLine("Main: All threads have ended");
Console.ReadLine();
}
}
}
Friday, June 6, 2008
Just Waiting On Thread
===Code Starts===
using System;
using System.Threading;
namespace ConsoleApplicationThread
{
public class MultiThreadStart
{
public static void ThreadMethod()
{
for (int i = 0; i < class="blsp-spelling-error" id="SPELLING_ERROR_4">WriteLine("ThreadMethod {1}: {0}", i,Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
}
}
public static void Main()
{
Console.WriteLine("Main thread: Create multiple threads");
Thread thread1 = new Thread(new ThreadStart(ThreadMethod));
Thread thread2 = new Thread(new ThreadStart(ThreadMethod));
Thread thread3 = new Thread(new ThreadStart(ThreadMethod));
Thread thread4 = new Thread(new ThreadStart(ThreadMethod));
Console.WriteLine("Main thread: Start threads");
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
Console.WriteLine("Main: Call Join() on each thread to wait for thread to end.");
thread1.Join();
thread2.Join();
thread3.Join();
thread4.Join();
Console.WriteLine("Main: All threads have ended");
Console.ReadLine();
}
}
}
Thursday, June 5, 2008
Event Handlers Are On Separate Threads
Below is some code from the Microsoft site that illustrates how to handle FileSystemWatcher events.
I added a few more write lines to report out the thread ids. This clearly shows the event handlers are occurring on separate threads. As an aside, this reinforces the 'bring on the cores' attitude to multicores, since applications tend to be multithreaded even the most simple of applications.
===Code Start===
public static void Main()
{
Watch();
}
public static void Watch()
{
string[] args = System.Environment.GetCommandLineArgs();
// If a directory is not specified, exit program.
if (args.Length != 2)
{
// Display the proper way to call the program.
Console.WriteLine("Usage: FileWatch.exe (directory)");
return;
}
// Create a new FileSystemWatcher and set its properties.
FileSystemWatcher watcher = new FileSystemWatcher();
watcher.Path = args[1];
/* Watch for changes in LastAccess and LastWrite times, and
the renaming of files or directories. */
watcher.NotifyFilter = NotifyFilters.LastAccess NotifyFilters.LastWrite
NotifyFilters.FileName NotifyFilters.DirectoryName;
// Only watch text files.
watcher.Filter = "*.txt";
// Add event handlers.
watcher.Changed += new FileSystemEventHandler(OnChanged);
watcher.Created += new FileSystemEventHandler(OnCreated);
watcher.Deleted += new FileSystemEventHandler(OnDeleted);
watcher.Renamed += new RenamedEventHandler(OnRenamed);
// Begin watching.
watcher.EnableRaisingEvents = true;
//
Console.WriteLine("main thread: " + System.Threading.Thread.CurrentThread.GetHashCode());
// Wait for the user to quit the program.
Console.WriteLine("Press \'q\' to quit the sample.");
while (Console.Read() != 'q') ;
}
// Define the event handlers.
private static void OnChanged(object source, FileSystemEventArgs e)
{
Console.WriteLine("OnChanged thread: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("File: " + e.FullPath + " " + e.ChangeType);
}
private static void OnCreated(object source, FileSystemEventArgs e)
{
Console.WriteLine("OnCreated thread: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("File: " + e.FullPath + " " + e.ChangeType);
}
private static void OnDeleted(object source, FileSystemEventArgs e)
{
Console.WriteLine("OnDeleted thread: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("File: " + e.FullPath + " " + e.ChangeType);
}
private static void OnRenamed(object source, RenamedEventArgs e)
{
Console.WriteLine("OnRenamed thread: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
// Specify what is done when a file is renamed.
Console.WriteLine("File: {0} renamed to {1}", e.OldFullPath, e.FullPath);
}
}
Tuesday, June 3, 2008
MsTest and NUnit; Using Both
But if it is not available across your company, you need not settle for the least common denominator of NUnit. And you may want to develop with MSTest but deploy with NUnit to keep the footprint light.
Fortunately there is an almost one to one mapping between the namespaces of the two.
So using the following conditional code at the top of your test class file, will enable you to alternate which testing harness you use.
==============
Code Start
==============
#if MSTEST
using Microsoft.VisualStudio.TestTools.UnitTesting;
#else
using NUnit.Framework;
using TestClass = NUnit.Framework.TestFixtureAttribute;
using TestMethod = NUnit.Framework.TestAttribute;
using TestInitialize = NUnit.Framework.SetUpAttribute;
using TestCleanup = NUnit.Framework.TearDownAttribute;
using ClassInitialize = NUnit.Framework.TestFixtureSetUpAttribute;
using ClassCleanup = NUnit.Framework.TestFixtureTearDownAttribute;
#endif
==Code End==
Some things like Categories have no direct equivalent so you will need to conditionally compile their attributes:
==Code Start==
[TestClass ]
#if MSTEST
//Intentionally empty
#else
[Category("Web Service")]
#endif
public class QA_Feature_2_1
==CodeEnd==
And another gotcha occurs around the test and suite set up and tear down functions:
==Code Start==
[ClassInitialize()]
#if MSTEST
public static void MyClassInitialize(TestContext testContext)
#else
public void MyClassInitialize()
#endif
=========================
Saturday, May 31, 2008
Starting Windows Service On Install
So I am flying along using all the tools and tricks while writing a windows service (aka NT service) using Visual Studio. Life is good. I want a windows service so the IDE has a templated project for it. The result is not to complicated. The templated windows service project leverages the Dot Net Framework which wraps all the difficult Win32 stuff.
Of course I will need to install my application and to say installing a windows service is difficult is an understatement. But once again there is a nice templated class I can add to my windows service project that leverages an excellent Dot Net Framework class. Again the result is clean, although with a small hiccup. There does not seem to be an option or flag or method to say 'start this service on install'.
This is a good example of Technical Debt. The installer for services is not complete. So I have to pay down this particular debt to get what I want. I am still way ahead and the IDE is still real useful.
If I double click the Service Installer object it created for me, the IDE creates the event handler 'sampleServiceInstaller_AfterInstall'. So all I need to do is get a reference to my service which the Framework class 'System.ServiceProcess.ServiceController' makes real easy and set its state.
There is a gotcha here if you try to start it when it is started, it considers that an error. Also if it has any difficulty at all in running this class tends to handle the problem by throwing an exception. Partly this is because the underlying Win32 api's it wraps behave this way. So for now I have an empty try catch handler. Which means I have piled up a bit of technical debt.
Now on to starting the service. The 'ServiceController' class has a service variable and if we create a switch case using the snippet for switch we get most of the following code:
private void sampleServiceInstaller_AfterInstall(object sender, InstallEventArgs e)
{
try
{
// serviceController
//
System.ServiceProcess.ServiceController serviceController = new ServiceController();
serviceController.ServiceName = "SampleService";
serviceController.Refresh();
switch (serviceController.Status)
{
case ServiceControllerStatus.ContinuePending:
break;
case ServiceControllerStatus.PausePending:
break;
case ServiceControllerStatus.Paused:
break;
case ServiceControllerStatus.Running:
break;
case ServiceControllerStatus.StartPending:
break;
case ServiceControllerStatus.StopPending:
break;
case ServiceControllerStatus.Stopped:
serviceController.Start();
break;
default:
break;
}
}
catch (Exception ex)
{
// Unhandled exception
}
}
There are a lot of states there. The one I am interested in is the Stopped state so I make the call
to start the service there.
Which leaves open the other states. I have several states that have pending in their name. What needs to happen is the program needs to wait and query the status again. The pending action will resolve one way or another. But the context makes this difficult. We are in an installer (technically we are doing post install actions, but the user will perceive it as part of the installer).
So we do not want to loop and keep waiting for this to end. We could launch a thread to watch over this situation. But then we face having the installer appear to exit while a thread is still working. It seems no matter where I turn I am going to pile up some technical debt.
Friday, May 16, 2008
The Power of Yield
To start here is the code which will ask for all the permutations and print them out.
///////////
static void Main(string[] args)
{
foreach (Listpermutation in permutations(9))
{
string result = string.Empty;
foreach (int number in permutation)
{
result += number.ToString();
}
Console.WriteLine(result);
}
Console.ReadLine();
}
///////////
If you use the refactor tools of Visual Studio it will generate the shell of the following function.
For the body, recursively call the permutations(int) function, then place the current item in all positions of resulting permutations. Of course the recursions have to stop somewhere so on the smallest set, a set of one, just return the list with one item.
///////////
private static IEnumerable permutations(int size)
{
if (size == 1)
{
Listlist = new List ();
list.Add(1);
yield return list;
}
else
{
foreach (Listpermutation in permutations(size - 1))
{
for (int i = 0; i <> list = new List(permutation); list.Insert(i, size); yield return list; } } } }
///////////
If you run this code, it generates all permutations as expected.
What is surprising is some of the performance aspects of this code.
Even though there is recursion involved here the recursion is only invoked 'n' times.
At the same time the memory being used is also a constant times 'n' squared.
And an additional kicker the number of cycles of execution is a constant (I think between 3 and 4) times 'n' factorial.
(The last is actually pretty good. If you had a really long program that hardcoded each permutation it would take 'n' factorial lines of code.)
If you study your Knuth you could probably unearth some code that performs better on one or more of these aspects. But the readability of such code is a bit obtuse and slight changes result in disasterous results where the errors are not obvious.
Compare this to how natural it was to write this code. The code reads as a description of how to generate permutations.
The performance comes from the strange nature of yield. It converts the method into a Generator instead of a Subroutine.
permalink
Wednesday, May 14, 2008
Yield The Danger
http://andrewboland.blogspot.com/2008/05/return-yield-and-return.html
I pointed out the odd implementation and behavior of the yield statement.
Here is an example of unexpected behavior of yield. Consider the code:
class Program
{
public static int globalCount;
static void Main(string[] args)
{
foreach (int number in collection())
{
Console.WriteLine(number.ToString());
SecretFunction();
}
Console.ReadLine();
}
private static IEnumerable
{
globalCount = 1;
yield return globalCount;
globalCount++;
yield return globalCount;
globalCount++;
yield return globalCount;
}
private static void SecretFunction()
{
//
}
}
/////////////////
As written this results in the output:
1
2
3
However if the body of SecretFunction() is replaced with
globalCount = 0;
The resulting output is:
1
1
1
According to the rules this is all correct but this might come as a surprise.
We are used to considering functions in isolation. In fact this is the prime reason for functions. By isolating code we reduce the cognitive load required. But given the way yield is implemented this cognitive load is radically increased. We cannot mentally checkoff the collection function, we have to be aware of this behavior.
It gets worse if we touch this code. Consider if we change the collection function with this allegedly equivalent code:
private static IEnumerable
{
List
list.Add(1);
list.Add(2);
list.Add(3);
return list;
}
/////////
Under both implementations of SecretFunction, it results in:
1
2
3
/////////
What are we to think here?
There is no doubt this will result in unexpected bugs being coded as result of this obscure behavior of yield.
permalink
Tuesday, May 13, 2008
Coroutines in C#
To recap some terminology, Generators like Coroutines are generalizations of Subroutines. Both Coroutines and Generators allow multple entry points to allow execution to be temporarly suspended and then later resumed. Whereas a Subroutine have only one point of entry and they return only once.
What distinguishes Coroutines from Generators is what is returned in the yield. Coroutines yield the next Coroutine to be invoked (or resumed). Generators yield a value to be returned to the parent function.
When yield is used it must be within an iterator block or a function which return IEnumerable. As such the resulting function is a Generator and not a Coroutine.
The good news is that Coroutines can be created or emulated from Generators by using a Dispatching function.
Consider the following code in C#:
class Program
{
static ListstringBuffer;
enum NextMethod
{
Produce,
Consume
}
static void Main(string[] args)
{
stringBuffer = new List();
Dispatcher();
}
private static void Dispatcher()
{
IEnumeratorenumeratorProduce = produceCollection().GetEnumerator();
IEnumeratorenumeratorConsume = consumeCollection().GetEnumerator();
IEnumeratorenumerator = enumeratorProduce;
while (enumerator.MoveNext())
{
NextMethod next = enumerator.Current;
if (next == NextMethod.Produce)
{
enumerator = enumeratorProduce;
}
if (next == NextMethod.Consume)
{
enumerator = enumeratorConsume;
}
}
}
private static IEnumerableproduceCollection()
{
int i = 0;
while (true)
{
stringBuffer.Add(i.ToString());
i++;
yield return NextMethod.Consume;
}
}
private static IEnumerableconsumeCollection()
{
int i = 0;
while (true)
{
Console.WriteLine(stringBuffer[0]);
stringBuffer.RemoveAt(0);
yield return NextMethod.Produce;
}
}
}
Ideally we would like produceCollection and consumeCollection to yield control to each other.
Here the same effect is accomplished by yielding an enum that is then interpreted by the Dispatcher function to invoke the correct Generator. If you set breakpoints on all the lines of the code you will see that execution within the Generator routines produceCollection and consumeCollection picks up on the line where it last left off.
permalink
Sunday, May 11, 2008
Return Yield and Return?
Debugging some c# code involving the yield statement revealed some interesting behavior.
The implementation of the yield statement is more complex than it appears on the surface.
Consider the following code:
class Program
{
static void Main(string[] args)
{
foreach (string state in collection())
{
Console.WriteLine(state);
}
Console.ReadLine();
}
private static IEnumerable collection()
{
// Console.WriteLine("before Alaska");
yield return "Alaska";
// Console.WriteLine("before Alabama");
yield return "Alabama";
// Console.WriteLine("before Kentucky");
yield return "Kentucky";
}
}
/////////////////// As expected this code results in:
Alaska
Alabama
Kentucky
Now uncomment the additional Console.Writeline statements and the code will result in:
before Alaska
Alaska
before Alabama
Alabama
before Kentucky
Kentucky
Unless you have discovered this already this should be unsettling. If you are not seeing what the big deal is set breakpoints on every line of code (stepping through is not enough). What you will see is the code appearing to hop in and back out of the collection function and then back in at the line after the line it left on. It is not executing the function repeatedly and it is not exiting it completely.
This behavior has few equivalents anywhere else in the c# language. There is power here (probably not widely known) and there is danger here (probably now widely understood).
permalink