Multiple Applications Reference Same .DLL [duplicate] - vb.net

My DLLs are loaded by a third-party application, which we can not customize. My assemblies have to be located in their own folder. I can not put them into GAC (my application has a requirement to be deployed using XCOPY).
When the root DLL tries to load resource or type from another DLL (in the same folder), the loading fails (FileNotFound).
Is it possible to add the folder where my DLLs are located to the assembly search path programmatically (from the root DLL)? I am not allowed to change the configuration files of the application.

Sounds like you could use the AppDomain.AssemblyResolve event and manually load the dependencies from your DLL directory.
Edit (from the comment):
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromSameFolder);
static Assembly LoadFromSameFolder(object sender, ResolveEventArgs args)
{
string folderPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string assemblyPath = Path.Combine(folderPath, new AssemblyName(args.Name).Name + ".dll");
if (!File.Exists(assemblyPath)) return null;
Assembly assembly = Assembly.LoadFrom(assemblyPath);
return assembly;
}

You can add a probing path to your application's .config file, but it will only work if the probing path is a contained within your application's base directory.

Update for Framework 4
Since Framework 4 raise the AssemblyResolve event also for resources actually this handler works better. It's based on the concept that localizations are in app subdirectories (one for localization with the name of the culture i.e. C:\MyApp\it for Italian)
Inside there are resources file.
The handler works also if the localization is country-region i.e. it-IT or pt-BR. In this case the handler "might be called multiple times: once for each culture in the fallback chain" [from MSDN]. This means that if we return null for "it-IT" resource file the framework raises the event asking for "it".
Event hook
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(currentDomain_AssemblyResolve);
Event handler
Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
//This handler is called only when the common language runtime tries to bind to the assembly and fails.
Assembly executingAssembly = Assembly.GetExecutingAssembly();
string applicationDirectory = Path.GetDirectoryName(executingAssembly.Location);
string[] fields = args.Name.Split(',');
string assemblyName = fields[0];
string assemblyCulture;
if (fields.Length < 2)
assemblyCulture = null;
else
assemblyCulture = fields[2].Substring(fields[2].IndexOf('=') + 1);
string assemblyFileName = assemblyName + ".dll";
string assemblyPath;
if (assemblyName.EndsWith(".resources"))
{
// Specific resources are located in app subdirectories
string resourceDirectory = Path.Combine(applicationDirectory, assemblyCulture);
assemblyPath = Path.Combine(resourceDirectory, assemblyFileName);
}
else
{
assemblyPath = Path.Combine(applicationDirectory, assemblyFileName);
}
if (File.Exists(assemblyPath))
{
//Load the assembly from the specified path.
Assembly loadingAssembly = Assembly.LoadFrom(assemblyPath);
//Return the loaded assembly.
return loadingAssembly;
}
else
{
return null;
}
}

The best explanation from MS itself:
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.AssemblyResolve += new ResolveEventHandler(MyResolveEventHandler);
private Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
//This handler is called only when the common language runtime tries to bind to the assembly and fails.
//Retrieve the list of referenced assemblies in an array of AssemblyName.
Assembly MyAssembly, objExecutingAssembly;
string strTempAssmbPath = "";
objExecutingAssembly = Assembly.GetExecutingAssembly();
AssemblyName[] arrReferencedAssmbNames = objExecutingAssembly.GetReferencedAssemblies();
//Loop through the array of referenced assembly names.
foreach(AssemblyName strAssmbName in arrReferencedAssmbNames)
{
//Check for the assembly names that have raised the "AssemblyResolve" event.
if(strAssmbName.FullName.Substring(0, strAssmbName.FullName.IndexOf(",")) == args.Name.Substring(0, args.Name.IndexOf(",")))
{
//Build the path of the assembly from where it has to be loaded.
strTempAssmbPath = "C:\\Myassemblies\\" + args.Name.Substring(0,args.Name.IndexOf(","))+".dll";
break;
}
}
//Load the assembly from the specified path.
MyAssembly = Assembly.LoadFrom(strTempAssmbPath);
//Return the loaded assembly.
return MyAssembly;
}

For C++/CLI users, here is #Mattias S' answer (which works for me):
using namespace System;
using namespace System::IO;
using namespace System::Reflection;
static Assembly ^LoadFromSameFolder(Object ^sender, ResolveEventArgs ^args)
{
String ^folderPath = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location);
String ^assemblyPath = Path::Combine(folderPath, (gcnew AssemblyName(args->Name))->Name + ".dll");
if (File::Exists(assemblyPath) == false) return nullptr;
Assembly ^assembly = Assembly::LoadFrom(assemblyPath);
return assembly;
}
// put this somewhere you know it will run (early, when the DLL gets loaded)
System::AppDomain ^currentDomain = AppDomain::CurrentDomain;
currentDomain->AssemblyResolve += gcnew ResolveEventHandler(LoadFromSameFolder);

I've used #Mattias S' solution. If you actually want to resolve dependencies from the same folder - you should try using Requesting assembly location, as shown below. args.RequestingAssembly should be checked for nullity.
System.AppDomain.CurrentDomain.AssemblyResolve += (s, args) =>
{
var loadedAssembly = System.AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName == args.Name).FirstOrDefault();
if(loadedAssembly != null)
{
return loadedAssembly;
}
if (args.RequestingAssembly == null) return null;
string folderPath = Path.GetDirectoryName(args.RequestingAssembly.Location);
string rawAssemblyPath = Path.Combine(folderPath, new System.Reflection.AssemblyName(args.Name).Name);
string assemblyPath = rawAssemblyPath + ".dll";
if (!File.Exists(assemblyPath))
{
assemblyPath = rawAssemblyPath + ".exe";
if (!File.Exists(assemblyPath)) return null;
}
var assembly = System.Reflection.Assembly.LoadFrom(assemblyPath);
return assembly;
};

I came here from another (marked duplicate) question about adding the probing tag to the App.Config file.
I want to add a sidenote to this - Visual studio had already generated an App.config file, however adding the probing tag to the pregenerated runtime tag did not work! you need a seperate runtime tag with the probing tag included. In short, your App.Config should look like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.Encoding.CodePages" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<!-- Discover assemblies in /lib -->
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="lib" />
</assemblyBinding>
</runtime>
</configuration>
This took some time to figure out so I am posting it here. Also credits to The PrettyBin NuGet Package. It is a package that moves the dlls automatically. I liked a more manual approach so I did not use it.
Also - here is a post build script that copies all .dll/.xml/.pdb to /Lib. This unclutters the /debug (or /release) folder, what I think people try to achieve.
:: Moves files to a subdirectory, to unclutter the application folder
:: Note that the new subdirectory should be probed so the dlls can be found.
SET path=$(TargetDir)\lib
if not exist "%path%" mkdir "%path%"
del /S /Q "%path%"
move /Y $(TargetDir)*.dll "%path%"
move /Y $(TargetDir)*.xml "%path%"
move /Y $(TargetDir)*.pdb "%path%"

look into AppDomain.AppendPrivatePath (deprecated) or AppDomainSetup.PrivateBinPath

Related

Project Doesn't Build After Installed Sitecore TDS

After setting up Sitecore TDS, my project will not build. I'm new to Visual Studio and also new to working with Sitecore. It seems that it cannot find a particular setting, but a Google search is not coming up with anything:
Severity Code Description Project Path File Line Suppression State
Error The "AnalyzeProject" task failed unexpectedly.
System.MissingFieldException: Field not found: 'HedgehogDevelopment.SitecoreProject.Tasks.SitecoreDeployInfo.ParsedItem'.
at HedgehogDevelopment.SitecoreProject.Analysis.TemplateStructure.Validate(Dictionary`2 projectItems, XDocument scprojDocument)
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.AnalysisEngine.<>c__DisplayClass4_1.<GetReport>b__0()
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.ExecutionTimer.Time(Action action)
at HedgehogDevelopment.SitecoreProject.Tasks.ProjectAnalysis.AnalysisEngine.GetReport(Dictionary`2 projectItems, XDocument scprojDocument)
at HedgehogDevelopment.SitecoreProject.Tasks.AnalyzeProject.Execute()
at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
at Microsoft.Build.BackEnd.TaskBuilder.<ExecuteInstantiatedTask>d__26.MoveNext() B2B.Core C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0 C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0\HedgehogDevelopment.SitecoreProject.targets 144
Apparently my project does still build and will run, but that error pops up each time regardless.
This can happen when you have TDS validations enabled and are missing some DLLs.
In this directory:
C:\Program Files (x86)\MSBuild\HedgehogDevelopment\SitecoreProject\v9.0\
Add the following DLLs:
Microsoft.Web.Infrastructure.dll
TDSWebDeploy.Services.Contracts.dll
If you have TDS installed, you may be able to source those DLLs from somewhere in your C:\Program Files (x86)\MSBuild\HedgehogDevelopment directory. If not, someone else on your team may have them.
You can also try disabling and enabling validations by right clicking your TDS project --> Properties --> Validations tab.
I have seen this issue on numerous occasions across numerous dev boxes. We have opened support tickets regarding it, but no conclusions were drawn.
The specific error is that the HedgehogDevelopment.SitecoreProject.Tasks.SitecoreDeployInfo.ParsedItem field is missing on an object. The fact that ParsedItem is mentioned implies that some form of parsing may be occurring, and that it didn't work as expected. This is conjecture, but if parsing is occurring, it would be worth ensuring that your serialized item files are accessible to the user/group that is performing this parsing.
Here is the code that is failing:
public override IEnumerable<Problem> Validate(Dictionary<Guid, SitecoreDeployInfo> projectItems, XDocument scprojDocument)
{
List<Problem> problems = new List<Problem>();
foreach (KeyValuePair<Guid, SitecoreDeployInfo> projectItem in projectItems)
{
// ** Presumably, it's failing here **
string item = projectItem.Value.ParsedItem.Properties["template"];
if (string.IsNullOrEmpty(item))
{
continue;
}
Guid guid = new Guid(item);
if (guid == TemplateStructure.TEMPLATE)
{
problems.AddRange(this.ValidateTemplate(projectItems, projectItem.Value.Item));
}
else if (guid != TemplateStructure.TEMPLATE_SECTION)
{
if (guid != TemplateStructure.TEMPLATE_FIELD)
{
continue;
}
problems.AddRange(this.ValidateField(projectItems, projectItem.Value.Item));
}
else
{
problems.AddRange(this.ValidateSection(projectItems, projectItem.Value.Item));
}
}
problems.RemoveAll((Problem r) => r == null);
return problems;
}
Here is the definition for SitecoreDeployInfo:
using HedgehogDevelopment.SitecoreCommon.Data.Items;
using System;
namespace HedgehogDevelopment.SitecoreProject.Tasks
{
public class SitecoreDeployInfo
{
public IItem ParsedItem;
public SitecoreItem Item;
public SitecoreDeployInfo()
{
}
}
}

How to Auto Increment Assembly or Assembly File Version, from MSBuild?

Constraints are:
Using Visual Studio 2017.
Needs to ultimately be called from a powershell script calling MSBuild.
Not sure its relevant, but needs to be able to build the following:
asp.net 461
asp.net-core 1.1 and 2.0 assemblies
Unsuccessful attempts so far:
How to have an auto incrementing version number (Visual Studio)? <- This works when building from VS only.
Code Generation in a Build Process -Described as Microsoft's latest document on using "TextTemplating" with MSBuild. States need to copy certain DLLs to build server.. Files are not located where specified in doc, and dont know where to copy them, I have found all files. Additionally modified .csproj's import Microsoft.TextTemplating.targets path to correct location but when running MSBuild I get. "error MSB4018: The "TransformTemplates" task failed unexpectedly."
This SO Answer - MSBuild support for T4 templates in Visual Studio 2017 RTM
<- Give same MSBuild Runtime error as above.
Example attempt of "Code Generation in a Build Process" That works on build from VS but not MSBuild:
placed in root of project - handleVersioning.tt:
<## template language="C#" #>
using System.Reflection;
[assembly: AssemblyVersion("1.0.0.*")]
[assembly: AssemblyFileVersion("<#= this.Year #>.<#= this.Month #>.<#= this.Day #>.<#= this.Minute #>")]
<#+
int Year = DateTime.UtcNow.Year;
int Month = DateTime.UtcNow.Month;
int Day = DateTime.UtcNow.Day;
int Minute = unchecked((int)DateTime.UtcNow.TimeOfDay.TotalMinutes);
#>
.csproj:
<Import Project="...hardcoded...\Microsoft.CSharp.targets" />
<!-- This is the important line: -->
<Import Project="...hardcoded...\TextTemplating\Microsoft.TextTemplating.targets" />
<PropertyGroup>
<TransformOnBuild>true</TransformOnBuild>
<OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles>
<TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>
Called like so:
msbuild myProject.csproj /t:TransformAll
U can write simple console application and in Project Properties > Build Events, add a "Pre-build event command line" like this: "D:\SomePath\MyAssemblyInfoPatcher.exe" "$(ProjectDir)Properties\AssemblyInfo.cs"
sample application code (works on VS 2022)
using System;
using System.IO;
using System.Linq;
namespace MyAssemblyInfoPatcher
{
internal class Program
{
static void Main(string[] args)
{
if (args.Length > 0)
{
string path = args[0].ToString();
Console.WriteLine(string.Format("Current App version is set to: {0}", path));
string now_date = DateTime.Now.ToString("yyyy.MM.dd.HHmm");
if (File.Exists(path))
{
string _AssemblyVersion = string.Empty;
string _AssemblyFileVersion = string.Empty;
var lines = File.ReadLines(string.Format(path));
for (int i = 0; i < lines.Count(); i++)
{
if (lines.ElementAt(i).ToString().StartsWith("[assembly: AssemblyVersion"))
{
_AssemblyVersion = lines.ElementAt(i).ToString();
}
else if (lines.ElementAt(i).ToString().StartsWith("[assembly: AssemblyFileVersion"))
{
_AssemblyFileVersion = lines.ElementAt(i).ToString();
}
}
string _replace_assembly = File.ReadAllText(path);
if (_AssemblyVersion != string.Empty)
{
_replace_assembly = _replace_assembly.Replace(_AssemblyVersion, string.Format("[assembly: AssemblyVersion(\"{0}\")]", now_date));
}
if (_AssemblyFileVersion != string.Empty)
{
_replace_assembly = _replace_assembly.Replace(_AssemblyFileVersion, string.Format("[assembly: AssemblyFileVersion(\"{0}\")]", now_date));
}
File.WriteAllText(path, _replace_assembly);
}
}
}
}
}

Setting the version number for .NET Core projects

What are the options for setting a project version with .NET Core / ASP.NET Core projects?
Found so far:
Set the version property in project.json. Source: DNX Overview, Working with DNX projects. This seems to set the AssemblyVersion, AssemblyFileVersion and AssemblyInformationalVersion unless overridden by an attribute (see next point).
Setting the AssemblyVersion, AssemblyFileVersion, AssemblyInformationalVersion attributes also seems to work and override the version property specified in project.json.
For example, including 'version':'4.1.1-*' in project.json and setting [assembly:AssemblyFileVersion("4.3.5.0")] in a .cs file will result in AssemblyVersion=4.1.1.0, AssemblyInformationalVersion=4.1.1.0 and AssemblyFileVersion=4.3.5.0
Is setting the version number via attributes, e.g. AssemblyFileVersion, still supported?
Have I missed something - are there other ways?
Context
The scenario I'm looking at is sharing a single version number between multiple related projects. Some of the projects are using .NET Core (project.json), others are using the full .NET Framework (.csproj). All are logically part of a single system and versioned together.
The strategy we used up until now is having a SharedAssemblyInfo.cs file at the root of our solution with the AssemblyVersion and AssemblyFileVersion attributes. The projects include a link to the file.
I'm looking for ways to achieve the same result with .NET Core projects, i.e. have a single file to modify.
You can create a Directory.Build.props file in the root/parent folder of your projects and set the version information there.
However, now you can add a new property to every project in one step by defining it in a single file called Directory.Build.props in the root folder that contains your source. When MSBuild runs, Microsoft.Common.props searches your directory structure for the Directory.Build.props file (and Microsoft.Common.targets looks for Directory.Build.targets). If it finds one, it imports the property. Directory.Build.props is a user-defined file that provides customizations to projects under a directory.
For example:
<Project>
<PropertyGroup>
<Version>0.0.0.0</Version>
<FileVersion>0.0.0.0</FileVersion>
<InformationalVersion>0.0.0.0.myversion</InformationalVersion>
</PropertyGroup>
</Project>
Another option for setting version info when calling build or publish is to use the undocumented /p option.
dotnet command internally passes these flags to MSBuild.
Example:
dotnet publish ./MyProject.csproj /p:Version="1.2.3" /p:InformationalVersion="1.2.3-qa"
See here for more information: https://github.com/dotnet/docs/issues/7568
Not sure if this helps, but you can set version suffixes at publish time. Our versions are usually datetime driven, so that developers don't have to remember to update them.
If your json has something like "1.0-*"
"dotnet publish --version-suffix 2016.01.02" will make it "1.0-2016.01.02".
It's important to stick to "semvar" standards, or else you'll get errors. Dotnet publish will tell you.
Why not just change the value in the project.json file. Using CakeBuild you could do something like this (optimizations probably possible)
Task("Bump").Does(() => {
var files = GetFiles(config.SrcDir + "**/project.json");
foreach(var file in files)
{
Information("Processing: {0}", file);
var path = file.ToString();
var trg = new StringBuilder();
var regExVersion = new System.Text.RegularExpressions.Regex("\"version\":(\\s)?\"0.0.0-\\*\",");
using (var src = System.IO.File.OpenRead(path))
{
using (var reader = new StreamReader(src))
{
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if(line == null)
continue;
line = regExVersion.Replace(line, string.Format("\"version\": \"{0}\",", config.SemVer));
trg.AppendLine(line);
}
}
}
System.IO.File.WriteAllText(path, trg.ToString());
}
});
Then if you have e.g. a UnitTest project that takes a dependency on the project, use "*" for dependency resolution.
Also, do the bump before doing dotnet restore. My order is as follows:
Task("Default")
.IsDependentOn("InitOutDir")
.IsDependentOn("Bump")
.IsDependentOn("Restore")
.IsDependentOn("Build")
.IsDependentOn("UnitTest");
Task("CI")
.IsDependentOn("Default")
.IsDependentOn("Pack");
Link to full build script: https://github.com/danielwertheim/Ensure.That/blob/3a278f05d940d9994f0fde9266c6f2c41900a884/build.cake
The actual values, e.g. the version is coming from importing a separate build.config file, in the build script:
#load "./buildconfig.cake"
var config = BuildConfig.Create(Context, BuildSystem);
The config file looks like this (taken from https://github.com/danielwertheim/Ensure.That/blob/3a278f05d940d9994f0fde9266c6f2c41900a884/buildconfig.cake):
public class BuildConfig
{
private const string Version = "5.0.0";
public readonly string SrcDir = "./src/";
public readonly string OutDir = "./build/";
public string Target { get; private set; }
public string Branch { get; private set; }
public string SemVer { get; private set; }
public string BuildProfile { get; private set; }
public bool IsTeamCityBuild { get; private set; }
public static BuildConfig Create(
ICakeContext context,
BuildSystem buildSystem)
{
if (context == null)
throw new ArgumentNullException("context");
var target = context.Argument("target", "Default");
var branch = context.Argument("branch", string.Empty);
var branchIsRelease = branch.ToLower() == "release";
var buildRevision = context.Argument("buildrevision", "0");
return new BuildConfig
{
Target = target,
Branch = branch,
SemVer = Version + (branchIsRelease ? string.Empty : "-b" + buildRevision),
BuildProfile = context.Argument("configuration", "Release"),
IsTeamCityBuild = buildSystem.TeamCity.IsRunningOnTeamCity
};
}
}
If you still want to have the Solution Level SharedVersionInfo.cs you can do it by adding these lines to your project.json file:
"buildOptions": {
"compile": {
"includeFiles": [
"../../SharedVersionInfo.cs"
]
}
}
Your relative path may vary, of course.
use external version.txt file with version, and prebuild step to publish this version in projects

Is it possible to read MSI properties from MSI files in chain before detect phase in bootstrapper?

In "Bundle.wxs" I have a "chain" with "MsiPackages" which contain "InstallConditions". In order for the user to decide which packages he/she wants installed/upgraded I would like to display properties found in them.
For instance, I want to read the property "ProductName" and "ProductVersion" in the "Property" table of every MSI in the chain and display it to the user next to a checkbox for every MSI in the chain. The checkbox is wired to the burn variable used in "InstallConditions".
But the problem is, it doesn't seem like I have access to the MSI files before the "Apply" step. They are not extracted from the Bootstrapper Application EXE before this step. So, my question is, Is there a way to load these values programatically in order to display them to the user before the Apply step? I could use variables and populate them myself with the values but this information is already in the MSI so this seems inefficient.
Is there a way to do this? Thanks for the help.
<Bundle>
<Variable Name="InstallProduct1" Type="string" Value="true" />
<Variable Name="ProductName1" Type="string" Value="My Product 1"/> <!-- Better way? -->
<Variable Name="ProductVersion1" Type="version" Value="1.2.3.4"/> <!-- Better way? -->
<Chain>
<MsiPackage SourceFile="my_product_1.msi"
InstallCondition="InstallProduct1">
</MsiPackage>
</Chain>
</Bundle>
WiX does generate a BootstrapperApplicationData.xml file which includes a lot of the information used to build the exe and is included in the files available at runtime. You can parse that file at runtime in order to access that metadata. Since the file, along with all of our assemblies and .msi files, are placed in a randomly-name temp folder, we can’t know ahead of time where the file will live, so we must use our assembly’s path to find it. You can then parse the XML to get the metadata.
I have a blog post with additional details here: https://www.wrightfully.com/part-3-of-writing-your-own-net-based-installer-with-wix-context-data/
In my case, I use the tag instead of '', so have some of the info available to me may not be there for you, so your experience may vary. I would suggest running a makeshift installer in debug mode and setting a breakpoint to inspect the contents of the XML in order to get a full list of what’s available.
Here’s an example of how I get data from the file in my ManagedBootstrapperApplication (c#). Note: in this example, my domain objects are MBAPrereqPackage, BundlePackage and PackageFeature, each of which take an XML node object in their constructor and further parse the data into the object’s properties.
const XNamespace ManifestNamespace = ( XNamespace) “http://schemas.microsoft.com/wix/2010/BootstrapperApplicationData” ;
public void Initialize()
{
//
// parse the ApplicationData to find included packages and features
//
var bundleManifestData = this.ApplicationData;
var bundleDisplayName = bundleManifestData
.Element(ManifestNamespace + “WixBundleProperties“ )
.Attribute( “DisplayName“)
.Value;
var mbaPrereqs = bundleManifestData.Descendants(ManifestNamespace + “WixMbaPrereqInformation“)
.Select(x => new MBAPrereqPackage(x))
.ToList();
//
//exclude the MBA prereq packages, such as the .Net 4 installer
//
var pkgs = bundleManifestData.Descendants(ManifestNamespace + “WixPackageProperties“)
.Select(x => new BundlePackage(x))
.Where(pkg => !mbaPrereqs.Any(preReq => preReq.PackageId == pkg.Id));
//
// Add the packages to a collection of BundlePackages
//
BundlePackages.AddRange(pkgs);
//
// check for features and associate them with their parent packages
//
var featureNodes = bundleManifestData.Descendants(ManifestNamespace + “WixPackageFeatureInfo“);
foreach ( var featureNode in featureNodes)
{
var feature = new PackageFeature(featureNode);
var parentPkg = BundlePackages.First(pkg => pkg.Id == feature.PackageId);
parentPkg.AllFeatures.Add(feature);
feature.Package = parentPkg;
}
}
///
/// Fetch BootstrapperApplicationData.xml and parse into XDocument.
///
public XElement ApplicationData
{
get
{
var workingFolder = Path.GetDirectoryName(this.GetType().Assembly.Location);
var bootstrapperDataFilePath = Path.Combine(workingFolder, “BootstrapperApplicationData.xml”);
using (var reader = new StreamReader(bootstrapperDataFilePath))
{
var xml = reader.ReadToEnd();
var xDoc = XDocument.Parse(xml);
return xDoc.Element(ManifestNamespace + “BootstrapperApplicationData“);
}
}
}

How to Email an Entire Log File through Nlog

Salvete! nLog for .NET has the capability to send log entries as email. But if we want to send the entire current log file, how can it be done, short of reading the log file into a string and passing that as an nLog {$message}? I don't see that nLog has a property in its mailtarget for attachments. How can this be done?
I just did this today in C#. I followed the answer provided by arkmuetz here How to get path of current target file using NLog in runtime? to first get the filename of the Nlog target. I then read the file and copied the contents of the file to another Nlog target which emailed the log in the body of the message.
First I created an NlogHelper Class
public static class NlogHelper
{
/// <summary>
/// Gets LogFileName by TargetName
/// </summary>
/// <param name="targetName">The nLog targetname for the specified logger.</param>
/// <returns></returns>
/// <remarks>https://stackoverflow.com/questions/11452645/how-to-get-path-of-current-target-file-using-nlog-in-runtime</remarks>
public static string GetLogFileName(string targetName)
{
string fileName = null;
if (LogManager.Configuration != null && LogManager.Configuration.ConfiguredNamedTargets.Count != 0)
{
Target target = LogManager.Configuration.FindTargetByName(targetName);
if (target == null)
{
throw new Exception("Could not find target named: " + targetName);
}
FileTarget fileTarget = null;
WrapperTargetBase wrapperTarget = target as WrapperTargetBase;
// Unwrap the target if necessary.
if (wrapperTarget == null)
{
fileTarget = target as FileTarget;
}
else
{
fileTarget = wrapperTarget.WrappedTarget as FileTarget;
}
if (fileTarget == null)
{
throw new Exception("Could not get a FileTarget from " + target.GetType());
}
var logEventInfo = new LogEventInfo { TimeStamp = DateTime.Now };
fileName = fileTarget.FileName.Render(logEventInfo);
}
else
{
throw new Exception("LogManager contains no Configuration or there are no named targets");
}
//if (!File.Exists(fileName))
//{
// throw new Exception("File " + fileName + " does not exist");
//}
return fileName;
}
}
Next I retrieved the file name in the constructor and stored it for later use.
_logLileName = NlogHelper.GetLogFileName("DailyFile");
For my purposes I created property that I later used to retrieve the contents of the log file.
public string LogFileContent
{
get
{
return File.ReadAllText(_logLileName);
}
}
Finally I added the contents of the file to the log to be emailed.
_logger.Error("Excel Builder LinkShare Error encountered -- Some companies did not complete succesfully!. Please review logs. \n" + mfb.LogFileContent);
Here is my nLog.config file thought will help make things more clear.
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
<targets>
<target name="console" xsi:type="ColoredConsole"
layout="${date:format=HH\:mm\:ss}|${level}|${stacktrace}|${message}" />
<target name="file" xsi:type="File" fileName="${basedir}/DomoExcelBuilderLog.txt"
layout="${stacktrace} ${message}" />
<target name="DailyFile" xsi:type="File" fileName="C:\Temp\Log${shortdate}.txt"
layout="${date:format=HH\:mm\:ss} --> ${message}" deleteOldFileOnStartup="true" />
<target name="file2" xsi:type="File" fileName="${basedir}/ServiceLog.txt"
layout="${stacktrace} ${message}" />
<target name="MyMailer" xsi:type="Mail"
smtpServer="some.server.com"
smtpPort="587"
smtpAuthentication="Basic"
smtpUsername="no-reply"
smtpPassword="somepassword"
enableSsl="true"
from="no-reply#some.server.com"
to="myemail#gmail.com"/>
</targets>
<rules>
<logger name="MultipleFileBuilder" minlevel="Trace" writeTo="DailyFile" />
<logger name="ExcelBuilderService" minlevel="Debug" writeto="MyMailer" />
</rules>
</nlog>
My application first logs to the logger named 'MultipleFileBuilder'. I then use the NLogHelper class to retrieve the contents of the Target 'DailiyFile'.
Hope this helps someoneout.
(NOTE: I removed the checking to see if the file exists in the 'GetLogFileName' method because in my case file didn't exist.)
I think you may use a combination of targets: default target + MethodCall and then send email manually or use Mail + Buffering to send a batch of records.