Fixing the error "Program has more than one entry point defined" for console apps containing xUnit tests

This post describes a problem I ran into when converting a test project from .NET Framework to .NET Core. The test project was a console app, so that specific tests could easily be run from the command line, as well as using the normal xUnit console runner. Unfortunately, after converting the project to .NET Core, the project would no longer compile, giving the error:

CS0017 Program has more than one entry point defined. Compile with /main to specify the type that contains the entry point.

This post digs into the root cause of the error, why it manifests, and how to fix it. The issue and solution are described in this GitHub issue.

The problematic test project

The configuration I ran into probably isn't that common, but I have seen it used in a few places. Essentially, you have a console application that contains xUnit (or some other testing framework like MSTest) tests. You can then easily run certain tests from the command line, without having to use the specific xUnit or MSTest test runner/harness.

For example, you might have some key integration tests that you want to be able to run on some machine that doesn't have the required unit testing runners installed. By using the console-app approach, you can simply call the test methods in the program's static void main method. Alternatively, you may want to include xUnit tests as part of your real app.

Consider the following example project. It consists of a single "integration" test in the CriticialTests class, and a Program.cs that runs the test on startup.

Why is there a problem?

so if they're console applications, surely they should have a static void main right?

Why are test project's console projects?

The detail of why a test project is a console application is a little subtle; it's not immediately obvious from looking at the .csproj project file.

For example, consider the following project file. This is for a .NET Core class library project, created using the "SDK style" .csproj file. There's not much to it, just the Sdk attribute and the TargetFramework (which is .NET Core 2.0):

It has an <IsPackable> project, so calling dotnet pack on the solution won't try and create a NuGet for this project

It has three NuGet packages for the .NET Test SDK, xUnit, and the xUnit adapater for dotnet test

It doesn't have an <OutputType> of Exe

The interesting point is that last bullet. I (and the documentation) stated that a test project is a console app, so why doesn't it have an <OutputType>?

The secret, is that the Microsoft.NET.Test.Sdk NuGet package is injecting the <OutputType> element when you build your project. It does this by including a .targets file in the package, which runs automatically when your project builds.

If you want to see for yourself, open the Microsoft.Net.Test.Sdk.targets file from the NuGet package (e.g. at %USERPROFILE%\.nuget\packages\microsoft.net.test.sdk\15.3.0\build\netcoreapp1.0) Alternatively, you can view the file on NuGet. The important part is:

So if the project is a .NET Core project, it adds the <OutputType>. That explains why the project is a console app, but it doesn't explain why we're getting a build error…

It also doesn't explain why the test SDK needs to do this in the first place, but I don't have the answer to that one. This comment by Brad Wilson suggests it's not actually required, and is there for legacy reasons more than anything else.

Why is there a build error?

The build error is actually a consequence of forcing the project to a console app. If you take a library project and simply add the <OutputType>Exe</OutputType> element to it, you'll get the following error instead:

CS5001 Program does not contain a static 'Main' method suitable for an entry point

A console app needs an "entry point" i.e. a method to run when the app is starting. So to convert a library project to a console app you must also add a Program class with a static void Main method.

Can you see the problem with that, given the Microsoft.Net.Test.Sdk<OutputType> behaviour?

If adding the Microsoft.Net.Test.Sdk package to a library project silently converted it to a console app, then you'd get a build error by default. You'd be forced to add a static void Main, even if you only ever wanted to run the app using dotnet test or Visual Studio's Test Explorer.

To get round this, the SDK package automatically generates a Program file for you if you're running on .NET Core. This ensures the build doesn't break when you add the package to a class library. You can see the MSBuild target that does this in the .targets file for the package. It creates a Program file using the correct language (VB or C#) and compiles it into your test project.

Which leads us back to the original error. If you already have a Program file in your project, the compiler doesn't know which file to choose as the entry point for your app, hence the message:

CS0017 Program has more than one entry point defined. Compile with /main to specify the type that contains the entry point.

So now we know exactly what's happening, we can fix it.

The solution

The error message gives you a hint as to how to fix it, Compile with /main, but the compiler is assuming you actually want both Program classes. In reality, we don't need the auto generated one at all, as we have our own.

Luckily, the Microsoft.Net.Test.Sdk.targets file uses a property to determine whether it should generate the file:

With the property added, we can build and run our application both using dotnet test, or by simply running the console app directly.

Summary

The Microsoft.Net.Test.Sdk NuGet package required for testing with the dotnet test framework includes an MSBuild .targets file that adds an <OutputType>Exe</OutputType> property to your test project, and automatically generates a Program file.

If your test project is already a console application, or includes a Program class with a static void main method, then you must disable the auto-generation of the program file. Add the following element to your test project's .csproj, inside a <PropertyGroup> element: