diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4ce6fdd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,340 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- Backup*.rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
\ No newline at end of file
diff --git a/BizTalkTrackedMessageExtractor.csproj b/BizTalkTrackedMessageExtractor.csproj
new file mode 100644
index 0000000..9f8ba48
--- /dev/null
+++ b/BizTalkTrackedMessageExtractor.csproj
@@ -0,0 +1,87 @@
+
+
+
+ Debug
+ AnyCPU
+ 8.0.50727
+ 2.0
+ {9B52911C-E1B8-4B68-B6AE-2FB9126AA502}
+ Exe
+ Properties
+ Ox.BizTalk.TrackedMessageExtractor
+ extractmessages
+ Extract-object.ico
+
+
+
+
+
+
+
+
+ v3.5
+
+
+
+
+ 2.0
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ False
+ C:\Program Files (x86)\Microsoft BizTalk Server 2013 R2\Developer Tools\Microsoft.BizTalk.ExplorerOM.dll
+
+
+ False
+ C:\Program Files (x86)\Microsoft BizTalk Server 2006\Microsoft.BizTalk.Operations.dll
+
+
+ False
+ C:\Program Files (x86)\Microsoft BizTalk Server 2006\Microsoft.BizTalk.Pipeline.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BizTalkTrackedMessageExtractor.sln b/BizTalkTrackedMessageExtractor.sln
new file mode 100644
index 0000000..ba8b613
--- /dev/null
+++ b/BizTalkTrackedMessageExtractor.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29613.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkTrackedMessageExtractor", "BizTalkTrackedMessageExtractor.csproj", "{9B52911C-E1B8-4B68-B6AE-2FB9126AA502}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{644C4428-4179-411E-83B6-38D4822337E9}"
+ ProjectSection(SolutionItems) = preProject
+ LICENSE.txt = LICENSE.txt
+ README.md = README.md
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9B52911C-E1B8-4B68-B6AE-2FB9126AA502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B52911C-E1B8-4B68-B6AE-2FB9126AA502}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B52911C-E1B8-4B68-B6AE-2FB9126AA502}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B52911C-E1B8-4B68-B6AE-2FB9126AA502}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4BAB3BE2-1F97-4BA3-9518-E1E3B335540A}
+ EndGlobalSection
+EndGlobal
diff --git a/ConsoleTools.cs b/ConsoleTools.cs
new file mode 100644
index 0000000..a4a903c
--- /dev/null
+++ b/ConsoleTools.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Text;
+
+namespace Ox.BizTalk.TrackedMessageExtractor
+{
+ ///
+ /// Console helper utilities
+ ///
+ internal class ConsoleTools
+ {
+ public const ConsoleColor CON_COLOUR_PROMPT = ConsoleColor.Cyan;
+ public const ConsoleColor CON_COLOUR_ERR = ConsoleColor.Red;
+ public const ConsoleColor CON_COLOUR_WARN = ConsoleColor.DarkYellow;
+
+ ///
+ /// Console prompt for input
+ ///
+ /// Type of input required (don't do bool eh?)
+ /// Text to display
+ /// Require an input or allow blank
+ /// Typed value
+ public static T PromptFor(string prompt, bool allowEmpty = false)
+ {
+ return PromptFor(prompt, String.Empty, allowEmpty);
+ }
+
+ ///
+ /// Console prompt for input
+ ///
+ /// Type of input required (don't do bool eh?)
+ /// Text to display
+ /// Pre-populated value (Empty is fine)
+ /// Require an input or allow blank
+ /// Typed value
+ public static T PromptFor(string prompt, string defaultValue, bool allowEmpty = false)
+ {
+ string value = null;
+ T result = default(T);
+ bool loop = true;
+ do
+ {
+
+ try
+ {
+ value = Prompt(prompt, defaultValue).Trim();
+ result = (T)Convert.ChangeType(value, typeof(T));
+ if (!allowEmpty && result == null || result.Equals(default(T)))
+ throw new InvalidOperationException("Value must be a " + typeof(T).Name);
+
+ loop = false;
+ }
+ catch
+ {
+ if (value.Length == 0 && allowEmpty)
+ {
+ // If we're allowing blanks, and no input has been provided, break gracefully.
+ loop = false;
+ }
+ else
+ {
+ // This is an invalid input for the conversion, display an error.
+ WriteError(String.Format("Cannot convert '{0}' to a {1}", value, typeof(T).Name));
+ }
+ }
+ } while (loop);
+
+
+
+ return result;
+ }
+
+ ///
+ /// Straight prompt for something
+ ///
+ /// Display text (a : will be added)
+ /// Value entered, unvalidated
+ public static string Prompt(string text)
+ {
+ WriteColourful(CON_COLOUR_PROMPT, text + ": ");
+ return Console.ReadLine();
+ }
+
+ ///
+ /// Prompt for something, with a pre-populated value displayed
+ ///
+ /// Display text (a : will be added)
+ /// Prepopulated text to provide
+ /// Value entered, unvalidated
+ public static string Prompt(string text, string defaultValue)
+ {
+ StringBuilder input = new StringBuilder(defaultValue);
+
+ bool loop = true;
+
+ WriteColourful(CON_COLOUR_PROMPT, text + ": ");
+ Console.Write(defaultValue);
+
+
+ do
+ {
+ ConsoleKeyInfo keyPress = Console.ReadKey(true);
+ switch (keyPress.Key)
+ {
+ case ConsoleKey.Backspace:
+ if (input.Length > 0)
+ {
+ input.Remove(input.Length - 1, 1);
+ if (Console.CursorLeft == 0)
+ {
+ Console.CursorTop = Console.CursorTop - 1;
+ Console.CursorLeft = Console.BufferWidth - 1;
+ Console.Write(" \b");
+ Console.CursorTop = Console.CursorTop - 1;
+ Console.CursorLeft = Console.BufferWidth - 1;
+ }
+ else
+ {
+ Console.Write("\b \b");
+ }
+ }
+ break;
+ case ConsoleKey.Enter:
+ Console.WriteLine();
+ loop = false;
+ break;
+ default:
+ Console.Write(keyPress.KeyChar);
+ input.Append(keyPress.KeyChar);
+ break;
+ }
+ } while (loop);
+
+ return input.ToString();
+ }
+
+ ///
+ /// Prints coloured text to the console
+ ///
+ /// Colour to use
+ /// Text to show
+ public static void WriteColourful(ConsoleColor colour, string text)
+ {
+ ConsoleColor current = Console.ForegroundColor;
+ Console.ForegroundColor = colour;
+
+ Console.Write(text);
+
+ Console.ForegroundColor = current;
+ }
+
+ ///
+ /// Prints coloured text to the console with a new line
+ ///
+ /// Colour to use
+ /// Text to show
+ public static void WriteColourfulLine(ConsoleColor colour, string text)
+ {
+ WriteColourful(colour, text + Environment.NewLine);
+ }
+
+ ///
+ /// Prints an error, in red!
+ ///
+ /// Text to display
+ public static void WriteError(string message)
+ {
+ WriteColourfulLine(CON_COLOUR_ERR, "** " + message);
+ }
+
+ ///
+ /// Prints an error, in red!
+ ///
+ /// Exception to display
+ public static void WriteError(Exception ex)
+ {
+ WriteColourful(CON_COLOUR_ERR, String.Format("** {0} **\n{1}\n", ex.Message, ex.StackTrace));
+ }
+ }
+}
diff --git a/Extract-object.ico b/Extract-object.ico
new file mode 100644
index 0000000..b695d2e
Binary files /dev/null and b/Extract-object.ico differ
diff --git a/Extractor.cs b/Extractor.cs
new file mode 100644
index 0000000..58db13f
--- /dev/null
+++ b/Extractor.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.BizTalk.Operations;
+using static Ox.BizTalk.TrackedMessageExtractor.ConsoleTools;
+
+namespace Ox.BizTalk.TrackedMessageExtractor
+{
+ ///
+ /// Extracts messages from BizTalk tracking database
+ ///
+ public class Extractor
+ {
+ private readonly Settings _settings;
+ private BizTalkOperations _btsOps;
+ private TrackingDatabase _trackingDatabase;
+
+ public Extractor(Settings settings)
+ {
+ this._settings = settings;
+ this._btsOps = new BizTalkOperations(settings.BizTalkMgmtHost, settings.BizTalkMgmtDb);
+ this._trackingDatabase = new TrackingDatabase(settings.BizTalkDTAHost, settings.BizTalkDTADb);
+ }
+
+ ///
+ /// Saves out a message to a file, using tracked filename and timestamp where possible.
+ ///
+ /// Message guid
+ public void ExtractMessage(string messageIdentifier)
+ {
+ bool retry = false;
+
+ messageIdentifier = messageIdentifier.Trim();
+
+ Guid messageId = new Guid(messageIdentifier);
+
+ do
+ {
+
+ try
+ {
+ retry = false;
+
+ Microsoft.BizTalk.Message.Interop.IBaseMessage msg = _btsOps.GetTrackedMessage(messageId, _trackingDatabase);
+
+ // Build an output filename
+ string saveFileName = msg.MessageID.ToString();
+ string saveFileExtension = ".txt";
+ string receivedFileName = (string)msg.Context.Read(_settings.FilenameProperty, _settings.FilenameSchema);
+
+ // If we've obtained a filename from the context
+ if (receivedFileName != null)
+ {
+ saveFileName = Path.GetFileNameWithoutExtension(receivedFileName);
+ saveFileExtension = Path.GetExtension(receivedFileName);
+ }
+
+ // Loop each message part
+ for (int i = 0; i < msg.PartCount; i++)
+ {
+ string partName;
+ Microsoft.BizTalk.Message.Interop.IBaseMessagePart part = msg.GetPartByIndex(i, out partName);
+
+ if (part != null && part.Data != null)
+ {
+ string fileName = String.Empty;
+
+ // Handle duplicate files names with an incrementing counter
+ int fileNameDuplicate = -1;
+ do
+ {
+ fileName = String.Format("{0}_{1}{3}{2}", saveFileName, partName, saveFileExtension, fileNameDuplicate++ >= 0 ? fileNameDuplicate.ToString() : String.Empty);
+ } while (File.Exists(fileName));
+
+ Console.WriteLine("Saving message {0} to {1}", messageId, fileName);
+
+ using (FileStream fs = new FileStream(fileName, FileMode.CreateNew))
+ {
+ byte[] buffer = new byte[1024];
+ int length = 0;
+ while ((length = part.Data.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ fs.Write(buffer, 0, length);
+ }
+
+ fs.Flush();
+ }
+
+ DateTime createdTime = DateTime.Now;
+
+ // Try to get the file created time
+ object prop = msg.Context.Read("FileCreationTime", "http://schemas.microsoft.com/BizTalk/2003/file-properties");
+
+ if (prop == null)
+ {
+ // If there was no file created time (e.g. it was a SOAP message), get the adapter finished time
+ prop = msg.Context.Read("AdapterReceiveCompleteTime", "http://schemas.microsoft.com/BizTalk/2003/messagetracking-properties");
+ }
+
+ if (prop != null)
+ {
+ createdTime = (DateTime)prop;
+ }
+
+ File.SetLastWriteTime(fileName, createdTime);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(ex);
+
+ do
+ {
+ string prompt = PromptFor("Would you like to retry this message? [Y/N]");
+ switch (prompt.ToUpper().Trim())
+ {
+ case "N":
+ return;
+ case "Y":
+ retry = true;
+ break;
+ default:
+ WriteColourfulLine(CON_COLOUR_WARN, "Invalid answer, Y or N please.");
+ break;
+ }
+ } while (!retry); // impossible for a false retry as we'll return on a N
+
+ }
+ } while (retry);
+
+ }
+ }
+}
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..b9d1577
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Alastair Grant.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the Software), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, andor sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..2f743f0
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,55 @@
+using System;
+using System.IO;
+
+using static Ox.BizTalk.TrackedMessageExtractor.ConsoleTools;
+
+namespace Ox.BizTalk.TrackedMessageExtractor
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ Console.WriteLine("BizTalk Bulk Tracked Message Extractor");
+ Console.WriteLine("Copyright (C) Alastair Grant. 2013.");
+ Console.WriteLine("======================================\n");
+
+ Settings settings = new Settings();
+
+ try
+ {
+ if (settings.ProcessArguments(args)) // Handle program arguments
+ {
+ settings.PromptForMissing(); // Fill in the blanks
+
+ using (StreamReader sr = new StreamReader(new FileStream(settings.InFile, FileMode.Open, FileAccess.Read)))
+ {
+ Extractor extractor = new Extractor(settings);
+
+ string line;
+ while ((line = sr.ReadLine()) != null)
+ {
+ if (line.Trim().Length > 0)
+ {
+ extractor.ExtractMessage(line);
+ }
+ }
+ }
+ }
+ }
+ catch(Exception ex)
+ {
+ WriteError(ex);
+ }
+
+ if (!settings.QuitWhenDone)
+ {
+ WriteColourful(ConsoleColor.Green, "Program Complete. Press ENTER to end...");
+ Console.ReadLine();
+ }
+ }
+
+
+
+
+ }
+}
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..e284520
--- /dev/null
+++ b/Properties/AssemblyInfo.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Ox.BizTalk.TrackedMessageExtractor")]
+[assembly: AssemblyDescription("BizTalk bulk message extractor")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Ox.BizTalk.TrackedMessageExtractor")]
+[assembly: AssemblyCopyright("Copyright © Alastair Grant 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("dca8476a-c2c3-4974-9ae8-c386522fc71d")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1a013d2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,61 @@
+# Ox.BizTalk.TrackedMessageExtractor #
+This utility is to extract a bunch of messages from the BizTalk tracking database on-bulk.
+
+I find it an invaluable tool for when things goes titsup and you need to replay messages in a BizTalk environment.
+
+For this to work, you need to have message body tracking on somewhere relevant (initial messages are a really good thing to track).
+
+# Basic Usage #
+Get yourself a list of message ids (guids) from your tracking database that meet your requirements.
+
+```
+SELECT TOP 10 [MessageInstance/InstanceID] FROM BizTalkDTADb..dtav_MessageFacts WHERE ... whatever ...
+```
+
+And pop them in a text file, one line per message id:
+
+```
+43336CC0-9D4D-4D27-A533-F3A45798EFA5
+D11E86E8-0584-4072-851E-555337320B98
+E8807B03-D9BE-4E95-9404-9BF7786D0FD0
+1FA28300-5410-404A-9A92-6998B6DA7E5C
+6822D96A-AC54-4FED-AD2C-596C6CABADAE
+31844458-0C06-44CF-A556-78539359A6A1
+```
+
+Run the program and either provide your text file path when prompted.
+
+# Configuration #
+The program will attempt to use WMI on the local machine to get the relevant BizTalk databases. If the local machine isn't connected to a BizTalk group this will fail and prompt you for the details.
+
+You will, naturally, need relevant permissions on the BizTalk databases to utilise this tool.
+
+# Dependencies #
+Microsoft BizTalk server needs to be installed for this tool to work/build as it relies on BizTalk management DLLs.
+
+Being a BizTalk component, this is obviously built against .NET Framework and not .NET Core.
+
+# Arguments #
+
+You can pass arguments into the application to kick everything off with one call.
+
+```
+Usage:
+ --in=PATH Line delimetered file of BizTalk message ids
+ --mgmthost=SQLHOST Hostname of Mgmt SQL server/instance to connect to
+ --dtahost=SQLHOST Hostname of the DTA SQL server/isntance
+ --mgmtdb=DB Database name of BizTalkMgmtDb
+ --dtadb=DB Database name of the BizTalkDTADb
+ --out=PATH Alternative output directory
+ --nameschema=URI Context namespace to use for deriving original filename
+ --nameproperty=NAME Context property name to use for deriving original filename
+ --quit Do not prompt for program closure at end
+```
+
+Normally for a quick/unattended run through you should be able to use:
+
+```
+extractmessages.exe --in=messages.txt --out=.\ --quit
+```
+
+This will parse the message ids supplied in the file messages.txt and output to the current working directory before ending the program without holding the application open.
\ No newline at end of file
diff --git a/Settings.cs b/Settings.cs
new file mode 100644
index 0000000..6225f45
--- /dev/null
+++ b/Settings.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.BizTalk.ExplorerOM;
+using System.IO;
+using static Ox.BizTalk.TrackedMessageExtractor.ConsoleTools;
+
+namespace Ox.BizTalk.TrackedMessageExtractor
+{
+ ///
+ /// Setings for the application
+ ///
+ public class Settings
+ {
+
+ public string InFile { get; set; }
+ public string OutputDirectory { get; set; }
+ public string FilenameProperty { get; set; }
+ public string FilenameSchema { get; set; }
+
+ public string BizTalkMgmtDb { get; set; }
+ public string BizTalkMgmtHost { get; set; }
+ public string BizTalkDTADb { get; set; }
+ public string BizTalkDTAHost { get; set; }
+
+ public bool QuitWhenDone { get; set; }
+
+ public Settings()
+ {
+ this.FilenameProperty = "ReceivedFileName";
+ this.FilenameSchema = "http://schemas.microsoft.com/BizTalk/2003/file-properties";
+ this.QuitWhenDone = false;
+
+ try
+ {
+ BizTalkWmiSearcher.PopulateWmiSettings(this);
+ }
+ catch { }
+ }
+
+ ///
+ /// Checks for missing settings and prompts for them.
+ ///
+ public void PromptForMissing()
+ {
+ if (String.IsNullOrEmpty(this.InFile))
+ {
+ do
+ {
+ var file = PromptFor("Message id file");
+ if (!File.Exists(file))
+ {
+ WriteColourfulLine(CON_COLOUR_WARN, "File does not exist.");
+ }
+ this.InFile = file;
+ } while (String.IsNullOrEmpty(this.InFile));
+ }
+
+ if(String.IsNullOrEmpty(this.OutputDirectory))
+ {
+ do
+ {
+ var dir = PromptFor("Output directory", Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location));
+ if (!Directory.Exists(dir))
+ {
+ WriteColourfulLine(CON_COLOUR_WARN, "Directory does not exist.");
+ }
+ this.OutputDirectory = dir;
+ } while (String.IsNullOrEmpty(this.OutputDirectory));
+ }
+
+ if (String.IsNullOrEmpty(this.BizTalkMgmtHost))
+ this.BizTalkMgmtHost = PromptFor("SQL Host for BizTalkMgmtDb");
+
+ if (String.IsNullOrEmpty(this.BizTalkMgmtDb))
+ this.BizTalkMgmtDb = PromptFor("Database name for BizTalkMgmtDb", "BizTalkMgmtDb");
+
+ if (String.IsNullOrEmpty(this.BizTalkDTAHost))
+ this.BizTalkDTAHost = PromptFor("SQL Host for BizTalkDTADb", this.BizTalkMgmtHost);
+
+ if (String.IsNullOrEmpty(this.BizTalkDTADb))
+ this.BizTalkDTADb = PromptFor("Database name for BizTalkDTADb", this.BizTalkDTADb);
+ }
+
+ ///
+ /// Handles external program arguments and converts them to settings
+ ///
+ /// External arguments
+ /// False means exit gracefully
+ public bool ProcessArguments(string[] args)
+ {
+ bool runprogram = true;
+
+ foreach (var arg in args)
+ {
+ string key = arg;
+ string value = arg;
+ // Split out arguments from values
+ if (arg.Contains("="))
+ {
+ var split = arg.Split('=');
+ if (split.Length != 2)
+ {
+ throw new ArgumentException("Argument '{0}' not understood.", arg);
+ }
+ key = split[0];
+ value = split[1];
+ }
+
+ switch (key.ToLower())
+ {
+ case "--help":
+ case "-h":
+ case "/?":
+ string about = "Bulk saves BizTalk messages from the BizTalkDTADb health and tracking database (if they exist).\nTracking needs to be enabled in BizTalk to save messages.\n\nMessages will be saved to the current working directory unless overridden.\n\nWMI will be used to locate the server, if available.";
+ Console.WriteLine(about + Environment.NewLine);
+ Console.WriteLine("Usage:");
+ Console.WriteLine(" --in=PATH Line delimetered file of BizTalk message ids");
+ Console.WriteLine(" --mgmthost=SQLHOST Hostname of Mgmt SQL server/instance to connect to");
+ Console.WriteLine(" --dtahost=SQLHOST Hostname of the DTA SQL server/isntance");
+ Console.WriteLine(" --mgmtdb=DB Database name of BizTalkMgmtDb");
+ Console.WriteLine(" --dtadb=DB Database name of the BizTalkDTADb");
+ Console.WriteLine(" --out=PATH Alternative output directory");
+ Console.WriteLine(" --nameschema=URI Context namespace to use for deriving original filename");
+ Console.WriteLine(" --nameproperty=NAME Context property name to use for deriving original filename");
+ Console.WriteLine(" --quit Do not prompt for program closure at end");
+
+ Console.WriteLine("\nExamples:");
+
+ Console.WriteLine(" --in=messages.txt --out=c:\\ --nameschema=http://schemas.microsoft.com/BizTalk/2003/file-properties --nameproperty=ReceivedFileName --mgmthost=localhost --mgmtdb=BizTalkMgmtDb --dtahost=localhost --dtadb=BizTalkDTADb");
+
+ runprogram = false;
+ break;
+
+ case "--in":
+ if(!File.Exists(value))
+ {
+ throw new ArgumentException("Invalid input file " + value);
+ }
+
+ this.InFile = value;
+ break;
+
+ case "--mgmthost":
+ this.BizTalkMgmtHost = value;
+ break;
+
+ case "--dtahost":
+ this.BizTalkDTAHost = value;
+ break;
+
+ case "--mgmtdb":
+ this.BizTalkMgmtDb = value;
+ break;
+
+ case "--dtadb":
+ this.BizTalkDTADb = value;
+ break;
+
+ case "--out":
+ if (!Directory.Exists(value))
+ {
+ throw new ArgumentException("Invalid output directory " + value);
+ }
+ this.OutputDirectory = value;
+ break;
+
+ case "--nameschema":
+ this.FilenameSchema = value;
+ break;
+
+ case "--nameproperty":
+ this.FilenameProperty = value;
+ break;
+
+ case "--quit":
+ this.QuitWhenDone = true;
+ break;
+
+ }
+ }
+ return runprogram;
+
+ }
+ }
+}
+
diff --git a/Wmi.cs b/Wmi.cs
new file mode 100644
index 0000000..4a24298
--- /dev/null
+++ b/Wmi.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management;
+using System.Text;
+
+namespace Ox.BizTalk.TrackedMessageExtractor
+{
+ ///
+ /// WMI searcher for BizTalk config
+ ///
+ internal static class BizTalkWmiSearcher
+ {
+ private const string MGMT_ROOT = @"root\MicrosoftBizTalkServer";
+
+ public static void PopulateWmiSettings(Settings settings)
+ {
+ ManagementObjectSearcher searcher = new ManagementObjectSearcher(MGMT_ROOT, "SELECT * FROM MSBTS_GroupSetting");
+
+ foreach (ManagementObject group in searcher.Get())
+ {
+ if (group != null)
+ {
+ group.Get();
+ settings.BizTalkMgmtDb = group.GetMgmtValueSafe("MgmtDbName");
+ settings.BizTalkMgmtHost = group.GetMgmtValueSafe("MgmtDbServerName");
+ settings.BizTalkDTADb = group.GetMgmtValueSafe("TrackingDBName");
+ settings.BizTalkDTAHost = group.GetMgmtValueSafe("TrackingDBServerName");
+ }
+ }
+ }
+
+ private static T GetMgmtValueSafe(this ManagementObject mgmtObj, string key)
+ {
+ T result = default(T);
+ try
+ {
+ if (mgmtObj != null)
+ {
+ result = (T)mgmtObj[key];
+ }
+ }
+ catch { }
+ return result;
+ }
+ }
+}
diff --git a/app.config b/app.config
new file mode 100644
index 0000000..2fa6e95
--- /dev/null
+++ b/app.config
@@ -0,0 +1,3 @@
+
+
+