ChatGPT解决这个技术问题 Extra ChatGPT

Can't read app.config in C# .NET Core unit test project with ConfigurationManager

I've created a simple unit test project to read an app.config file. Target framework is Core 2.0. I also created a Core 2.0 console app, to sanity-check myself to make sure I wasn't doing anything weird (same test passed as expected in a .NET 4.6.1 unit test project).

The console app reads the app.config fine, but the unit test method fails and I cannot figure out why. Both are using a copy of the same app.config (not added as a link) and both have the System.Configuration.ConfigurationManager v4.4.1 NuGet package installed.

The App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="Test1" value ="This is test 1."/>
    <add key="Test2" value ="42"/>
    <add key="Test3" value ="-42"/>
    <add key="Test4" value="true"/>
    <add key="Test5" value="false"/>
    <add key="Test6" value ="101.101"/>
    <add key="Test7" value ="-1.2345"/>
  </appSettings>
</configuration>

The Unit Test

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Configuration;

namespace ConfigTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod()]
        public void ConfigTest()
        {
            foreach (string s in ConfigurationManager.AppSettings.AllKeys)
            {
                System.Console.WriteLine(s);
                System.Diagnostics.Debug.WriteLine(s);
            }

            //AllKeys.Length is 0? Should be 7...
            Assert.IsTrue(ConfigurationManager.AppSettings.AllKeys.Length == 7);
        }
    }
}

The Console App

using System;
using System.Configuration;

namespace ConfigTestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (string s in ConfigurationManager.AppSettings.AllKeys)
            {
                Console.WriteLine(s);
                System.Diagnostics.Debug.WriteLine(s);
            }

            //Outputs 7 as expected
            Console.WriteLine(ConfigurationManager.AppSettings.AllKeys.Length);
        }
    }
}  

Given that I'm still pretty new to the whole .NET Core world, am I doing something totally incorrect here? I sort of just feel crazy at the moment...

https://i.stack.imgur.com/O8si3.jpg

are you sure that correct .config is linked w/ the test project and that test project doesn't have its own .config that you read in the test?
ConfigurationManager.AppSettings[0]; work?
@PawełŁukasik - The only 2 configs I have in the entire solution directory are copies of what you see above (apart from the *.dll.configs that get generated). The full path as listed in the properties window is what I expect them to be.
@MichaelEvanchik - Nope. ArgumentOutOfRangeException.
@MartinUllrich - The unit test finds 0 config items (it should find 7 - I'll edit the code to make that more clear). The app is finding all 7 as expected. Did I understand your question correctly?

P
Paul Hatcher

Looking through the github issue's comments, I found a work around that can go in the msbuild file...

<Target Name="CopyCustomContent" AfterTargets="AfterBuild">
  <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\testhost.dll.config" />
</Target>

This makes it easier to verify existing tests under .NET Core before porting the configuration data over to json configuration files.

Edit

If running under Resharper, the previous answer doesn't work as Resharper proxies the assembly, so you need

<Target Name="CopyCustomContent" AfterTargets="AfterBuild">
  <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\ReSharperTestRunner64.dll.config" />
</Target>

Not sure if this is specific to VS 2022 (which is now 64-bit), but for the version of ReSharper and VS at the time of this writing (Apr 2022), the config file had to be named ReSharperTestRunner.dll.config (no 64) for it to work. Hoping someone finds this useful.
@MikeLoux I'll check it out and update if necessary
3
3DPrintScanner

If you check the result of the call to ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

It should tell you where the required configuration file should be while running unit tests for that assembly.

I found that instead of having an app.config file, ConfigurationManager was looking for a testhost.dll.config file.

This was for a project targeting netcoreapp2.1, with a reference to Microsoft.NET.Test.Sdk,NUnit 3.11 and Nunit3TestAdapter 3.12.0


See answer below for a way to get the app.config file to testhost.dll.config
Note: I had to set the file property "Copy to Output Directory" to "Copy Always" in addition to renaming it from app.config to testhost.dll.config for it to work. Thanks for the rename tip!
This is what I used to determine that the current version of R# was looking for ReSharperTestRunner.dll.config and not (anymore) ReSharperTestRunner64.dll.config.
l
littlerani

.CORE 3.1 To find out what dll.config file was being used, I debugged the test by adding this line and looking to see what the value is.

string path = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;

Then I found out resharper was using testhost.dll.config and VStest was using testhost.x86.dll.config. I needed to add the following lines to the project file.

  <Target Name="CopyCustomContent" AfterTargets="AfterBuild">
    <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\testhost.dll.config" />
    <Copy SourceFiles="app.config" DestinationFiles="$(OutDir)\testhost.x86.dll.config" />
  </Target>

testhost.x86.dll.config was also the name that I needed for my net core 3.0 project. This also worked with the rename the app.config -> testhost.x86.dll.config and setting the copy to output directory property to copy always.
M
Mario

I came across the same issue with my xunit tests and solved it by using the instance of Configuration from ConfigurationManager. I put the static (normal) way it works in core, framework (but not unit tests) before I show the alternative way it works in all three:

        var appSettingValFromStatic = ConfigurationManager.AppSettings["mySetting"];
        var appSettingValFromInstance = ConfigurationManager.OpenExeConfiguration(Assembly.GetExecutingAssembly().Location).AppSettings.Settings["mySetting"].Value;

And here is a similar/related issue. In case anyone needs to get a section you can do a similar thing, though the type must change in the app config:

<configSections>
    <section name="customAppSettingsSection" type="System.Configuration.AppSettingsSection"/>
    <section name="customNameValueSectionHandlerSection" type="System.Configuration.NameValueSectionHandler"/>
</configSections>

<customAppSettingsSection>
    <add key="customKey" value="customValue" />
</customAppSettingsSection>

<customNameValueSectionHandlerSection>
    <add key="customKey" value="customValue" />
</customNameValueSectionHandlerSection>

Code to grab section:

        var valFromStatic = ((NameValueCollection)ConfigurationManager.GetSection("customNameValueSectionHandlerSection"))["customKey"];
        var valFromInstance = ((AppSettingsSection)ConfigurationManager.OpenExeConfiguration(Assembly.GetExecutingAssembly().Location).GetSection("customAppSettingsSection")).Settings["customKey"].Value;

I feel like I am also crazy, and I know there are newer ways of doing config in core, but if one wants to do something cross-platform this is the only way I know how. I'd be very interested if anyone has alternatives


D
Daniel

For my mixed .NET-Core & .NET-Framework project, I added the following to the unit test global setup:

#if NETCOREAPP
using System.Configuration;
using System.IO;
using System.Reflection;
#endif

...

// In your global setup:
#if NETCOREAPP
    string configFile = $"{Assembly.GetExecutingAssembly().Location}.config";
    string outputConfigFile = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).FilePath;
    File.Copy(configFile, outputConfigFile, true);
#endif

This copies the config file to the output path testhost.dll.config but should be resilient enough to account for future changes in the testing framework.

Or you can copy to below, which amounts to the same thing:

string outputConfigFile = Path.Combine(Path.GetDirectoryName(configFile), $"{Path.GetFileName(Assembly.GetEntryAssembly().Location)}.config");

Credit to @stop-cran and @PaulHatcher's solutions, this is a combination of those two.


M
MoonStom

None of the answers given here provide a viable workaround when you're dealing with code accessing directly the static ConfigurationManager properties such as AppSettings or ConnectionStrings.

The truth is, it is not possible at the moment. You can read through the discussion here to understand why: https://github.com/dotnet/corefx/issues/22101

There is talk to implement the support for it here: https://github.com/Microsoft/vstest/issues/1758

In my opinion it makes sense to support this scenario since it's been working on the .NET Framework plus System.Configuration.ConfigurationManager is a .NET Standard 2.0 library now.


I agree with your last point completely. Sure, it's not the "new/preferred" way of doing things, but it certainly worked in .NET and seemed incredibly strange to me at the time that it didn't work here.
s
stop-cran

A hacky, but working way is to copy the config to the same folder as an entry assembly, whatever it is:

[SetUpFixture]
public class ConfigKludge
{
    [OneTimeSetUp]
    public void Setup() =>
        File.Copy(
            Assembly.GetExecutingAssembly().Location + ".config",
            Assembly.GetEntryAssembly().Location + ".config",
            true);

    [OneTimeTearDown]
    public void Teardown() =>
        File.Delete(Assembly.GetEntryAssembly().Location + ".config");
}

Apart from adding this class, the only thing to make it work is to include app.config file in test project (without any copying options). It should be copied to the output folder as <your test project name>.dll.config at the build step, because it's kind of default logic.

Note the documentation for OneTimeSetUpAttribute:

Summary: Identifies a method that is called once to perform setup before any child tests are run.

Although it should work for parallel test runs for a single project, there could be obvious troubles when running two test projects simultaneously, since the config would get overwritten.

However, it is still suitable for containerized test runs, like in Travis.


J
Jehedi Zafoom

When we answer such well-researched and well-articulated question, we should better assume that it is being asked by an informed and intelligent being. And instead of patronizing them with the obvious about new, great ways of writing tonnes of boilerplate code to parse all sort of JSON et al, being forced on us and shoved down our throat by know-betters, we should focus on answering to the point.

Since the OP is already using System.Configuration to access settings, they already know how to arrive at this point. The only thing that is missing is one little touch: adding this line to the post-build event:

copy $(OutDir)<appname>.dll.config $(OutDir)testhost.dll.config

where is the project being unit-tested.

I applaud everyone who is still using (originally lame but workable) implementation of app.config because doing so protects our and our clients' investment in technology instead of reinventing the wheel. Amen.


M
Martin Ullrich

The ConfigurationManager API will only use the configuration of the app that is currently running. In a unit test project, this means the app.config of the test project, not the console application.

.NET Core Applications aren't supposed to use app.config or ConfigurationManager, as it is a legacy "full framework" configuration system.

Consider using Microsoft.Extensions.Configuration instead to read JSON, XML or INI configuration files. See this doc: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration


I'm a little confused as to what your first paragraph is implying.. The unit test project does have it's own app.config.
Then add one and see what happens.
The test project already has one? I was implying that you add an app.config to the unit test project and see if the AppSettings will show up.
You are doing a unit test and in unit test your concentration should be the particular method trying to test and should remove extraneous dependencies. in this case, try mocking/moleing(use Microsoft Mole and Pex or moc) system.configuration class
did you ever figure out a solution to this? Seems silly that ConfigurationManager work in framework, standard, and core, except for in unit tests
a
aboy021

Mercifully there is now a way to set the name of the expected configuration file at runtime. You can set the APP_CONFIG_FILE data for the current app domain.

I created the following SetUpFixture to do this automatically:

[SetUpFixture]
public class SetUpFixture
{
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        var testDllName = Assembly.GetAssembly(GetType())
                                  .GetName()
                                  .Name;
        var configName = testDllName + ".dll.config";
        AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", configName);
    }
}

The relevant GitHub discussions are:

ConfigurationManager doesn't find config file with "dotnet test" · Issue #22720 · dotnet/runtime

Provide a way to override the global configuration file path · Issue #931 · dotnet/runtime

Respect AppContext.SetData with APP_CONFIG_FILE key by krwq · Pull Request #56748 · dotnet/runtime


F
Fidel Orozco

Usually in .NET Framework projects, any App.config file was copied to the bin folder by Visual Studio, with the name of your executable (myApp.exe.config) so it could be reachable in runtime. Not anymore in .NET Standard or Core Framework. You must manually copy and set the file in the bin/debug or release folder. After that it could be get with something like:

                string AssemblyName = System.IO.Path.GetFileName(System.Reflection.Assembly.GetEntryAssembly().GetName().CodeBase);
            AppConfig = (System.Configuration.Configuration)System.Configuration.ConfigurationManager.OpenExeConfiguration(AssemblyName);

K
KH from UA

While app.config exists in the root project folder add below string to Post-build event command line

xcopy /Y $(ProjectDir)app.config $(ProjectDir)$(OutDir)testhost.dll.config*

There are twelve existing answers to this question, including a top-voted, accepted answer with forty six votes. Are you certain your solution hasn't already been given? If not, why do you believe your approach improves upon the existing proposals, which have been validated by the community? Offering an explanation is always useful on Stack Overflow, but it's especially important where the question has been resolved to the satisfaction of both the OP and the community. Help readers out by explaining what your answer does different and when it might be preferred.
sure, I missed the answer given by Jehedi Zafoom in this thread, his solution is very close to mine. While selected answer requires creating and editing of msbild file, there is a dedicated Post-build event tool that is targeted for resolving of post-build issues. This approach is essential in large solutions , when each project uses the same app.config file which is located in dedicated folder.
A
Amir Touitou

Add the configuration file

First, add a appconfig.json file to the Integration test project

Configure the appconfig.json file to be copied to the output directory by updating

https://i.stack.imgur.com/vWblN.png

Add NuGet package

Microsoft.Extensions.Configuration.Json

Use the configuration in your unit tests

[TestClass]
public class IntegrationTests
{


    public IntegrationTests()
    {
        var config = new ConfigurationBuilder().AddJsonFile("appconfig.json").Build();

        _numberOfPumps = Convert.ToInt32(config["NumberOfPumps"]);

        _numberOfMessages = Convert.ToInt32(config["NumberOfMessages"]);

        _databaseUrl = config["DatabaseUrlAddress"];
    }
} 

AddJsonFile not exist in .net core 3.1