Reduce catalog to Phase 1 ACC/PROD view

This commit is contained in:
2026-07-27 18:03:39 +02:00
parent 2b5b97c869
commit 53fcfb84a4
17 changed files with 1208 additions and 1555 deletions
+213 -193
View File
@@ -1,220 +1,240 @@
# Technische Dokumentation: BEW BizTalk Application Catalog # Technische Dokumentation: BEW BizTalk Application Catalog Phase 1
## 1. Zielbild ## 1. Ziel und Abgrenzung
Das Werkzeug erstellt je Umgebung eine eigenständige Microsoft-Excel-Arbeitsmappe mit der vollständigen Liste der auf dem lokalen BizTalk Server 2020 installierten Anwendungen und den wichtigsten zugeordneten Kernparametern. Das Werkzeug bildet ausschließlich die Phase-1-Sicht `BizTalk Anwendungen & Adapter` ab. Das fachliche Ergebnis ist ein einblättriger ACC/PRD-Katalog mit:
1. BizTalk-Anwendung
2. Umgebung
3. Adapter-Typ und Anzahl
4. Endpunktanzahl
5. Vorkommen in ACC
6. Vorkommen in PRD
7. Hinweis bei einseitigem Vorkommen
Es ist kein technisches Vollinventar. Frühere Detailbereiche zu System, SQL, Status, Ports, Orchestrierungen, Schemas, Maps, Pipelines, Assemblies, Hosts, Handlern, Abdeckung und Findings sind nicht Bestandteil der XLSX und werden nicht mehr erhoben.
## 2. Verteilte Erfassung
ACC und PROD besitzen jeweils einen lokalen BizTalk Server 2020. Ein Prozess auf einem Server soll die andere Umgebung nicht remote abfragen.
```text ```text
ACC ACC BizTalk Server PROD BizTalk Server
+-- 1 BizTalk Server 2020 / Windows Server 2019 <-- Tool lokal ausführen | |
+-- 1 SQL Server für die BizTalk-Datenbanken | lokales WMI | lokales WMI
v v
PROD ACC JSON + lokale XLSX PROD JSON + lokale XLSX
+-- 1 BizTalk Server 2020 / Windows Server 2019 <-- Tool lokal ausführen \ /
+-- 1 SQL Server für die BizTalk-Datenbanken \ /
+---- Merge auf beliebigem -----+
Windows-Rechner
|
v
gemeinsame ACC/PRD-XLSX
``` ```
ACC und PROD werden getrennt inventarisiert. Da es pro Umgebung nur einen BizTalk-Knoten gibt, ist kein Node-Abgleich erforderlich. Der JSON-Snapshot ist das kleine Austauschformat zwischen den getrennten Erfassungen. Er enthält weder Endpunktadressen noch Credentials.
## 2. Architektur ## 3. Komponenten
```text ```text
Administrative cmd.exe CommandLineOptions
| +-- validiert lokalen Modus oder Merge-Modus
+-- run-inventory.cmd ACC|PROD
| BizTalkWmiCollector
+-- BizTalkApplicationCatalog.exe +-- MSBTS_Application
| +-- MSBTS_ReceivePort
+-- SystemCollector +-- MSBTS_SendPort
| +-- Win32_OperatingSystem +-- MSBTS_ReceiveLocation
| +-- lokale BizTalk Registry
| +-- MSBTS_GroupSetting SnapshotStore
| +-- normalisiert je Anwendung Adapterzählungen
+-- BizTalkWmiCollector +-- schreibt/liest DataContract-JSON
| +-- vollständige Anwendungsliste +-- validiert Schema, Summen, Umgebung und Vollständigkeit
| +-- Artefaktdetails
| +-- Hosts und Handler XlsxReportWriter
| +-- Abdeckungsstatus pro Klasse +-- genau ein Blatt "Phase 1"
| +-- Office Open XML ohne Office-Interop
+-- XlsxReportWriter +-- Titel, Filter, Freeze Pane und Summenzeile
+-- Office Open XML / ZIP
+-- Inline Strings, keine Shared-String-Abhängigkeit
+-- Filter, Freeze Panes, Styles
+-- atomarer Dateiaustausch
``` ```
Die Anwendung referenziert keine `Microsoft.BizTalk.*`-Assembly. Die einzige BizTalk-Schnittstelle ist der lokal installierte WMI-Provider. Dadurch besteht das Deployment nur aus EXE, Konfiguration, Startskript und Dokumentation. ## 4. WMI-Datenmodell
## 3. Toolchain und Kompatibilität ### 4.1 Anwendungen
Die Build-Baseline ist absichtlich auf die BizTalk-2020-Umgebung ausgerichtet: `MSBTS_Application` ist die Pflichtquelle der Anwendungsliste. Verwendet wird nur `Name` beziehungsweise `ApplicationName`.
- Visual Studio 2019 / Solution Version 16 Die Abfrage muss mindestens eine Anwendung liefern. Andernfalls ist der lokale Lauf unvollständig.
- MSBuild 16.x, insbesondere 16.11
- klassisches MSBuild-Projektformat mit `ToolsVersion="15.0"` ### 4.2 Receive-Endpunkte
Eine `MSBTS_ReceiveLocation` zählt als ein Endpunkt. Der Adapter wird defensiv aus `AdapterName`, `TransportType` oder `PTTransportType` gelesen.
Da eine Receive Location abhängig vom WMI-Provider keine direkte `ApplicationName` liefern kann, wird vorher `MSBTS_ReceivePort` gelesen. Über `ReceivePortName` wird die Anwendung aufgelöst. Ein Receive Port zählt selbst nicht als Endpunkt.
### 4.3 Send-Endpunkte
Bei `MSBTS_SendPort` zählt die primäre Transportkonfiguration als ein Endpunkt. Ist eine sekundäre Transportkonfiguration vorhanden, zählt sie als zusätzlicher Endpunkt.
Primärer Adapter:
- `PTTransportType`
- Fallback `PrimaryTransportType`
- Fallback `AdapterName`
- Fallback `TransportType`
Sekundärer Adapter:
- `STTransportType`
- Fallback `SecondaryTransportType`
Fehlt ein gelieferter Adaptername, wird `Unbekannt` verwendet. Ein Endpunkt ohne Anwendungszuordnung macht den Lauf unvollständig, weil er sonst in der Anwendungssicht verloren ginge.
## 5. Snapshot-Format
Das JSON enthält nur normalisierte Katalogdaten:
```json
{
"SchemaVersion": 1,
"EnvironmentName": "ACC",
"ComputerName": "BIZTALK-ACC",
"CreatedUtc": "2026-07-27T13:00:00.0000000Z",
"IsComplete": true,
"Applications": [
{
"Name": "ExampleApplication",
"EndpointCount": 2,
"AdapterCounts": [
{ "AdapterType": "FILE", "Count": 1 },
{ "AdapterType": "SFTP", "Count": 1 }
]
}
]
}
```
Vor einem Merge werden geprüft:
- `SchemaVersion` ist `1`
- erwartete Umgebung ist exakt `ACC` beziehungsweise `PROD`
- `IsComplete` ist `true`
- Namen und Adaptertypen sind nicht leer
- Zählwerte sind nicht negativ
- Summe der Adapterzählungen entspricht `EndpointCount`
JSON und XLSX werden über eine temporäre Datei geschrieben und erst nach erfolgreichem Abschluss auf den endgültigen Dateinamen verschoben.
## 6. Merge-Regeln
Anwendungsnamen werden ohne Beachtung der Groß-/Kleinschreibung verglichen. Für die Anzeige wird bei einer gemeinsamen Anwendung bevorzugt die Schreibweise aus ACC verwendet.
| Situation | Umgebung | ACC | PRD | Hinweis |
| --- | --- | --- | --- | --- |
| in beiden vorhanden | `ACC + PRD` | `✓` | `✓` | leer |
| nur ACC | `nur ACC` | `✓` | `` | `Nur in ACC vorhanden` |
| nur PROD | `nur PRD` | `` | `✓` | `Nur in PRD vorhanden nicht in ACC` |
Die Endpunktanzahl einer gemeinsamen Zeile ist die Summe beider Umgebungen.
Sind die Adapterzählungen identisch, erscheint nur eine kompakte Darstellung:
```text
FILE (1x) / SFTP (1x)
```
Bei einer Abweichung bleiben beide Nachweise sichtbar:
```text
ACC: FILE (2x) | PRD: FILE (1x) / SFTP (1x)
```
Damit wird keine Übereinstimmung suggeriert, wenn lediglich die Gesamtzahl gleich ist.
## 7. Excel-Aufbau
Die Arbeitsmappe enthält genau:
```text
xl/worksheets/sheet1.xml -> Blatt "Phase 1"
```
Das Blatt besteht aus:
- Zeile 1: zusammengeführter Titel
- Zeile 2: sieben Spaltenüberschriften
- ab Zeile 3: alphabetisch sortierte Anwendungen
- letzte Zeile: zusammengeführte Summenzeile
Der Filter umfasst nur Kopf- und Datenzeilen, nicht die Summenzeile. Die ersten beiden Zeilen sind fixiert. Zelltexte werden als `inlineStr` gespeichert. Office und COM-Interop sind nicht erforderlich.
## 8. Fehlerverhalten
Alle vier WMI-Abfragen bilden einen Pflichtabschnitt. Ein Fehler führt zu:
- Exitcode `1`
- Logeintrag
- JSON mit `IsComplete: false`
- lokaler XLSX auf Basis der bis dahin ermittelten Daten
Der Merge lehnt einen solchen JSON-Snapshot ab. Dadurch werden technische Lücken nicht als bestätigte Nullwerte in den ACC/PRD-Katalog übernommen.
Ungültige Parameter, fehlende Dateien, fehlerhaftes JSON und Schreibfehler liefern Exitcode `2`.
## 9. Sicherheit
- ausschließlich lesende lokale WMI-Abfragen
- keine Änderungen an BizTalk-Konfiguration oder Runtime
- keine SQL-Abfragen
- keine Endpunktadressen
- keine Binding-Exporte
- keine Credentials, Secrets oder Tokens
- keine Netzwerkverbindungen zu fachlichen Endpunkten
Anwendungsnamen, Servernamen und Adapterverteilungen bleiben interne Betriebsinformationen. JSON, XLSX und Log sind geschützt abzulegen.
## 10. Build und Test
Baseline:
- Visual Studio 2019 / MSBuild 16.x
- klassisches MSBuild-Projektformat
- .NET Framework 4.7.2 - .NET Framework 4.7.2
- C# 7.3 - C# 7.3
- keine SDK-Style-Projekte - keine NuGet-Pakete
- kein `PackageReference` - kein .NET SDK erforderlich
- keine `global.json`
- keine NuGet-Abhängigkeit
Damit wird nicht das installierte .NET SDK 8 oder neuer ausgewählt. Der bekannte Fehler „.NET SDK 8.x requires at least MSBuild 17.8.3“ kann bei dieser Solution nicht durch eine SDK-Auswahl entstehen.
## 4. Datenquellen
### 4.1 Pflichtquelle
`MSBTS_Application` liefert die Primärliste aller Anwendungen. Liefert diese Klasse keine Datensätze oder kann sie nicht gelesen werden, gilt der Pflichtabschnitt als fehlgeschlagen und der Prozess endet nach der dennoch versuchten XLSX-Erzeugung mit Exitcode `1`.
Aufgenommene Parameter:
- `Name` beziehungsweise `ApplicationName`
- `Description`
- `Status`
- `IsDefault`, soweit vom Provider geliefert
### 4.2 Optionale Artefaktklassen
| WMI-Klasse | Berichtstyp |
| --- | --- |
| `MSBTS_Orchestration` | Orchestrierung |
| `MSBTS_SendPort` | Send Port |
| `MSBTS_SendPortGroup` | Send Port Group |
| `MSBTS_ReceivePort` | Receive Port |
| `MSBTS_ReceiveLocation` | Receive Location |
| `MSBTS_Assembly` | Assembly |
| `MSBTS_Schema` | Schema |
| `MSBTS_Map` | Map |
| `MSBTS_Pipeline` | Pipeline |
Die WMI-Klassen können abhängig von Installation, Providerstand und Berechtigung unterschiedliche Properties anbieten. Deshalb fragt der Collector `SELECT *` ab und liest bekannte Properties defensiv. Eine nicht vorhandene Property wird leer gelassen und beendet die Klasse nicht.
Receive Ports werden vor Receive Locations gelesen. Fehlt an einer Receive Location die direkte `ApplicationName`, wird die Anwendung über `ReceivePortName` bestmöglich aufgelöst.
### 4.3 Hosts und Handler
- `MSBTS_HostSetting`
- `MSBTS_HostInstance`
- `MSBTS_ReceiveHandler`
- `MSBTS_SendHandler2`
- Fallback `MSBTS_SendHandler`
Erfasst werden Name, Server, Status, Hosttyp, Windows-Gruppe, 32-Bit-Kennzeichen, Trusted-Kennzeichen und Adaptername, soweit vorhanden.
### 4.4 Systeminformationen
- `Win32_OperatingSystem`
- `HKLM\SOFTWARE\Microsoft\BizTalk Server\3.0`
- `HKLM\SOFTWARE\Microsoft\BizTalk Server\3.0\Administration`
- `MSBTS_GroupSetting`
Registry-Werte werden in 64- und 32-Bit-Ansicht gelesen. Kommandozeilenwerte für Management Server und Datenbank überschreiben ermittelte Werte.
## 5. Abdeckungsmodell
Jede abgefragte BizTalk-WMI-Klasse erzeugt einen Datensatz im Blatt `Abdeckung`:
| Status | Bedeutung |
| --- | --- |
| `Vollständig` | Abfrage erfolgreich; die angegebene Zeilenzahl ist technisch belegt |
| `Begrenzt` | Abfrage erfolgreich, aber das konfigurierte Zeilenlimit wurde erreicht |
| `Nicht verfügbar` | Klasse oder Berechtigung nicht verfügbar; eine Null ist nicht fachlich bestätigt |
| `Fehler` | Pflichtquelle lieferte kein verwertbares Ergebnis |
Dieses Modell verhindert die irreführende Interpretation eines fehlenden WMI-Ergebnisses als „keine Artefakte vorhanden“.
`MaxRowsPerArtifactType` begrenzt optional die Details pro Artefakttyp. Der Standard ist `10000`. Die Anwendungsliste aus `MSBTS_Application` wird nicht begrenzt.
## 6. Excel-Erzeugung
Die `.xlsx`-Datei ist ein OPC-/ZIP-Paket mit folgenden Kernteilen:
```text
[Content_Types].xml
_rels/.rels
docProps/core.xml
docProps/app.xml
xl/workbook.xml
xl/_rels/workbook.xml.rels
xl/styles.xml
xl/worksheets/sheet1.xml ... sheet10.xml
```
Zelltexte werden als `inlineStr` geschrieben. Dadurch wird keine Shared-String-Tabelle im Arbeitsspeicher aufgebaut. Zahlen werden als numerische Zellen gespeichert und können in Excel direkt summiert oder gefiltert werden.
Der Writer:
1. erzeugt eine eindeutige temporäre Datei im Zielordner,
2. schreibt und schließt alle XML-/ZIP-Teile,
3. entfernt eine gegebenenfalls vorhandene Zieldatei gleichen Namens,
4. verschiebt die vollständige temporäre Datei atomar auf den Zielnamen,
5. löscht temporäre Reste auch im Fehlerfall.
Ein einzelner Zelltext wird auf das Excel-Limit von 32.767 Zeichen begrenzt. Nicht XML-konforme Steuerzeichen werden entfernt.
## 7. Resilienz
- `SafeCollector` isoliert voneinander unabhängige Abschnitte.
- Jeder Abschnitt schreibt Start, Ende, Dauer und Fehler in Konsole und Log.
- Optionale WMI-Klassen werden einzeln behandelt.
- Timeout und maximale Detailzeilen sind in `App.config` konfigurierbar.
- Die XLSX wird auch bei optionalen Lücken erstellt.
- Die vollständige Anwendungsliste ist ein Pflichtabschnitt.
- Findings erklären Lücken und nennen eine konkrete Prüfung.
## 8. Datenschutz und Secrets
Das Inventar soll keine Credentials enthalten. Bekannte Kennwort-, Secret- und Tokenmuster in Text- und Adresswerten werden durch `[REDACTED]` ersetzt. Das ersetzt keine Schutzklassifizierung: interne Adressen, Servernamen, Hostgruppen und Anwendungsbezeichnungen bleiben sensible Betriebsinformationen.
Es werden keine Kennwörter abgefragt, keine Bindings exportiert und keine aktiven Endpunktverbindungen getestet.
## 9. Build, Test und Paketierung
Windows-Build:
```cmd ```cmd
scripts\build-release.cmd scripts\build-release.cmd
``` ```
Der Build: Der Self-Test benötigt kein BizTalk und prüft:
1. findet MSBuild über `vswhere.exe` oder `PATH`, - JSON-Schreiben und -Lesen
2. baut `Release|Any CPU`, - Snapshot-Validierung
3. führt das Testprogramm aus, - ACC/PRD-Union
4. führt den Self-Test der eigentlichen EXE aus. - Adapteraggregation
- Endpunktsummen
- ein einziges Blatt `Phase 1`
- erforderliche OPC-/XLSX-Teile
- XML-Wohlgeformtheit
- Titel- und Summen-Merges
- korrekten Filterbereich
Der Self-Test benötigt kein BizTalk. Er prüft: ## 11. Betriebsvalidierung
- Secret-Redaktion
- Erstellung aller zehn Tabellenblätter
- Vorhandensein der erforderlichen OPC-/XLSX-Teile
- XML-Wohlgeformtheit aller Paketbestandteile
- erwartete Blattnamen
- Abwesenheit des Testkennworts
Deployment:
```cmd
scripts\package-release.cmd
```
Quell-ZIP und certutil-dekodierbarer Text:
```cmd
scripts\package-source.cmd
```
## 10. Validierung in ACC und PROD
Je Umgebung: Je Umgebung:
1. EXE mit `--self-test` starten. 1. `--self-test` erfolgreich ausführen.
2. Inventar in einer administrativen `cmd.exe` ausführen. 2. Lokalen Lauf in administrativer `cmd.exe` starten.
3. Exitcode und Log prüfen. 3. Exitcode `0` und Log prüfen.
4. XLSX öffnen und Blatt `Abdeckung` prüfen. 4. Anwendungsgesamtzahl mit der BizTalk Administration Console vergleichen.
5. Zahl der Anwendungen mit der BizTalk Administration Console vergleichen. 5. Send Ports, sekundäre Transporte und Receive Locations stichprobenartig zählen.
6. Stichproben für Ports, Orchestrierungen und Assemblies durchführen. 6. Nur die vollständige JSON-Datei für den Merge verwenden.
7. XLSX und Log im geschützten Umgebungsordner archivieren.
ACC- und PROD-Dateien dürfen nicht zusammengeführt werden, ohne die Spalten `Umgebung` und `Server` beizubehalten. Nach dem Merge:
1. Summen für ACC und PROD mit den lokalen Dateien vergleichen.
2. Anwendungen mit `nur ACC` und `nur PRD` fachlich prüfen.
3. Abweichend beschriftete ACC-/PRD-Adapterverteilungen prüfen.
4. Ergebnis geschützt archivieren.
+88 -131
View File
@@ -1,82 +1,61 @@
# BEW BizTalk Application Catalog # BEW BizTalk Application Catalog Phase 1
`BEW BizTalk Application Catalog` inventarisiert die auf einem BizTalk Server 2020 installierten Anwendungen und erzeugt eine kompakte, filterbare Microsoft-Excel-Arbeitsmappe (`.xlsx`). Das Tool erzeugt nur die für die Phase-1-Excel-Sicht benötigten Daten:
Das Tool wird lokal einmal auf dem BizTalk-Server in `ACC` und einmal auf dem BizTalk-Server in `PROD` ausgeführt. Beide Umgebungen bestehen jeweils aus: | Spalte | Inhalt |
- einem BizTalk Server 2020 auf Windows Server 2019
- einem separaten SQL Server für die BizTalk-Datenbanken
Microsoft Excel, Microsoft Office, PowerShell, Internetzugriff und ein .NET SDK werden auf dem Zielserver nicht benötigt.
## Ergebnis
Die erzeugte Arbeitsmappe enthält zehn Tabellenblätter:
| Blatt | Inhalt |
| --- | --- | | --- | --- |
| `Übersicht` | Umgebung, Server, BizTalk-/Windows-Metadaten, Gesamtzahlen und Laufstatus | | `BizTalk-Anwendung` | Name der BizTalk-Anwendung |
| `Anwendungen` | vollständige, kompakte Anwendungsliste mit Status, Beschreibung und Artefaktzahlen | | `Umgebung` | `ACC + PRD`, `nur ACC` oder `nur PRD` |
| `Artefakte` | konsolidierte technische Detailansicht | | `Adapter-Typ` | Adapter mit Anzahl, zum Beispiel `FILE (2x) / SFTP (1x)` |
| `Ports` | Send Ports, Send Port Groups, Receive Ports und Receive Locations | | `Anzahl Endpunkte` | Summe der Endpunkte in den berücksichtigten Umgebungen |
| `Orchestrierungen` | Orchestrierungen, Status und Hostzuordnung | | `BizTalk-Anwendung in ACC` | `✓` oder `` |
| `Schemas-Maps-Pipelines` | Schemas, Maps und Pipelines | | `BizTalk-Anwendung in PRD` | `✓` oder `` |
| `Assemblies` | bereitgestellte BizTalk-Assemblies | | `Hinweis` | Kennzeichnung einer nur einseitig vorhandenen Anwendung |
| `Hosts-Handler` | Hosts, Hostinstanzen sowie Receive-/Send-Handler |
| `Abdeckung` | Erfolg, Zeilenzahl und mögliche Lücken jeder WMI-Datenquelle |
| `Findings` | Warnungen, Fehler und empfohlene Prüfungen |
Alle Detailblätter besitzen Filter, fixierte Kopfzeilen und angepasste Spaltenbreiten. Die Arbeitsmappe enthält exakt ein Blatt namens `Phase 1`. Es werden keine weiteren Übersichts-, Artefakt-, Port-, Host-, Abdeckungs- oder Findings-Blätter erzeugt.
## Kernparameter je Anwendung ## Ablauf
Das Blatt `Anwendungen` enthält unter anderem: ACC und PROD sind getrennte BizTalk-Umgebungen. Deshalb arbeitet das Tool in zwei Schritten:
- Umgebung und ausführenden BizTalk Server 1. Auf dem lokalen ACC-BizTalk-Server wird ein ACC-JSON-Snapshot erzeugt.
- Anwendungsname, Beschreibung, Status und Standardanwendungskennzeichen 2. Auf dem lokalen PROD-BizTalk-Server wird ein PROD-JSON-Snapshot erzeugt.
- Gesamtzahl zugeordneter Artefakte 3. Beide JSON-Dateien werden mit demselben Tool zur gemeinsamen Excel-Sicht zusammengeführt.
- Anzahl Orchestrierungen
- Anzahl Send Ports und Send Port Groups
- Anzahl Receive Ports und Receive Locations
- Anzahl Assemblies, Schemas, Maps und Pipelines
- erkannte Hosts beziehungsweise Handler
- verwendete Adapter
- expliziten Status der Detailabdeckung
Die vollständige Anwendungsliste stammt aus `MSBTS_Application`. Eine nicht verfügbare optionale Artefaktklasse wird nicht stillschweigend als fachliche Null interpretiert, sondern im Blatt `Abdeckung` ausgewiesen. Jeder lokale Lauf erzeugt zusätzlich eine einblättrige Excel-Sicht für die jeweilige Umgebung. Erst der Merge liefert den belastbaren ACC/PRD-Vergleich wie in der Zielansicht.
## Sicherheits- und Betriebsprinzip ## Erfasste Daten
- ausschließlich lokale, lesende WMI- und Registry-Abfragen Das Tool liest ausschließlich:
- keine Änderungen an Anwendungen, Ports, Hosts oder Runtime-Instanzen
- keine direkte SQL-Abfrage und keine Datenbankänderung
- keine Office-Automation und keine COM-Interop
- zusätzliche Redigierung versehentlich gelieferter Kennwort-/Tokenwerte
- atomare XLSX-Erzeugung über eine temporäre Datei
- Fehler optionaler WMI-Klassen stoppen die übrige Erfassung nicht
- identische Fortschrittsausgabe auf Konsole und in der Logdatei
Die XLSX- und Logdatei enthält interne Server-, Anwendungs-, Host- und Endpunktinformationen und muss entsprechend geschützt abgelegt werden. - `MSBTS_Application` für die vollständige Anwendungsliste
- `MSBTS_SendPort` für primäre und gegebenenfalls sekundäre Send-Endpunkte
- `MSBTS_ReceivePort` nur zur Zuordnung einer Receive Location zur Anwendung
- `MSBTS_ReceiveLocation` für Receive-Endpunkte
## Voraussetzungen auf ACC und PROD Ein Endpunkt ist:
- lokale Ausführung auf dem jeweiligen BizTalk Server 2020 - die primäre Transportkonfiguration eines Send Ports,
- Windows Server 2019 - eine konfigurierte sekundäre Transportkonfiguration eines Send Ports oder
- administrative `cmd.exe` empfohlen - eine Receive Location.
- Konto mit Leserechten auf `root\MicrosoftBizTalkServer`, regulär ein BizTalk-Administratoren- oder geeignetes Operatorenkonto
Receive Ports selbst sind Container und werden nicht als Endpunkt gezählt. Anwendungen ohne Endpunkt bleiben mit `0` und Adapter-Typ `` in der Sicht enthalten.
Nicht erfasst werden insbesondere Adressen, Credentials, Beschreibungen, Status, Hosts, Handler, Orchestrierungen, Schemas, Maps, Pipelines, Assemblies, Betriebssystem- oder SQL-Daten.
## Voraussetzungen
- BizTalk Server 2020 auf Windows Server 2019
- lokale Ausführung auf dem jeweiligen BizTalk-Server
- .NET Framework 4.7.2 oder höher - .NET Framework 4.7.2 oder höher
- funktionierender lokaler BizTalk-WMI-Provider - Leserechte auf `root\MicrosoftBizTalkServer`
- administrative `cmd.exe` empfohlen
Microsoft Office ist ausdrücklich keine Voraussetzung. Microsoft Excel, Microsoft Office, PowerShell, Internetzugriff, NuGet und ein .NET SDK werden auf dem Zielserver nicht benötigt.
## Schnellstart ## Lokalen Snapshot erzeugen
1. Deployment-Ordner auf den BizTalk Server kopieren. Zuerst den Self-Test ausführen:
2. Administrative `cmd.exe` öffnen.
3. Self-Test ohne BizTalk-Zugriff ausführen.
4. Inventar für die jeweilige Umgebung erzeugen.
Self-Test:
```cmd ```cmd
BizTalkApplicationCatalog.exe --self-test BizTalkApplicationCatalog.exe --self-test
@@ -94,117 +73,95 @@ PROD:
run-inventory.cmd PROD C:\BizTalk-Doku\PROD run-inventory.cmd PROD C:\BizTalk-Doku\PROD
``` ```
Ergebnis eines erfolgreichen lokalen Laufs:
```text
Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.json
Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.xlsx
Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.log
```
Nur JSON-Dateien eines Laufs mit Exitcode `0` dürfen zusammengeführt werden. Ein unvollständiger Snapshot trägt `IsComplete: false` und wird vom Merge abgelehnt.
## ACC und PROD zusammenführen
Beide JSON-Dateien in einen gemeinsamen, geschützten Ordner kopieren und ausführen:
```cmd
run-merge.cmd ^
C:\BizTalk-Doku\ACC\Phase1-BizTalk-Anwendungskatalog-ACC-SERVER-20260727-150000.json ^
C:\BizTalk-Doku\PROD\Phase1-BizTalk-Anwendungskatalog-PROD-SERVER-20260727-151500.json ^
C:\BizTalk-Doku\ACC-PROD
```
Direkter Aufruf: Direkter Aufruf:
```cmd ```cmd
BizTalkApplicationCatalog.exe ^ BizTalkApplicationCatalog.exe ^
--environment ACC ^ --merge ^
--output C:\BizTalk-Doku\ACC --acc-json C:\BizTalk-Doku\ACC\ACC.json ^
--prod-json C:\BizTalk-Doku\PROD\PROD.json ^
--output C:\BizTalk-Doku\ACC-PROD
``` ```
## Konsolenausgabe Die gemeinsame Datei heißt beispielsweise:
Das Tool zeigt laufend, was es gerade erfasst und wann die Excel-Datei geschrieben wurde:
```text ```text
[INFO] Starte Abschnitt: Anwendungen und Artefakte Phase1-BizTalk-Anwendungskatalog-ACC-PROD-20260727-153000.xlsx
[INFO] 34 installierte BizTalk-Anwendung(en) gefunden.
[INFO] Send Port: 87 Datensatz/Datensätze.
[INFO] Erzeuge Microsoft-Excel-Datei: C:\BizTalk-Doku\ACC\...
[INFO] Excel-Datei erfolgreich erzeugt: C:\BizTalk-Doku\ACC\...
``` ```
## Ausgabedateien Bei identischer Adapterverteilung in ACC und PROD wird sie einmal angezeigt. Bei Abweichungen kennzeichnet die Zelle beide Werte, zum Beispiel:
```text ```text
BizTalk-Anwendungsinventar-ACC-BIZTALKSERVER-20260727-150000.xlsx ACC: FILE (2x) | PRD: FILE (1x) / SFTP (1x)
BizTalk-Anwendungsinventar-ACC-BIZTALKSERVER-20260727-150000.log
``` ```
`Anzahl Endpunkte` ist im Merge die Summe aus ACC und PROD.
## Optionen und Exitcodes ## Optionen und Exitcodes
| Option | Bedeutung | | Option | Bedeutung |
| --- | --- | | --- | --- |
| `--environment NAME` | Umgebung, regulär `ACC` oder `PROD` | | `--environment ACC\|PROD` | lokaler Inventarlauf |
| `--output PFAD` | Zielordner für XLSX und Log | | `--output PFAD` | Zielordner |
| `--management-server NAME` | optionaler Management-SQL-Server-Hinweis | | `--merge` | ACC- und PROD-Snapshot zusammenführen |
| `--management-database NAME` | optionale Management-Datenbank; Fallback `BizTalkMgmtDb` | | `--acc-json PFAD` | vollständiger ACC-Snapshot |
| `--self-test` | prüft XLSX-Struktur, Tabellen und Secret-Redaktion | | `--prod-json PFAD` | vollständiger PROD-Snapshot |
| `--help` | zeigt die Hilfe | | `--self-test` | JSON-, Merge- und XLSX-Test ohne BizTalk |
| `--help` | Hilfe |
| Exitcode | Bedeutung | | Exitcode | Bedeutung |
| ---: | --- | | ---: | --- |
| `0` | vollständige Anwendungserfassung und XLSX erfolgreich | | `0` | Snapshot beziehungsweise Merge vollständig erzeugt |
| `1` | XLSX erzeugt, aber ein Pflichtabschnitt war unvollständig | | `1` | lokaler Pflichtabschnitt unvollständig; JSON nicht mergen |
| `2` | Aufruf-, Ausgabe- oder Berichtserzeugungsfehler | | `2` | Aufruf-, Datei-, JSON- oder XLSX-Fehler |
## Build mit Visual Studio 2019 ## Build
Die Solution vermeidet bewusst den beim früheren IIS Inventory aufgetretenen .NET-SDK-/MSBuild-Konflikt: Die Solution verwendet das klassische MSBuild-Projektformat für Visual Studio 2019 / MSBuild 16.x, .NET Framework 4.7.2 und C# 7.3. Es gibt keine NuGet-Abhängigkeiten.
| Komponente | Vorgabe |
| --- | --- |
| Ziel | .NET Framework 4.7.2 |
| Sprache | C# 7.3 |
| Solution | Visual Studio 2019, Formatversion 16 |
| Projektformat | klassisches MSBuild mit `ToolsVersion="15.0"` |
| MSBuild | 16.x, einschließlich 16.11 |
| NuGet | keine Pakete und kein Restore |
| .NET SDK / `global.json` | nicht erforderlich |
Voraussetzung auf dem Buildrechner ist das `.NET Framework 4.7.2 Developer/Targeting Pack`.
```cmd ```cmd
scripts\build-release.cmd scripts\build-release.cmd
scripts\package-release.cmd scripts\package-release.cmd
``` ```
Das Deployment liegt anschließend unter: ## Quellpaket als certutil-Text
```text
artifacts\BizTalkApplicationCatalog-deploy\
```
## Quellpaket und Base64-Text
Für eine dateibasierte Übergabe:
```cmd ```cmd
scripts\package-source.cmd scripts\package-source.cmd
``` ```
Erzeugt werden: Erzeugt:
```text ```text
artifacts\BizTalkApplicationCatalog-source.zip artifacts\BizTalkApplicationCatalog-source.zip
artifacts\BizTalkApplicationCatalog-source.zip.txt artifacts\BizTalkApplicationCatalog-source.zip.txt
``` ```
Dekodieren unter Windows: Dekodieren:
```cmd ```cmd
certutil -decode BizTalkApplicationCatalog-source.zip.txt BizTalkApplicationCatalog-source.zip certutil -decode BizTalkApplicationCatalog-source.zip.txt BizTalkApplicationCatalog-source.zip
``` ```
## Gitea Technische Details stehen in [Dokumentation.md](Dokumentation.md).
Das Repository enthält `.gitea/workflows/build.yml`. Ein Windows-Runner mit Visual Studio 2019 Build Tools und .NET Framework 4.7.2 Developer Pack baut die Solution, führt Tests und Self-Test aus und stellt den Deployment-Ordner als Artefakt bereit.
## Troubleshooting
### Solution lässt sich in VS 2019 nicht öffnen
Prüfen, dass wirklich diese klassische Solution geöffnet wird und das .NET Framework 4.7.2 Developer Pack installiert ist. Das Repository enthält weder SDK-Style-Projekte noch `global.json` oder `PackageReference`.
### Keine Anwendungen
- Tool lokal auf dem BizTalk Server ausführen
- Konto und WMI-Leserechte prüfen
- Namespace `root\MicrosoftBizTalkServer` prüfen
- BizTalk Administration Console auf demselben Konto testen
### Einzelne Zählwerte sind 0
Zuerst das Blatt `Abdeckung` prüfen. Nur bei Status `Vollständig` ist die Null durch eine erfolgreich gelesene WMI-Klasse belegt. Bei `Nicht verfügbar` oder `Begrenzt` muss die Ursache geprüft werden.
Weitere technische Details stehen in [Dokumentation.md](Dokumentation.md).
+2 -2
View File
@@ -19,7 +19,7 @@ if "%~2"=="" (
) )
echo ============================================================ echo ============================================================
echo BEW BizTalk Application Catalog echo BEW BizTalk Application Catalog - Phase 1
echo Umgebung: %ENVIRONMENT% echo Umgebung: %ENVIRONMENT%
echo Server: %COMPUTERNAME% echo Server: %COMPUTERNAME%
echo Ausgabe: %OUTPUT% echo Ausgabe: %OUTPUT%
@@ -34,7 +34,7 @@ echo.
if "%EXITCODE%"=="0" ( if "%EXITCODE%"=="0" (
echo Inventar erfolgreich erstellt. echo Inventar erfolgreich erstellt.
) else if "%EXITCODE%"=="1" ( ) else if "%EXITCODE%"=="1" (
echo Inventar mit Teilfehler erstellt. XLSX, Abdeckung, Findings und Log pruefen. echo Inventar unvollstaendig. JSON darf nicht zusammengefuehrt werden; Log pruefen.
) else ( ) else (
echo Inventar konnte nicht erstellt werden. echo Inventar konnte nicht erstellt werden.
) )
+26
View File
@@ -0,0 +1,26 @@
@echo off
setlocal EnableExtensions
set "APPDIR=%~dp0"
set "EXE=%APPDIR%BizTalkApplicationCatalog.exe"
if not exist "%EXE%" (
echo FEHLER: Anwendung fehlt: "%EXE%"
exit /b 2
)
if "%~1"=="" goto :usage
if "%~2"=="" goto :usage
if "%~3"=="" (
set "OUTPUT=%CD%\BizTalk-Anwendungskatalog\ACC-PROD"
) else (
set "OUTPUT=%~3"
)
"%EXE%" --merge --acc-json "%~1" --prod-json "%~2" --output "%OUTPUT%"
exit /b %ERRORLEVEL%
:usage
echo Aufruf:
echo run-merge.cmd ACC-SNAPSHOT.json PROD-SNAPSHOT.json [AUSGABEORDNER]
exit /b 2
+1
View File
@@ -16,6 +16,7 @@ copy "%BIN%\BizTalkApplicationCatalog.exe" "%DEPLOY%\" >nul
copy "%BIN%\BizTalkApplicationCatalog.exe.config" "%DEPLOY%\" >nul copy "%BIN%\BizTalkApplicationCatalog.exe.config" "%DEPLOY%\" >nul
if exist "%BIN%\BizTalkApplicationCatalog.pdb" copy "%BIN%\BizTalkApplicationCatalog.pdb" "%DEPLOY%\" >nul if exist "%BIN%\BizTalkApplicationCatalog.pdb" copy "%BIN%\BizTalkApplicationCatalog.pdb" "%DEPLOY%\" >nul
copy "%ROOT%\deployment\run-inventory.cmd" "%DEPLOY%\" >nul copy "%ROOT%\deployment\run-inventory.cmd" "%DEPLOY%\" >nul
copy "%ROOT%\deployment\run-merge.cmd" "%DEPLOY%\" >nul
copy "%ROOT%\Readme.md" "%DEPLOY%\" >nul copy "%ROOT%\Readme.md" "%DEPLOY%\" >nul
copy "%ROOT%\Dokumentation.md" "%DEPLOY%\" >nul copy "%ROOT%\Dokumentation.md" "%DEPLOY%\" >nul
-1
View File
@@ -5,6 +5,5 @@
</startup> </startup>
<appSettings> <appSettings>
<add key="WmiTimeoutSeconds" value="30" /> <add key="WmiTimeoutSeconds" value="30" />
<add key="MaxRowsPerArtifactType" value="10000" />
</appSettings> </appSettings>
</configuration> </configuration>
@@ -41,18 +41,18 @@
<Reference Include="System.IO.Compression" /> <Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" /> <Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Management" /> <Reference Include="System.Management" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Collectors\BizTalkWmiCollector.cs" /> <Compile Include="Collectors\BizTalkWmiCollector.cs" />
<Compile Include="Collectors\SystemCollector.cs" />
<Compile Include="Configuration\CommandLineOptions.cs" /> <Compile Include="Configuration\CommandLineOptions.cs" />
<Compile Include="Infrastructure\ConsoleFileLogger.cs" /> <Compile Include="Infrastructure\ConsoleFileLogger.cs" />
<Compile Include="Infrastructure\SafeCollector.cs" /> <Compile Include="Infrastructure\SafeCollector.cs" />
<Compile Include="Infrastructure\SelfTestRunner.cs" /> <Compile Include="Infrastructure\SelfTestRunner.cs" />
<Compile Include="Infrastructure\SensitiveDataSanitizer.cs" />
<Compile Include="Models\InventoryModels.cs" /> <Compile Include="Models\InventoryModels.cs" />
<Compile Include="Persistence\SnapshotStore.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reporting\XlsxReportWriter.cs" /> <Compile Include="Reporting\XlsxReportWriter.cs" />
@@ -3,14 +3,13 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Management; using System.Management;
using System.Runtime.InteropServices;
using BizTalkApplicationCatalog.Infrastructure; using BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models; using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Collectors namespace BizTalkApplicationCatalog.Collectors
{ {
/// <summary> /// <summary>
/// Liest die installierten Anwendungen und zugehörigen Artefakte über den lokalen BizTalk-WMI-Provider. /// Liest nur die Daten, die für die Phase-1-Ansicht erforderlich sind.
/// </summary> /// </summary>
internal sealed class BizTalkWmiCollector internal sealed class BizTalkWmiCollector
{ {
@@ -18,282 +17,156 @@ namespace BizTalkApplicationCatalog.Collectors
private readonly ConsoleFileLogger logger; private readonly ConsoleFileLogger logger;
private readonly ManagementScope scope; private readonly ManagementScope scope;
private readonly TimeSpan timeout; private readonly TimeSpan timeout;
private readonly int maxRowsPerType;
private readonly Dictionary<string, string> receivePortApplications = private readonly Dictionary<string, string> receivePortApplications =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public BizTalkWmiCollector( public BizTalkWmiCollector(
InventoryDocument document, InventoryDocument document,
ConsoleFileLogger logger, ConsoleFileLogger logger,
int timeoutSeconds, int timeoutSeconds)
int maxRowsPerType)
{ {
this.document = document; this.document = document;
this.logger = logger; this.logger = logger;
timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds)); timeout = TimeSpan.FromSeconds(Math.Max(5, timeoutSeconds));
this.maxRowsPerType = Math.Max(100, maxRowsPerType);
scope = new ManagementScope( scope = new ManagementScope(
@"\\" + Environment.MachineName + @"\root\MicrosoftBizTalkServer"); @"\\" + Environment.MachineName + @"\root\MicrosoftBizTalkServer");
scope.Options.Timeout = timeout; scope.Options.Timeout = timeout;
} }
/// <summary>
/// Stellt zuerst die vollständige Primärliste her und ergänzt anschließend optionale Details.
/// </summary>
public void Collect() public void Collect()
{ {
scope.Connect(); scope.Connect();
CollectApplications(); CollectApplications();
CollectReceivePortMappings();
// Receive Ports werden vor Receive Locations gelesen, damit deren Anwendung CollectSendPortEndpoints();
// auch dann aufgelöst werden kann, wenn die Location sie nicht direkt liefert. CollectReceiveLocationEndpoints();
foreach (var descriptor in ArtifactDescriptors())
{
CollectOptionalArtifacts(descriptor);
}
CollectOptionalHosts("MSBTS_HostSetting", "Host");
CollectOptionalHosts("MSBTS_HostInstance", "Hostinstanz");
CollectOptionalHosts("MSBTS_ReceiveHandler", "Receive Handler");
if (!CollectOptionalHosts("MSBTS_SendHandler2", "Send Handler"))
{
CollectOptionalHosts("MSBTS_SendHandler", "Send Handler");
}
document.Applications.Sort((left, right) => document.Applications.Sort((left, right) =>
string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase)); string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
document.Artifacts.Sort(CompareArtifacts); document.Endpoints.Sort((left, right) =>
document.Hosts.Sort((left, right) =>
{ {
var category = string.Compare(left.Category, right.Category, StringComparison.OrdinalIgnoreCase); var application = string.Compare(
return category != 0 left.ApplicationName,
? category right.ApplicationName,
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
return application != 0
? application
: string.Compare(
left.AdapterType,
right.AdapterType,
StringComparison.OrdinalIgnoreCase);
}); });
} }
private void CollectApplications() private void CollectApplications()
{ {
var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_Application")) foreach (var row in Query("SELECT * FROM MSBTS_Application"))
{ {
using (row) using (row)
{ {
var name = First(row, "Name", "ApplicationName"); var name = First(row, "Name", "ApplicationName");
if (string.IsNullOrWhiteSpace(name)) continue; if (!string.IsNullOrWhiteSpace(name))
document.Applications.Add(new ApplicationRecord
{ {
Name = name, document.Applications.Add(new ApplicationRecord { Name = name.Trim() });
Description = First(row, "Description"), }
Status = FormatStatus(First(row, "Status")),
IsDefault = FormatBoolean(First(row, "IsDefault")),
Source = "MSBTS_Application"
});
count++;
} }
} }
document.Coverage.Add(new CoverageRecord if (document.Applications.Count == 0)
{
DataSource = "MSBTS_Application",
Status = count > 0 ? "Vollständig" : "Fehler",
RowCount = count,
Required = "Ja",
Message = count > 0
? "Primärquelle der vollständigen Anwendungsliste."
: "Keine Anwendung geliefert."
});
if (count == 0)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
"MSBTS_Application lieferte keine Anwendungen. Zielserver und Berechtigung prüfen."); "MSBTS_Application lieferte keine Anwendungen.");
} }
logger.Info(count + " installierte BizTalk-Anwendung(en) gefunden."); logger.Info(document.Applications.Count
+ " BizTalk-Anwendung(en) gefunden.");
} }
private void CollectOptionalArtifacts(ArtifactDescriptor descriptor) private void CollectReceivePortMappings()
{
try
{ {
var count = 0; var count = 0;
var truncated = false; foreach (var row in Query("SELECT * FROM MSBTS_ReceivePort"))
foreach (var row in Query("SELECT * FROM " + descriptor.ClassName))
{ {
using (row) using (row)
{ {
if (count >= maxRowsPerType) var name = First(row, "Name", "ReceivePortName");
{ var application = First(row, "ApplicationName", "Application");
truncated = true;
break;
}
var record = CreateArtifact(row, descriptor.DisplayName);
if (string.IsNullOrWhiteSpace(record.Name)) continue;
if (record.Type == "Receive Port")
{
receivePortApplications[record.Name] = record.ApplicationName;
}
if (record.Type == "Receive Location"
&& string.IsNullOrWhiteSpace(record.ApplicationName)
&& receivePortApplications.ContainsKey(record.ParentName))
{
record.ApplicationName = receivePortApplications[record.ParentName];
}
document.Artifacts.Add(record);
count++;
}
}
document.Coverage.Add(new CoverageRecord
{
DataSource = descriptor.ClassName,
Status = truncated ? "Begrenzt" : "Vollständig",
RowCount = count,
Required = "Nein",
Message = truncated
? "Detailzeilen auf " + maxRowsPerType + " begrenzt."
: "WMI-Klasse erfolgreich gelesen."
});
logger.Info(descriptor.DisplayName + ": " + count + " Datensatz/Datensätze.");
if (truncated)
{
AddFinding(
"Warnung",
descriptor.DisplayName,
"Detailzeilen wurden bei " + maxRowsPerType + " Einträgen begrenzt.",
"MaxRowsPerArtifactType kontrolliert erhöhen und Inventar erneut ausführen.");
}
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
document.Coverage.Add(new CoverageRecord
{
DataSource = descriptor.ClassName,
Status = "Nicht verfügbar",
RowCount = 0,
Required = "Nein",
Message = exception.Message
});
AddFinding(
"Hinweis",
descriptor.DisplayName,
"Optionale WMI-Klasse konnte nicht gelesen werden: " + exception.Message,
"Berechtigung und Verfügbarkeit der WMI-Klasse prüfen; Wert 0 nicht als fachlich bestätigt werten.");
logger.Warning(descriptor.ClassName + " nicht verfügbar: " + exception.Message);
}
}
private bool CollectOptionalHosts(string className, string category)
{
try
{
var count = 0;
foreach (var row in Query("SELECT * FROM " + className))
{
using (row)
{
var name = First(row, "Name", "HostName", "AdapterName", "RunningServer");
if (string.IsNullOrWhiteSpace(name)) continue; if (string.IsNullOrWhiteSpace(name)) continue;
document.Hosts.Add(new HostRecord receivePortApplications[name] = application;
{
Category = category,
Name = name,
Server = First(row, "RunningServer", "ServerName"),
Status = FormatStatus(First(row, "ServiceState", "Status")),
Type = First(row, "HostType"),
WindowsGroup = First(row, "NTGroupName"),
Is32BitOnly = FormatBoolean(First(row, "IsHost32BitOnly")),
Trusted = FormatBoolean(First(row, "AuthTrusted")),
AdapterName = First(row, "AdapterName")
});
count++; count++;
} }
} }
document.Coverage.Add(new CoverageRecord logger.Info(count + " Receive-Port-Zuordnung(en) gelesen.");
{
DataSource = className,
Status = "Vollständig",
RowCount = count,
Required = "Nein",
Message = "WMI-Klasse erfolgreich gelesen."
});
logger.Info(category + ": " + count + " Datensatz/Datensätze.");
return true;
}
catch (Exception exception) when (IsRecoverableWmiException(exception))
{
document.Coverage.Add(new CoverageRecord
{
DataSource = className,
Status = "Nicht verfügbar",
RowCount = 0,
Required = "Nein",
Message = exception.Message
});
logger.Warning(className + " nicht verfügbar: " + exception.Message);
return false;
}
} }
private ArtifactRecord CreateArtifact(ManagementBaseObject row, string type) private void CollectSendPortEndpoints()
{ {
var record = new ArtifactRecord var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_SendPort"))
{ {
Type = type, using (row)
ApplicationName = First(row, "ApplicationName", "Application"), {
Name = First( var application = First(row, "ApplicationName", "Application");
row, AddEndpoint(application, First(
"Name",
"AssemblyName",
"FullName",
"ReceivePortName",
"OrchestrationName"),
Status = FormatStatus(First(row, "Status", "ServiceStatus", "IsDisabled")),
HostName = First(row, "HostName", "SendHandler", "ReceiveHandler"),
AdapterName = First(
row, row,
"PTTransportType", "PTTransportType",
"PrimaryTransportType",
"AdapterName", "AdapterName",
"TransportType"), "TransportType"));
ParentName = First(row, "ReceivePortName", "SendPortGroupName"), count++;
Address = SensitiveDataSanitizer.Sanitize(First(
row,
"PTAddress",
"InboundTransportURL",
"Address"))
};
AddProperty(row, record, "Description"); var secondaryAdapter = First(
AddProperty(row, record, "IsTwoWay"); row,
AddProperty(row, record, "IsDynamic"); "STTransportType",
AddProperty(row, record, "IsDisabled"); "SecondaryTransportType");
AddProperty(row, record, "ReceivePipeline"); if (!string.IsNullOrWhiteSpace(secondaryAdapter))
AddProperty(row, record, "SendPipeline"); {
AddProperty(row, record, "STTransportType"); AddEndpoint(application, secondaryAdapter);
AddProperty(row, record, "STAddress"); count++;
AddProperty(row, record, "AssemblyName"); }
AddProperty(row, record, "FullName"); }
AddProperty(row, record, "TargetNameSpace"); }
AddProperty(row, record, "RootName"); logger.Info(count + " Send-Endpunkt(e) gefunden.");
AddProperty(row, record, "Tracking");
return record;
} }
private static IEnumerable<ArtifactDescriptor> ArtifactDescriptors() private void CollectReceiveLocationEndpoints()
{ {
return new[] var count = 0;
foreach (var row in Query("SELECT * FROM MSBTS_ReceiveLocation"))
{ {
new ArtifactDescriptor("MSBTS_Orchestration", "Orchestrierung"), using (row)
new ArtifactDescriptor("MSBTS_SendPort", "Send Port"), {
new ArtifactDescriptor("MSBTS_SendPortGroup", "Send Port Group"), var application = First(row, "ApplicationName", "Application");
new ArtifactDescriptor("MSBTS_ReceivePort", "Receive Port"), if (string.IsNullOrWhiteSpace(application))
new ArtifactDescriptor("MSBTS_ReceiveLocation", "Receive Location"), {
new ArtifactDescriptor("MSBTS_Assembly", "Assembly"), var receivePort = First(row, "ReceivePortName");
new ArtifactDescriptor("MSBTS_Schema", "Schema"), receivePortApplications.TryGetValue(receivePort, out application);
new ArtifactDescriptor("MSBTS_Map", "Map"), }
new ArtifactDescriptor("MSBTS_Pipeline", "Pipeline")
}; AddEndpoint(
application,
First(row, "AdapterName", "TransportType", "PTTransportType"));
count++;
}
}
logger.Info(count + " Receive-Endpunkt(e) gefunden.");
}
private void AddEndpoint(string applicationName, string adapterType)
{
if (string.IsNullOrWhiteSpace(applicationName))
{
throw new InvalidOperationException(
"Ein Endpunkt konnte keiner BizTalk-Anwendung zugeordnet werden.");
}
document.Endpoints.Add(new EndpointRecord
{
ApplicationName = applicationName.Trim(),
AdapterType = string.IsNullOrWhiteSpace(adapterType)
? "Unbekannt"
: adapterType.Trim()
});
} }
private List<ManagementObject> Query(string query) private List<ManagementObject> Query(string query)
@@ -304,38 +177,15 @@ namespace BizTalkApplicationCatalog.Collectors
Rewindable = false, Rewindable = false,
Timeout = timeout Timeout = timeout
}; };
using (var searcher = new ManagementObjectSearcher(scope, new ObjectQuery(query), options)) using (var searcher = new ManagementObjectSearcher(
scope,
new ObjectQuery(query),
options))
{ {
return searcher.Get().Cast<ManagementObject>().ToList(); return searcher.Get().Cast<ManagementObject>().ToList();
} }
} }
private void AddFinding(string severity, string area, string message, string action)
{
document.Findings.Add(new Finding
{
Severity = severity,
Area = area,
Message = message,
RecommendedAction = action
});
}
private static void AddProperty(
ManagementBaseObject row,
ArtifactRecord target,
string propertyName)
{
var value = First(row, propertyName);
if (!string.IsNullOrWhiteSpace(value))
{
target.Properties.Add(new NameValueRecord(
propertyName,
SensitiveDataSanitizer.RedactProperty(propertyName, value),
"WMI"));
}
}
private static string First(ManagementBaseObject row, params string[] names) private static string First(ManagementBaseObject row, params string[] names)
{ {
foreach (var name in names) foreach (var name in names)
@@ -344,78 +194,15 @@ namespace BizTalkApplicationCatalog.Collectors
{ {
var value = row[name]; var value = row[name];
if (value == null) continue; if (value == null) continue;
var text = ConvertValue(value); var text = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(text)) return text; if (!string.IsNullOrWhiteSpace(text)) return text;
} }
catch (Exception exception) when (IsRecoverableWmiException(exception)) catch (ManagementException)
{ {
// Die Eigenschaft ist in dieser BizTalk-Version/Klasse nicht vorhanden. // WMI-Properties unterscheiden sich zwischen Providerständen.
} }
} }
return string.Empty; return string.Empty;
} }
private static string ConvertValue(object value)
{
var array = value as Array;
if (array != null && !(value is byte[]))
{
return string.Join(", ", array.Cast<object>().Select(item =>
Convert.ToString(item, CultureInfo.InvariantCulture)));
}
return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty;
}
private static bool IsRecoverableWmiException(Exception exception)
{
return exception is ManagementException
|| exception is COMException
|| exception is UnauthorizedAccessException;
}
private static string FormatStatus(string value)
{
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
switch (value.Trim())
{
case "1": return "Gestoppt (1)";
case "2": return "Gestartet (2)";
case "3": return "Teilweise gestartet (3)";
case "True": return "Ja";
case "False": return "Nein";
default: return value;
}
}
private static string FormatBoolean(string value)
{
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
if (value.Equals("True", StringComparison.OrdinalIgnoreCase) || value == "1") return "Ja";
if (value.Equals("False", StringComparison.OrdinalIgnoreCase) || value == "0") return "Nein";
return value;
}
private static int CompareArtifacts(ArtifactRecord left, ArtifactRecord right)
{
var application = string.Compare(
left.ApplicationName, right.ApplicationName, StringComparison.OrdinalIgnoreCase);
if (application != 0) return application;
var type = string.Compare(left.Type, right.Type, StringComparison.OrdinalIgnoreCase);
return type != 0
? type
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
}
private sealed class ArtifactDescriptor
{
public ArtifactDescriptor(string className, string displayName)
{
ClassName = className;
DisplayName = displayName;
}
public string ClassName { get; private set; }
public string DisplayName { get; private set; }
}
} }
} }
@@ -1,223 +0,0 @@
using System;
using System.Globalization;
using System.Management;
using System.Reflection;
using Microsoft.Win32;
using BizTalkApplicationCatalog.Configuration;
using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Collectors
{
/// <summary>
/// Erfasst lokale Windows-, BizTalk- und Gruppenmetadaten ausschließlich lesend.
/// </summary>
internal sealed class SystemCollector
{
private const string ProductKey = @"SOFTWARE\Microsoft\BizTalk Server\3.0";
private const string AdministrationKey = ProductKey + @"\Administration";
private readonly CommandLineOptions options;
public SystemCollector(CommandLineOptions options)
{
this.options = options;
}
public void Collect(InventoryDocument document)
{
document.ComputerName = Environment.MachineName;
document.ToolVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
Add(document, "Ausführender Benutzer", Environment.UserDomainName + "\\" + Environment.UserName, "Prozess");
Add(document, "64-Bit-Betriebssystem", Environment.Is64BitOperatingSystem ? "Ja" : "Nein", "Prozess");
Add(document, "64-Bit-Prozess", Environment.Is64BitProcess ? "Ja" : "Nein", "Prozess");
Add(document, ".NET Runtime", Environment.Version.ToString(), "Prozess");
CollectOperatingSystem(document);
CollectRegistry(document);
CollectGroupSetting(document);
if (!string.IsNullOrWhiteSpace(options.ManagementServer))
{
document.ManagementServer = options.ManagementServer;
}
if (!string.IsNullOrWhiteSpace(options.ManagementDatabase))
{
document.ManagementDatabase = options.ManagementDatabase;
}
if (string.IsNullOrWhiteSpace(document.ManagementDatabase))
{
document.ManagementDatabase = "BizTalkMgmtDb";
}
Add(document, "BizTalk Management SQL Server", Unknown(document.ManagementServer), "Ermittelt/Parameter");
Add(document, "BizTalk Management Database", document.ManagementDatabase, "Ermittelt/Parameter");
if (document.EnvironmentName != "ACC" && document.EnvironmentName != "PROD")
{
document.Findings.Add(new Finding
{
Severity = "Warnung",
Area = "Aufruf",
Message = "Die Umgebung ist weder ACC noch PROD: " + document.EnvironmentName,
RecommendedAction = "Umgebung und Zielserver vor der Ablage bestätigen."
});
}
}
private static void CollectOperatingSystem(InventoryDocument document)
{
using (var searcher = new ManagementObjectSearcher(
"root\\cimv2",
"SELECT Caption,Version,BuildNumber,OSArchitecture,LastBootUpTime FROM Win32_OperatingSystem"))
{
foreach (ManagementObject row in searcher.Get())
{
using (row)
{
Add(document, "Betriebssystem", Value(row, "Caption"), "Win32_OperatingSystem");
Add(document, "Windows-Version", Value(row, "Version"), "Win32_OperatingSystem");
Add(document, "Windows-Build", Value(row, "BuildNumber"), "Win32_OperatingSystem");
Add(document, "Architektur", Value(row, "OSArchitecture"), "Win32_OperatingSystem");
Add(document, "Letzter Systemstart", WmiDate(Value(row, "LastBootUpTime")), "Win32_OperatingSystem");
break;
}
}
}
}
private static void CollectRegistry(InventoryDocument document)
{
foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
{
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var product = baseKey.OpenSubKey(ProductKey, false))
{
if (product != null)
{
AddRegistry(document, product, "ProductName", "BizTalk Produktname", view);
AddRegistry(document, product, "ProductVersion", "BizTalk Produktversion", view);
AddRegistry(document, product, "Edition", "BizTalk Edition", view);
}
}
using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
using (var administration = baseKey.OpenSubKey(AdministrationKey, false))
{
if (administration == null)
{
continue;
}
if (string.IsNullOrWhiteSpace(document.ManagementServer))
{
document.ManagementServer = FirstRegistry(
administration, "MgmtDBServer", "ManagementDBServer");
}
if (string.IsNullOrWhiteSpace(document.ManagementDatabase))
{
document.ManagementDatabase = FirstRegistry(
administration, "MgmtDBName", "ManagementDBName");
}
}
}
}
private static void CollectGroupSetting(InventoryDocument document)
{
try
{
using (var searcher = new ManagementObjectSearcher(
@"root\MicrosoftBizTalkServer", "SELECT * FROM MSBTS_GroupSetting"))
{
foreach (ManagementObject row in searcher.Get())
{
using (row)
{
var server = Value(row, "MgmtDbServerName");
var database = Value(row, "MgmtDbName");
if (!string.IsNullOrWhiteSpace(server)) document.ManagementServer = server;
if (!string.IsNullOrWhiteSpace(database)) document.ManagementDatabase = database;
Add(document, "BizTalk Gruppenname", Value(row, "Name"), "MSBTS_GroupSetting");
Add(document, "BizTalk Administratorengruppe", Value(row, "BizTalkAdministratorGroup"), "MSBTS_GroupSetting");
Add(document, "BizTalk Operatorengruppe", Value(row, "BizTalkOperatorGroup"), "MSBTS_GroupSetting");
Add(document, "Enterprise SSO Server", Value(row, "SSOServerName"), "MSBTS_GroupSetting");
break;
}
}
}
}
catch (ManagementException exception)
{
document.Findings.Add(new Finding
{
Severity = "Hinweis",
Area = "MSBTS_GroupSetting",
Message = "Gruppenmetadaten konnten nicht vollständig gelesen werden: " + exception.Message,
RecommendedAction = "WMI-Berechtigung prüfen; die Anwendungserfassung läuft unabhängig weiter."
});
}
}
private static void AddRegistry(
InventoryDocument document,
RegistryKey key,
string valueName,
string displayName,
RegistryView view)
{
var value = Convert.ToString(key.GetValue(valueName), CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(value)
&& !document.SystemProperties.Exists(item => item.Name == displayName))
{
Add(document, displayName, value, "Registry " + view);
}
}
private static string FirstRegistry(RegistryKey key, params string[] names)
{
foreach (var name in names)
{
var value = Convert.ToString(key.GetValue(name), CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(value)) return value.Trim();
}
return string.Empty;
}
private static string Value(ManagementBaseObject row, string property)
{
try
{
return Convert.ToString(row[property], CultureInfo.InvariantCulture) ?? string.Empty;
}
catch (ManagementException)
{
return string.Empty;
}
}
private static void Add(InventoryDocument document, string name, string value, string source)
{
if (!string.IsNullOrWhiteSpace(value))
{
document.SystemProperties.Add(new NameValueRecord(name, value, source));
}
}
private static string WmiDate(string value)
{
if (string.IsNullOrWhiteSpace(value)) return "Unbekannt";
try
{
return ManagementDateTimeConverter.ToDateTime(value)
.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
}
catch (ArgumentOutOfRangeException)
{
return value;
}
}
private static string Unknown(string value)
{
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value;
}
}
}
@@ -4,9 +4,6 @@ using System.Text.RegularExpressions;
namespace BizTalkApplicationCatalog.Configuration namespace BizTalkApplicationCatalog.Configuration
{ {
/// <summary>
/// Validiert die Kommandozeile und stellt ausschließlich normalisierte Werte bereit.
/// </summary>
internal sealed class CommandLineOptions internal sealed class CommandLineOptions
{ {
private static readonly Regex UnsafeEnvironment = private static readonly Regex UnsafeEnvironment =
@@ -14,14 +11,12 @@ namespace BizTalkApplicationCatalog.Configuration
public string EnvironmentName { get; private set; } public string EnvironmentName { get; private set; }
public string OutputDirectory { get; private set; } public string OutputDirectory { get; private set; }
public string ManagementServer { get; private set; } public string AccSnapshotPath { get; private set; }
public string ManagementDatabase { get; private set; } public string ProdSnapshotPath { get; private set; }
public bool MergeMode { get; private set; }
public bool SelfTest { get; private set; } public bool SelfTest { get; private set; }
public bool ShowHelp { get; private set; } public bool ShowHelp { get; private set; }
/// <summary>
/// Parst die Argumente. Unbekannte oder unvollständige Optionen werden abgelehnt.
/// </summary>
public static CommandLineOptions Parse(string[] args) public static CommandLineOptions Parse(string[] args)
{ {
var result = new CommandLineOptions(); var result = new CommandLineOptions();
@@ -36,11 +31,14 @@ namespace BizTalkApplicationCatalog.Configuration
case "--output": case "--output":
result.OutputDirectory = Value(args, ref index, argument); result.OutputDirectory = Value(args, ref index, argument);
break; break;
case "--management-server": case "--merge":
result.ManagementServer = Value(args, ref index, argument); result.MergeMode = true;
break; break;
case "--management-database": case "--acc-json":
result.ManagementDatabase = Value(args, ref index, argument); result.AccSnapshotPath = Value(args, ref index, argument);
break;
case "--prod-json":
result.ProdSnapshotPath = Value(args, ref index, argument);
break; break;
case "--self-test": case "--self-test":
result.SelfTest = true; result.SelfTest = true;
@@ -55,31 +53,54 @@ namespace BizTalkApplicationCatalog.Configuration
} }
} }
if (result.SelfTest || result.ShowHelp) if (result.SelfTest || result.ShowHelp) return result;
{
return result;
}
if (result.MergeMode)
{
if (!string.IsNullOrWhiteSpace(result.EnvironmentName))
{
throw new ArgumentException(
"--environment darf nicht mit --merge kombiniert werden.");
}
if (string.IsNullOrWhiteSpace(result.AccSnapshotPath)
|| string.IsNullOrWhiteSpace(result.ProdSnapshotPath))
{
throw new ArgumentException(
"--merge benötigt --acc-json und --prod-json.");
}
result.AccSnapshotPath = FullPath(result.AccSnapshotPath);
result.ProdSnapshotPath = FullPath(result.ProdSnapshotPath);
}
else
{
if (!string.IsNullOrWhiteSpace(result.AccSnapshotPath)
|| !string.IsNullOrWhiteSpace(result.ProdSnapshotPath))
{
throw new ArgumentException(
"--acc-json und --prod-json sind nur mit --merge zulässig.");
}
if (string.IsNullOrWhiteSpace(result.EnvironmentName)) if (string.IsNullOrWhiteSpace(result.EnvironmentName))
{ {
throw new ArgumentException("--environment fehlt."); throw new ArgumentException("--environment fehlt.");
} }
result.EnvironmentName = UnsafeEnvironment.Replace( result.EnvironmentName = UnsafeEnvironment.Replace(
result.EnvironmentName.Trim().ToUpperInvariant(), "-").Trim('-'); result.EnvironmentName.Trim().ToUpperInvariant(),
if (result.EnvironmentName.Length == 0) "-").Trim('-');
if (result.EnvironmentName != "ACC" && result.EnvironmentName != "PROD")
{ {
throw new ArgumentException("--environment enthält keinen gültigen Namen."); throw new ArgumentException(
"--environment muss ACC oder PROD sein.");
}
} }
if (string.IsNullOrWhiteSpace(result.OutputDirectory)) if (string.IsNullOrWhiteSpace(result.OutputDirectory))
{ {
result.OutputDirectory = Path.Combine( result.OutputDirectory = Path.Combine(
Environment.CurrentDirectory, "BizTalk-Anwendungsinventar", result.EnvironmentName); Environment.CurrentDirectory,
"BizTalk-Anwendungskatalog",
result.MergeMode ? "ACC-PROD" : result.EnvironmentName);
} }
result.OutputDirectory = FullPath(result.OutputDirectory);
result.OutputDirectory = Path.GetFullPath(
Environment.ExpandEnvironmentVariables(result.OutputDirectory));
return result; return result;
} }
@@ -87,16 +108,22 @@ namespace BizTalkApplicationCatalog.Configuration
{ {
return string.Join(Environment.NewLine, new[] return string.Join(Environment.NewLine, new[]
{ {
"BEW BizTalk Application Catalog", "BEW BizTalk Application Catalog Phase 1",
string.Empty, string.Empty,
"Aufruf:", "Lokalen Snapshot und lokale Excel-Sicht erzeugen:",
" BizTalkApplicationCatalog.exe --environment ACC [--output PFAD]", " BizTalkApplicationCatalog.exe --environment ACC --output PFAD",
" BizTalkApplicationCatalog.exe --environment PROD [--output PFAD]", " BizTalkApplicationCatalog.exe --environment PROD --output PFAD",
string.Empty,
"ACC und PROD zur gemeinsamen Excel-Sicht zusammenführen:",
" BizTalkApplicationCatalog.exe --merge --acc-json ACC.json --prod-json PROD.json --output PFAD",
string.Empty, string.Empty,
"Optionen:", "Optionen:",
" --management-server NAME Optionaler SQL-Server-Hinweis.", " --environment ACC|PROD Lokal zu inventarisierende Umgebung.",
" --management-database NAME Optionale Management-Datenbank.", " --output PFAD Zielordner für XLSX, JSON und Log.",
" --self-test Prüft XLSX-Struktur und Kernlogik ohne BizTalk.", " --merge Führt zwei vollständige JSON-Snapshots zusammen.",
" --acc-json PFAD Vollständiger ACC-Snapshot.",
" --prod-json PFAD Vollständiger PROD-Snapshot.",
" --self-test Prüft JSON, Merge und XLSX ohne BizTalk.",
" --help Diese Hilfe." " --help Diese Hilfe."
}); });
} }
@@ -104,12 +131,17 @@ namespace BizTalkApplicationCatalog.Configuration
private static string Value(string[] args, ref int index, string option) private static string Value(string[] args, ref int index, string option)
{ {
index++; index++;
if (index >= args.Length || args[index].StartsWith("--", StringComparison.Ordinal)) if (index >= args.Length
|| args[index].StartsWith("--", StringComparison.Ordinal))
{ {
throw new ArgumentException("Wert für " + option + " fehlt."); throw new ArgumentException("Wert für " + option + " fehlt.");
} }
return args[index]; return args[index];
} }
private static string FullPath(string value)
{
return Path.GetFullPath(Environment.ExpandEnvironmentVariables(value));
}
} }
} }
@@ -1,27 +1,22 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using BizTalkApplicationCatalog.Models; using BizTalkApplicationCatalog.Models;
using BizTalkApplicationCatalog.Persistence;
using BizTalkApplicationCatalog.Reporting; using BizTalkApplicationCatalog.Reporting;
namespace BizTalkApplicationCatalog.Infrastructure namespace BizTalkApplicationCatalog.Infrastructure
{ {
/// <summary> /// <summary>
/// Prüft die zentrale Berichtslogik ohne Zugriff auf Windows- oder BizTalk-WMI. /// Prüft Snapshot, Merge und Excel-Ausgabe ohne BizTalk-Zugriff.
/// </summary> /// </summary>
internal static class SelfTestRunner internal static class SelfTestRunner
{ {
public static void Run(string outputPath = null) public static void Run(string outputPath = null)
{ {
Assert(SensitiveDataSanitizer.Sanitize(
"https://host/path?password=secret&client=100")
== "https://host/path?password=[REDACTED]&client=100",
"Secret-Redaktion ist fehlerhaft.");
var keepOutput = !string.IsNullOrWhiteSpace(outputPath); var keepOutput = !string.IsNullOrWhiteSpace(outputPath);
var directory = keepOutput var directory = keepOutput
? Path.GetDirectoryName(Path.GetFullPath(outputPath)) ? Path.GetDirectoryName(Path.GetFullPath(outputPath))
@@ -29,11 +24,26 @@ namespace BizTalkApplicationCatalog.Infrastructure
Path.GetTempPath(), Path.GetTempPath(),
"BizTalkApplicationCatalogTests-" + Guid.NewGuid().ToString("N")); "BizTalkApplicationCatalogTests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
var path = keepOutput ? Path.GetFullPath(outputPath) : Path.Combine(directory, "test.xlsx"); var path = keepOutput
? Path.GetFullPath(outputPath)
: Path.Combine(directory, "phase1-test.xlsx");
var jsonPath = Path.Combine(
directory,
"phase1-test-" + Guid.NewGuid().ToString("N") + ".json");
try try
{ {
var document = SampleDocument(); var acc = AccSnapshot();
new XlsxReportWriter().Write(document, path); SnapshotStore.Save(acc, jsonPath);
var loadedAcc = SnapshotStore.Load(jsonPath);
SnapshotStore.ValidateForMerge(loadedAcc, "ACC");
Assert(
loadedAcc.Applications.Count == 2,
"JSON-Roundtrip hat Anwendungen verloren.");
var prod = ProdSnapshot();
SnapshotStore.ValidateForMerge(prod, "PROD");
new XlsxReportWriter().Write(new[] { loadedAcc, prod }, path);
Assert(File.Exists(path), "XLSX-Datei wurde nicht erzeugt."); Assert(File.Exists(path), "XLSX-Datei wurde nicht erzeugt.");
using (var archive = ZipFile.OpenRead(path)) using (var archive = ZipFile.OpenRead(path))
@@ -46,19 +56,18 @@ namespace BizTalkApplicationCatalog.Infrastructure
"docProps/app.xml", "docProps/app.xml",
"xl/workbook.xml", "xl/workbook.xml",
"xl/_rels/workbook.xml.rels", "xl/_rels/workbook.xml.rels",
"xl/styles.xml" "xl/styles.xml",
"xl/worksheets/sheet1.xml"
}; };
foreach (var name in required) foreach (var name in required)
{
Assert(archive.GetEntry(name) != null, "XLSX-Part fehlt: " + name);
}
for (var index = 1; index <= 10; index++)
{ {
Assert( Assert(
archive.GetEntry("xl/worksheets/sheet" + index + ".xml") != null, archive.GetEntry(name) != null,
"Arbeitsblatt fehlt: " + index); "XLSX-Part fehlt: " + name);
} }
Assert(
archive.GetEntry("xl/worksheets/sheet2.xml") == null,
"Die Arbeitsmappe enthält mehr als ein Datenblatt.");
foreach (var entry in archive.Entries.Where(item => foreach (var entry in archive.Entries.Where(item =>
item.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase) item.FullName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
@@ -74,42 +83,60 @@ namespace BizTalkApplicationCatalog.Infrastructure
XNamespace spreadsheet = XNamespace spreadsheet =
"http://schemas.openxmlformats.org/spreadsheetml/2006/main"; "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
var names = workbook.Descendants(spreadsheet + "sheet") var names = workbook.Descendants(spreadsheet + "sheet")
.Select(item => (string)item.Attribute("name")).ToList(); .Select(item => (string)item.Attribute("name"))
Assert(names.Contains("Anwendungen"), "Anwendungsblatt fehlt."); .ToList();
Assert(names.Contains("Abdeckung"), "Abdeckungsblatt fehlt."); Assert(
names.SequenceEqual(new[] { "Phase 1" }),
"Es wird nicht exakt das Blatt 'Phase 1' erzeugt.");
var contentTypes = LoadXml(archive, "[Content_Types].xml"); var contentTypes = LoadXml(archive, "[Content_Types].xml");
XNamespace contentTypeNamespace = XNamespace contentTypeNamespace =
"http://schemas.openxmlformats.org/package/2006/content-types"; "http://schemas.openxmlformats.org/package/2006/content-types";
Assert( Assert(
contentTypes.Root.Elements(contentTypeNamespace + "Override").Count() == 14, contentTypes.Root
"Content-Type-Overrides sind unvollständig oder im falschen Namespace."); .Elements(contentTypeNamespace + "Override")
.Count() == 5,
"Content-Type-Overrides sind unvollständig.");
var relationships = LoadXml(archive, "_rels/.rels"); var worksheet = LoadXml(
XNamespace relationshipNamespace = archive,
"http://schemas.openxmlformats.org/package/2006/relationships"; "xl/worksheets/sheet1.xml");
Assert( Assert(
relationships.Root.Elements(relationshipNamespace + "Relationship").Count() == 3, worksheet.Descendants(spreadsheet + "mergeCell")
"Paketbeziehungen sind unvollständig oder im falschen Namespace."); .Select(item => (string)item.Attribute("ref"))
.Contains("A1:G1"),
"Titelzeile ist nicht zusammengeführt.");
Assert(
(string)worksheet
.Descendants(spreadsheet + "autoFilter")
.Single()
.Attribute("ref") == "A2:G5",
"Filterbereich ist falsch.");
var combinedText = new StringBuilder(); string text;
foreach (var entry in archive.Entries.Where(item => using (var reader = new StreamReader(
item.FullName.StartsWith("xl/worksheets/", StringComparison.Ordinal))) archive.GetEntry("xl/worksheets/sheet1.xml").Open(),
Encoding.UTF8))
{ {
using (var reader = new StreamReader(entry.Open(), Encoding.UTF8)) text = reader.ReadToEnd();
{
combinedText.Append(reader.ReadToEnd());
} }
} Assert(text.Contains("BizTalk-Anwendung in ACC"), "ACC-Spalte fehlt.");
Assert(!combinedText.ToString().Contains("supersecret"), "XLSX enthält Testkennwort."); Assert(text.Contains("BizTalk-Anwendung in PRD"), "PRD-Spalte fehlt.");
Assert(combinedText.ToString().Contains("[REDACTED]"), "Redaktionsmarker fehlt."); Assert(text.Contains("FILE (1x) / SFTP (1x)"), "Adapteraggregation ist falsch.");
Assert(text.Contains("Nur in ACC vorhanden"), "ACC-Hinweis fehlt.");
Assert(text.Contains("Nur in PRD vorhanden"), "PRD-Hinweis fehlt.");
Assert(text.Contains("Gesamt: 3 BizTalk-Anwendungen"), "Summenzeile ist falsch.");
} }
} }
finally finally
{ {
try try
{ {
if (!keepOutput && Directory.Exists(directory)) Directory.Delete(directory, true); if (File.Exists(jsonPath)) File.Delete(jsonPath);
if (!keepOutput && Directory.Exists(directory))
{
Directory.Delete(directory, true);
}
} }
catch (IOException) catch (IOException)
{ {
@@ -120,58 +147,56 @@ namespace BizTalkApplicationCatalog.Infrastructure
} }
} }
private static InventoryDocument SampleDocument() private static CatalogSnapshot AccSnapshot()
{ {
var document = new InventoryDocument var snapshot = Snapshot("ACC", "BIZTALK-ACC");
snapshot.Applications.Add(Application(
"OrderProcessing",
new AdapterCount { AdapterType = "FILE", Count = 1 },
new AdapterCount { AdapterType = "SFTP", Count = 1 }));
snapshot.Applications.Add(Application(
"AccOnly",
new AdapterCount { AdapterType = "FILE", Count = 1 }));
return snapshot;
}
private static CatalogSnapshot ProdSnapshot()
{ {
EnvironmentName = "ACC", var snapshot = Snapshot("PROD", "BIZTALK-PROD");
ComputerName = "BIZTALK-ACC", snapshot.Applications.Add(Application(
ToolVersion = "1.0.0.0", "OrderProcessing",
ManagementServer = "SQL-ACC", new AdapterCount { AdapterType = "FILE", Count = 1 },
ManagementDatabase = "BizTalkMgmtDb", new AdapterCount { AdapterType = "SFTP", Count = 1 }));
StartedUtc = DateTime.UtcNow.AddSeconds(-1), snapshot.Applications.Add(Application(
CompletedUtc = DateTime.UtcNow "ProdOnly",
new AdapterCount { AdapterType = "SFTP", Count = 1 }));
return snapshot;
}
private static CatalogSnapshot Snapshot(
string environment,
string computer)
{
return new CatalogSnapshot
{
EnvironmentName = environment,
ComputerName = computer,
CreatedUtc = DateTime.UtcNow.ToString("o"),
IsComplete = true
}; };
document.SystemProperties.Add(new NameValueRecord( }
"Betriebssystem", "Windows Server 2019", "Self-Test"));
document.Applications.Add(new ApplicationRecord private static CatalogApplication Application(
string name,
params AdapterCount[] adapters)
{ {
Name = "OrderProcessing", var result = new CatalogApplication
Description = "Aufträge & Sonderzeichen <Test>",
Status = "Gestartet (2)",
IsDefault = "Nein",
Source = "Self-Test"
});
var port = new ArtifactRecord
{ {
Type = "Send Port", Name = name,
ApplicationName = "OrderProcessing", EndpointCount = adapters.Sum(item => item.Count)
Name = "Send_Orders",
Status = "Gestartet (2)",
HostName = "SendHost",
AdapterName = "WCF-Custom",
Address = SensitiveDataSanitizer.Sanitize(
"https://service/orders?password=supersecret")
}; };
port.Properties.Add(new NameValueRecord("IsTwoWay", "True", "Self-Test")); result.AdapterCounts.AddRange(adapters);
document.Artifacts.Add(port); return result;
document.Coverage.Add(new CoverageRecord
{
DataSource = "MSBTS_Application",
Status = "Vollständig",
RowCount = 1,
Required = "Ja",
Message = "Self-Test"
});
document.SectionStatuses.Add(new SectionStatus
{
Name = "Self-Test",
Status = "Erfolgreich",
Message = "OK",
Required = true,
DurationMilliseconds = 1
});
return document;
} }
private static XDocument LoadXml(ZipArchive archive, string name) private static XDocument LoadXml(ZipArchive archive, string name)
@@ -1,39 +0,0 @@
using System;
using System.Text.RegularExpressions;
namespace BizTalkApplicationCatalog.Infrastructure
{
/// <summary>
/// Verhindert, dass versehentlich Kennwörter oder Token aus WMI-Textwerten in den Bericht gelangen.
/// </summary>
internal static class SensitiveDataSanitizer
{
private static readonly Regex NamedSecret = new Regex(
@"(?i)(password|passwd|pwd|secret|token|clientsecret|accesskey)\s*([=:])\s*([^;&\s""']+)",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static string Sanitize(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return NamedSecret.Replace(value, match =>
match.Groups[1].Value + match.Groups[2].Value + "[REDACTED]");
}
public static string RedactProperty(string name, string value)
{
if (!string.IsNullOrWhiteSpace(name)
&& (name.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("secret", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("token", StringComparison.OrdinalIgnoreCase) >= 0))
{
return "[REDACTED]";
}
return Sanitize(value);
}
}
}
@@ -1,37 +1,29 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Serialization;
namespace BizTalkApplicationCatalog.Models namespace BizTalkApplicationCatalog.Models
{ {
/// <summary> /// <summary>
/// Enthält die vollständig normalisierten Ergebnisse eines Inventarlaufs. /// Arbeitsmodell eines lokalen, ausschließlich lesenden Inventarlaufs.
/// </summary> /// </summary>
internal sealed class InventoryDocument internal sealed class InventoryDocument
{ {
public InventoryDocument() public InventoryDocument()
{ {
SystemProperties = new List<NameValueRecord>();
Applications = new List<ApplicationRecord>(); Applications = new List<ApplicationRecord>();
Artifacts = new List<ArtifactRecord>(); Endpoints = new List<EndpointRecord>();
Hosts = new List<HostRecord>();
Coverage = new List<CoverageRecord>();
Findings = new List<Finding>(); Findings = new List<Finding>();
SectionStatuses = new List<SectionStatus>(); SectionStatuses = new List<SectionStatus>();
} }
public string EnvironmentName { get; set; } public string EnvironmentName { get; set; }
public string ComputerName { get; set; } public string ComputerName { get; set; }
public string ToolVersion { get; set; }
public string ManagementServer { get; set; }
public string ManagementDatabase { get; set; }
public DateTime StartedUtc { get; set; } public DateTime StartedUtc { get; set; }
public DateTime CompletedUtc { get; set; } public DateTime CompletedUtc { get; set; }
public List<NameValueRecord> SystemProperties { get; private set; }
public List<ApplicationRecord> Applications { get; private set; } public List<ApplicationRecord> Applications { get; private set; }
public List<ArtifactRecord> Artifacts { get; private set; } public List<EndpointRecord> Endpoints { get; private set; }
public List<HostRecord> Hosts { get; private set; }
public List<CoverageRecord> Coverage { get; private set; }
public List<Finding> Findings { get; private set; } public List<Finding> Findings { get; private set; }
public List<SectionStatus> SectionStatuses { get; private set; } public List<SectionStatus> SectionStatuses { get; private set; }
@@ -48,57 +40,12 @@ namespace BizTalkApplicationCatalog.Models
internal sealed class ApplicationRecord internal sealed class ApplicationRecord
{ {
public string Name { get; set; } public string Name { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public string IsDefault { get; set; }
public string Source { get; set; }
} }
internal sealed class ArtifactRecord internal sealed class EndpointRecord
{ {
public ArtifactRecord()
{
Properties = new List<NameValueRecord>();
}
public string Type { get; set; }
public string ApplicationName { get; set; } public string ApplicationName { get; set; }
public string Name { get; set; } public string AdapterType { get; set; }
public string Status { get; set; }
public string HostName { get; set; }
public string AdapterName { get; set; }
public string ParentName { get; set; }
public string Address { get; set; }
public List<NameValueRecord> Properties { get; private set; }
public string Property(string name)
{
var match = Properties.FirstOrDefault(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
return match == null ? string.Empty : match.Value;
}
}
internal sealed class HostRecord
{
public string Category { get; set; }
public string Name { get; set; }
public string Server { get; set; }
public string Status { get; set; }
public string Type { get; set; }
public string WindowsGroup { get; set; }
public string Is32BitOnly { get; set; }
public string Trusted { get; set; }
public string AdapterName { get; set; }
}
internal sealed class CoverageRecord
{
public string DataSource { get; set; }
public string Status { get; set; }
public int RowCount { get; set; }
public string Required { get; set; }
public string Message { get; set; }
} }
internal sealed class Finding internal sealed class Finding
@@ -118,21 +65,62 @@ namespace BizTalkApplicationCatalog.Models
public long DurationMilliseconds { get; set; } public long DurationMilliseconds { get; set; }
} }
internal sealed class NameValueRecord /// <summary>
/// Kleines, transportierbares JSON-Modell für den ACC/PRD-Abgleich.
/// </summary>
[DataContract]
internal sealed class CatalogSnapshot
{ {
public NameValueRecord() public CatalogSnapshot()
{ {
SchemaVersion = 1;
Applications = new List<CatalogApplication>();
} }
public NameValueRecord(string name, string value, string source = "") [DataMember(Order = 1)]
{ public int SchemaVersion { get; set; }
Name = name ?? string.Empty;
Value = value ?? string.Empty; [DataMember(Order = 2)]
Source = source ?? string.Empty; public string EnvironmentName { get; set; }
[DataMember(Order = 3)]
public string ComputerName { get; set; }
[DataMember(Order = 4)]
public string CreatedUtc { get; set; }
[DataMember(Order = 5)]
public bool IsComplete { get; set; }
[DataMember(Order = 6)]
public List<CatalogApplication> Applications { get; set; }
} }
[DataContract]
internal sealed class CatalogApplication
{
public CatalogApplication()
{
AdapterCounts = new List<AdapterCount>();
}
[DataMember(Order = 1)]
public string Name { get; set; } public string Name { get; set; }
public string Value { get; set; }
public string Source { get; set; } [DataMember(Order = 2)]
public int EndpointCount { get; set; }
[DataMember(Order = 3)]
public List<AdapterCount> AdapterCounts { get; set; }
}
[DataContract]
internal sealed class AdapterCount
{
[DataMember(Order = 1)]
public string AdapterType { get; set; }
[DataMember(Order = 2)]
public int Count { get; set; }
} }
} }
@@ -0,0 +1,162 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Persistence
{
/// <summary>
/// Erzeugt und liest die kompakten JSON-Snapshots für den ACC/PRD-Abgleich.
/// </summary>
internal static class SnapshotStore
{
public static CatalogSnapshot FromInventory(
InventoryDocument document,
bool isComplete)
{
if (document == null) throw new ArgumentNullException("document");
var snapshot = new CatalogSnapshot
{
EnvironmentName = document.EnvironmentName,
ComputerName = document.ComputerName,
CreatedUtc = document.CompletedUtc.ToString("o"),
IsComplete = isComplete
};
foreach (var application in document.Applications)
{
var endpoints = document.Endpoints.Where(item =>
string.Equals(
item.ApplicationName,
application.Name,
StringComparison.OrdinalIgnoreCase)).ToList();
var catalogApplication = new CatalogApplication
{
Name = application.Name,
EndpointCount = endpoints.Count
};
foreach (var adapter in endpoints
.GroupBy(item => item.AdapterType, StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
{
catalogApplication.AdapterCounts.Add(new AdapterCount
{
AdapterType = adapter.Key,
Count = adapter.Count()
});
}
snapshot.Applications.Add(catalogApplication);
}
return snapshot;
}
public static void Save(CatalogSnapshot snapshot, string path)
{
if (snapshot == null) throw new ArgumentNullException("snapshot");
var fullPath = Path.GetFullPath(path);
var directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory);
var temporaryPath = fullPath + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
using (var stream = File.Create(temporaryPath))
using (var writer = JsonReaderWriterFactory.CreateJsonWriter(
stream,
Encoding.UTF8,
true,
true))
{
Serializer().WriteObject(writer, snapshot);
}
if (File.Exists(fullPath)) File.Delete(fullPath);
File.Move(temporaryPath, fullPath);
}
finally
{
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
}
}
public static CatalogSnapshot Load(string path)
{
var fullPath = Path.GetFullPath(path);
using (var stream = File.OpenRead(fullPath))
{
var snapshot = Serializer().ReadObject(stream) as CatalogSnapshot;
Validate(snapshot, fullPath);
return snapshot;
}
}
public static void ValidateForMerge(
CatalogSnapshot snapshot,
string expectedEnvironment)
{
Validate(snapshot, expectedEnvironment + "-Snapshot");
if (!snapshot.IsComplete)
{
throw new InvalidDataException(
expectedEnvironment + "-Snapshot ist unvollständig und darf nicht zusammengeführt werden.");
}
if (!string.Equals(
snapshot.EnvironmentName,
expectedEnvironment,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException(
"Erwartet wurde ein " + expectedEnvironment
+ "-Snapshot, gefunden wurde "
+ snapshot.EnvironmentName + ".");
}
}
private static DataContractJsonSerializer Serializer()
{
return new DataContractJsonSerializer(typeof(CatalogSnapshot));
}
private static void Validate(CatalogSnapshot snapshot, string source)
{
if (snapshot == null)
{
throw new InvalidDataException("Leerer JSON-Snapshot: " + source);
}
if (snapshot.SchemaVersion != 1)
{
throw new InvalidDataException(
"Nicht unterstützte Snapshot-Version in "
+ source + ": " + snapshot.SchemaVersion + ".");
}
if (string.IsNullOrWhiteSpace(snapshot.EnvironmentName))
{
throw new InvalidDataException(
"Umgebung fehlt im JSON-Snapshot: " + source);
}
if (snapshot.Applications == null)
{
throw new InvalidDataException(
"Anwendungsliste fehlt im JSON-Snapshot: " + source);
}
if (snapshot.Applications.Any(item =>
item == null
|| string.IsNullOrWhiteSpace(item.Name)
|| item.EndpointCount < 0
|| item.AdapterCounts == null
|| item.AdapterCounts.Any(adapter =>
adapter == null
|| string.IsNullOrWhiteSpace(adapter.AdapterType)
|| adapter.Count < 0)
|| item.AdapterCounts.Sum(adapter => adapter.Count) != item.EndpointCount))
{
throw new InvalidDataException(
"Ungültige Anwendungs- oder Adapterdaten im JSON-Snapshot: " + source);
}
}
}
}
+64 -80
View File
@@ -7,13 +7,11 @@ using BizTalkApplicationCatalog.Collectors;
using BizTalkApplicationCatalog.Configuration; using BizTalkApplicationCatalog.Configuration;
using BizTalkApplicationCatalog.Infrastructure; using BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models; using BizTalkApplicationCatalog.Models;
using BizTalkApplicationCatalog.Persistence;
using BizTalkApplicationCatalog.Reporting; using BizTalkApplicationCatalog.Reporting;
namespace BizTalkApplicationCatalog namespace BizTalkApplicationCatalog
{ {
/// <summary>
/// Orchestriert die read-only Erfassung, die Fortschrittsanzeige und den Excel-Export.
/// </summary>
internal static class Program internal static class Program
{ {
private static int Main(string[] args) private static int Main(string[] args)
@@ -36,30 +34,32 @@ namespace BizTalkApplicationCatalog
Console.WriteLine(CommandLineOptions.Usage()); Console.WriteLine(CommandLineOptions.Usage());
return 0; return 0;
} }
if (options.SelfTest) if (options.SelfTest) return RunSelfTest();
{
return RunSelfTest();
}
try try
{ {
Directory.CreateDirectory(options.OutputDirectory); Directory.CreateDirectory(options.OutputDirectory);
return options.MergeMode
? RunMerge(options)
: RunInventory(options);
} }
catch (Exception exception) when ( catch (Exception exception)
exception is IOException
|| exception is UnauthorizedAccessException
|| exception is ArgumentException)
{ {
Console.Error.WriteLine("FEHLER: Ausgabeordner kann nicht erstellt werden: " + exception.Message); Console.Error.WriteLine("FEHLER: " + exception);
return 2; return 2;
} }
}
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); private static int RunInventory(CommandLineOptions options)
var safeMachine = FileName(Environment.MachineName); {
var baseName = "BizTalk-Anwendungsinventar-" var timestamp = Timestamp();
+ options.EnvironmentName + "-" + safeMachine + "-" + timestamp; var baseName = "Phase1-BizTalk-Anwendungskatalog-"
+ options.EnvironmentName + "-"
+ FileName(Environment.MachineName) + "-"
+ timestamp;
var logPath = Path.Combine(options.OutputDirectory, baseName + ".log"); var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx"); var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx");
var snapshotPath = Path.Combine(options.OutputDirectory, baseName + ".json");
using (var logger = new ConsoleFileLogger(logPath)) using (var logger = new ConsoleFileLogger(logPath))
{ {
@@ -70,90 +70,66 @@ namespace BizTalkApplicationCatalog
StartedUtc = DateTime.UtcNow StartedUtc = DateTime.UtcNow
}; };
logger.Info("BEW BizTalk Application Catalog startet."); logger.Info("BEW BizTalk Application Catalog Phase 1 startet.");
logger.Info("Umgebung: " + options.EnvironmentName); logger.Info("Umgebung: " + options.EnvironmentName);
logger.Info("Server: " + Environment.MachineName); logger.Info("Erfasst werden nur Anwendungen, Adaptertypen und Endpunkte.");
logger.Info("Ausgabeordner: " + options.OutputDirectory); logger.Info("Modus: ausschließlich lesender lokaler BizTalk-WMI-Zugriff.");
logger.Info("Modus: ausschließlich lesend; Excel wird ohne Office erzeugt.");
var safe = new SafeCollector(document, logger); new SafeCollector(document, logger).Execute(
safe.Execute( "Phase-1-Anwendungen und Endpunkte",
"System und BizTalk-Gruppe",
false,
() => new SystemCollector(options).Collect(document));
safe.Execute(
"Anwendungen und Artefakte",
true, true,
() => new BizTalkWmiCollector( () => new BizTalkWmiCollector(
document, document,
logger, logger,
ReadInt("WmiTimeoutSeconds", 30), ReadInt("WmiTimeoutSeconds", 30)).Collect());
ReadInt("MaxRowsPerArtifactType", 10000)).Collect());
Evaluate(document);
document.CompletedUtc = DateTime.UtcNow; document.CompletedUtc = DateTime.UtcNow;
try var snapshot = SnapshotStore.FromInventory(
{ document,
logger.Info("Erzeuge Microsoft-Excel-Datei: " + reportPath); !document.HasRequiredFailure);
new XlsxReportWriter().Write(document, reportPath);
logger.Info("Excel-Datei erfolgreich erzeugt: " + reportPath); SnapshotStore.Save(snapshot, snapshotPath);
logger.Info("Logdatei: " + logPath); logger.Info("JSON-Snapshot erzeugt: " + snapshotPath);
} new XlsxReportWriter().Write(new[] { snapshot }, reportPath);
catch (Exception exception) logger.Info("Einblättrige Excel-Sicht erzeugt: " + reportPath);
{ logger.Info("Anwendungen: " + snapshot.Applications.Count);
logger.Error("Excel-Datei konnte nicht erzeugt werden: " + exception); logger.Info("Endpunkte: "
return 2; + snapshot.Applications.Sum(item => item.EndpointCount));
}
logger.Info("Anwendungen: " + document.Applications.Count);
logger.Info("Artefakte: " + document.Artifacts.Count);
logger.Info("Findings: " + document.Findings.Count);
if (document.HasRequiredFailure) if (document.HasRequiredFailure)
{ {
logger.Warning("Inventar wurde mit einem Fehler in einem Pflichtabschnitt erzeugt."); logger.Warning(
"Snapshot ist unvollständig und wird beim Merge abgelehnt.");
return 1; return 1;
} }
logger.Info("Inventarisierung erfolgreich abgeschlossen."); logger.Info("Lokale Inventarisierung erfolgreich abgeschlossen.");
return 0; return 0;
} }
} }
private static void Evaluate(InventoryDocument document) private static int RunMerge(CommandLineOptions options)
{ {
if (document.Applications.Count == 0) var baseName = "Phase1-BizTalk-Anwendungskatalog-ACC-PROD-"
{ + Timestamp();
document.Findings.Add(new Finding var logPath = Path.Combine(options.OutputDirectory, baseName + ".log");
{ var reportPath = Path.Combine(options.OutputDirectory, baseName + ".xlsx");
Severity = "Fehler",
Area = "Anwendungen",
Message = "Keine installierte BizTalk-Anwendung ermittelt.",
RecommendedAction = "Lokal auf dem BizTalk Server mit ausreichenden WMI-Leserechten ausführen."
});
}
var withoutApplication = document.Artifacts.Count(item => using (var logger = new ConsoleFileLogger(logPath))
string.IsNullOrWhiteSpace(item.ApplicationName));
if (withoutApplication > 0)
{ {
document.Findings.Add(new Finding logger.Info("Führe ACC- und PROD-JSON-Snapshots zusammen.");
{ var acc = SnapshotStore.Load(options.AccSnapshotPath);
Severity = "Warnung", var prod = SnapshotStore.Load(options.ProdSnapshotPath);
Area = "Artefaktzuordnung", SnapshotStore.ValidateForMerge(acc, "ACC");
Message = withoutApplication + " Artefakt(e) besitzen keine von WMI gelieferte Anwendungszuordnung.", SnapshotStore.ValidateForMerge(prod, "PROD");
RecommendedAction = "Artefakte im Blatt 'Artefakte' prüfen und bei Bedarf mit der BizTalk Administration Console abgleichen." new XlsxReportWriter().Write(new[] { acc, prod }, reportPath);
}); logger.Info("Gemeinsame Phase-1-Excel-Sicht erzeugt: " + reportPath);
} logger.Info("Anwendungen gesamt: "
+ acc.Applications.Select(item => item.Name)
if (document.Artifacts.Count == 0) .Concat(prod.Applications.Select(item => item.Name))
{ .Distinct(StringComparer.OrdinalIgnoreCase)
document.Findings.Add(new Finding .Count());
{ return 0;
Severity = "Warnung",
Area = "Artefakte",
Message = "Keine Artefaktdetails wurden ermittelt; die Anwendungsliste kann dennoch vollständig sein.",
RecommendedAction = "Blatt 'Abdeckung' sowie WMI-Klassen und Berechtigungen prüfen."
});
} }
} }
@@ -162,7 +138,8 @@ namespace BizTalkApplicationCatalog
try try
{ {
SelfTestRunner.Run(); SelfTestRunner.Run();
Console.WriteLine("Self-Test erfolgreich: XLSX-Paket, Tabellen und Secret-Redaktion sind gültig."); Console.WriteLine(
"Self-Test erfolgreich: JSON, ACC/PRD-Merge und einblättrige XLSX sind gültig.");
return 0; return 0;
} }
catch (Exception exception) catch (Exception exception)
@@ -184,6 +161,13 @@ namespace BizTalkApplicationCatalog
: fallback; : fallback;
} }
private static string Timestamp()
{
return DateTime.Now.ToString(
"yyyyMMdd-HHmmss",
CultureInfo.InvariantCulture);
}
private static string FileName(string value) private static string FileName(string value)
{ {
var invalid = Path.GetInvalidFileNameChars(); var invalid = Path.GetInvalidFileNameChars();
@@ -3,12 +3,12 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
[assembly: AssemblyTitle("BEW BizTalk Application Catalog")] [assembly: AssemblyTitle("BEW BizTalk Application Catalog")]
[assembly: AssemblyDescription("Read-only BizTalk 2020 application inventory with Microsoft Excel output")] [assembly: AssemblyDescription("Read-only BizTalk 2020 Phase-1 application and endpoint catalog")]
[assembly: AssemblyCompany("JR IT Services")] [assembly: AssemblyCompany("JR IT Services")]
[assembly: AssemblyProduct("BEW BizTalk Application Catalog")] [assembly: AssemblyProduct("BEW BizTalk Application Catalog")]
[assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
[assembly: Guid("41cb5701-3fbc-49f4-856a-6ae930b8513d")] [assembly: Guid("41cb5701-3fbc-49f4-856a-6ae930b8513d")]
[assembly: InternalsVisibleTo("BizTalkApplicationCatalog.Tests")] [assembly: InternalsVisibleTo("BizTalkApplicationCatalog.Tests")]
[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyVersion("2.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyFileVersion("2.0.0.0")]
@@ -6,14 +6,12 @@ using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using BizTalkApplicationCatalog.Infrastructure;
using BizTalkApplicationCatalog.Models; using BizTalkApplicationCatalog.Models;
namespace BizTalkApplicationCatalog.Reporting namespace BizTalkApplicationCatalog.Reporting
{ {
/// <summary> /// <summary>
/// Erzeugt eine filterbare Microsoft-Excel-Arbeitsmappe direkt als Office Open XML. /// Erzeugt genau ein Excel-Blatt für die Phase-1-Anwendungssicht.
/// Excel oder eine Office-Interop-Installation werden nicht benötigt.
/// </summary> /// </summary>
internal sealed class XlsxReportWriter internal sealed class XlsxReportWriter
{ {
@@ -22,13 +20,19 @@ namespace BizTalkApplicationCatalog.Reporting
private const string RelationshipsNamespace = private const string RelationshipsNamespace =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"; "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
/// <summary> public void Write(IEnumerable<CatalogSnapshot> source, string outputPath)
/// Schreibt atomar: Erst nach erfolgreichem Abschluss ersetzt die temporäre Datei das Ziel.
/// </summary>
public void Write(InventoryDocument document, string outputPath)
{ {
if (document == null) throw new ArgumentNullException("document"); if (source == null) throw new ArgumentNullException("source");
if (string.IsNullOrWhiteSpace(outputPath)) throw new ArgumentException("Ausgabepfad fehlt."); if (string.IsNullOrWhiteSpace(outputPath))
{
throw new ArgumentException("Ausgabepfad fehlt.");
}
var snapshots = source.ToList();
if (snapshots.Count == 0)
{
throw new ArgumentException("Mindestens ein Snapshot ist erforderlich.");
}
var fullPath = Path.GetFullPath(outputPath); var fullPath = Path.GetFullPath(outputPath);
var directory = Path.GetDirectoryName(fullPath); var directory = Path.GetDirectoryName(fullPath);
@@ -37,20 +41,17 @@ namespace BizTalkApplicationCatalog.Reporting
try try
{ {
var sheets = BuildSheets(document); var sheet = BuildPhaseOneSheet(snapshots);
using (var archive = ZipFile.Open(temporaryPath, ZipArchiveMode.Create)) using (var archive = ZipFile.Open(temporaryPath, ZipArchiveMode.Create))
{ {
WriteContentTypes(archive, sheets.Count); WriteContentTypes(archive);
WritePackageRelationships(archive); WritePackageRelationships(archive);
WriteCoreProperties(archive, document); WriteCoreProperties(archive, snapshots);
WriteApplicationProperties(archive); WriteApplicationProperties(archive);
WriteWorkbook(archive, sheets); WriteWorkbook(archive, sheet);
WriteWorkbookRelationships(archive, sheets.Count); WriteWorkbookRelationships(archive);
WriteStyles(archive); WriteStyles(archive);
for (var index = 0; index < sheets.Count; index++) WriteWorksheet(archive, sheet);
{
WriteWorksheet(archive, index + 1, sheets[index]);
}
} }
if (File.Exists(fullPath)) File.Delete(fullPath); if (File.Exists(fullPath)) File.Delete(fullPath);
@@ -62,296 +63,224 @@ namespace BizTalkApplicationCatalog.Reporting
} }
} }
internal static List<SheetDefinition> BuildSheets(InventoryDocument document) internal static SheetDefinition BuildPhaseOneSheet(
{ IList<CatalogSnapshot> snapshots)
return new List<SheetDefinition>
{
BuildOverview(document),
BuildApplications(document),
BuildArtifacts("Artefakte", document.Artifacts),
BuildArtifacts("Ports", document.Artifacts.Where(item =>
item.Type == "Send Port"
|| item.Type == "Send Port Group"
|| item.Type == "Receive Port"
|| item.Type == "Receive Location")),
BuildArtifacts("Orchestrierungen", document.Artifacts.Where(item =>
item.Type == "Orchestrierung")),
BuildArtifacts("Schemas-Maps-Pipelines", document.Artifacts.Where(item =>
item.Type == "Schema" || item.Type == "Map" || item.Type == "Pipeline")),
BuildArtifacts("Assemblies", document.Artifacts.Where(item =>
item.Type == "Assembly")),
BuildHosts(document),
BuildCoverage(document),
BuildFindings(document)
};
}
private static SheetDefinition BuildOverview(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[] { "BEW BizTalk Application Catalog", "" },
new object[] { "Umgebung", document.EnvironmentName },
new object[] { "BizTalk Server", document.ComputerName },
new object[] { "Erzeugt (lokal)", document.CompletedUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) },
new object[] { "Toolversion", document.ToolVersion },
new object[] { "Management SQL Server", Empty(document.ManagementServer) },
new object[] { "Management Database", Empty(document.ManagementDatabase) },
new object[] { "Anwendungen gesamt", document.Applications.Count },
new object[] { "Artefakte gesamt", document.Artifacts.Count },
new object[] { "Findings", document.Findings.Count },
new object[] { "", "" },
new object[] { "Artefakttyp", "Anzahl" }
};
foreach (var group in document.Artifacts
.GroupBy(item => item.Type)
.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
{
rows.Add(new object[] { group.Key, group.Count() });
}
rows.Add(new object[] { "", "" });
rows.Add(new object[] { "Systemparameter", "Wert", "Quelle" });
foreach (var property in document.SystemProperties)
{
rows.Add(new object[] { property.Name, property.Value, property.Source });
}
rows.Add(new object[] { "", "" });
rows.Add(new object[] { "Erfassungsabschnitt", "Status", "Pflicht", "Dauer (ms)", "Meldung" });
foreach (var section in document.SectionStatuses)
{
rows.Add(new object[]
{
section.Name,
section.Status,
section.Required ? "Ja" : "Nein",
section.DurationMilliseconds,
section.Message
});
}
return new SheetDefinition("Übersicht", rows, false, 0);
}
private static SheetDefinition BuildApplications(InventoryDocument document)
{
var headers = new object[]
{
"Umgebung", "Server", "Anwendung", "Status", "Standard", "Beschreibung",
"Artefakte gesamt", "Orchestrierungen", "Send Ports", "Send Port Groups",
"Receive Ports", "Receive Locations", "Assemblies", "Schemas", "Maps",
"Pipelines", "Hosts", "Adapter", "Detailabdeckung"
};
var rows = new List<object[]> { headers };
var coverage = ArtifactCoverage(document);
foreach (var application in document.Applications)
{
var artifacts = document.Artifacts.Where(item =>
string.Equals(item.ApplicationName, application.Name, StringComparison.OrdinalIgnoreCase)).ToList();
rows.Add(new object[]
{
document.EnvironmentName,
document.ComputerName,
application.Name,
application.Status,
application.IsDefault,
application.Description,
artifacts.Count,
Count(artifacts, "Orchestrierung"),
Count(artifacts, "Send Port"),
Count(artifacts, "Send Port Group"),
Count(artifacts, "Receive Port"),
Count(artifacts, "Receive Location"),
Count(artifacts, "Assembly"),
Count(artifacts, "Schema"),
Count(artifacts, "Map"),
Count(artifacts, "Pipeline"),
JoinDistinct(artifacts.Select(item => item.HostName)),
JoinDistinct(artifacts.Select(item => item.AdapterName)),
coverage
});
}
return new SheetDefinition("Anwendungen", rows, true, 1);
}
private static SheetDefinition BuildArtifacts(
string sheetName,
IEnumerable<ArtifactRecord> source)
{ {
var acc = snapshots.FirstOrDefault(item =>
string.Equals(item.EnvironmentName, "ACC", StringComparison.OrdinalIgnoreCase));
var prod = snapshots.FirstOrDefault(item =>
string.Equals(item.EnvironmentName, "PROD", StringComparison.OrdinalIgnoreCase));
var rows = new List<object[]> var rows = new List<object[]>
{ {
new object[] new object[]
{ {
"Anwendung", "Typ", "Name", "Status", "Host/Handler", "Adapter", snapshots.Count > 1
"Übergeordnet", "Adresse", "Beschreibung", "Two-Way", "Dynamisch", ? "Phase 1 BizTalk Anwendungen & Adapter (Quelle: ACC/PROD JSON-Snapshots)"
"Deaktiviert", "Receive Pipeline", "Send Pipeline", "Secondary Adapter", : "Phase 1 BizTalk Anwendungen & Adapter (Quelle: lokaler BizTalk-WMI-Provider)",
"Secondary Adresse", "Assembly/FullName", "Namespace", "Root", "Tracking" "", "", "", "", "", ""
} },
};
foreach (var item in source)
{
rows.Add(new object[]
{
item.ApplicationName,
item.Type,
item.Name,
item.Status,
item.HostName,
item.AdapterName,
item.ParentName,
item.Address,
item.Property("Description"),
item.Property("IsTwoWay"),
item.Property("IsDynamic"),
item.Property("IsDisabled"),
item.Property("ReceivePipeline"),
item.Property("SendPipeline"),
item.Property("STTransportType"),
SensitiveDataSanitizer.Sanitize(item.Property("STAddress")),
FirstNonEmpty(item.Property("AssemblyName"), item.Property("FullName")),
item.Property("TargetNameSpace"),
item.Property("RootName"),
item.Property("Tracking")
});
}
return new SheetDefinition(sheetName, rows, true, 1);
}
private static SheetDefinition BuildHosts(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[] new object[]
{ {
"Kategorie", "Name", "Server", "Status", "Typ", "Windows-Gruppe", "BizTalk-Anwendung",
"Nur 32 Bit", "Vertrauenswürdig", "Adapter" "Umgebung",
"Adapter-Typ",
"Anzahl Endpunkte",
"BizTalk-Anwendung in ACC",
"BizTalk-Anwendung in PRD",
"Hinweis"
} }
}; };
foreach (var item in document.Hosts)
{
rows.Add(new object[]
{
item.Category, item.Name, item.Server, item.Status, item.Type,
item.WindowsGroup, item.Is32BitOnly, item.Trusted, item.AdapterName
});
}
return new SheetDefinition("Hosts-Handler", rows, true, 1);
}
private static SheetDefinition BuildCoverage(InventoryDocument document) var names = snapshots
{ .SelectMany(item => item.Applications)
var rows = new List<object[]> .Select(item => item.Name)
{
new object[] { "Datenquelle", "Status", "Zeilen", "Pflicht", "Meldung" }
};
foreach (var item in document.Coverage)
{
rows.Add(new object[]
{
item.DataSource, item.Status, item.RowCount, item.Required, item.Message
});
}
return new SheetDefinition("Abdeckung", rows, true, 1);
}
private static SheetDefinition BuildFindings(InventoryDocument document)
{
var rows = new List<object[]>
{
new object[] { "Schweregrad", "Bereich", "Feststellung", "Empfohlene Aktion" }
};
foreach (var item in document.Findings)
{
rows.Add(new object[]
{
item.Severity, item.Area, item.Message, item.RecommendedAction
});
}
if (document.Findings.Count == 0)
{
rows.Add(new object[] { "Information", "Gesamt", "Keine Findings.", "" });
}
return new SheetDefinition("Findings", rows, true, 1);
}
private static string ArtifactCoverage(InventoryDocument document)
{
var incomplete = document.Coverage
.Where(item => item.DataSource.StartsWith("MSBTS_", StringComparison.Ordinal)
&& item.DataSource != "MSBTS_Application"
&& item.Status != "Vollständig")
.Select(item => item.DataSource + ": " + item.Status)
.ToList();
return incomplete.Count == 0
? "Vollständig"
: "Teilweise siehe Abdeckung: " + string.Join(", ", incomplete);
}
private static int Count(IEnumerable<ArtifactRecord> artifacts, string type)
{
return artifacts.Count(item => item.Type == type);
}
private static string JoinDistinct(IEnumerable<string> values)
{
return string.Join(
", ",
values.Where(item => !string.IsNullOrWhiteSpace(item))
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(item => item, StringComparer.OrdinalIgnoreCase)); .OrderBy(item => item, StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var name in names)
{
var accApplication = Find(acc, name);
var prodApplication = Find(prod, name);
var environments = snapshots
.Where(snapshot => Find(snapshot, name) != null)
.Select(snapshot => snapshot.EnvironmentName)
.OrderBy(EnvironmentOrder)
.ToList();
rows.Add(new object[]
{
CanonicalName(accApplication, prodApplication, name),
EnvironmentText(accApplication, prodApplication, environments),
AdapterText(accApplication, prodApplication, environments),
environments.Sum(environment =>
{
var snapshot = snapshots.First(item =>
string.Equals(
item.EnvironmentName,
environment,
StringComparison.OrdinalIgnoreCase));
return Find(snapshot, name).EndpointCount;
}),
accApplication == null ? "" : "✓",
prodApplication == null ? "" : "✓",
Hint(accApplication, prodApplication, environments)
});
} }
private static string FirstNonEmpty(params string[] values) var accApplications = acc == null ? 0 : acc.Applications.Count;
var prodApplications = prod == null ? 0 : prod.Applications.Count;
var accEndpoints = acc == null ? 0 : acc.Applications.Sum(item => item.EndpointCount);
var prodEndpoints = prod == null ? 0 : prod.Applications.Sum(item => item.EndpointCount);
rows.Add(new object[]
{ {
return values.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item)) ?? string.Empty; "Gesamt: " + names.Count
+ " BizTalk-Anwendungen | ACC: " + accApplications
+ " | PRD: " + prodApplications
+ " | ACC Endpunkte: " + accEndpoints
+ " | PRD Endpunkte: " + prodEndpoints,
"", "", "", "", "", ""
});
return new SheetDefinition(
"Phase 1",
rows,
2,
2,
rows.Count - 1,
new[] { 34d, 14d, 28d, 18d, 28d, 28d, 42d });
} }
private static string Empty(string value) private static CatalogApplication Find(
CatalogSnapshot snapshot,
string applicationName)
{ {
return string.IsNullOrWhiteSpace(value) ? "Nicht ermittelt" : value; return snapshot == null
? null
: snapshot.Applications.FirstOrDefault(item =>
string.Equals(
item.Name,
applicationName,
StringComparison.OrdinalIgnoreCase));
} }
private static void WriteWorksheet(ZipArchive archive, int sheetNumber, SheetDefinition sheet) private static string CanonicalName(
CatalogApplication acc,
CatalogApplication prod,
string fallback)
{ {
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet" + sheetNumber + ".xml")) if (acc != null) return acc.Name;
if (prod != null) return prod.Name;
return fallback;
}
private static string EnvironmentText(
CatalogApplication acc,
CatalogApplication prod,
IList<string> environments)
{
if (acc != null && prod != null) return "ACC + PRD";
if (acc != null) return "nur ACC";
if (prod != null) return "nur PRD";
return environments.Count == 0 ? "" : string.Join(" + ", environments);
}
private static string AdapterText(
CatalogApplication acc,
CatalogApplication prod,
IList<string> environments)
{
var accText = AdapterCounts(acc);
var prodText = AdapterCounts(prod);
if (acc != null && prod != null)
{
return string.Equals(accText, prodText, StringComparison.OrdinalIgnoreCase)
? accText
: "ACC: " + accText + " | PRD: " + prodText;
}
if (acc != null) return accText;
if (prod != null) return prodText;
return environments.Count == 0 ? "" : "Unbekannt";
}
private static string AdapterCounts(CatalogApplication application)
{
if (application == null || application.AdapterCounts.Count == 0) return "";
return string.Join(
" / ",
application.AdapterCounts
.OrderBy(item => item.AdapterType, StringComparer.OrdinalIgnoreCase)
.Select(item => item.AdapterType + " (" + item.Count + "x)"));
}
private static string Hint(
CatalogApplication acc,
CatalogApplication prod,
IList<string> environments)
{
if (acc != null && prod == null) return "Nur in ACC vorhanden";
if (prod != null && acc == null) return "Nur in PRD vorhanden nicht in ACC";
if (environments.Count == 1)
{
return "Nur in " + environments[0] + " vorhanden";
}
return string.Empty;
}
private static int EnvironmentOrder(string environment)
{
if (string.Equals(environment, "ACC", StringComparison.OrdinalIgnoreCase)) return 0;
if (string.Equals(environment, "PROD", StringComparison.OrdinalIgnoreCase)) return 1;
return 2;
}
private static void WriteWorksheet(ZipArchive archive, SheetDefinition sheet)
{
using (var writer = CreateXmlWriter(archive, "xl/worksheets/sheet1.xml"))
{ {
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("worksheet", SpreadsheetNamespace); writer.WriteStartElement("worksheet", SpreadsheetNamespace);
writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace); writer.WriteAttributeString("xmlns", "r", null, RelationshipsNamespace);
WriteSheetViews(writer, sheet.FreezeRows); WriteSheetViews(writer, sheet.FreezeRows);
WriteColumns(writer, sheet); WriteColumns(writer, sheet.ColumnWidths);
writer.WriteStartElement("sheetData", SpreadsheetNamespace); writer.WriteStartElement("sheetData", SpreadsheetNamespace);
for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++) for (var rowIndex = 0; rowIndex < sheet.Rows.Count; rowIndex++)
{ {
writer.WriteStartElement("row", SpreadsheetNamespace); writer.WriteStartElement("row", SpreadsheetNamespace);
writer.WriteAttributeString("r", (rowIndex + 1).ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString(
for (var columnIndex = 0; columnIndex < sheet.Rows[rowIndex].Length; columnIndex++) "r",
(rowIndex + 1).ToString(CultureInfo.InvariantCulture));
for (var columnIndex = 0;
columnIndex < sheet.Rows[rowIndex].Length;
columnIndex++)
{ {
var style = RowStyle(sheet, rowIndex);
WriteCell( WriteCell(
writer, writer,
columnIndex + 1, columnIndex + 1,
rowIndex + 1, rowIndex + 1,
sheet.Rows[rowIndex][columnIndex], sheet.Rows[rowIndex][columnIndex],
style); RowStyle(sheet, rowIndex));
} }
writer.WriteEndElement(); writer.WriteEndElement();
} }
writer.WriteEndElement(); writer.WriteEndElement();
if (sheet.AutoFilter && sheet.Rows.Count > 0 && sheet.MaximumColumns > 0)
{
writer.WriteStartElement("autoFilter", SpreadsheetNamespace); writer.WriteStartElement("autoFilter", SpreadsheetNamespace);
writer.WriteAttributeString( writer.WriteAttributeString(
"ref", "ref",
"A1:" + ColumnName(sheet.MaximumColumns) + sheet.Rows.Count); "A" + sheet.FilterHeaderRow
+ ":G" + (sheet.FooterRowIndex + 1 - 1));
writer.WriteEndElement();
writer.WriteStartElement("mergeCells", SpreadsheetNamespace);
writer.WriteAttributeString("count", "2");
WriteMergedCell(writer, "A1:G1");
WriteMergedCell(
writer,
"A" + (sheet.FooterRowIndex + 1)
+ ":G" + (sheet.FooterRowIndex + 1));
writer.WriteEndElement(); writer.WriteEndElement();
}
writer.WriteStartElement("pageMargins", SpreadsheetNamespace); writer.WriteStartElement("pageMargins", SpreadsheetNamespace);
writer.WriteAttributeString("left", "0.25"); writer.WriteAttributeString("left", "0.25");
@@ -366,54 +295,47 @@ namespace BizTalkApplicationCatalog.Reporting
} }
} }
private static void WriteMergedCell(XmlWriter writer, string range)
{
writer.WriteStartElement("mergeCell", SpreadsheetNamespace);
writer.WriteAttributeString("ref", range);
writer.WriteEndElement();
}
private static int RowStyle(SheetDefinition sheet, int rowIndex) private static int RowStyle(SheetDefinition sheet, int rowIndex)
{
if (sheet.Name == "Übersicht")
{ {
if (rowIndex == 0) return 2; if (rowIndex == 0) return 2;
var first = Convert.ToString(sheet.Rows[rowIndex].FirstOrDefault(), CultureInfo.InvariantCulture); if (rowIndex == 1) return 1;
if (first == "Artefakttyp" if (rowIndex == sheet.FooterRowIndex) return 3;
|| first == "Systemparameter"
|| first == "Erfassungsabschnitt") return 1;
return 0; return 0;
} }
return rowIndex == 0 ? 1 : 0;
}
private static void WriteSheetViews(XmlWriter writer, int freezeRows) private static void WriteSheetViews(XmlWriter writer, int freezeRows)
{ {
writer.WriteStartElement("sheetViews", SpreadsheetNamespace); writer.WriteStartElement("sheetViews", SpreadsheetNamespace);
writer.WriteStartElement("sheetView", SpreadsheetNamespace); writer.WriteStartElement("sheetView", SpreadsheetNamespace);
writer.WriteAttributeString("workbookViewId", "0"); writer.WriteAttributeString("workbookViewId", "0");
if (freezeRows > 0)
{
writer.WriteStartElement("pane", SpreadsheetNamespace); writer.WriteStartElement("pane", SpreadsheetNamespace);
writer.WriteAttributeString("ySplit", freezeRows.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString(
"ySplit",
freezeRows.ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("topLeftCell", "A" + (freezeRows + 1)); writer.WriteAttributeString("topLeftCell", "A" + (freezeRows + 1));
writer.WriteAttributeString("activePane", "bottomLeft"); writer.WriteAttributeString("activePane", "bottomLeft");
writer.WriteAttributeString("state", "frozen"); writer.WriteAttributeString("state", "frozen");
writer.WriteEndElement(); writer.WriteEndElement();
}
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndElement(); writer.WriteEndElement();
} }
private static void WriteColumns(XmlWriter writer, SheetDefinition sheet) private static void WriteColumns(XmlWriter writer, double[] widths)
{ {
writer.WriteStartElement("cols", SpreadsheetNamespace); writer.WriteStartElement("cols", SpreadsheetNamespace);
for (var columnIndex = 0; columnIndex < sheet.MaximumColumns; columnIndex++) for (var index = 0; index < widths.Length; index++)
{ {
var width = 10;
foreach (var row in sheet.Rows)
{
if (columnIndex >= row.Length) continue;
var length = Convert.ToString(row[columnIndex], CultureInfo.InvariantCulture).Length + 2;
width = Math.Max(width, Math.Min(60, length));
}
writer.WriteStartElement("col", SpreadsheetNamespace); writer.WriteStartElement("col", SpreadsheetNamespace);
writer.WriteAttributeString("min", (columnIndex + 1).ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("min", (index + 1).ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("max", (columnIndex + 1).ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("max", (index + 1).ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("width", width.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("width", widths[index].ToString(CultureInfo.InvariantCulture));
writer.WriteAttributeString("customWidth", "1"); writer.WriteAttributeString("customWidth", "1");
writer.WriteEndElement(); writer.WriteEndElement();
} }
@@ -429,7 +351,10 @@ namespace BizTalkApplicationCatalog.Reporting
{ {
writer.WriteStartElement("c", SpreadsheetNamespace); writer.WriteStartElement("c", SpreadsheetNamespace);
writer.WriteAttributeString("r", ColumnName(column) + row); writer.WriteAttributeString("r", ColumnName(column) + row);
if (style > 0) writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture)); if (style > 0)
{
writer.WriteAttributeString("s", style.ToString(CultureInfo.InvariantCulture));
}
if (IsNumber(value)) if (IsNumber(value))
{ {
writer.WriteStartElement("v", SpreadsheetNamespace); writer.WriteStartElement("v", SpreadsheetNamespace);
@@ -442,7 +367,8 @@ namespace BizTalkApplicationCatalog.Reporting
writer.WriteStartElement("is", SpreadsheetNamespace); writer.WriteStartElement("is", SpreadsheetNamespace);
writer.WriteStartElement("t", SpreadsheetNamespace); writer.WriteStartElement("t", SpreadsheetNamespace);
writer.WriteAttributeString("xml", "space", null, "preserve"); writer.WriteAttributeString("xml", "space", null, "preserve");
writer.WriteString(ExcelText(Convert.ToString(value, CultureInfo.InvariantCulture))); writer.WriteString(ExcelText(
Convert.ToString(value, CultureInfo.InvariantCulture)));
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndElement(); writer.WriteEndElement();
} }
@@ -479,25 +405,21 @@ namespace BizTalkApplicationCatalog.Reporting
return result; return result;
} }
private static void WriteContentTypes(ZipArchive archive, int sheetCount) private static void WriteContentTypes(ZipArchive archive)
{ {
using (var writer = CreateXmlWriter(archive, "[Content_Types].xml")) using (var writer = CreateXmlWriter(archive, "[Content_Types].xml"))
{ {
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types"); writer.WriteStartElement(
"Types",
"http://schemas.openxmlformats.org/package/2006/content-types");
WriteDefault(writer, "rels", "application/vnd.openxmlformats-package.relationships+xml"); WriteDefault(writer, "rels", "application/vnd.openxmlformats-package.relationships+xml");
WriteDefault(writer, "xml", "application/xml"); WriteDefault(writer, "xml", "application/xml");
WriteOverride(writer, "/xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"); WriteOverride(writer, "/xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
WriteOverride(writer, "/xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"); WriteOverride(writer, "/xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
WriteOverride(writer, "/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml"); WriteOverride(writer, "/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml");
WriteOverride(writer, "/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml"); WriteOverride(writer, "/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml");
for (var index = 1; index <= sheetCount; index++) WriteOverride(writer, "/xl/worksheets/sheet1.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
{
WriteOverride(
writer,
"/xl/worksheets/sheet" + index + ".xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
}
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndDocument(); writer.WriteEndDocument();
} }
@@ -524,7 +446,9 @@ namespace BizTalkApplicationCatalog.Reporting
using (var writer = CreateXmlWriter(archive, "_rels/.rels")) using (var writer = CreateXmlWriter(archive, "_rels/.rels"))
{ {
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships"); writer.WriteStartElement(
"Relationships",
"http://schemas.openxmlformats.org/package/2006/relationships");
WriteRelationship(writer, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", "xl/workbook.xml"); WriteRelationship(writer, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", "xl/workbook.xml");
WriteRelationship(writer, "rId2", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", "docProps/core.xml"); WriteRelationship(writer, "rId2", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", "docProps/core.xml");
WriteRelationship(writer, "rId3", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", "docProps/app.xml"); WriteRelationship(writer, "rId3", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", "docProps/app.xml");
@@ -533,7 +457,9 @@ namespace BizTalkApplicationCatalog.Reporting
} }
} }
private static void WriteWorkbook(ZipArchive archive, List<SheetDefinition> sheets) private static void WriteWorkbook(
ZipArchive archive,
SheetDefinition sheet)
{ {
using (var writer = CreateXmlWriter(archive, "xl/workbook.xml")) using (var writer = CreateXmlWriter(archive, "xl/workbook.xml"))
{ {
@@ -546,45 +472,39 @@ namespace BizTalkApplicationCatalog.Reporting
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteStartElement("sheets", SpreadsheetNamespace); writer.WriteStartElement("sheets", SpreadsheetNamespace);
for (var index = 0; index < sheets.Count; index++)
{
writer.WriteStartElement("sheet", SpreadsheetNamespace); writer.WriteStartElement("sheet", SpreadsheetNamespace);
writer.WriteAttributeString("name", sheets[index].Name); writer.WriteAttributeString("name", sheet.Name);
writer.WriteAttributeString("sheetId", (index + 1).ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("sheetId", "1");
writer.WriteAttributeString("r", "id", RelationshipsNamespace, "rId" + (index + 1)); writer.WriteAttributeString("r", "id", RelationshipsNamespace, "rId1");
writer.WriteEndElement(); writer.WriteEndElement();
}
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndDocument(); writer.WriteEndDocument();
} }
} }
private static void WriteWorkbookRelationships(ZipArchive archive, int sheetCount) private static void WriteWorkbookRelationships(ZipArchive archive)
{ {
using (var writer = CreateXmlWriter(archive, "xl/_rels/workbook.xml.rels")) using (var writer = CreateXmlWriter(
archive,
"xl/_rels/workbook.xml.rels"))
{ {
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships"); writer.WriteStartElement(
for (var index = 1; index <= sheetCount; index++) "Relationships",
{ "http://schemas.openxmlformats.org/package/2006/relationships");
WriteRelationship( WriteRelationship(writer, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet", "worksheets/sheet1.xml");
writer, WriteRelationship(writer, "rId2", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", "styles.xml");
"rId" + index,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
"worksheets/sheet" + index + ".xml");
}
WriteRelationship(
writer,
"rId" + (sheetCount + 1),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"styles.xml");
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndDocument(); writer.WriteEndDocument();
} }
} }
private static void WriteRelationship(XmlWriter writer, string id, string type, string target) private static void WriteRelationship(
XmlWriter writer,
string id,
string type,
string target)
{ {
writer.WriteStartElement("Relationship"); writer.WriteStartElement("Relationship");
writer.WriteAttributeString("Id", id); writer.WriteAttributeString("Id", id);
@@ -603,14 +523,14 @@ namespace BizTalkApplicationCatalog.Reporting
writer.WriteAttributeString("count", "3"); writer.WriteAttributeString("count", "3");
WriteFont(writer, false, "000000", 10); WriteFont(writer, false, "000000", 10);
WriteFont(writer, true, "FFFFFF", 10); WriteFont(writer, true, "FFFFFF", 10);
WriteFont(writer, true, "FFFFFF", 16); WriteFont(writer, true, "FFFFFF", 14);
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteStartElement("fills", SpreadsheetNamespace); writer.WriteStartElement("fills", SpreadsheetNamespace);
writer.WriteAttributeString("count", "4"); writer.WriteAttributeString("count", "4");
WritePatternFill(writer, "none", null); WritePatternFill(writer, "none", null);
WritePatternFill(writer, "gray125", null); WritePatternFill(writer, "gray125", null);
WritePatternFill(writer, "solid", "1F4E78"); WritePatternFill(writer, "solid", "33475B");
WritePatternFill(writer, "solid", "2F75B5"); WritePatternFill(writer, "solid", "005A8B");
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteStartElement("borders", SpreadsheetNamespace); writer.WriteStartElement("borders", SpreadsheetNamespace);
writer.WriteAttributeString("count", "2"); writer.WriteAttributeString("count", "2");
@@ -619,13 +539,14 @@ namespace BizTalkApplicationCatalog.Reporting
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteStartElement("cellStyleXfs", SpreadsheetNamespace); writer.WriteStartElement("cellStyleXfs", SpreadsheetNamespace);
writer.WriteAttributeString("count", "1"); writer.WriteAttributeString("count", "1");
WriteXf(writer, 0, 0, 0, false); WriteXf(writer, 0, 0, 0);
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteStartElement("cellXfs", SpreadsheetNamespace); writer.WriteStartElement("cellXfs", SpreadsheetNamespace);
writer.WriteAttributeString("count", "3"); writer.WriteAttributeString("count", "4");
WriteXf(writer, 0, 0, 0, true); WriteXf(writer, 0, 0, 0);
WriteXf(writer, 1, 2, 1, true); WriteXf(writer, 1, 2, 1);
WriteXf(writer, 2, 3, 1, true); WriteXf(writer, 2, 3, 1);
WriteXf(writer, 1, 2, 1);
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteStartElement("cellStyles", SpreadsheetNamespace); writer.WriteStartElement("cellStyles", SpreadsheetNamespace);
writer.WriteAttributeString("count", "1"); writer.WriteAttributeString("count", "1");
@@ -640,7 +561,11 @@ namespace BizTalkApplicationCatalog.Reporting
} }
} }
private static void WriteFont(XmlWriter writer, bool bold, string color, int size) private static void WriteFont(
XmlWriter writer,
bool bold,
string color,
int size)
{ {
writer.WriteStartElement("font", SpreadsheetNamespace); writer.WriteStartElement("font", SpreadsheetNamespace);
if (bold) writer.WriteElementString("b", SpreadsheetNamespace, string.Empty); if (bold) writer.WriteElementString("b", SpreadsheetNamespace, string.Empty);
@@ -656,7 +581,10 @@ namespace BizTalkApplicationCatalog.Reporting
writer.WriteEndElement(); writer.WriteEndElement();
} }
private static void WritePatternFill(XmlWriter writer, string pattern, string color) private static void WritePatternFill(
XmlWriter writer,
string pattern,
string color)
{ {
writer.WriteStartElement("fill", SpreadsheetNamespace); writer.WriteStartElement("fill", SpreadsheetNamespace);
writer.WriteStartElement("patternFill", SpreadsheetNamespace); writer.WriteStartElement("patternFill", SpreadsheetNamespace);
@@ -697,8 +625,7 @@ namespace BizTalkApplicationCatalog.Reporting
XmlWriter writer, XmlWriter writer,
int fontId, int fontId,
int fillId, int fillId,
int borderId, int borderId)
bool alignment)
{ {
writer.WriteStartElement("xf", SpreadsheetNamespace); writer.WriteStartElement("xf", SpreadsheetNamespace);
writer.WriteAttributeString("numFmtId", "0"); writer.WriteAttributeString("numFmtId", "0");
@@ -709,32 +636,33 @@ namespace BizTalkApplicationCatalog.Reporting
if (fontId > 0) writer.WriteAttributeString("applyFont", "1"); if (fontId > 0) writer.WriteAttributeString("applyFont", "1");
if (fillId > 0) writer.WriteAttributeString("applyFill", "1"); if (fillId > 0) writer.WriteAttributeString("applyFill", "1");
if (borderId > 0) writer.WriteAttributeString("applyBorder", "1"); if (borderId > 0) writer.WriteAttributeString("applyBorder", "1");
if (alignment)
{
writer.WriteAttributeString("applyAlignment", "1"); writer.WriteAttributeString("applyAlignment", "1");
writer.WriteStartElement("alignment", SpreadsheetNamespace); writer.WriteStartElement("alignment", SpreadsheetNamespace);
writer.WriteAttributeString("vertical", "top"); writer.WriteAttributeString("vertical", "top");
writer.WriteAttributeString("wrapText", "1"); writer.WriteAttributeString("wrapText", "1");
writer.WriteEndElement(); writer.WriteEndElement();
}
writer.WriteEndElement(); writer.WriteEndElement();
} }
private static void WriteCoreProperties(ZipArchive archive, InventoryDocument document) private static void WriteCoreProperties(
ZipArchive archive,
IList<CatalogSnapshot> snapshots)
{ {
using (var writer = CreateXmlWriter(archive, "docProps/core.xml")) using (var writer = CreateXmlWriter(archive, "docProps/core.xml"))
{ {
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("cp", "coreProperties", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"); writer.WriteStartElement(
"cp",
"coreProperties",
"http://schemas.openxmlformats.org/package/2006/metadata/core-properties");
writer.WriteAttributeString("xmlns", "dc", null, "http://purl.org/dc/elements/1.1/"); writer.WriteAttributeString("xmlns", "dc", null, "http://purl.org/dc/elements/1.1/");
writer.WriteAttributeString("xmlns", "dcterms", null, "http://purl.org/dc/terms/"); writer.WriteAttributeString("xmlns", "dcterms", null, "http://purl.org/dc/terms/");
writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance"); writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
writer.WriteElementString("dc", "title", "http://purl.org/dc/elements/1.1/", "BizTalk-Anwendungsinventar " + document.EnvironmentName); writer.WriteElementString("dc", "title", "http://purl.org/dc/elements/1.1/", "Phase 1 BizTalk Anwendungen & Adapter");
writer.WriteElementString("dc", "creator", "http://purl.org/dc/elements/1.1/", "BEW BizTalk Application Catalog"); writer.WriteElementString("dc", "creator", "http://purl.org/dc/elements/1.1/", "BEW BizTalk Application Catalog");
writer.WriteElementString("cp", "lastModifiedBy", "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", "BEW BizTalk Application Catalog");
writer.WriteStartElement("dcterms", "created", "http://purl.org/dc/terms/"); writer.WriteStartElement("dcterms", "created", "http://purl.org/dc/terms/");
writer.WriteAttributeString("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance", "dcterms:W3CDTF"); writer.WriteAttributeString("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance", "dcterms:W3CDTF");
writer.WriteString(document.CompletedUtc.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)); writer.WriteString(DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture));
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndDocument(); writer.WriteEndDocument();
@@ -746,16 +674,20 @@ namespace BizTalkApplicationCatalog.Reporting
using (var writer = CreateXmlWriter(archive, "docProps/app.xml")) using (var writer = CreateXmlWriter(archive, "docProps/app.xml"))
{ {
writer.WriteStartDocument(); writer.WriteStartDocument();
writer.WriteStartElement("Properties", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"); writer.WriteStartElement(
"Properties",
"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties");
writer.WriteAttributeString("xmlns", "vt", null, "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"); writer.WriteAttributeString("xmlns", "vt", null, "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes");
writer.WriteElementString("Application", "BEW BizTalk Application Catalog"); writer.WriteElementString("Application", "BEW BizTalk Application Catalog");
writer.WriteElementString("AppVersion", "1.0"); writer.WriteElementString("AppVersion", "2.0");
writer.WriteEndElement(); writer.WriteEndElement();
writer.WriteEndDocument(); writer.WriteEndDocument();
} }
} }
private static XmlWriter CreateXmlWriter(ZipArchive archive, string path) private static XmlWriter CreateXmlWriter(
ZipArchive archive,
string path)
{ {
var entry = archive.CreateEntry(path, CompressionLevel.Optimal); var entry = archive.CreateEntry(path, CompressionLevel.Optimal);
return XmlWriter.Create(entry.Open(), new XmlWriterSettings return XmlWriter.Create(entry.Open(), new XmlWriterSettings
@@ -771,23 +703,25 @@ namespace BizTalkApplicationCatalog.Reporting
public SheetDefinition( public SheetDefinition(
string name, string name,
List<object[]> rows, List<object[]> rows,
bool autoFilter, int freezeRows,
int freezeRows) int filterHeaderRow,
int footerRowIndex,
double[] columnWidths)
{ {
Name = name; Name = name;
Rows = rows; Rows = rows;
AutoFilter = autoFilter;
FreezeRows = freezeRows; FreezeRows = freezeRows;
FilterHeaderRow = filterHeaderRow;
FooterRowIndex = footerRowIndex;
ColumnWidths = columnWidths;
} }
public string Name { get; private set; } public string Name { get; private set; }
public List<object[]> Rows { get; private set; } public List<object[]> Rows { get; private set; }
public bool AutoFilter { get; private set; }
public int FreezeRows { get; private set; } public int FreezeRows { get; private set; }
public int MaximumColumns public int FilterHeaderRow { get; private set; }
{ public int FooterRowIndex { get; private set; }
get { return Rows.Count == 0 ? 0 : Rows.Max(item => item.Length); } public double[] ColumnWidths { get; private set; }
}
} }
} }
} }