diff --git a/TBF/Properties/AssemblyInfo.cs b/TBF/Properties/AssemblyInfo.cs
index 07c0ea1cf..c357216c3 100644
--- a/TBF/Properties/AssemblyInfo.cs
+++ b/TBF/Properties/AssemblyInfo.cs
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// Build Number
// Revision
//
-[assembly: AssemblyVersion("3.9.3145.100")]
-[assembly: AssemblyFileVersion("3.9.3145.100")]
+[assembly: AssemblyVersion("3.9.3145.101")]
+[assembly: AssemblyFileVersion("3.9.3145.101")]
diff --git a/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs b/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs
index dc6452826..47ac047a8 100644
--- a/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs
+++ b/TBF/Rig/Output/DB/ResultsWriter/ResultsWriter.cs
@@ -242,6 +242,13 @@ namespace TBF.Rig.Output.DB.ResultsWriter
///
/// Writes all enabled water meters in a batch.
///
+ ///
+ /// XML payload targets are generated once per batch. Every enabled water
+ /// meter contributes one logical repeat record to the same XML document.
+ ///
+ /// Legacy Insert output keeps its existing behavior: every published
+ /// regular meter test is written with its concrete test ID context.
+ ///
public void WriteBatchResults(
Batch batch)
{
@@ -256,11 +263,25 @@ namespace TBF.Rig.Output.DB.ResultsWriter
WriterCfg storageCfg =
GetDataStorageWriterConfiguration();
- XmlPayloadRequestBuilder payloadBuilder =
- storageCfg.UsesXmlPayload()
- ? new XmlPayloadRequestBuilder(
- storageCfg)
- : null;
+ if (storageCfg.UsesXmlPayload())
+ {
+ WriteXmlBatchResults(
+ batch,
+ storageCfg);
+
+ return;
+ }
+
+ if (storageCfg.WriteMode !=
+ WriteMode.Insert)
+ {
+ throw new NotSupportedException(
+ string.Format(
+ "ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.",
+ storageCfg.DataStorageType,
+ storageCfg.TechnologyType,
+ storageCfg.WriteMode));
+ }
foreach (WaterMeter wm
in batch.WaterMeters)
@@ -271,104 +292,141 @@ namespace TBF.Rig.Output.DB.ResultsWriter
continue;
}
- DataWriteRequest targetRequest;
-
- if (storageCfg.IsStoredProcedurePayloadTarget())
- {
- PayloadGenerationRequest generationRequest =
- BuildPayloadGenerationRequest(
- wm);
-
- XmlPayloadBuildResult buildResult =
- payloadBuilder.Build(
- generationRequest,
- Convert.ToString(
- wm.SerialNr));
-
- targetRequest =
- buildResult.Request;
-
- if (!string.IsNullOrWhiteSpace(
- buildResult.ArchiveFilePath))
- {
- log.InfoFormat(
- "XML payload archived to '{0}'.",
- buildResult.ArchiveFilePath);
- }
- }
- else if (storageCfg.IsXmlFileTarget())
- {
- PayloadGenerationRequest generationRequest =
- BuildPayloadGenerationRequest(
- wm);
-
- XmlPayloadBuildResult buildResult =
- payloadBuilder.BuildFile(
- generationRequest,
- Convert.ToString(
- wm.SerialNr));
-
- targetRequest =
- buildResult.Request;
-
- log.InfoFormat(
- "XML file payload generated for WM position {0}: {1}",
- wm.WMPosition,
- buildResult.OutputFileName);
- }
- else if (storageCfg.WriteMode ==
- WriteMode.Insert)
- {
- targetRequest =
- BuildInsertRequest(
- wm);
-
- if (targetRequest.InsertItems.Count == 0)
- {
- log.WarnFormat(
- "No values to write for WM position {0}",
- wm.WMPosition);
-
- continue;
- }
- }
- else
- {
- throw new NotSupportedException(
- string.Format(
- "ResultsWriter does not support UniDataStorageWriter target '{0}' / '{1}' / '{2}'.",
- storageCfg.DataStorageType,
- storageCfg.TechnologyType,
- storageCfg.WriteMode));
- }
-
- WriterDiagnosticResult result =
- dataStorageWriter.SetData(
- targetRequest);
-
- if (!result.Success)
- {
- throw new Exception(
- result.Message);
- }
-
- log.WarnFormat(
- "ResultsWriter wrote WM position {0}. Target={1}, Technology={2}, Mode={3}.",
- wm.WMPosition,
- storageCfg.DataStorageType,
- storageCfg.TechnologyType,
- storageCfg.WriteMode);
+ //
+ // Insert mode needs the same test context as the legacy TBF
+ // result writers. One row is generated for every published
+ // regular meter test and all selected items are evaluated
+ // with the actual mtr.Name() as test ID.
+ //
+ WriteInsertResults(
+ wm,
+ storageCfg);
}
}
+ ///
+ /// Generates and writes one XML payload for the complete batch.
+ ///
+ private void WriteXmlBatchResults(
+ Batch batch,
+ WriterCfg storageCfg)
+ {
+ IList enabledMeters =
+ batch.WaterMeters
+ .Where(
+ wm =>
+ wm != null &&
+ !wm.Disabled)
+ .ToList();
+
+ if (enabledMeters.Count == 0)
+ return;
+
+ XmlPayloadRequestBuilder payloadBuilder =
+ new XmlPayloadRequestBuilder(
+ storageCfg);
+
+ PayloadGenerationRequest generationRequest =
+ BuildBatchPayloadGenerationRequest(
+ enabledMeters);
+
+ string payloadIdentifier =
+ BuildBatchPayloadIdentifier(
+ enabledMeters);
+
+ XmlPayloadBuildResult buildResult;
+
+ if (storageCfg.IsStoredProcedurePayloadTarget())
+ {
+ buildResult =
+ payloadBuilder.Build(
+ generationRequest,
+ payloadIdentifier);
+ }
+ else if (storageCfg.IsXmlFileTarget())
+ {
+ buildResult =
+ payloadBuilder.BuildFile(
+ generationRequest,
+ payloadIdentifier);
+ }
+ else
+ {
+ throw new NotSupportedException(
+ string.Format(
+ "Unsupported XML payload target '{0}' / '{1}' / '{2}'.",
+ storageCfg.DataStorageType,
+ storageCfg.TechnologyType,
+ storageCfg.WriteMode));
+ }
+
+ if (!string.IsNullOrWhiteSpace(
+ buildResult.ArchiveFilePath))
+ {
+ log.InfoFormat(
+ "XML payload archived to '{0}'.",
+ buildResult.ArchiveFilePath);
+ }
+
+ if (!string.IsNullOrWhiteSpace(
+ buildResult.OutputFileName))
+ {
+ log.InfoFormat(
+ "XML batch payload generated: {0}",
+ buildResult.OutputFileName);
+ }
+
+ WriterDiagnosticResult result =
+ dataStorageWriter.SetData(
+ buildResult.Request);
+
+ if (!result.Success)
+ {
+ throw new Exception(
+ result.Message);
+ }
+
+ log.WarnFormat(
+ "ResultsWriter wrote one XML payload for {0} enabled water meter(s). Target={1}, Technology={2}, Mode={3}.",
+ enabledMeters.Count,
+ storageCfg.DataStorageType,
+ storageCfg.TechnologyType,
+ storageCfg.WriteMode);
+ }
+
+ ///
+ /// Creates a stable identifier used by the XML request builder for one batch.
+ ///
+ private string BuildBatchPayloadIdentifier(
+ IList enabledMeters)
+ {
+ if (enabledMeters == null ||
+ enabledMeters.Count == 0)
+ {
+ return "BATCH";
+ }
+
+ string firstSerial =
+ Convert.ToString(
+ enabledMeters[0].SerialNr);
+
+ if (string.IsNullOrWhiteSpace(
+ firstSerial))
+ {
+ return "BATCH";
+ }
+
+ return firstSerial;
+ }
+
///
/// Generates a dry-run XML payload without calling the database.
///
///
- /// Batch used to evaluate one-time TBF mappings.
+ /// Batch used to evaluate one-time TBF mappings and provide preview context.
///
///
- /// Number of repeated GROUP/TEST instances generated for the preview.
+ /// Number of logical repeated result records generated for the preview.
///
public XmlPayloadBuildResult GeneratePreviewPayload(
Batch simulationBatch,
@@ -427,10 +485,70 @@ namespace TBF.Rig.Output.DB.ResultsWriter
}
///
- /// Creates the legacy insert request.
+ /// Writes Insert rows for one water meter using the actual TBF test context.
+ ///
+ ///
+ /// One row is written for every published regular meter test.
+ /// The actual test ID is passed to WMeterRsltItemSpec.Print().
+ ///
+ private void WriteInsertResults(
+ WaterMeter wm,
+ WriterCfg storageCfg)
+ {
+ IList publishedTests =
+ wm.RegularMeterTestRslts()
+ .Where(
+ mtr =>
+ mtr != null &&
+ mtr.Publish() ==
+ Publish.Always)
+ .ToList();
+
+ //
+ // Preserve meter/batch data even if there is no published test.
+ //
+ if (publishedTests.Count == 0)
+ {
+ DataWriteRequest fallbackRequest =
+ BuildInsertRequest(
+ wm,
+ string.Empty);
+
+ WriteInsertRequest(
+ wm,
+ string.Empty,
+ fallbackRequest,
+ storageCfg);
+
+ return;
+ }
+
+ foreach (MeterTestRslt mtr
+ in publishedTests)
+ {
+ string testId =
+ mtr.Name() ??
+ string.Empty;
+
+ DataWriteRequest request =
+ BuildInsertRequest(
+ wm,
+ testId);
+
+ WriteInsertRequest(
+ wm,
+ testId,
+ request,
+ storageCfg);
+ }
+ }
+
+ ///
+ /// Builds one Insert request for one water meter and one concrete test.
///
private DataWriteRequest BuildInsertRequest(
- WaterMeter wm)
+ WaterMeter wm,
+ string testId)
{
DataWriteRequest request =
new DataWriteRequest
@@ -441,7 +559,7 @@ namespace TBF.Rig.Output.DB.ResultsWriter
if (resultsWriterCfg.SelectedItems == null)
return request;
- foreach (var item
+ foreach (Results.WMeterRsltItemSpec item
in resultsWriterCfg.SelectedItems)
{
if (item == null ||
@@ -458,7 +576,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter
item.Caption,
Value =
- item.Print(wm)
+ item.Print(
+ wm,
+ testId)
});
}
@@ -466,74 +586,290 @@ namespace TBF.Rig.Output.DB.ResultsWriter
}
///
- /// Creates the runtime XML mapping request for one meter.
+ /// Executes one prepared Insert request.
///
- private PayloadGenerationRequest BuildPayloadGenerationRequest(
- WaterMeter wm)
+ private void WriteInsertRequest(
+ WaterMeter wm,
+ string testId,
+ DataWriteRequest request,
+ WriterCfg storageCfg)
+ {
+ if (request == null ||
+ request.InsertItems.Count == 0)
+ {
+ log.WarnFormat(
+ "No values to write for WM position {0}",
+ wm.WMPosition);
+
+ return;
+ }
+
+ WriterDiagnosticResult result =
+ dataStorageWriter.SetData(
+ request);
+
+ if (!result.Success)
+ {
+ throw new Exception(
+ result.Message);
+ }
+
+ log.WarnFormat(
+ "ResultsWriter wrote WM position {0}, TestID='{1}'. Target={2}, Technology={3}, Mode={4}.",
+ wm.WMPosition,
+ testId,
+ storageCfg.DataStorageType,
+ storageCfg.TechnologyType,
+ storageCfg.WriteMode);
+ }
+
+ ///
+ /// Creates one runtime XML mapping request for the complete batch.
+ ///
+ ///
+ /// Enabled water meters that must be represented in the generated payload.
+ ///
+ ///
+ ///
+ /// Every enabled water meter creates one logical repeat record. All
+ /// destinations located inside the repeating XML prototype are populated
+ /// into that same record.
+ ///
+ ///
+ ///
+ /// Result evaluation first uses the meter-level context. If that does not
+ /// produce a value, the same result item is evaluated against the actual
+ /// published measurement-test IDs in procedure order. This preserves
+ /// meter-level values while also allowing test-result variables to be
+ /// populated in the XML record.
+ ///
+ ///
+ private PayloadGenerationRequest BuildBatchPayloadGenerationRequest(
+ IList enabledMeters)
{
PayloadGenerationRequest request =
new PayloadGenerationRequest();
- if (resultsWriterCfg.SelectedItems == null)
- return request;
-
- int sourceIndex = 0;
-
- foreach (var item
- in resultsWriterCfg.SelectedItems)
+ if (enabledMeters == null ||
+ enabledMeters.Count == 0 ||
+ resultsWriterCfg.SelectedItems == null)
{
- if (item == null ||
- string.IsNullOrWhiteSpace(
- item.Caption))
- {
- continue;
- }
+ return request;
+ }
+
+ List singleItems =
+ resultsWriterCfg.SelectedItems
+ .Where(
+ item =>
+ item != null &&
+ !string.IsNullOrWhiteSpace(
+ item.Caption) &&
+ !IsRepeatDestination(
+ item.Caption))
+ .ToList();
+
+ List repeatItems =
+ resultsWriterCfg.SelectedItems
+ .Where(
+ item =>
+ item != null &&
+ !string.IsNullOrWhiteSpace(
+ item.Caption) &&
+ IsRepeatDestination(
+ item.Caption))
+ .ToList();
+
+ //
+ // One-time XML destinations exist only once in the document.
+ // Evaluate them from the first enabled water meter. This preserves
+ // the existing one-time mapping model while repeat destinations are
+ // created once for every enabled water meter.
+ //
+ WaterMeter firstMeter =
+ enabledMeters[0];
+
+ for (int singleIndex = 0;
+ singleIndex < singleItems.Count;
+ singleIndex++)
+ {
+ Results.WMeterRsltItemSpec item =
+ singleItems[
+ singleIndex];
string sourceKey =
- "Result." +
- sourceIndex.ToString(
- CultureInfo.InvariantCulture);
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Batch.Single.{0}",
+ singleIndex);
- sourceIndex++;
+ request.SingleMappings.Add(
+ new PayloadMapping
+ {
+ SourceKey =
+ sourceKey,
- if (IsRepeatDestination(
- item.Caption))
+ DestinationPath =
+ item.Caption
+ });
+
+ request.SingleValues.SetValue(
+ sourceKey,
+ EvaluateXmlMappedValue(
+ firstMeter,
+ item));
+ }
+
+ //
+ // Every enabled water meter becomes one logical repeat record.
+ //
+ for (int meterIndex = 0;
+ meterIndex < enabledMeters.Count;
+ meterIndex++)
+ {
+ WaterMeter wm =
+ enabledMeters[
+ meterIndex];
+
+ PayloadRepeatRecord record =
+ new PayloadRepeatRecord();
+
+ for (int repeatIndex = 0;
+ repeatIndex < repeatItems.Count;
+ repeatIndex++)
{
- PayloadRepeatRecord record =
- new PayloadRepeatRecord();
+ Results.WMeterRsltItemSpec item =
+ repeatItems[
+ repeatIndex];
+
+ string sourceKey =
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Batch.Repeat.{0}.{1}",
+ meterIndex,
+ repeatIndex);
AddRecordMapping(
record,
sourceKey,
item.Caption,
- item.Print(wm));
+ EvaluateXmlMappedValue(
+ wm,
+ item));
+ }
+ if (record.Mappings.Count > 0)
+ {
request.RepeatRecords.Add(
record);
}
- else
- {
- request.SingleMappings.Add(
- new PayloadMapping
- {
- SourceKey =
- sourceKey,
-
- DestinationPath =
- item.Caption
- });
-
- request.SingleValues.SetValue(
- sourceKey,
- item.Print(wm));
- }
}
return request;
}
///
- /// Creates a preview request with an exact number of simulated tests.
+ /// Evaluates one configured result item for XML output.
+ ///
+ ///
+ /// Meter/batch variables normally return a value directly from
+ /// Print(wm). Test-result variables require a concrete test ID. For those
+ /// variables the method evaluates the published regular meter tests in
+ /// measurement-procedure order and uses the first non-empty value.
+ ///
+ /// This fallback is intentionally applied only when the meter-level
+ /// evaluation is empty.
+ ///
+ private string EvaluateXmlMappedValue(
+ WaterMeter wm,
+ Results.WMeterRsltItemSpec item)
+ {
+ if (wm == null ||
+ item == null)
+ {
+ return string.Empty;
+ }
+
+ string meterValue =
+ item.Print(
+ wm);
+
+ if (!string.IsNullOrWhiteSpace(
+ meterValue))
+ {
+ return meterValue;
+ }
+
+ IList publishedTests =
+ wm.RegularMeterTestRslts()
+ .Where(
+ mtr =>
+ mtr != null &&
+ mtr.Publish() ==
+ Publish.Always)
+ .ToList();
+
+ string firstResolvedValue =
+ string.Empty;
+
+ string firstResolvedTestId =
+ string.Empty;
+
+ foreach (MeterTestRslt mtr
+ in publishedTests)
+ {
+ string testId =
+ mtr.Name() ??
+ string.Empty;
+
+ string testValue =
+ item.Print(
+ wm,
+ testId);
+
+ if (string.IsNullOrWhiteSpace(
+ testValue))
+ {
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(
+ firstResolvedValue))
+ {
+ firstResolvedValue =
+ testValue;
+
+ firstResolvedTestId =
+ testId;
+
+ continue;
+ }
+
+ //
+ // A single XML destination can hold only one scalar value.
+ // If more than one test produces a different value for the same
+ // mapped result, preserve deterministic procedure order and keep
+ // the first value, but make the ambiguity visible in the log.
+ //
+ if (!string.Equals(
+ firstResolvedValue,
+ testValue,
+ StringComparison.Ordinal))
+ {
+ log.WarnFormat(
+ "XML mapping '{0}' produced values in more than one test context for WM position {1}. Using first test '{2}'.",
+ item.Caption,
+ wm.WMPosition,
+ firstResolvedTestId);
+
+ break;
+ }
+ }
+
+ return firstResolvedValue;
+ }
+
+ ///
+ /// Creates a preview request with an exact number of simulated repeat records.
///
private PayloadGenerationRequest BuildPreviewGenerationRequest(
WaterMeter wm,
@@ -563,10 +899,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter
item.Caption))
{
//
- // Repeated XML attributes have no predefined semantics.
- // Keep the selected TBF result exactly as configured:
- // its evaluated value is written only to its selected
- // repeating destination.
+ // A repeating mapping defines a destination inside the
+ // repeat prototype. The mapping itself must not create a
+ // separate repeat record.
//
repeatItems.Add(
item);
@@ -584,11 +919,9 @@ namespace TBF.Rig.Output.DB.ResultsWriter
wm);
//
- // A minimal simulation batch does not contain every
- // possible TBF result source. Never leave a configured
- // preview destination empty only because its runtime
- // simulation source is unavailable. Use a valid,
- // type-appropriate and clearly synthetic value instead.
+ // A minimal simulation batch does not contain every possible
+ // TBF result source. Use a clearly synthetic type-appropriate
+ // value when the evaluated preview value is unavailable.
//
if (string.IsNullOrWhiteSpace(
previewValue))
@@ -617,25 +950,38 @@ namespace TBF.Rig.Output.DB.ResultsWriter
}
}
- if (repeatItems.Count > 0)
+ //
+ // Create the requested number of logical repeat records. Every record
+ // receives all configured repeating mappings, so one simulated record
+ // is structurally equivalent to one generated runtime record.
+ //
+ for (int recordIndex = 0;
+ recordIndex < simulatedTestCount &&
+ repeatItems.Count > 0;
+ recordIndex++)
{
- for (int testIndex = 0;
- testIndex < simulatedTestCount;
- testIndex++)
+ PayloadRepeatRecord record =
+ new PayloadRepeatRecord();
+
+ for (int repeatIndex = 0;
+ repeatIndex < repeatItems.Count;
+ repeatIndex++)
{
Results.WMeterRsltItemSpec item =
repeatItems[
- testIndex %
- repeatItems.Count];
-
- PayloadRepeatRecord record =
- new PayloadRepeatRecord();
+ repeatIndex];
string sourceKey =
string.Format(
CultureInfo.InvariantCulture,
- "Preview.Repeat.{0}",
- testIndex);
+ "Preview.Repeat.{0}.{1}",
+ recordIndex,
+ repeatIndex);
+
+ int simulationValueIndex =
+ recordIndex *
+ repeatItems.Count +
+ repeatIndex;
AddRecordMapping(
record,
@@ -643,11 +989,11 @@ namespace TBF.Rig.Output.DB.ResultsWriter
item.Caption,
CreateUniversalSimulationValue(
item,
- testIndex));
-
- request.RepeatRecords.Add(
- record);
+ simulationValueIndex));
}
+
+ request.RepeatRecords.Add(
+ record);
}
return request;