Monday, June 29, 2020

Misusing C#: Multiple Main() Methods

Sometimes you run across things you should probably never do in real life but are possible in your development environment. Having more than one "Main()" method in an application is one of those things in the .NET world.
An application can have more than one "static Main()" method. The only condition is that you must indicate which is the "real" entry point (through a project setting or on the command line).
Note: the code for this project is on GitHub: jeremybytes/misusing-c-sharp. Today's project is in the "MultipleMain" folder, but there will be other projects coming in the near future.

Let's look at a program that misuses this feature.

A Straight-Forward Console Application?
This is a console application with the following "Program" class (from the Program.cs file in the "MultipleMain" project mentioned above):

The "Program" class has a "static Main()" method that puts "Hello World!" on the console. If you supply a command-line parameter (such as your name), then it will say hello to you.

Since this is a .NET Core 3.1 project, lets go to the command line to build and run the application.

Build & Run
On the command line, a "dotnet build" will build the application.

Then we can use "dotnet run" to run the application.

Wait, what?

This isn't what I expected at all. What's happening?

static Main() Methods
This application misuses the normal entry point for .NET applications: "static Main()".
  1. "static Main()" is the entry point to an application.
  2. "static Main()" can be in any class.
  3. In C# 8, "static Main()" can be in an interface.
  4. You can only have one "static Main()" method per application.*
*Note: if you have more than one "static Main()" you must specify which one is the application entry point.

See that * above? It's actually very important. Normally, we are only allowed to have one "static Main()" in an application, BUT we can have more than one as long as we also tell the application which one is the entry point. This can be done with a compiler directive or with an application property. (We'll see this below.)

Another static Main() Method
Our project has another "static Main()" buried in the "DataTypes.cs" file in the "Library" folder.

I did a little bit of obfuscation with naming just for extra fun. In addition, if we open the "DataTypes.cs" file, we'll see some innocuous data types at the top of the file (from the DataTypes.cs file in the "Library" folder):

But if we scroll down a bit, we find another "static Main()" in the "IWhyNot" interface (from the same file):

This method generates 5000 random numbers before printing out "This is not what I expected!".

Note: I named this the "IWhyNot" interface in honor of my favorite comment in the language design notes. You can check out this article for more information: C# 8 Interfaces: Static Main() -- Why Not?

This is the current entry point for the application.

Specifying the "real" static Main() Method
The reason this application works this way (and builds at all) is because there is a "StartupObject" attribute in the project file.

Here is the project file (from the MultipleMain.csproj file on GitHub):

Notice the "<StartupObject>" tag. This has the fully-qualified name for the type that contains the "real" static Main() method. In this case, it is "MultipleMain.DataTypes.IWhyNot".

Overriding the StartupObject at Build
It is possible to override the "StartupObject" on the command line. Here's a new "dotnet build" that shows this.

dotnet build -p:StartupObject=MultipleMain.Program -t:Rebuild

The "dotnet build" command has a couple of parameters. The first is "-p:StartupObject=MultipleMain.Program".

The "-p" parameter adds a "property" to the build. Here we are specifying the value to use for the "StartupObject" property (this is the same property that we saw in the project file). The new value for this is "MultipleMain.Program" -- the "Program" class that we saw first.

The second parameter that we have is "-t:Rebuild"

The "-t" parameter sets a "target". This is important for our scenario.

Normally, when we "dotnet build", it uses the same methodology as "Build" from Visual Studio. If all the files in a project are unchanged, the project is not re-built. If we were to use a normal build here, then the executable would *not* be rebuilt because none of the source files (or even the project file) have changed.

So we need to specify that we want to do a "Rebuild". This is the same as "Rebuild" in Visual Studio. All of the projects are re-built whether the source files have changed or not.

So this "dotnet build" command forces a rebuild of our project using the "StartupObject" specified (and the command-line parameter overrides any values from the project file).

Now when we run the application, we get the expected results:

Using "dotnet run" gives us the expected "Hello World!". And if we use "dotnet run -- Jeremy" (which passes "Jeremy" to the program as a command-line parameter), we get the output "Hello, Jeremy".

Note: "dotnet run" will do a build if necessary. Like a normal build, this is based on changes to the source files. Since the source files are unchanged here, "dotnet run" uses the existing executable / assembly.

More Options
Because we have the command-line option for specifying the "StartupObject", we do not need to put it into the project file.

As an experiment, let's remove the "StartupObject" attribute from the "MultipleMain.csproj" file and try to build the application in Visual Studio.

We get the same error using "dotnet build" (with no additional parameters):

This gives us an error because we have not specified the entry point for the application. If we follow the error code (CS0017), we come across this article: -main (C# Compiler Options).

This shows us that we can use a "-main" command-line parameter when we're using csc.exe (the command-line C# compiler). The "-main" option is not exposed in MSBuild, and so is not available using the "dotnet build" command.

The other option in the article is to set the "StartupObject" property in the project file. And that's the route that we took here.

If we use "dotnet build" *with* the StartupObject property, then we can build successfully. We do not need the "StartupObject" attribute in the project file at all (as long as we supply the value at compile-time).

Be Kind
Please don't do this to your co-workers. Many developers are not aware of this feature. So if you bury a "static Main()" somewhere in the code and then set the "StarupObject" in the project file, another developer will probably spend a very long time figuring out what's going on.

Remember: Just because you can doesn't mean that you should.

Happy Coding!

7 comments:

  1. I'm sure if you were to search long enough and hard enough you could find a legitimate use-case for this 'feature'. But OMG it sure could be a pain for someone who isn't in the know.

    ReplyDelete
    Replies
    1. Java allows this: very useful for running unit tests or directly exercising some class code with a very simple harness...

      Delete
  2. This is used all the time in the java world. It can be pretty useful.

    ReplyDelete
  3. This is evil :) Could we actually make it even evil-er, by defining StartupObject as an environment variable (which we can), and, maybe even include a .cs file with our evil StartupObject from a totally different, unexpected location by somehow extending the Itemgroup via environment variables? This calls for a different experiment and blog post, I think (will need some more thought) :)

    ReplyDelete
    Replies
    1. You could include a secret .cs file using a Source Generator?

      Delete
  4. I often do this when building services. I will have one entry point for development and a second for deployment.

    ReplyDelete
  5. Indirections are an evil but often necessary part of programming. He who has the least wins in the end.

    ReplyDelete