Can't load dll from Roslyn in a source generator - c#

I'm doing a C# source generator and I want the developer to influence the output of the generated types based on a class with a specified interface that he will implement.
The interface is declared in a project called Core.dll.
namespace Core
{
public interface ITask
{
void Run();
}
}
The source generator gets called, compiles the class implementing the interface and executes a method.
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Collections.Immutable;
using DynamicCompilationNetStandard2._0;
namespace Generator;
[Generator]
public partial class Generator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (x, _) => x is ClassDeclarationSyntax c,
transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(x => x is not null);
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Item1, source.Item2, spc));
}
private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes, SourceProductionContext context)
{
if (classes.IsDefaultOrEmpty)
{
return;
}
try
{
var source = """
using System;
using Core;
namespace Consumer
{
public class MyTask : ITask
{
public void Run()
{
Console.WriteLine("Finished");
}
}
}
""";
Executor.Execute(source);
}
catch (Exception ex)
{
throw;
}
}
}
The executor class dynamically compiles the class and runs it.
using Basic.Reference.Assemblies;
using Core;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System;
using System.IO;
using System.Reflection;
namespace DynamicCompilationNetStandard2._0
{
public static class Executor
{
public static void Execute(string source)
{
var syntaxTree = SyntaxFactory.ParseSyntaxTree(source);
var compilation = CSharpCompilation.Create(assemblyName: Path.GetRandomFileName())
.WithReferenceAssemblies(ReferenceAssemblyKind.NetStandard20)
.AddReferences(
MetadataReference.CreateFromFile(typeof(ITask).Assembly.Location))
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
.AddSyntaxTrees(syntaxTree);
using (var ms = new MemoryStream())
{
var result = compilation.Emit(ms);
if (!result.Success)
{
throw new Exception(result.ToString());
}
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
try
{
var types = assembly.GetTypes();
}
catch (Exception ex)
{
throw;
}
dynamic task = assembly.CreateInstance("Consumer.MyTask");
task.Run();
}
}
}
}
All the framework references are correctly loaded using reference assemblies by using the nuget package Basic.Reference.Assemblies. The only one not loading is my library Core.dll but it is added as a reference to the CSharpCompilation. When I read the FusionLogs, it's like Roslyn can't load a Dll outside of it's folder.
=== Pre-bind state information ===
LOG: DisplayName = Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
(Fully-specified)
LOG: Appbase = file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/
LOG: Initial PrivatePath = NULL
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: G:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\Roslyn\csc.exe.Config
LOG: Using host configuration file:
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core.DLL.
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core/Core.DLL.
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core.EXE.
LOG: Attempting download of new URL file:///G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn/Core/Core.EXE.
To verify that everything else is good, I created a console app in .Net 7 executing the same code and it works!
using DynamicCompilationNetStandard2._0;
var source = """
using System;
using Core;
namespace Consumer
{
public class MyTask : ITask
{
public void Run()
{
Console.WriteLine("Finished");
}
}
}
""";
Executor.Execute(source);
If I put Core.dll manually inside the G:/Program Files/Microsoft Visual Studio/2022/Preview/MSBuild/Current/Bin/Roslyn folder, it works with the source generator but I don't want the developer to copy the file manually.
The question is, how can I make it work with a source generator? It is only a simple Dll to load.
The sources are ready to debug with the source generator. https://github.com/adampaquette/DynamicCompilationTests
Make sure to have the component .NET Compiler Platform SDK installed in VS2022.
Thanks!
Update
Here is the Generator.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
<IsRoslynComponent>true</IsRoslynComponent>
<PreserveCompilationContext>true</PreserveCompilationContext>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies" Version="1.4.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.4.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\DynamicCompilationNetStandard2.0\DynamicCompilationNetStandard2.0.csproj" />
</ItemGroup>
</Project>
Edit
One fix was to resolve the DLL inside the no context context for the debug to work but when compiling a consumer project the DLL is not found.
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
private Assembly? CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) =>
args.Name == "FluentType.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
? AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.FullName == args.Name)
: null;

Assuming your source generator will be packaged in a nuget package, you will have to manually add all assemblies that the source generator uses to the package.
This means the package needs to contain all assemblies in the transitive closure of the source generator's dependencies.
The only exceptions are the target framework assemblies and certain assemblies that will be provided by the roslyn environment at source generation runtime, like System.Collections.Immutable.
Be aware that the latter can differ depending on compilation environment.
Compiling in Visual Studio will run the .NET Framework version of MSBuild.
Compiling via dotnet CLI will run the .NET Core version of MSBuild.
Those are different applications running in different runtimes and will cause different assemblies and/or different assembly versions to be available. If you want your source generator to work for both Visual Studio and the dotnet CLI you need to be careful here.
I once wrote a source generator as a PoC and ran into similar problems.
You can find it at https://github.com/shuebner/XsdToSource. Maybe it can help you as a reference.
Other helpful resources include
https://www.meziantou.net/packaging-a-roslyn-analyzer-with-nuget-dependencies.htm
https://natemcmaster.com/blog/2017/07/05/msbuild-task-in-nuget/ (this is about MSBuild tasks, but explains some ways in which the MSBuild runtime environments differs)

Related

understanding CLR/System.IO.FileNotFoundException "Could not load file or assembly ... or one of its dependencies"

I have an issue with using System.Text.Json insinde my C# class library, which is a SolidWorks addin. It is probably an instance of DLL hell as described here.
Since this approach does not work, I might be able to figure something out if I understand a bit more about this issue. Maybe someone can help?
First - my code.
My 'csproj' file:
<Project Sdk="Microsoft.NET.Sdk">
<!-- general stuff -->
<PropertyGroup>
<TargetFrameworks>net48</TargetFrameworks>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
<!-- references: the top two are SolidWorks API (needed for making a SolidWorks addin -->
<ItemGroup>
<PackageReference Include="com.solidworks.core" Version="29.5.1" />
<PackageReference Include="com.solidworks.tools" Version="21.5.0" />
<PackageReference Include="System.Text.Json" Version="6.0.2" />
</ItemGroup>
<!-- In order to have the addin available within SolidWorks,
it's dll needs to be registered in the codebase. For convenience
we automatically register on build and unregister on clean. -->
<Target Name="Register" AfterTargets="AfterBuild">
<Exec Command="%windir%\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe "$(TargetPath)" /codebase" />
</Target>
<Target Name="Unregister" BeforeTargets="BeforeClean">
<Exec Command="%windir%\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe "$(TargetPath)" /u" />
</Target>
</Project>
The relevant parts of my cs file:
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;
using SolidWorks...; // all the SolidWorks usings required
namespace SwxAddin
{
[Guid("acb6f17b-9738-4f11-a324-30e05625ff89")]
[ComVisible(true)]
public class SwxAddinImpl : ISwAddin
{
// will be called on addin load in SolidWorks
public bool ConnectToSW(object swx, int addinId)
{
var jsonText = "{ \"foo\": { \"bar\": 2 } }";
var doc = System.Text.Json.JsonDocument.Parse(jsonText); // exception occurs
return swx != null;
}
// will be called on addin unload in SolidWorks
public bool DisconnectFromSW() { return true; }
// This is run when registering the dll. It writes some stuff into the
// SolidWorks registry to make the addin available.
[ComRegisterFunction]
protected static void RegisterFunction(Type type) { ... }
// This is run when unregistering the dll. It removes the stuff from the
// SolidWorks registry that was written into it by RegisterFunction.
[ComUnregisterFunction]
protected static void UnregisterFunction(Type type) { ... }
}
}
When I run SolidWorks after building (and thus, registering my dll in the codebase) and debug it, I get a runtime error on
var doc = System.Text.Json.JsonDocument.Parse(jsonText);
saying
Exception has occurred: CLR/System.IO.FileNotFoundException An
exception of type 'System.IO.FileNotFoundException' occurred in
System.Text.Json.dll but was not handled in user code: 'Could not load
file or assembly 'System.Runtime.CompilerServices.Unsafe,
Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or
one of its dependencies. The system cannot find the file specified.'
. As mentioned above, I did try adding
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
to my csproj file, resulting in the following .dll.config file in my bin/Debug folder:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
but the runtime error still occurs.
So I'd like to actually understand the issue instead of just following cooking recipes. Here are some things I tried and thoughts:
The error says the issue is inside System.Text.Json.dll. I understand it so that the file System.Text.Json.dll, which lies in location A, expects a file System.Runtime.CompilerServices.Unsafe.dll of version 4.0.4.1 in location B, but in location B there is a different version of file System.Runtime.CompilerServices.Unsafe.dll (or no file of that name at all).
=> Can anyone tell me which locations A and B we are talking about? Is it a certain folder? Is it the GAC? In case it is the GAC, are we actually talking about files, or something else?
I checked the (for me) most probable location, the folder $myProjectPath\bin\Debug\net48. There I can find (amongst others) both dlls System.Text.Json.dll and System.Runtime.CompilerServices.Unsafe.dll. I opened both in some decompilation tool to check their versions and the versions of their references. This is what I found:
System.Text.Json.dll has version 6.0.0.2 and references System.Runtime.CompilerServices.Unsafe.dll of version 6.0.0.0.
System.Runtime.CompilerServices.Unsafe.dll has version 6.0.0.0.
=> So the required version and the present version of System.Runtime.CompilerServices.Unsafe.dll do align. Why do I then get the error? Doesn't this just mean that location A and B are NOT $myProjectPath\bin\Debug\net48? Or is the referenced version ignored under some circumstances? What kind of cirumstances?
I built a standalone console app, just using System.Text.Json and containing the two lines
var jsonText = "{ \"foo\": { \"bar\": 2 } }";
var doc = System.Text.Json.JsonDocument.Parse(jsonText);
inside it Main method. No runtime error occurs there. So SolidWorks must be the culprit somehow, even if it is not mentioned in the runtime error message.
This article covers dll hell and gives suggestions for troubleshooting. I checked the modules (Debug -> Windows -> Modules) in Visual Studio. It turns out that just before the error occurs
System.Text.Json version 6.0.0.0 is already loaded (from my $myProjectPath\bin\Debug\net48 folder).
System.Runtime.CompilerServices.Unsafe is not loaded.
=> But if System.Runtime.CompilerServices.Unsafe has not been loaded before, why does System.Text.Json want to load version 4.0.4.1 instead of the version specified in its own references (6.0.0.0)? Where does the 4.0.4.1 come from?
Thanks to M Kloster's comment, I could work around the issue by manually loading the assembly - although this unfortunately does not help understanding the issue.
First I inserted the line
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);
into the ConnectToSW method (as first line).
Then I implemented MyResolveEventHandler like so:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
var nameCompilerServicesUnsafe = "System.Runtime.CompilerServices.Unsafe";
if (args.Name == nameCompilerServicesUnsafe + ", Version=4.0.4.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
{
var assemblyPath = Assembly.GetCallingAssembly().Location;
if (Path.GetFileName(assemblyPath) == "System.Text.Json.dll")
{
var assemblyFolder = Path.GetDirectoryName(assemblyPath);
var pathCompilerServicesUnsafe = Path.Combine(assemblyFolder, nameCompilerServicesUnsafe + ".dll");
if (File.Exists(pathCompilerServicesUnsafe))
return Assembly.LoadFile(pathCompilerServicesUnsafe);
}
}
return null;
}
Now, whenever an assembly cannot be loaded by the automatic mechanism, MyResolveEventHandler will be called.
Here I just check if it is System.Text.Json.dll trying to load System.Runtime.CompilerServices.Unsafe version 4.0.4.1, and if yes, I return the assembly System.Runtime.CompilerServices.Unsafe.dll from System.Text.Json.dll's location folder.
Strangely, this allowed me to confirm that the System.Text.Json.dll trying to load System.Runtime.CompilerServices.Unsafe version 4.0.4.1 really is the one located in my $myProjectPath\bin\Debug\net48 folder. Which makes no sense to me, as the decompilation tool told me that the file $myProjectPath\bin\Debug\net48\System.Text.Json.dll references System.Runtime.CompilerServices.Unsafe version 6.0.0.0, not 4.0.4.1.
And as I said in my question, the issue does not occur outside of SolidWorks (e.g. in a standalone console app). So SolidWorks must somehow interfere in the (automatic) assembly resolving mechanism, maybe redirecting bindings? Very mysterious... Is there a way to turn that off?

Azure function binding to container error when unzipping file to blob storage

I have been trying to set up a Blobtrigger function that when a zip file is uploaded to a container it unzips and uploads it to the same container. I got the solution from Azure Function: Unzip file works in debug but not in production.
public static class Function1
{
[FunctionName("Function1")]
public static async Task Run([BlobTrigger("raw-nppes/{inputBlobName}", Connection = "key")] Stream inputBlob, string inputBlobName,
Binder binder,
ILogger log)
{
log.LogInformation($"Blob trigger function received blob\n Name:{inputBlobName} \n Size: {inputBlob.Length} Bytes");
if (Path.GetExtension(inputBlobName)?.ToLower() == ".zip")
{
// We use the first char of the input file name as a dynamic part in the container. (Note: You should check if this is a valid char for the container name)
var container = $"my-dynamic-container-{inputBlobName.Substring(0, 1).ToLower()}";
var attributes = new Attribute[]
{
new BlobAttribute($"{container}", FileAccess.ReadWrite),
new StorageAccountAttribute("AzureWebJobsStorage")
};
var outputContainer = await binder.BindAsync<CloudBlobContainer>(attributes);
await outputContainer.CreateIfNotExistsAsync();
var archive = new ZipArchive(inputBlob);
foreach (var entry in archive.Entries)
{
// we write the output files to a directory with the same name as the input blob. Change as required
var blockBlob = outputContainer.GetBlockBlobReference($"{inputBlobName}/{entry.FullName}");
using (var fileStream = entry.Open())
{
if (entry.Length > 0)
{
log.LogInformation($"Extracting - {entry.FullName} to - {blockBlob.Name}");
await blockBlob.UploadFromStreamAsync(fileStream);
}
}
}
}
else
{
log.LogInformation("Not a zip file. Ignoring");
}
}
}
}
I got this error: Microsoft.Azure.WebJobs.Host: Can't bind Blob to type 'Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer'
with this possible cause: Tried binding to 'Microsoft.Azure.Storage.Blob.CloudBlobDirectory, Microsoft.Azure.Storage.Blob, Version=11.1.7.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' but user type assembly was 'Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer, Microsoft.WindowsAzure.Storage, Version=9.3.1.0
I don't know what that possible cause actually means. I have looked EVERYWHERE on here, github, and social.msdn but no answer. I have no idea what the issue is.
It seems that your assembly is too old, please install the latest Microsoft.Azure.Storage.Blob assembly:
https://www.nuget.org/packages/Microsoft.Azure.Storage.Blob/11.2.2
Then use using Microsoft.Azure.Storage.Blob; instead of using Microsoft.WindowsAzure.Storage.Blob;.
The csproj file like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Storage.Blob" Version="11.2.2" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.3" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>
Regarding the difference between them, you can refer to this post:
What is the difference between the Microsoft.Azure.Storage and WindowsAzure.Storage Nuget packages?
Pay attention on the "Important" section of the Changelog!
I was doing the same error.
https://github.com/Azure/azure-webjobs-sdk/releases/tag/storage-v4.0.0
Important Storage namespaces have changed, so you may need to update
your code if you are using any of the Storage types directly. Your
code may continue to compile as you may have an implicit reference to
the old Storage SDK, but it will fail at runtime if these have not
been updated in your code:
For blobs and queues, Microsoft.WindowsAzure.Storage.* namespaces are
now Microsoft.Azure.Storage.. For tables, the namespaces are now
Microsoft.Azure.Cosmos.Table..
Check your using ;)

How can I get my .NET Core 3 single file app to find the appsettings.json file?

How should a single-file .Net Core 3.0 Web API application be configured to look for the appsettings.json file that is in the same directory that the single-file application is built to?
After running
dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true
The directory looks like this:
XX/XX/XXXX XX:XX PM <DIR> .
XX/XX/XXXX XX:XX PM <DIR> ..
XX/XX/XXXX XX:XX PM 134 appsettings.json
XX/XX/XXXX XX:XX PM 92,899,983 APPNAME.exe
XX/XX/XXXX XX:XX PM 541 web.config
3 File(s) 92,900,658 bytes
However, attempting to run APPNAME.exe results in the following error
An exception occurred, System.IO.FileNotFoundException: The configuration file 'appsettings.json' was not found and is not optional. The physical path is 'C:\Users\USERNAME\AppData\Local\Temp\.net\APPNAME\kyl3yc02.5zs\appsettings.json'.
at Microsoft.Extensions.Configuration.FileConfigurationProvider.HandleException(ExceptionDispatchInfo info)
at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load(Boolean reload)
at Microsoft.Extensions.Configuration.FileConfigurationProvider.Load()
at Microsoft.Extensions.Configuration.ConfigurationRoot..ctor(IList`1 providers)
at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
at Microsoft.AspNetCore.Hosting.WebHostBuilder.BuildCommonServices(AggregateException& hostingStartupErrors)
at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
...
I tried solutions from a similar, but distinct question, as well as other Stack Overflow questions.
I attempted to pass the following to SetBasePath()
Directory.GetCurrentDirectory()
environment.ContentRootPath
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)
Each led to the same error.
The root of the issue is that the PublishSingleFile binary is unzipped and run from a temp directory.
In the case of this single file app, the location it was looking appsettings.json was the following directory:
C:\Users\USERNAME\AppData\Local\Temp\.net\APPNAME\kyl3yc02.5zs
All of the above methods point to the place that the file is unzipped to, which is different than the place it was run from.
I found an issue on GitHub here titled PublishSingleFile excluding appsettings not working as expected.
That pointed to another issue here titled single file publish: AppContext.BaseDirectory doesn't point to apphost directory
In it, a solution was to try Process.GetCurrentProcess().MainModule.FileName
The following code configured the application to look at the directory that the single-executable application was run from, rather than the place that the binaries were extracted to.
config.SetBasePath(GetBasePath());
config.AddJsonFile("appsettings.json", false);
The GetBasePath() implementation:
private string GetBasePath()
{
using var processModule = Process.GetCurrentProcess().MainModule;
return Path.GetDirectoryName(processModule?.FileName);
}
If you're okay with having files used at runtime outside of the executable, then you could just flag the files you want outside, in csproj. This method allows for live changes and such in a known location.
<ItemGroup>
<None Include="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
<None Include="appsettings.Development.json;appsettings.QA.json;appsettings.Production.json;">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<DependentUpon>appsettings.json</DependentUpon>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>
<ItemGroup>
<None Include="Views\Test.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</None>
</ItemGroup>
If this is not acceptable, and must have ONLY a single file, I pass the single-file-extracted path as the root path in my host setup. This allows configuration, and razor (which I add after), to find its files as normal.
// when using single file exe, the hosts config loader defaults to GetCurrentDirectory
// which is where the exe is, not where the bundle (with appsettings) has been extracted.
// when running in debug (from output folder) there is effectively no difference
var realPath = Directory.GetParent(System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;
var host = Host.CreateDefaultBuilder(args).UseContentRoot(realPath);
Note, to truly make a single file, and no PDB, you'll also need:
<DebugType>None</DebugType>
My application is on .NET Core 3.1, is published as a single file and runs as a Windows Service (which may or may not have an impact on the issue).
The proposed solution with Process.GetCurrentProcess().MainModule.FileName as the content root works for me, but only if I set the content root in the right place:
This works:
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseContentRoot(...);
webBuilder.UseStartup<Startup>();
});
This does not work:
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.UseContentRoot(...)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
this is the "piggy back" answer(s) area.
First, upvote the above answer by "RS" that I reference in this answer. That was the magic.
Short answer is "use RS's answer AND set that value in all the right places.". I show the 2 places to SET the values below.
My specific ADDITION (not mentioned anywhere else) is:
IConfigurationBuilder builder = new ConfigurationBuilder()
/* IMPORTANT line below */
.SetBasePath(realPath)
Longer answer is:
I needed the above answers AND I have some additions.
In my output (i'll show code later), here is the difference between the 2 answers above.
GetBasePath='/mybuilddir/myOut'
realPath='/var/tmp/.net/MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne/jhvc5zwc.g25'
where '/mybuilddir/myOut' was the location here I published my single file..in my docker definition file.
GetBasePath did NOT work when using PublishSingleFile
"realPath" was the way I finally got it to work. Aka, the answer above. : How can I get my .NET Core 3 single file app to find the appsettings.json file?
and when you see the value of "realPath"...then it all makes sense. the singleFile is being extracted ~somewhere....and RS figured out the magic sauce on where that extraction place is.
I will show my entire Program.cs, that will give context to everything.
Note, I had to set "realPath" in TWO places.
I marked the important things with
/* IMPORTANT
Full code below, which (again) borrows from RS's answer : How can I get my .NET Core 3 single file app to find the appsettings.json file?
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
namespace MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne
{
public static class Program
{
public static async Task<int> Main(string[] args)
{
/* easy concrete logger that uses a file for demos */
Serilog.ILogger lgr = new Serilog.LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne.log.txt", rollingInterval: Serilog.RollingInterval.Day)
.CreateLogger();
try
{
/* look at the Project-Properties/Debug(Tab) for this environment variable */
string environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
Console.WriteLine(string.Format("ASPNETCORE_ENVIRONMENT='{0}'", environmentName));
Console.WriteLine(string.Empty);
string basePath = Directory.GetCurrentDirectory();
basePath = GetBasePath();
Console.WriteLine(string.Format("GetBasePath='{0}'", basePath));
Console.WriteLine(string.Empty);
// when using single file exe, the hosts config loader defaults to GetCurrentDirectory
// which is where the exe is, not where the bundle (with appsettings) has been extracted.
// when running in debug (from output folder) there is effectively no difference
/* IMPORTANT 3 lines below */
string realPath = Directory.GetParent(System.Reflection.Assembly.GetExecutingAssembly().Location).FullName;
Console.WriteLine(string.Format("realPath='{0}'", realPath));
Console.WriteLine(string.Empty);
IConfigurationBuilder builder = new ConfigurationBuilder()
/* IMPORTANT line below */
.SetBasePath(realPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environmentName}.json", true, true)
.AddEnvironmentVariables();
IConfigurationRoot configuration = builder.Build();
IHost host = Host.CreateDefaultBuilder(args)
/* IMPORTANT line below */
.UseContentRoot(realPath)
.UseSystemd()
.ConfigureServices((hostContext, services) => AppendDi(services, configuration, lgr)).Build();
await host.StartAsync();
await host.WaitForShutdownAsync();
}
catch (Exception ex)
{
string flattenMsg = GenerateFullFlatMessage(ex, true);
Console.WriteLine(flattenMsg);
}
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
return 0;
}
private static string GetBasePath()
{
using var processModule = System.Diagnostics.Process.GetCurrentProcess().MainModule;
return Path.GetDirectoryName(processModule?.FileName);
}
private static string GenerateFullFlatMessage(Exception ex)
{
return GenerateFullFlatMessage(ex, false);
}
private static void AppendDi(IServiceCollection servColl, IConfiguration configuration, Serilog.ILogger lgr)
{
servColl
.AddSingleton(lgr)
.AddLogging();
servColl.AddHostedService<TimedHostedService>(); /* from https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio and/or https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/fundamentals/host/hosted-services/samples/3.x/BackgroundTasksSample/Services/TimedHostedService.cs */
servColl.AddLogging(blder =>
{
blder.AddConsole().SetMinimumLevel(LogLevel.Trace);
blder.SetMinimumLevel(LogLevel.Trace);
blder.AddSerilog(logger: lgr, dispose: true);
});
Console.WriteLine("Using UseInMemoryDatabase");
servColl.AddDbContext<WorkerServiceExampleOneDbContext>(options => options.UseInMemoryDatabase(databaseName: "WorkerServiceExampleOneInMemoryDatabase"));
}
private static string GenerateFullFlatMessage(Exception ex, bool showStackTrace)
{
string returnValue;
StringBuilder sb = new StringBuilder();
Exception nestedEx = ex;
while (nestedEx != null)
{
if (!string.IsNullOrEmpty(nestedEx.Message))
{
sb.Append(nestedEx.Message + System.Environment.NewLine);
}
if (showStackTrace && !string.IsNullOrEmpty(nestedEx.StackTrace))
{
sb.Append(nestedEx.StackTrace + System.Environment.NewLine);
}
if (ex is AggregateException)
{
AggregateException ae = ex as AggregateException;
foreach (Exception aeflatEx in ae.Flatten().InnerExceptions)
{
if (!string.IsNullOrEmpty(aeflatEx.Message))
{
sb.Append(aeflatEx.Message + System.Environment.NewLine);
}
if (showStackTrace && !string.IsNullOrEmpty(aeflatEx.StackTrace))
{
sb.Append(aeflatEx.StackTrace + System.Environment.NewLine);
}
}
}
nestedEx = nestedEx.InnerException;
}
returnValue = sb.ToString();
return returnValue;
}
}
}
and my toplayer csproj contents:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- allows one line of code to get a txt file logger #simple #notForProduction -->
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
and my docker file for kicks:
# See https://hub.docker.com/_/microsoft-dotnet-core-sdk/
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS buildImage
WORKDIR /mybuilddir
# Copy sln and csprojs and restore as distinct layers
COPY ./src/Solutions/MyCompany.MyExamples.WorkerServiceExampleOne.sln ./src/Solutions/
COPY ./src/ConsoleOne/*.csproj ./src/ConsoleOne/
RUN dotnet restore ./src/Solutions/MyCompany.MyExamples.WorkerServiceExampleOne.sln
COPY ./src ./src
RUN dotnet publish "./src/ConsoleOne/MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne.csproj" -c Release -o myOut -r linux-x64 /p:PublishSingleFile=true /p:DebugType=None --framework netcoreapp3.1
# See https://hub.docker.com/_/microsoft-dotnet-core-runtime/
FROM mcr.microsoft.com/dotnet/core/runtime:3.1 AS runtime
WORKDIR /myrundir
COPY --from=buildImage /mybuilddir/myOut ./
# this line is wrong for PublishSingleFile ### ENTRYPOINT ["dotnet", "MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne.dll"]
#below is probably right...i was still working on this at time of posting this answer
./myOut/MyCompany.MyExamples.WorkerServiceExampleOne.ConsoleOne

Is there an easy way to add multiple projects to a solution?

In some solutions I use direct references to internal projects during development and switch to nuget references for release. However, adding 30+ projects to new solutions manually is a tedious task.
I was wondering whether there is a quicker way to do this. With projects it's pretty easy because you just copy/paste the xml but solution files are not that easy to edit but maybe there is some trick?
You can use the dotnet CLI.
For Linux:
dotnet sln MySolution.sln add **/*.csproj
For Windows PowerShell:
dotnet sln MySolution.sln add (Get-ChildItem -Recurse *.csproj)
I've done some reaserch on the visualstudio extensibility based on #JamesFaix link and I managed to put together this small piece of code:
using System;
using System.IO;
using System.Linq;
using EnvDTE;
using EnvDTE80;
class Program
{
static void Main(string[] args)
{
// VS2019
var dteType = Type.GetTypeFromProgID("VisualStudio.DTE.16.0", true);
var dte = (EnvDTE.DTE)System.Activator.CreateInstance(dteType);
var sln = (SolutionClass)dte.Solution;
// Solution to add projects to
sln.Open(#"C:\Projects\MyProject\MySolution.sln");
// Projects should be added to the "lib" solution-folder.
var lib = FindSolutionFolderOrCreate(sln, "lib");
// These projects should be added.
var projectPaths = new[]
{
#"C:\Projects\MyLibs\Lib1.csproj",
#"C:\Projects\MyLibs\Lib2.csproj"
};
foreach (var path in projectPaths)
{
var name = Path.GetFileNameWithoutExtension(path);
// If project not already in the solution-folder then add it.
if(!(lib.Parent.ProjectItems.Cast<ProjectItem>().Any(pi => pi.Name == name)))
{
lib.AddFromFile(path);
}
}
dte.Solution.Close(true);
}
private static SolutionFolder FindSolutionFolderOrCreate(SolutionClass sln, string folderName)
{
foreach (var x in sln.Projects)
{
if (x is Project p && p.Name.Equals(folderName, StringComparison.OrdinalIgnoreCase))
{
return (SolutionFolder)p.Object;
}
}
var proj = (sln as Solution2).AddSolutionFolder(folderName);
return (SolutionFolder)proj.Object;
}
}
*.csproj file of this utility:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!--<TargetFramework>netcoreapp2.2</TargetFramework>-->
<TargetFramework>net47</TargetFramework>
<EnableUnmanagedDebugging>true</EnableUnmanagedDebugging>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Reference Include="EnvDTE">
<HintPath>..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\PublicAssemblies\envdte.dll</HintPath>
</Reference>
<Reference Include="EnvDTE80">
<HintPath>..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\PublicAssemblies\envdte80.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
I'm using net47 here because netcoreapp doesn't allow you to debug COM objects which makes it really a painful experience.
You'll also need references to the two EnvDTE files.

How to read/get a PropertyGroup value from a .csproj file using C# in a .NET Core 2 classlib project?

I want to get the value of the element <Location>SourceFiles/ConnectionStrings.json</Location> that is child of <PropertyGroup /> using C#. This is located at the .csproj file for a .NET Core 2 classlib project. The structure is as follow:
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Location>SharedSettingsProvider.SourceFiles/ConnectionStrings.json</Location>
</PropertyGroup>
Which class can I use from .NET Core libraries to achieve this? (not .NET framework)
Update 1:
I want to read the value when the application (that this .csproj file builds) runs. Both before and after deployment.
Thanks
As has been discussed in comments, csproj content only controls predefined build tasks and aren't available at run-time.
But msbuild is flexible and other methods could be used to persist some values to be available at run time.
One possible approach is to create a custom assembly attribute:
[System.AttributeUsage(System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
sealed class ConfigurationLocationAttribute : System.Attribute
{
public string ConfigurationLocation { get; }
public ConfigurationLocationAttribute(string configurationLocation)
{
this.ConfigurationLocation = configurationLocation;
}
}
which can then be used in the auto-generated assembly attributes from inside the csproj file:
<PropertyGroup>
<ConfigurationLocation>https://my-config.service/customer2.json</ConfigurationLocation>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="An.Example.ConfigurationLocationAttribute">
<_Parameter1>"$(ConfigurationLocation)"</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
And then used at run time in code:
static void Main(string[] args)
{
var configurationLocation = Assembly.GetEntryAssembly()
.GetCustomAttribute<ConfigurationLocationAttribute>()
.ConfigurationLocation;
Console.WriteLine($"Should get config from {configurationLocation}");
}

Categories