-
Notifications
You must be signed in to change notification settings - Fork 22
CDA Tutorial
We’ve been getting a lot of requests from our user community regarding the creation of CDA documents in Everest lately. This has been a tutorial that I’ve been meaning to write for quite some time however never seemed to get a chance to sit down and put pen to paper (err keyboard to screen).
This tutorial will be a multi-part tutorial where we’ll create a CDA document with some clinical data. I’ll be using the Antepartum Summary (APS) profile from IHE’s PCC technical framework to illustrate some of the basics of creating a CDA with sections. Note that the tutorial won’t encapsulate the entire APS creation process, just the highlights.
The first thing that I always do to make creating CDA’s a little easier to some scaffolding code. For this project, we’ll create a new Visual Studio console application and add references to:
- MARC.Everest.dll
- MARC.Everest.RMIM.UV.CDAr2.dll
- MARC.Everest.Formatters.XML.ITS1.dll
- MARC.Everest.Formatters.XML.DT.R1.dll
One of the things we’ll want to do is print, or save our CDA to the console or file. To do this, we’ll create a static class called EverestUtils which contains some helper functions, I’ve called my function PrintStructure:
/// <summary> /// Print a structure to the console /// </summary> public static void PrintStructure(IGraphable structure) { // Create a formatter, this takes a model in memory and outputs it in XML using (XmlIts1Formatter fmtr = new XmlIts1Formatter()) { fmtr.Settings = SettingsType.DefaultUniProcessor; // We want to use CDA data types using (ClinicalDocumentDatatypeFormatter dtfmtr = new ClinicalDocumentDatatypeFormatter()) { // This is a good idea to prevent validation errors fmtr.ValidateConformance = false; // This instructs the XML ITS1 Formatter we want to use CDA datatypes fmtr.GraphAides.Add(dtfmtr);// Output in a nice indented manner using (XmlWriter xw = XmlWriter.Create(Console.Out, new XmlWriterSettings() { Indent = true })) { fmtr.Graph(xw, structure); } } }
}
This function takes an IGraphable (known as a structure in Everest) and writes it to the console.
One of the things we’ll also need to do in our program is to pass complex data around such as information about people, locations, identifiers, etc. If we were doing a real integration, these would be your model classes from your internal data sources. I’m just going to create a simple set of classes to store this data.
public class PatientData { public String GivenName { get; set; } public String FamilyName { get; set; } public DateTime DateOfBirth { get; set; } public String Address { get; set; } public String Gender { get; set; } public String Id { get; set; } public String MothersId { get; set; } public String MothersName { get; set; } public string City { get; set; } public string State { get; set; } public List<keyvaluepair><string , string>> OtherIds { get; set; } }public class PhysicianData { public string Name { get; set; } public string AddressLine { get; set; } public string City { get; set; } public string Postal { get; set; } public string Id { get; set; } public string PhysicianId { get; set; } public string[] PhysicianName { get; set; } }
These classes are in no way complete and merely will be used to illustrate where data goes in the CDA.
In Everest, documents are constructed and stored in a ClinicalDocument instance. The ClinicalDocument instance represents the HL7 model for a CDA document. In order to make CDA construction a little easier I’m going to write a class called CdaUtils which will be used throughout the rest of the tutorial to construct the pieces of a CDA.
Identifiers in CDA consist of two parts: a domain identifier in the form of an OID which identifies the organization or authority of an identity, and the identifier. These are represented in an Instance Identifier (II) as root and extension properties respectively. My data model uses a KeyValuePair<String,String> to hold this data, so we need to map it.
/// <summary> /// Translates a dictionary of oid/ids to a set of II /// </summary> public static SET<II> CreateIdList(List<KeyValuePair<string, string>> identifiers) { SET<II> retVal = new SET<II>(); foreach (var id in identifiers) retVal.Add(new II(id.Key, id.Value)); return retVal;}
All CDA’s (regardless of the template) will use the same model ClinicalDocument. In order to increase reuse, I’m going to write a method called CreateCDAHeader which will be responsible for creating a CDA header in a generic way.
The first part of this function will create the ClinicalDocument object, and set some key properties. In a CDA these are title, template identifiers, confidentiality code, and date/time of creation.
private static ClinicalDocument CreateCDA(LIST<II> templateIds, CE<String> code, ST title, TS docDate, PatientData recordTarget, PatientData motherOfRCT, PatientData fatherOfRCT, PhysicianData author, params Section[] sections) { // First, we need to create a clinical document ClinicalDocument doc = new ClinicalDocument() { TemplateId = templateIds, // Identifiers Id = Guid.NewGuid(), // A unique id for the document Code = code, // The code for the document Title = title, // TypeId = new II("2.16.840.1.113883.1.3", "POCD_HD000040"), // This value is static and identifies the HL7 type RealmCode = SET<CS<BindingRealm>>.CreateSET(BindingRealm.UniversalRealmOrContextUsedInEveryInstance), // This is UV some profiles require US EffectiveTime = docDate, ConfidentialityCode = x_BasicConfidentialityKind.Normal, // Confidentiality of data within the CDA LanguageCode = "en-US", // Language of the CDA };
Next, the CDA will need to be assigned a patient. In HL7v3 speak this is the RecordTarget. The data for RecordTarget is passed as my PatientData object in my model. I will map this to the CDA RecordTarget
// Next we need to append the RecordTarget to the CDA RecordTarget rctPatient = new RecordTarget() { ContextControlCode = ContextControl.OverridingPropagating, PatientRole = new PatientRole() { Id = CreateIdList(recordTarget.OtherIds), // These are "other MRNs" we know about in the CDA Addr = SET<AD>.CreateSET(AD.FromSimpleAddress(PostalAddressUse.HomeAddress, recordTarget.Address, null, recordTarget.City, recordTarget.State, "CA", null)), // We create the address from parts Patient = new Patient() { Name = SET<PN>.CreateSET(PN.FromFamilyGiven(EntityNameUse.Legal, recordTarget.FamilyName, recordTarget.GivenName)), // PAtient name AdministrativeGenderCode = recordTarget.Gender, // GEnder BirthTime = new TS(recordTarget.DateOfBirth, DatePrecision.Day) // Day of birth } } }; // WE also need to add our local identifier (example: from our database/MRN) to the CDA // You will need to use your own OID rctPatient.PatientRole.Id.Add(new II("1.2.3.4.5.6", recordTarget.Id));doc.RecordTarget.Add(rctPatient);
Next, we need to attach the Author data to the CDA, this is done via the Author property:
// We now want to create an author Author docAuthor = new Author() { ContextControlCode = ContextControl.AdditivePropagating, Time = docDate, // When the document was created AssignedAuthor = new AssignedAuthor() { Id = SET<II>.CreateSET(new II("1.2.3.4.5.6.1", author.PhysicianId)), // Physician's identifiers (or how we know the physician) Addr = new SET<AD>() { AD.FromSimpleAddress(PostalAddressUse.PhysicalVisit, author.AddressLine, null, author.City, "ON", "CA", author.Postal) }, // The author's address AssignedAuthorChoice = AuthorChoice.CreatePerson( SET<PN>.CreateSET(PN.FromFamilyGiven(EntityNameUse.Legal, author.PhysicianName[0], author.PhysicianName[1])) // The author's name ), RepresentedOrganization = new Organization() { Id = new SET<II>(new II("1.2.3.4.5.6.2", author.OrgId)), // Organization the physician represents Name = SET<ON>.CreateSET(new ON(null, new ENXP[] { new ENXP(author.OrgName) })) // The name of the organization } } }; doc.Author.Add(docAuthor);
The next step is to assign a custodian. A custodian in CDA speak (paraphrasing) is the organization that is responsible for maintaining the document.
// The document custodian is the source of truth for the document, i.e. the organization // that is responsible for storing/maintaining the document. Custodian docCustodian = new Custodian( new AssignedCustodian( new CustodianOrganization( SET<ii>.CreateSET(new II("2.3.4.5.6.7", "304930-3")), new ON(null, new ENXP[] { new ENXP("ACME Medical Centres") }), null, AD.FromSimpleAddress(PostalAddressUse.PhysicalVisit, "123 Main St.", null, "Hamilton", "ON", "CA", "L0R2A0") ) ) ); doc.Custodian = docCustodian;
Next, the function will assign the Mother and Father of the patient if they’re available, this process is similar to the record target and is done via the Participation property.
// If the "mother" of the patient is provided, let's add the mother's data if (motherOfRCT != null) { doc.Participant.Add(new Participant1(ParticipationType.IND, ContextControl.OverridingNonpropagating, null, new IVL<TS>(new TS(recordTarget.DateOfBirth, DatePrecision.Year)), null) { AssociatedEntity = new AssociatedEntity(RoleClassAssociative.NextOfKin, CreateIdList(motherOfRCT.OtherIds), new CE<string>("MTH", "2.16.840.1.113883.5.111", null, null, "Mother", null), SET<AD>.CreateSET(AD.FromSimpleAddress(PostalAddressUse.HomeAddress, motherOfRCT.Address, null, motherOfRCT.City, motherOfRCT.State, "CA", null)), SET<TEL>.CreateSET(new TEL() { NullFlavor = NullFlavor.NoInformation }), new Person(SET<PN>.CreateSET(PN.FromFamilyGiven(EntityNameUse.Legal, motherOfRCT.FamilyName, motherOfRCT.GivenName))), null) }); }if (fatherOfRCT != null) { doc.Participant.Add(new Participant1(ParticipationType.IND, ContextControl.OverridingNonpropagating, null, new IVL<TS>(new TS(recordTarget.DateOfBirth, DatePrecision.Year)), null) { AssociatedEntity = new AssociatedEntity(RoleClassAssociative.NextOfKin, CreateIdList(fatherOfRCT.OtherIds), new CE<string>("FTH", "2.16.840.1.113883.5.111", null, null, "Father", null), SET<AD>.CreateSET(AD.FromSimpleAddress(PostalAddressUse.HomeAddress, fatherOfRCT.Address, null, fatherOfRCT.City, fatherOfRCT.State, "CA", null)), SET<TEL>.CreateSET(new TEL() { NullFlavor = NullFlavor.NoInformation }), new Person(SET<PN>.CreateSET(PN.FromFamilyGiven(EntityNameUse.Legal, fatherOfRCT.FamilyName, fatherOfRCT.GivenName))), null) }); }
Finally, the sections are added to the document (the ones that were passed in).
// Next we want to setup the body of the document doc.Component = new Component2( ActRelationshipHasComponent.HasComponent, true, new StructuredBody() // This document will be structured document );// Now we add the sections passed to us foreach (var sct in sections) doc.Component.GetBodyChoiceIfStructuredBody().Component.Add(new Component3( ActRelationshipHasComponent.HasComponent, true, sct)); return doc;
}
The next function in our CDA utility class will call the CreateCDA function to construct a CDA header which claims it is an Antepartum Summary (APS) document. It is possible to adapt this function to create any type of CDA template. This function understands “father” to mean father of the fetus (if applied). This is a special requirement of the IHE PCC APS profile.
public static ClinicalDocument CreateAPSDocument(PatientData recordTarget, PatientData father, PhysicianData author, DateTime docDate, params Section[] sections) { var doc = CreateCDA( LIST<II>.CreateList(new II("1.3.6.1.4.1.19376.1.5.3.1.1.2"), new II("1.3.6.1.4.1.19376.1.5.3.1.1.11.2")), new CE<String>("57055-6", "2.16.840.1.113883.6.1", "LOINC", null, "Antepartum Summary Note", null), "Antepartum Summary", docDate, recordTarget, null, null, author, sections );// Father of fetus if (father != null) { SET<II> fatherIds = new SET<II>(); foreach (var id in father.OtherIds) fatherIds.Add(new II(id.Key, id.Value)); doc.Participant.Add(new Participant1(ParticipationType.IND, ContextControl.OverridingNonpropagating, null, new IVL<TS>(new TS(recordTarget.DateOfBirth, DatePrecision.Year)), null) { AssociatedEntity = new AssociatedEntity(RoleClassAssociative.NextOfKin, fatherIds, new CE<string>("xx-fatherofbaby", "2.16.840.1.113883.6.96", null, null, "Father of fetus", null), SET<AD>.CreateSET(AD.FromSimpleAddress(PostalAddressUse.HomeAddress, father.Address, null, father.City, father.State, "CA", null)), SET<TEL>.CreateSET(new TEL() { NullFlavor = NullFlavor.NoInformation }), new Person(SET<PN>.CreateSET(PN.FromFamilyGiven(EntityNameUse.Legal, father.FamilyName, father.GivenName))), null) }); } return doc;
}
The last step is to edit our main() function to actually create this CDA.
static void Main(string[] args) { PatientData pat = new PatientData() { Address = "123 Main Street West", City = "Hamilton", DateOfBirth = new DateTime(1995, 04, 03), FamilyName = "Smith", Gender = "F", GivenName = "Sarah", Id = "102-30343", OtherIds = new List<keyvaluepair><string , string>>() { new KeyValuePair<string , string>("2.16.2.3.2.3.2.4", "123-231-435") }, State = "ON" };PhysicianData aut = new PhysicianData() { AddressLine = "35 King Street West", City = "Hamilton", OrgId = "123-1221", OrgName = "Good Health Clinics", PhysicianId = "1023433-ON", PhysicianName = new string[] { "Dr.", "Francis", "F", "Family" }, Postal = "L0R2A0" }; // Create the CDA ClinicalDocument doc = CdaUtil.CreateAPSDocument(pat, null, aut, DateTime.Now); EverestUtils.PrintStructure(doc); Console.ReadKey();
}
When we run the completed program the output will be shown on the console:
The next tutorial will illustrate how the document can be enriched with sections.