//private HL7Segment CreateEvent(TriggerEvent tr) //{ // HL7Segment e = null; // var segmentsConfig = this._repo.GetSegmentBy(tr.Version, tr.Segment); // try // { // e = new HL7Segment(tr.EventType, tr.Segment, tr.Version, (int)tr.Sequence, (bool)tr.IsOptional, (bool)tr.IsRepeated); // } // catch (ArgumentNullException) // { // } // if (e != null) // { // var segmentArray = this._hl7.MessageToken.GetSegmentData(tr.Segment); // foreach (Segment s in segmentsConfig) // { // try // { // HL7SegmentColumn segment = new HL7SegmentColumn(s.Sequence, s.Length, s.Version, s.Name, s.DataType, s.IsRequired, s.IsRepeating); // segment.SetValue(s.DataType, segmentArray[s.Sequence]); // e.AddSegmentEvent(segment); // } // catch { } // } // } // else // { // this.LogInfoMessage(string.Format("No Segment class exists for {0}.", tr.Segment)); // } // return e; //} private HL7Segment CreateFromSegmentString(string rawSegment, TriggerEvent tr) { HL7Segment e = null; Segment[] segmentsConfig = tr.SegmentCollection; if (segmentsConfig != null) { e = new HL7Segment(tr.EventType, tr.Segment, tr.Version, (int)tr.Sequence, (bool)tr.IsOptional, (bool)tr.IsRepeated); var segmentArray = rawSegment.Split(this._hl7.MessageToken.FieldSeparator); foreach (Segment s in segmentsConfig) { try { HL7SegmentField segment = new HL7SegmentField(s.Sequence, s.Length, s.Version, s.Name, s.DataType, s.IsRequired, s.IsRepeating); segment.SetValue(s.DataType, segmentArray[s.Sequence]); e.AddSegmentEvent(segment); } catch { } } } return(e); }
/// <summary> /// Parse HL7 message using the local DB for Segments and trigger events /// </summary> /// <param name="message">Raw HL7 formated string. Multiple message file should be formated with \r\n after each message.</param> /// <returns>HL7 object with segments and events</returns> public HL7Message Parse(string message) { string[] tempMessage = message.Split(new string[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i <= tempMessage.Length - 1; i++) { string segment = tempMessage[i].Substring(0, 3); if (segment == "MSH") { tempMessage[i] = string.Format("{0}", tempMessage[i]); } tempMessage[i].Trim(); } this._hl7 = new HL7Message(tempMessage); base.LogInfoMessage(string.Format("Creating HL7 message object from {0}", message)); //The Fields in the message should drive this, to handle multiple similar segments. TriggerEvent[] triggerEvents = this._repo.GetTriggerEventsBy(this._hl7.MessageToken.MessageVersion, this._hl7.MessageToken.MessageType, this._hl7.MessageToken.EventType).AsParallel().ToArray <TriggerEvent>(); base.LogInfoMessage(string.Format("Fetching TriggerEvent configuration for verions {0} {1}_{2}", this._hl7.MessageToken.MessageVersion , this._hl7.MessageToken.MessageType , this._hl7.MessageToken.EventType)); if (triggerEvents.Length == 0) { throw new ParserException(string.Format("No trigger events found for version {0}, message type {1}_{2}", this._hl7.MessageToken.MessageVersion, this._hl7.MessageToken.MessageType, this._hl7.MessageToken.EventType)); } else { foreach (string s in this._hl7.MessageToken.Segments) { string segId = s.Substring(0, 3); base.LogInfoMessage(string.Format("Searching TriggerEvent configuration for segment {0}", segId)); TriggerEvent tr = triggerEvents.Where(x => x.Segment == segId).FirstOrDefault(); if (tr != null) { HL7Segment newEvent = this.CreateFromSegmentString(s, tr); base.LogInfoMessage(string.Format("Successfully created HL7 segment {0} for TriggerEvent {1}_{2}", newEvent.Name, tr.MessageType, tr.EventType)); if (newEvent != null) { _hl7.AddEventSegment(newEvent); } } } return(_hl7); } }
public async Task <FormatResult <TSchema> > FormatAsync(Stream output, EntityResult <TSchema> input) { using (var writer = new StreamWriter(output)) { for (int i = 0;; i++) { TSchema entity; if (!input.TryGetEntity(i, out entity)) { break; } HL7Segment segment = entity as HL7Segment; if (segment == null) { throw new MacheteSchemaException($"The entity is not an HL7 segment: {TypeCache.GetShortName(entity.GetType())}"); } if (i > 0) { await writer.WriteAsync(_settings.SegmentSeparator); } IEntityFormatter entityFormatter; if (_schema.TryGetEntityFormatter(entity, out entityFormatter)) { var context = new StringBuilderFormatContext(); context.AddSettings(_settings); entityFormatter.Format(context, entity); await writer.WriteAsync(context.ToString()).ConfigureAwait(false); } else { throw new MacheteSchemaException($"The entity type was not found: {TypeCache.GetShortName(entity.GetType())}"); } } } return(new HL7FormatResult <TSchema>()); }
/// <summary> /// Create and HL7 formated file from the TriggerEvent Array /// </summary> /// <param name="hl7Data"> /// TriggerEvent array with the segment schema and data. /// </param> /// <param name="filePath">Path to save the HL7 file, including file name</param> /// <returns> /// True: successful file creation /// False: failed to create the file /// </returns> public bool CreateFile(HL7Message hl7Data, string filePath) { bool isSuccess = true; StringBuilder hl7File = new StringBuilder(); try { foreach (HL7Segment hl7SegmentItem in hl7Data.Segments) //KeyValuePair<int,HL7Segment> hl7SegmentItem in hl7Data.Segments) { StringBuilder sbProp = new StringBuilder(); HL7Segment tr = hl7SegmentItem;//.Value; sbProp.AppendFormat("{0}|", tr.Name); foreach (HL7SegmentField column in tr.Segments) { sbProp.AppendFormat("{0}|", column.Value); LogInfoMessage(string.Format("Successfully added segment column {0} to segment {1}", column.Name, tr.Name)); } sbProp.Remove(sbProp.Length - 1, 1); hl7File.AppendLine(sbProp.ToString()); LogInfoMessage(string.Format("Successfully added segment {0} to file {1}", tr.Name, filePath)); } } catch (Exception ex) { LogErrorMessage(string.Format("Failed to create HL7 data object for file {0}. ERROR: {1}", filePath, ex.Message)); } try { File.WriteAllText(filePath, hl7File.ToString()); LogInfoMessage(string.Format("Creating file {0}", filePath)); } catch (Exception ex) { LogErrorMessage(string.Format("Failed to create file to {0}. ERROR: {1}", filePath, ex.Message)); } return(isSuccess); }
static void Main(string[] args) { var msgText = @"MSH|^~\&|CERNER||PriorityHealth||20191020050600+1000||ORU^R01|Q479004375T431430612|P|2.3|" + "\r" + @"PID|||001677980~212323112^^^AUTH^MR^TESTING||SMITH^CURTIS||19680219|M||||||||||929645156318|123456789|" + "\r" + @"PD1||||1234567890^LAST^FIRST^M^^^^^NPI|" + "\r" + @"OBR|1|341856649^HNAM_ORDERID|000002006326002362|648088^Basic Metabolic Panel|||20061122151600|||||||||1620^Hooker^Robert^L||||||20061122154733|||F|||||||||||20061122140000|" + "\r" + @"OBX|1|NM|GLU^Glucose Lvl||59|mg/dL|65-99^65^99|L|||F|||20061122154733|" + "\r" + @"OBX|2|NM|ALT^Alanine Aminotransferase||23|umol/L|2-20^65^1000|H|||F|||20061122154733|" + "\r" + @"OBX|3|NM|8893-0^Pulse rate^LN^78564009^^SCT|1.9.2.1|70|bpm^^ISO+||N|||F|||201706071449+1000" + "\r"; try { Console.WriteLine("How to use the HL7 Enumerator for Decoding HL7 Messages"); Console.WriteLine("-------------------------------------------------------\r\n"); // Dont want to process the whole message? Use the Parse Only Method to check fields before processing // Here is an example: UseParseOnlyToConfirmWeCanProcessTheMessage(msgText); Console.WriteLine("\r\nProcessing ORU Message " + HL7Message.ParseOnly(msgText, "MSH.10")); // Process the HL7 message by Creating a HL7 Message Object // NOTE: this will always succeed - an exception should never be thrown, so there is no need // to test for it. HL7Message mesg = msgText; // yes, it is that simple, but the method below implements some other syntax versions AlternateMethodsForCreatingAHL7MessageObject(msgText); // Extract a Single field - say for logging purposes // Use the Element property to extract ANY specific field/subfield OR Group of fields/subFields DisplayExtractingAnElementMessage(); Console.WriteLine($"Message received from Sending system {mesg.Element("MSH.3")}"); // Extract a Whole Segment Console.Write("\r\nOR a whole PD1 Segment :\r\n "); Console.WriteLine(mesg.Element("PD1")); // Extract a Repeating Field Console.WriteLine("\r\nOR a SET of repeating Fields: eg PID.3[]"); var c = 0; mesg.Element("PID.3[]").ForEach(id => Console.WriteLine($"Repitition {++c}: {id}")); // Use the ALLSegments property with LINQ to extract and manipulate any part/subpart of a message // as a Low Level HL7Element DisplayUsingLinq(); Console.WriteLine("Eg Test Names: "); var OBXTestNames = mesg.AllSegments("OBX").Select(o => o.Element("*.3.2")); // Note the use of the "*" wildcard - avoids needing to specify the Segment level. // Use Implicit String Casting to Output any Element as a string. Console.WriteLine($"\r\nThe message contains {OBXTestNames.Count()} Test Results:"); foreach (string testName in OBXTestNames) { Console.WriteLine($" {testName}"); } // Use LINQ on ANY element at any level to iterate over sub elements Console.Write("\r\nUse LINQ at the Element (field/component) Level to get information\r\nEg: "); var NonEmptyComponentsInPD1 = mesg.Element("PD1.4").Where(f => !string.IsNullOrWhiteSpace(f)); Console.WriteLine($"PID.3 has {(mesg.Element("PD1.4").Count)} components, and {NonEmptyComponentsInPD1.Count()} have data"); foreach (var component in NonEmptyComponentsInPD1) { Console.WriteLine($" {component}"); } // Use self defined Constants for field definitions to make your code cleaner. DisplayConstantsMessage(); const string PatientFamilyName = "PID.5.1"; const string PatientGivenName = "PID.5.2"; Console.WriteLine($"Patients Name is : {mesg.Element(PatientFamilyName)}, {mesg.Element(PatientGivenName)}."); DisplayDataTypesMessage(); // Use the "HL7Types" Namespace Helpers to safely get Numbered Field Values from components // Remember that Element FIELDS are ZERO based but in Segments the Segment Name is 0 so the FIELD Numbers // still correspond to the HL7 field numbers. const int fldSequence = 1; const int fldDataType = 2; const int fldTestName = 3; const int fldValue = 5; const int fldUnits = 6; const int fldRefRange = 7; const int fldAbFlag = 8; // BUT Components are numbered 0 to n (not 1 to n). const int cvTestName = 1; const int cvTestCode = 0; // Now use our defintions to extract using Data Type primitives ElementValue and IndexedElement Console.WriteLine("\r\nUse Safe Element Options to Output only the Numeric test Results:\r\n"); mesg.AllSegments("OBX") // Test Segments .Where(o => o.ElementValue(fldDataType).Equals("NM")) // numeric fields .Select(o => $"{o.ElementValue(fldSequence),2}. " + $"{o.IndexedElement(fldTestName).ElementValue(cvTestName),-30} " + // test name $"({o.IndexedElement(fldTestName).ElementValue(cvTestCode),-6}) " + // test code $"{o.IndexedElement(fldValue),6} " + // test value $"{o.IndexedElement(fldUnits).ElementValue(0),-6} " + // units $"({o.IndexedElement(fldRefRange).ElementValue(0),-6}) " + // Range first field $"{(o.ElementValue(fldAbFlag).Equals("N") ? "" : o.ElementValue(fldAbFlag))}" // Abflag ).ToList() .ForEach(testResult => Console.WriteLine($" {testResult}")); // Use the HL7 Data Type DisplayCommonDataTypes(); // Use the DataType Extensions for Strict Typing of known Data Types. Console.WriteLine("\r\nUse A Common Data type to Extract Type safe properties"); Console.WriteLine("Eg: PID.5 (Patient Name) is an XPN DataType (often repeating)"); var ptName = mesg.Element("PID.5").AsXPN(); // note, in this case it is ok AsXPN() because we know there is only 1 replicate. // for repeating fields, use AsXPNs() to return an IEnumerable of XPN Console.WriteLine($"Patients Name is : {ptName.FamilyName}, {ptName.GivenName}."); // Using a CX Data Type for repeating fields.. var ids = mesg.Element("PID.3[]"); var patientIds = ids.AsCXs(); // we know from the spec, that PID.3 is a replicating field of CX 's. The included AsCXs method is // a simple extension method returning a CX_CompositeId type (supporting the IHL7Type interface). // If the include CX datatype is not suitable for your purpose, it is very easy to make your own. // Just Implement the HL7Type interface using the low level methods. // // NOTE: if you ask for a simple CX for a Replicating field, you will get a DEBUG exception indicating // that you should use an Enumerable type for this field Console.WriteLine($"Found {patientIds?.Count()} Ids using DataType Assigment"); foreach (var cx in patientIds) { // remember that certain elements in a CX MAY be null. Console.WriteLine($"ID: {cx?.ID}"); Console.WriteLine($" IDType: {cx?.IdentifierTypeCode}"); Console.WriteLine($" Authority:{cx?.AssigningAuthority?.NamespaceId?.BestValue}"); Console.WriteLine($" Facility: {cx?.AssigningFacility?.NamespaceId?.BestValue}"); } /// One Alternative in the case where no type has been defined, /// you can use the low level elements OR Self Element references to extract the data Console.WriteLine($" Found {ids.Count} Ids Using Elements (low level)"); foreach (var id in ids) { if (id.Count > 0) { Console.WriteLine($"ID: {id[0]}"); } else { Console.WriteLine($"ID: {id}"); // implict string casting } if (id.Count > 4) { Console.WriteLine($" IDType:{id[4]}"); } if (id.Count > 3) { Console.WriteLine($" Issuer:{id[3]}"); } } /// HOWEVER - it is better to define your own Custom Type. /// See the Custom Data Types example in this repository. DisplayCustomDataTypesMessage(); // Data Type Support. DisplayHL7TablesMessage(); // Coded Values have 3 propertes. Value, CodedValue and Best Value. // Use the Property 'BestValue' in most cases for display as it returns either Value // or Coded Value depending on whether a table is attached and the value is known in the table. // When no table is defined for the Coded Value will always be empty var pt1IdTypeCode = patientIds.Skip(1).First().IdentifierTypeCode; Console.WriteLine("\r\nIdentifier Properties: with NO table attached."); Console.WriteLine($" Value (Unvalidated) : {pt1IdTypeCode.Value}"); Console.WriteLine($" CodedValue (Validated) : {pt1IdTypeCode.CodedValue}"); Console.WriteLine($" BestValue (Coded Or Value): {pt1IdTypeCode.BestValue}"); Console.WriteLine($" Description : {pt1IdTypeCode.Description}"); Console.WriteLine($" Notes : {pt1IdTypeCode.Notes?.First()}..."); // Attach manually created Table to CX we just read. // Manually Create a Data Table into In-Memory Provider Console.WriteLine("\r\nManually Create Tables to validate the properties"); var tables = ManuallyCreateTablesIntoAInMemoryProvider(); pt1IdTypeCode.Table = tables.GetCodeTable("0203"); Console.WriteLine("\r\nIdentifier Properties: with table 0203 attached."); Console.WriteLine($" Value (Unvalidated) : {pt1IdTypeCode.Value}"); Console.WriteLine($" CodedValue (Validated) : {pt1IdTypeCode.CodedValue}"); Console.WriteLine($" BestValue (Coded Or Value): {pt1IdTypeCode.BestValue}"); Console.WriteLine($" Description : {pt1IdTypeCode.Description}"); Console.WriteLine($" Notes : {pt1IdTypeCode.Notes.First()}..."); // Automatically Attach suitable tables to the Data type as we read Assign it. // Assuming we hav the required tables, each datatype consists of a set of // Identifiers where data might be coded. DisplayAutomaticDataTableAssignment(); // First Use a Table Provider as a source of tables. // The File Provider can access any table in a folder by TableID is very useful for this purpose var HL7TablesFolder = Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6), "ExampleTables"); Console.WriteLine($"The process is: First, Get an instance of a table provider"); Console.WriteLine("Eg: The 'FolderDataTableProvider'"); var tablesFromFolder = new FolderDataTableProvider(HL7TablesFolder); Console.WriteLine($"Using tables in folder:{tablesFromFolder.Folder}"); Console.WriteLine("Next, Look at the DataType (eg CX) to determine the number of Coded Elements in total"); Console.WriteLine($"A CX data has {CX_CompositeId.TotalCodedFieldCount} Coded elements. I need a List of corresponding table names"); // You will need 1 table reference for each Coded Item, in exact order that they will be constructed. Console.WriteLine("Add Required Table names to a String List IN Constructor Order\r\n"); var tableIds = new List <string>(); tableIds.Add("0061"); // CX_CompositeId.CheckDigitScheme needs table 0061 (we dont have that, but include it anyway) tableIds.Add("0363"); // CX_CompositeId.AssigningAuthority.NamespaceId needs table 0363 tableIds.Add("0301"); // CX_CompositeId.AssigningAuthority.UniversalIDType needs table 0301 tableIds.Add("0203"); // CX_CompositeId.IdentifierTypeCode needs table 0203 tableIds.Add(""); // CX_CompositeId.AssigningFacility.NamespaceId needs some unknown table - leave it blank. tableIds.Add("0301"); // CX_CompositeId.AssigningFacility.UniversalIDType needs 0301 again (include it again) Console.WriteLine("Table Ids required for PID.5 (CX)"); tableIds.ForEach(t => Console.WriteLine($" {t}")); var validatedPatientIds = ids.AsCXs(tableIds, tablesFromFolder); c = 0; validatedPatientIds.ToList().ForEach(id => { Console.WriteLine($"\r\nID {++c} {id.ID}"); var bestValue = id.CheckDigitScheme?.BestValue; var isValid = (id.CheckDigitScheme == null) ? "" : (id.CheckDigitScheme.IsValid.Value) ? "Yes" : "No"; var description = id.CheckDigitScheme.Description; Console.WriteLine($" CheckDigitScheme : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningAuthority?.NamespaceId.BestValue; isValid = (id.AssigningAuthority?.NamespaceId == null) ? "" : (id.AssigningAuthority.NamespaceId.IsValid.Value) ? "Yes" : "No"; description = id.AssigningAuthority?.NamespaceId.Description; Console.WriteLine($" AssigningAuthority.NamespaceId : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningAuthority?.UniversalIdType.BestValue; isValid = (id.AssigningAuthority?.UniversalIdType == null) ? "" : (id.AssigningAuthority.UniversalIdType.IsValid.Value) ? "Yes" : "No"; description = id.AssigningAuthority?.UniversalIdType.Description; Console.WriteLine($" AssigningAuthority.UniversalIdType: {bestValue,7} Valid: {isValid} {description}"); bestValue = id.IdentifierTypeCode?.BestValue; isValid = (id.IdentifierTypeCode == null) ? "" : (id.IdentifierTypeCode.IsValid.Value) ? "Yes" : "No"; description = id.IdentifierTypeCode?.Description; Console.WriteLine($" IdentifierTypeCode : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningFacility?.NamespaceId.BestValue; isValid = (id.AssigningFacility?.NamespaceId == null) ? "" : (id.AssigningFacility.NamespaceId.IsValid.Value) ? "Yes" : "No"; description = id.AssigningFacility?.NamespaceId.Description; Console.WriteLine($" AssigningFacility.NamespaceId : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningFacility?.UniversalIdType?.BestValue; isValid = (id.AssigningFacility?.UniversalIdType == null) ? "" : (id.AssigningFacility.UniversalIdType.IsValid.Value) ? "Yes" : "No"; description = id.AssigningFacility?.UniversalIdType?.Description; Console.WriteLine($" AssigningFacility.UniversalIdType : {bestValue,7} Valid: {isValid} {description}"); } ); DisplayTimeAndNumericConversion(); var ptDOB = "PID.7"; var mesgTime = "MSH.7"; var dob = mesg.Element(ptDOB); var mesgTs = mesg.Element(mesgTime); DisplayAsDateTimeInformation(); Console.WriteLine($"DOB.AsDateTime() {mesg.Element(ptDOB)} does not have Timezone, so assume Local"); Console.WriteLine($" {dob.AsDateTime()} = {dob.AsDateTime().Value.ToString("o")}"); Console.WriteLine($"msgTs.AsDateTime() {mesg.Element(mesgTime)} has Time Timezone. Stored as UTC."); Console.WriteLine($" {mesgTs.AsDateTime()} = {mesgTs.AsDateTime().Value.ToString("o")}"); DisplayAsLocalDateTimeInformation(); Console.WriteLine($"DOB {mesg.Element(ptDOB)} No timezone, so display Current LOCAL"); Console.WriteLine($" {dob.AsLocalTime()} = {dob.AsLocalTime().Value.ToString("o")}"); Console.WriteLine($"msgTime {mesg.Element(mesgTime)} Has TimeZone Shows in Current local"); Console.WriteLine($" {mesgTs.AsLocalTime()} = {mesgTs.AsLocalTime().Value.ToString("o")}"); DisplayAsUTCDateTimeInformation(); Console.WriteLine($"DOB {mesg.Element(ptDOB)} No Timezone, So safe to assume local to you"); Console.WriteLine($" {dob.AsUTCTime()} = {dob.AsUTCTime().Value.ToString("o")}"); Console.WriteLine($"DOB {mesg.Element(ptDOB)} No Timezone, but can be forced to UTC."); Console.WriteLine($" {dob.AsDateTime(DateTimeStyles.AssumeUniversal).Value.ToString("o")}"); Console.WriteLine($"msgTime {mesg.Element(mesgTime)} is converted to UTC Time."); Console.WriteLine($" {mesgTs.AsUTCTime()} = {mesgTs.AsUTCTime().Value.ToString("o")}"); // NM_Numeric data Type supports implicity, explicit and has As<Type> Methods. DisplayNumericConversion(); const string ResultValue = "OBX[1].5"; // Examples: var result1 = mesg.Element(ResultValue).AsNM(); int nmAsInt = result1.AsInteger(); var nmAsFloat = result1.AsFloat(); double nmAsdouble = result1; var nmAsCurrency = (decimal)result1; Console.WriteLine($"Result 1 ToString() {result1}"); Console.WriteLine($"Result 1 {nmAsInt.GetType().Name} value {nmAsInt}"); Console.WriteLine($"Result 1 {nmAsFloat.GetType().Name} value {nmAsFloat:0.0}"); Console.WriteLine($"Result 1 {nmAsdouble.GetType().Name} value {nmAsdouble:0.00000000}"); Console.WriteLine($"Result 1 {nmAsCurrency.GetType().Name} value {nmAsCurrency:0.00}"); DisplayComposingMessages(); // Compose a Segment Console.WriteLine("\r\n Composing a Segment using DataTypes"); // Composed PID: PID||1234567^4^M11^ADT01^MR^University Hospital|1234567^4^M11^ADT01^MR^University Hospital~8003608833357361^^^AUSHIC^NI^ var patientIdentifiers = new List <CX_CompositeId>() { new CX_CompositeId() { ID = "1234567", CheckDigit = "4", CheckDigitScheme = "M11", AssigningAuthority = new HD_HierarchicDesignator() { NamespaceId = "ADT01" }, IdentifierTypeCode = "MR", AssigningFacility = new HD_HierarchicDesignator() { NamespaceId = "University Hospital" } }, new CX_CompositeId() { ID = "8003608833357361", AssigningAuthority = new HD_HierarchicDesignator() { NamespaceId = "AUSHIC" }, IdentifierTypeCode = "NI" } }; // Compose a PID var pid = new HL7Segment("PID"); pid.SetField(2, patientIdentifiers.First()); pid.SetField(3, patientIdentifiers.AsElement()); Console.WriteLine("\r\nComposed PID: " + pid); } catch (Exception e) { Console.WriteLine("Unexpected exception : {0}", e.Message); } Console.ReadLine(); }