Inline MS Build Task in separate AppDomain - reflection

End Goal:
I would like to have a custom build task that takes my compiled assembly, and extracts all instances of a particular attribute for automated documentation and uninstallation. (In this case the GUID attribute for a set of COM-visible types).
Problem:
After reading some examples, the prospect of using an Inline Build Task was rather tempting. However, my task needs to reflect across the built assemblies and extract certain meta-data from it (specifically attributes).
The catch is reflecting over the assembly will lock the output file until the AppDomain is unloaded, which in this case appears to be only when Visual Studio is closed. The result: the build can only once per session.
I see that there exists special build task classes, namely AppDomainIsolatedTask but I can find no examples or evidence that this class can be utilized for an inline task.
Question:
Is it possible to run an Inline Build task in a separate AppDomain? If so, then how?
Code Sample: (as short as possible)
<UsingTask TaskName="InDomainTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<Task><Code Type="Class" Language="cs"><![CDATA[
public class InDomainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Log.LogMessage("InDomainTask AppDomain.Id = " + System.AppDomain.CurrentDomain.Id);
return true;
}
}
]]></Code></Task>
</UsingTask>
<UsingTask TaskName="OutDomainTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<Task><Code Type="Class" Language="cs"><![CDATA[
[Microsoft.Build.Framework.LoadInSeparateAppDomain]
public class OutDomainTask : Microsoft.Build.Utilities.AppDomainIsolatedTask
{
public override bool Execute()
{
Log.LogMessage("OutDomainTask AppDomain.Id = " + System.AppDomain.CurrentDomain.Id);
return true;
}
}
]]></Code></Task>
</UsingTask>
<Target Name="AfterBuild" AfterTargets="Compile">
<InDomainTask />
<OutDomainTask />
</Target>
The build output of these is:
1> InDomainTask AppDomain.Id = 1
1> OutDomainTask AppDomain.Id = 1

No, it's not possible.
Inline tasks use the CodeTaskFactory to create the task, and if you take a look at the code on GitHub, you will see that after compiling an assembly that contains the code, an instance of the task is creating using Activator.CreateInstance. This means it is always created in the same AppDomain as MSBuild.
Compare that to pre-compiled tasks, which use the TaskLoader class to create an instance of the task, and that TaskLoader looks for the LoadInSeparateAppDomainAttribute on the task type and creates an instance in a separate AppDomain if it's found.
The easiest solution is to turn your inline code into a pre-compiled task. It's really easy to do:
Create a class library project
Reference Microsoft.Build.Utilities.
Create a class for your task.
Compile the project.
Replace your UsingTask element with one that specifies the assembly that contains the tasks.
Here's an example:
Your class library that contains the tasks:
public class InDomainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
Log.LogMessage(System.AppDomain.CurrentDomain.FriendlyName);
return true;
}
}
public class OutDomainTask : Microsoft.Build.Utilities.AppDomainIsolatedTask
{
public override bool Execute()
{
Log.LogMessage(System.AppDomain.CurrentDomain.FriendlyName);
return true;
}
}
Your MSBuild file:
<UsingTask TaskName="InDomainTask" AssemblyFile="path\to\the\class\library.dll"/>
<UsingTask TaskName="OutDomainTask" AssemblyFile="path\to\the\class\library.dll"/>
<Target Name="AfterBuild" AfterTargets="Compile">
<InDomainTask/>
<OutDomainTask/>
</Target>
This outputs:
MSBuild.exe
taskAppDomain (in-proc)

Alternatively, you can run your job in separate MsBuild.exe process using Exec command and pass in same current targets file and needed properties.
<Project ToolsVersion="15.0" DefaultTargets="RunCodeFromTargetPath" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<RunCodeFromTargetPathAfterTargets>PrepareForRun</RunCodeFromTargetPathAfterTargets>
</PropertyGroup>
<Target Name="RunCodeFromTargetPath" AfterTargets="$(RunCodeFromTargetPathAfterTargets)">
<PropertyGroup Condition="'$(PlatformTarget)' == 'x64'">
<MsBuildFolderLocation >\amd64</MsBuildFolderLocation><!--support for x64 only assemblies -->
</PropertyGroup>
<Exec Command=""$(MSBuildBinPath)$(MsBuildFolderLocation)\MSBuild.exe" /target:RunCodeFromTargetPathInternal /p:TargetPath="$(TargetPath.Replace('\','\\'))" "$(MSBuildThisFileFullPath)"" />
</Target>
<Target Name="RunCodeFromTargetPathInternal">
<RunCodeFromTargetPathTask />
</Target>
<UsingTask
TaskName="RunCodeFromTargetPathTask"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<Task>
<Reference Include="$(TargetPath)" />
<Code Type="Fragment" Language="cs">
<![CDATA[
// your code here
]]>
</Code>
</Task>
</UsingTask>
</Project>

Related

Manage logging configuration with NLog in .NET Core 3

I'm using NLog in a .NET Core 3.1 worker service application.
Following the tutorial of NLog I inserted an nlog.config file to manage the configuration.
Now I'm confused because I have three points where I configure the logging:
In the code where I need to create a logger in a dependency injection context
// Other code...
services.AddScoped<IApplyJcdsCommandsJob, ApplyJcdsCommandsJob>(provider =>
{
var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.ClearProviders()
.AddFilter("Microsoft", Microsoft.Extensions.Logging.LogLevel.Trace)
.AddFilter("System", Microsoft.Extensions.Logging.LogLevel.Trace)
.AddFilter("ApplyJcdsCommandsJob", Microsoft.Extensions.Logging.LogLevel.Trace)
//.AddConsole()
//.AddEventLog();
.AddNLog(configuration);
});
Microsoft.Extensions.Logging.ILogger logger = loggerFactory.CreateLogger<CommandsJob>();
return new CommandsJob(logger);
})
// Other code...
In appSettings.json
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Trace",
"System": "Trace",
"Microsoft": "Trace"
}
}
}
In NLog.config
The default config file produced by the nuget package installation:
<!-- a section of the config -->
<targets>
<target xsi:type="File" name="f" fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="f" />
</rules>
<!-- ... -->
What I see is that if I remove the Nlog.config file, the log file will not be created.
Other changes seam to have no effect.
How are this configurations related?
What is the best way to switch on/off the logging and set the level?
People that decide to use NLog usually also want to disable all MEL-filtering to avoid the confusion with two filtering systems. So the NLog wiki-tutorial is targeted those users.
I guess people who are MEL-users first will probably just use new HostBuilder().CreateDefaultBuilder().Build() (Will setup everything with all guns enabled).
But if staying with the simple example, then you need to remove:
loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
And add:
loggingBuilder.AddConfiguration(config.GetSection("Logging"));
So it looks like this:
serviceCollection.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddConfiguration(config.GetSection("Logging"));
loggingBuilder.AddNLog(config);
})
ILoggingBuilder.AddConfiguration can be found at Nuget: Microsoft.Extensions.Logging.Configuration
AddNLog registers NLog like any other Microsoft Extension Logger (MEL) LoggingProvider (Similar to AddConsole).
This means NLog only gets log-output that has been "approved" by the MEL-ILogger. So any filtering configured in MEL will prevent logevents from reaching NLog.
NLog still has the ability to redirect based on Logger-names and LogLevel-severity to the wanted NLog-targets.
You can decide if you want to use MEL-Filtering or NLog-Filtering, or a combination of both. But if you just want to use "pure" NLog then just create an instance of NLog.Extensions.Logging.NLogLoggerFactory. It is a specialized ILoggerFactory that ignores MEL-Filtering-Configuration.
Btw. it is a little weird that you create an isolated LoggerFactory for each CommandsJob-instance. Would think that you would register the type in the dependency injection-framework, and let it inject constructor-parameters. See also this example:
https://github.com/NLog/NLog.Extensions.Logging/blob/master/examples/NetCore2/ConsoleExample/Program.cs
Where LoggerFactory is created with AddLogging(...) and where the Runner is registered in ServiceCollection for dependency-injection. When creating instance of Runner then dependency-injection will automatically provide ILogger as constructor-parameter.

ASP.NET Web API - App.config does not work?

I have simple self-host Web API application. I installed EnityFramework6 package and added following lines in App.config under <enityframework> section:
<contexts>
<context type="simple_api.MyContext, simple_api">
<databaseInitializer type="simple_api.MyInitializer, simple_api" />
</context>
</contexts>
Initializer class is like following:
public class MyInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<Context>
{
protected override void Seed(Context context)
{
Console.log("My seed method");
var persons = new List<Person> { };
var john = new Person() { Firstname = "John", Lastname = "Doe" };
context.Persons.Add(john);
context.SaveChanges();
}
}
I enabled and created migration, but the problem is that running it does not trigger my Seed method:
PM> Update-Database
Specify the '-Verbose' flag to view the SQL statements being applied to the target database.
Applying explicit migrations: [201805210804206_initial].
Applying explicit migration: 201805210804206_initial.
Running Seed method.
I also tried changing App.config, but It seems to be totally ignored, because <context type="foobar, nomatterwhatishere"> does not trigger any warning nor error.
What can be the problem?
--
By the way, when I configured log4net, file was ignored also and I have to call log4net.Config.XmlConfigurator.Configure(). Maybe there is similar thing for EntityFramework?
I was wrong: config file works, but Seed method for DropCreateDatabaseIfModelChanges is not triggered. I replaced it with DropCreateDatabaseAlways and after querying model it throwed exception:
Failed to set database initializer of type 'simple_api.MyInitializer, simple_api' for DbContext type 'simple_api.MyContext, simple_api' specified in the application configuration. See inner exception for details.
After some debugging I figured out that namespace is simple_api, but assembly name is simple-api, so configuration should be as follows:
...
<entityframework>
<contexts>
<context type="simple_api.MyContext, simple-api">
<databaseInitializer type="simple_api.MyInitializer, simple-api" />
</context>
</contexts>
...
</entityframework>
...
Now everything is working, but I am not sure why Seed was
not called for DropCreateDatabaseIfModelChanges.

Copy css/js files with msdeploy on one-click deployment

I deployed my ASP.NET MVC 5 application using the one-click deployment in Visual Studio.
Meanwhile I need to copy some css/js files to a centralized folder, so that they can be used from different applications of my solution.
It seems that msdeploy has some options to solve this. But where/how can I pass parameters to msdeploy using Visual Studio 2015?
One of options is:
we need some class that inherits Task. It will copy files.
public class CopyFilesToFolder : Task
{
[Required]
public string FolderPathForSource { get; set; }
[Required]
public string OutputFolder { get; set; }
public override bool Execute()
{
//Do copy here
return true;
}
}
For example YouProject is the what you want do deploy.
We need to edit YourProject.csproj file:
register dll with CopyFilesToFolder class and add target for MsDeploy
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="CopyFilesToFolder" AssemblyFile="..\..\pathTodll\dllWithCopyFilesToFolderClass.dll">
</UsingTask>
<Target Name="CopyFilesToFolderForPublish">
<CopyFilesToFolder FolderPathForSource="$(ProjectDir)\\$(_PackageTempDir)\\Content\\" OutputFolder="C:\\Deploy\\Content\\">
</CopyFilesToFolder>
</Target>
<!-- ... -->
</Project>
As you see there we can pass parameters to class we create earlier.
And the last, in YourProject.pubxml file we told MsDeploy that when he putt together all files needed for package execute CopyFilesToFolderForPublish target
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- Other properties -->
<OnAfterCopyAllFilesToSingleFolderForPackage>CopyFilesToFolderForPublish;</OnAfterCopyAllFilesToSingleFolderForPackage>
</PropertyGroup>
</Project>

Exception running application when adding insights to application using Application Insights Status Monitor Preview

I'm playing around with the preview, and tried adding insights to a IIS web application deployed locally on my machine. It's a .Net 4.5 application running in a nothing out of the ordinary application pool. When starting the application after adding insights, I get this exception:
Could not load file or assembly 'Microsoft.ApplicationInsights.Extensions.Intercept_x64.dll' or one of its dependencies. The module was expected to contain an assembly manifest.
I tried "Enable 32-Bit Applications" to both true and false with no difference in result.
Has anyone experienced a similar error?
Unfortunately ASP.NET tries to load literally everything that is in \bin as managed assemblies
Microsoft.ApplicationInsights.Extensions.Intercept_x64.dll is not a managed assembly, but ASP.NET Web App should not fail with yellow page in this case, you would see it only in FusLogvw.
Do you use any web publishing?
Did you precompile your web site on publish?
Could you provide full stack trace of the exception?
I've just come across this issue, and after a few hours found it was due to a conflict with FluentSecurity.
It's detailed here: https://github.com/kristofferahl/FluentSecurity/issues/70
The work-around was to add the following lines just before calling SecurityConfigurator.Configure():
SecurityDoctor.Current.EventListenerScannerSetup = scan =>
{
scan.ExcludeAssembly(file => Path.GetFileName(file).Equals("Microsoft.ApplicationInsights.Extensions.Intercept_x64.dll"));
scan.ExcludeAssembly(file => Path.GetFileName(file).Equals("Microsoft.ApplicationInsights.Extensions.Intercept_x86.dll"));
};
Hope this helps somebody else.
My inner exception pointed to WebActivator. So I Uninstall-Package WebActivator -Force, added the appropriate calls in Application_Start and all was good again.
I'm still testing this but I think I've resolved this problem....
The solution is based on the same solution as the SQL Spatial Types native .dll solution; if you know this you'll see the similarity between this and that package.
Step 1
Go Create a new subdirectory in the MVC project and under this two more sub-directories; I used :
MVCRoot ---> ApplicationInsights/x86
---> ApplicationInsights/x64
Under each directory add a linked item from the package directory, this was :
../packages\Microsoft.Diagnostics.Instrumentation.Extensions.Intercept.0.12.0-build02810\lib\native\x64\Microsoft.ApplicationInsights.Extensions.Intercept_x64.dll
and
../packages\Microsoft.Diagnostics.Instrumentation.Extensions.Intercept.0.12.0-build02810\lib\native\x86\Microsoft.ApplicationInsights.Extensions.Intercept_x86.dll
respectively.
I then add this code in a file in the 'root' of the AppplicationInsights folder called loader.cs which looked like this:
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace ApplicationInsights
{
/// <summary>
/// Utility methods related to CLR Types for SQL Server
/// </summary>
internal class Utilities
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr LoadLibrary(string libname);
/// <summary>
/// Loads the required native assemblies for the current architecture (x86 or x64)
/// </summary>
/// <param name="rootApplicationPath">
/// Root path of the current application. Use Server.MapPath(".") for ASP.NET applications
/// and AppDomain.CurrentDomain.BaseDirectory for desktop applications.
/// </param>
public static void LoadNativeAssemblies(string rootApplicationPath)
{
var nativeBinaryPath = IntPtr.Size > 4
? Path.Combine(rootApplicationPath, #"ApplicationInsights\x64\")
: Path.Combine(rootApplicationPath, #"ApplicationInsights\x86\");
CheckAddDllPath(nativeBinaryPath);
// LoadNativeAssembly(nativeBinaryPath,
// IntPtr.Size > 4
// ? "Microsoft.ApplicationInsights.Extensions.Intercept_x64.dll"
// : "Microsoft.ApplicationInsights.Extensions.Intercept_x86.dll");
}
public static void CheckAddDllPath(string dllPath)
{
// find path to 'bin' folder
var pathsToAdd = Path.Combine(new string[] { AppDomain.CurrentDomain.BaseDirectory, dllPath });
// get current search path from environment
var path = Environment.GetEnvironmentVariable("PATH") ?? "";
// add 'bin' folder to search path if not already present
if (!path.Split(Path.PathSeparator).Contains(pathsToAdd, StringComparer.CurrentCultureIgnoreCase))
{
path = string.Join(Path.PathSeparator.ToString(), new string[] { path, pathsToAdd });
Environment.SetEnvironmentVariable("PATH", path);
}
}
private static void LoadNativeAssembly(string nativeBinaryPath, string assemblyName)
{
var path = Path.Combine(nativeBinaryPath, assemblyName);
var ptr = LoadLibrary(path);
if (ptr == IntPtr.Zero)
{
throw new Exception(string.Format(
"Error loading {0} (ErrorCode: {1})",
assemblyName,
Marshal.GetLastWin32Error()));
}
}
}
}
I then added this to the global.asax this so:
protected override void Application_Start(object sender, EventArgs e)
{
ApplicationInsights.Utilities.LoadNativeAssemblies(Server.MapPath("~/bin"));
}
So far it seems to be passed events so far as I can tell. All come back and update this should I find a problem with what I've done.
At least the MVC application now starts :-)
UPDATE: This is not the end of the story :-(
I had to also modify the Microsoft.Diagnostics.Instrumentation.Extensions.Intercept.props file which is under the build directory of the package to make it not include the files into the bin directory.
When I was done it looked like this :
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)\..\lib\native\x86\Microsoft.ApplicationInsights.Extensions.Intercept_x86.dll">
<CopyToOutputDirectory>None</CopyToOutputDirectory>
</None>
<None Include="$(MSBuildThisFileDirectory)\..\lib\native\x64\Microsoft.ApplicationInsights.Extensions.Intercept_x64.dll">
<CopyToOutputDirectory>None</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
I've had to check in this package into my source control system as I think I'm now going to be faced with the problem with Continuous Build restoring a fresh copy of the package which I don't want.
I can't wait for MS to come up with a proper fix for this.
I've just deleted everything in my /bin folder and it seemed to have resolved the issue. Not sure what happen or anything, it's a project i haven't touched in ages. But it solved it :)

Microsoft Unity - Is it possible to change the registered type at runtime?

I am currently using Unity with MOQ to do my unit testing for WCF. In the application's code, I have the following:
private void MyMethod()
{
.....
.....
_proxy = new UnityContainer().LoadConfiguration().Resolve<IMyInterface>();
.....
}
In the application's app.config, I have the following:
<container>
<register type="IMyInterface" mapTo="MyActualObject" />
</container>
In the unit test's app.config, I replace that with my mock object implementation of the proxy.
<container>
<register type="IMyInterface" mapTo="MyMockObject" />
</container>
That is all working fine. But what I would like to do further is for certain tests, I would like to replace MyMockObject with a different mock object implementation.
Is it possible to change the registered type at runtime? I have tried modifying the application config during runtime but it fails to detect the change.
Configuration appConfig = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
var section = (UnityConfigurationSection)appConfig.GetSection("unity");
section.Containers[0].Registrations[0].MapToName = "AnotherMockObject";
appConfig.Save();
Thanks!!
Yes its possible.
You can configure Unity as many times as you want. If there's a conflict the most recent definition wins.
In your case if you want to make a runtime change, use the fluent API instead of the config file. Try something like this:
IUnityContainer container = new UnityContainer();
container.LoadConfiguration();
container.RegisterType<IMyInterface, AnotherMockObject>();
// use AnotherMockObject
_proxy = Resolve<IMyInterface>();
The documentation for Registering Types and Type Mappings

Resources