I have looked into a simple design pattern today and decided to have a go with a generic implementation of this.
The class is sealed with a private constructor. It will for a generic pattern of type T instantiate a 'Singleton', i.e.
same objects will be returned via the static Instance property for a given type T. It is also possible to 'Init' to a custom
object in case you want to set the Singleton object to a custom inited object of type T. If you skip initing call -
this will just use the parameterless constructor. But you can only do a call to Init once (i.e. call the Init method) ,
so do this initing only in the application startup code for example.
Of course, we could instead create a more distilled version, disallowing 'Initing' singleton objects not allowing sending in an adjusted
instance of type T as the inited singleton - and also Singleton is considered
a pattern which should be implemented per class and not in a generic manner like this. Also, it violates the Single responsibility principle
as the object of type T has not control of how in instantiates itself. But Singleton pattern is an accepted creational pattern.
publicsealedclassSingleton<T> whereT : class, new()
{
privatestatic Lazy<T> InstanceProxy
{
get
{
if (_instanceObj?.IsValueCreated != true)
{
_instanceObj = new Lazy<T>(() => new T());
}
return _instanceObj;
}
}
privatestatic Lazy<T>? _instanceObj;
publicstatic T Instance { get { return InstanceProxy.Value; } }
publicstaticvoidInit(Lazy<T> instance)
{
if (_instanceObj?.IsValueCreated == true)
{
thrownew ArgumentException($"A Singleton for the type <T> is already set");
}
_instanceObj = instance ?? thrownew ArgumentNullException(nameof(instance));
}
privateSingleton()
{
}
}
var aeroplane = new Aeroplane
{
Manufacturer = "Boeing",
Model = "747",
PassengerCount = 350,
YearBuilt = 2005
};
var aeroPlane3 = Singleton<Aeroplane>.Instance;
var aeroPlane4 = Singleton<Aeroplane>.Instance;
Console.WriteLine($"Aeroplane3 and aeroplane4 is same object? {Object.ReferenceEquals(aeroPlane3, aeroPlane4)}");
Outputs 'true'.
Trying to re-init type T Singleton to another object fails :
var aeroplane2 = new Aeroplane
{
Manufacturer = "Sopwith Aviation Company",
Model = "Sophwith Camel",
PassengerCount = 1,
YearBuilt = 1917
};
Singleton<Aeroplane>.Init(new Lazy<Aeroplane>(aeroplane2));
You can of course just access the Singleton with initing it - as mentioned it will call the default public constructor of type T and set an this 'default' instance of T as the singleton for type T.
Possible you could have a way of setting a custom constructor here instead of passing an object as a sort of improved 'factory pattern'. We could for example in the Init method specify which constructor method
to call in the initing and pass in parameters for example.
var aeroplaneDefaultInstantiated = Singleton<Aeroplane>.Instance;
Note : Default instantiation - calls the parameterless public constructor of type T. So you must do the initialization inside the public parameterless constructor if you skip the Init method.
We can also allow sending in a custom constructor to the Singleton class by offering another init method. Here, we can send in parameters of given types and therefore identify the constructor to call. This of course demands that such a constructor exists.
It offers another way of setting a singleton object. We now can either set the singleton object for type T via :
An instance that calls the default parameterless constructor and sets this object as the singleton for type T
A customized instance in case you want to have more fine tuned initalized object to be set as the singleton for type T
An instance that calls a specified constructor of the type T and sets the created instance as the singleton for type T
Sample I tested this out with (dump method calls below are done inside Linqpad 7 - so paste this code into there) :
voidMain()
{
Singleton<Aeroplane>.Init(newobject[] { "Nieuport IV", 1911 });
//Singleton<Aeroplane>.Init(new object[] { "Nieuport V", 1914 });var aeroplaneTwo = Singleton<Aeroplane>.Instance;
var aeroplaneThree = Singleton<Aeroplane>.Instance;
Object.ReferenceEquals(aeroplaneTwo, aeroplaneThree).Dump();
aeroplaneTwo.Dump();
aeroplaneThree.Dump();
}
publicclassAeroplane
{
publicstring? Model { get; set; }
publicstring? Manufacturer { get; set; }
publicint YearBuilt { get; set; }
publicint PassengerCount { get; set; }
publicAeroplane()
{
}
publicAeroplane(string model, int yearBuilt)
{
Model = model;
YearBuilt = yearBuilt;
if (YearBuilt < 1913) {
PassengerCount = 1;
}
}
}
publicsealedclassSingleton<T> whereT : class, new()
{
privatestatic Lazy<T> InstanceProxy
{
get
{
if (_instanceObj?.IsValueCreated != true)
{
_instanceObj = new Lazy<T>(() => new T());
}
return _instanceObj;
}
}
privatestatic Lazy<T>? _instanceObj;
publicstaticvoidInit(object[] constructorParams)
{
if (_instanceObj?.IsValueCreated == true)
{
thrownew ArgumentException($"A Singleton for the type <{typeof(T).Name}> is already set");
}
var constructor = typeof(T).GetConstructor(constructorParams.Select(p => p.GetType()).ToArray());
if (constructor == null)
{
string typenamesParams = string.Join(",", constructorParams.Select(p => p.GetType()));
thrownew ArgumentException($"Could not find a constructor of type {typeof(T).Name} with the parameter types {typenamesParams}");
}
var instanceObj = constructor.Invoke(constructorParams);
_instanceObj = new Lazy<T>(instanceObj as T);
}
publicstatic T Instance { get { return InstanceProxy.Value; } }
publicstaticvoidInit(Lazy<T> instance)
{
if (_instanceObj?.IsValueCreated == true)
{
thrownew ArgumentException($"A Singleton for the type <T> is already set");
}
_instanceObj = instance ?? thrownew ArgumentNullException(nameof(instance));
}
privateSingleton()
{
}
}
Once more, we disallow calling the Init method many times, here we call a specific constructor to init as the Singleton object.
I have been looking into repository pattern for EF Core today. This is what I got :
using SomeAcme.Interfaces;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
namespaceSomeAcme.DAL.Pattern
{
publicclassRepository<T> : IRepository<T> whereT : class
{
private System.Data.Entity.DbContext GetContext()
{
returnnew SomeAcmeDbContext as System.Data.Entity.DbContext;
}
private T ExecuteQuery(Func<T, System.Data.Entity.DbContext, T> query, T entity)
{
using (var context = GetContext())
{
T result = query(entity, context);
return result;
}
}
public T Add(T entity, bool saveImmediate = false)
{
return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
{
var entityDb = dbContext.Set<T>().Add(entity);
if (saveImmediate)
SaveChanges(dbContext);
return entityDb;
}, entity);
}
public T Add(T entity, bool saveImmediate = false, paramsobject[] keys)
{
return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
{
dbContext.Entry(obj).State = EntityState.Added;
if (saveImmediate)
SaveChanges(dbContext);
var entityInDb = dbContext.Set<T>().Find(keys);
return entityInDb;
}, entity);
}
public IEnumerable<T> AddRange(IEnumerable<T> entities, bool saveImmediate)
{
using (var dbContext = GetContext())
{
var entitites = dbContext.Set<T>().AddRange(entities);
if (saveImmediate)
SaveChanges(dbContext);
return entitites;
}
}
public T Delete(bool saveImmediate = false, paramsobject[] keyValues)
{
using (var dbContext = GetContext())
{
var entity = dbContext.Set<T>().Find(keyValues);
if (entity == null)
returnnull;
var entry = dbContext.Entry(entity);
if (entry == null)
returnnull;
entry.State = EntityState.Deleted;
if (saveImmediate)
SaveChanges(dbContext);
return entity;
}
}
///<summary>/// Note - requiring here that we have defined primary key(s) on the target tables ! ///</summary>///<param name="keyValues"></param>///<returns></returns>public T Get(paramsobject[] keyValues)
{
using (var dbContext = GetContext())
{
var entity = dbContext.Set<T>().Find(keyValues);
dbContext.Entry(entity).State = EntityState.Detached;
return entity;
}
}
public IList<T> GetAll(bool asNoTracking = true)
{
using (var dbContext = GetContext())
{
return asNoTracking ? dbContext.Set<T>().AsNoTracking().ToList() : dbContext.Set<T>().ToList();
}
}
public IList<T> GetAllByCondition(Expression<Func<T, bool>> condition, bool asNoTracking = true)
{
using (var dbContext = GetContext())
{
IQueryable<T> query = asNoTracking ? dbContext.Set<T>().AsNoTracking() : dbContext.Set<T>();
var entities = query.Where(condition);
return entities.ToList();
}
}
public T GetFirstByCondition(Expression<Func<T, bool>> condition)
{
return GetAllByCondition(condition).FirstOrDefault();
}
public T GetByKeyValues(bool asNoTracking, paramsobject[] keyValues)
{
using (var dbContext = GetContext())
{
var entity = asNoTracking ? dbContext.Set<T>().AsNoTracking().FirstOrDefault() : dbContext.Set<T>().Find(keyValues);
return entity;
}
}
publicvoidSaveChanges(object context)
{
var dbContext = context as System.Data.Entity.DbContext;
if (dbContext == null)
{
thrownew ArgumentException("Must be of type System.Data.Entity.DbContext", nameof(context));
}
dbContext.SaveChanges();
}
public T Update(T entity, bool saveImmediate = false, paramsobject[] keyValues)
{
return ExecuteQuery((T obj, System.Data.Entity.DbContext dbContext) =>
{
var entityInDb = dbContext.Set<T>().Find(keyValues);
if (entityInDb == null)
returnnull;
dbContext.Entry(entityInDb).CurrentValues.SetValues(obj);
if (saveImmediate)
{
SaveChanges(dbContext);
}
return obj;
}, entity);
}
}
}
And here are some unit tests against a database of mine (containing some integration tests)
using AutoMapper;
using FluentAssertions;
using SomeAcme.Common;
using SomeAcme.Common.DataContract;
using SomeAcme.Data.EntityFramework.Managers;
using SomeAcme.Data.EntityFramework.Models;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
namespaceSomeAcme.Service.Implementation.Test.Pattern
{
[TestFixture]
publicclassRepositoryTest
{
private IMapper _mapper;
[SetUp]
publicvoidTestInitialize()
{
IntegrationTestBootstrapper.Run();
System.Threading.Thread.CurrentPrincipal = new ConcreteClaimsPrincipal(SomeAcme.Administrator, "107455", "Testutvikler, Ivrig");
var configuration = new MapperConfiguration(cfg =>
{
cfg.CreateMap>PasSystem, PasSystemDataContract>();
});
#if DEBUG// only during development, validate your mappings; remove it before release// configuration.AssertConfigurationIsValid();#endif
_mapper = configuration.CreateMapper();
}
[Test]
publicvoidGetAllReturnsExpected()
{
var pasSystemRepository = new Repository>PasSystem>();
var allPasSystem = pasSystemRepository.GetAll()?.Select(x => _mapper.Map>PasSystemDataContract>(x)).ToList();
allPasSystem?.Count().Should().BePositive();
}
[Test]
publicvoidGetReturnsExpected()
{
var jobTitleRepo = new Repository>Title>();
var jobTitle = jobTitleRepo.Get(6);
Assert.IsNotNull(jobTitle);
jobTitle.Text.Should().Be("Anestesisykepleier");
}
[Test]
publicvoidAddRangeDoesNotThrow()
{
var postponementCauseRepo = new Repository>PostponementCause>();
var postponementCauses = new List>PostponementCause>()
{
new PostponementCause{ FreshOrganizationalUnitId = 107455, IsActive = true, Text = "Personel not available"},
new PostponementCause{ FreshOrganizationalUnitId = 107455, IsActive = true, Text = "Personel already busy"},
};
postponementCauseRepo.AddRange(postponementCauses, true);
}
[Test]
publicvoidGetByConditionReturnsNonEmpty()
{
var timeMatrixRepository = new Repository>TimeMatrix>();
var timematrices = timeMatrixRepository.GetAllByCondition(t => t.PreOperation > 80).ToList();
timematrices.Count().Should().BePositive();
}
[Test]
publicvoidAddTimeMatrixToDbViaRepositorySucceeds()
{
var timeMatrixRepository = new Repository>TimeMatrix>();
var timeMatrix = new TimeMatrix
{
Code = "T100",
IsActive = false,
PostOperation = 11,
PreOperation = 11,
};
var timeMatrixSavedToDb = timeMatrixRepository.Add(timeMatrix, true);
timeMatrixSavedToDb.TimeMatrixId.Should().BePositive();
}
[Test]
publicvoidAddAndUpdateTimeMatrixToDbViaRepositorySucceeds()
{
var timeMatrixRepository = new Repository>TimeMatrix>();
var timeMatrix = new TimeMatrix
{
Code = "T100",
IsActive = false,
PostOperation = 11,
PreOperation = 11,
};
var timeMatrixSavedToDb = timeMatrixRepository.Add(timeMatrix, true);
timeMatrixSavedToDb.TimeMatrixId.Should().BePositive();
timeMatrix.Code = "T200";
timeMatrix.PreOperation = 11;
timeMatrixSavedToDb = timeMatrixRepository.Update(timeMatrix, true, timeMatrixSavedToDb.TimeMatrixId);
timeMatrixSavedToDb.Code.Should().Be("T200");
timeMatrix.PreOperation.Should().Be(11);
}
[Test]
publicvoidAddAndUpdateAndDeleteTimeMatrixToDbViaRepositorySucceeds()
{
var timeMatrixRepository = new Repository>TimeMatrix>();
var timeMatrix = new TimeMatrix
{
Code = "T100",
IsActive = false,
PostOperation = 11,
PreOperation = 11,
};
var timeMatrixSavedToDb = timeMatrixRepository.Add(timeMatrix, true);
timeMatrixSavedToDb.TimeMatrixId.Should().BePositive();
timeMatrix.Code = "T300";
timeMatrix.PreOperation = 11;
timeMatrixSavedToDb = timeMatrixRepository.Update(timeMatrix, true, timeMatrixSavedToDb.TimeMatrixId);
timeMatrixSavedToDb.Code.Should().Be("T300");
timeMatrixSavedToDb.PreOperation.Should().Be(11);
timeMatrixSavedToDb = timeMatrixRepository.Delete(true, timeMatrixSavedToDb.TimeMatrixId);
timeMatrixSavedToDb.Should().NotBeNull();
}
[Test]
publicvoidAddPostOfficeToDbViaRepositorySucceeds()
{
var postOfficeRepository = new Repository>PostOffice>();
var postOffice = new PostOffice
{
PostalPlace = "Steinkjer",
PostalCode = "7724"
};
var postOfficeSavedToDb = postOfficeRepository.Add(postOffice, true);
postOfficeSavedToDb.Should().NotBeNull();
}
}
}
Also note the usage of Auto mapper here to automatically map between POCO entity objects to DTO (Data transfer objects, usually data contracts for example).
Building a useful repository pattern in EF Core and combining it with Automapper (available on nuget) will probably reduce your Data Access Layer logic a bit .. In many cases maybe an understatement..
Note - this code have passed unit tests, but not been used in production (yet). Methods that demands keyValues to find entities do require your table to have primary keys on the table. This still is 'demo code' and WIP (Work in progress).
Works !
This article shows some extension methods to extract data from FHIR bundles, where FHIR stands for Fast Healthcare Interoperability Resources. The standard is used as a global or country specific standard with its own variants.
It is also a standard that allows for extensibility and its goal is to define interoperability and an information model definining resources which are then comprised of smaller elements which can define different kinds of information.
FHIR also defines API standards and is defined in different formats such as XML and json.
We will look into some example extension methods for retrieving data deep inside a FHIR Bundle. A bundle is a top level data which got a lot of components in a hierarchical structure as we define the data in XML or JSON for example, i.e. a
tree structure.
Let us say that we want to retrieve data like medications a pasient is taking as his or her DDD (Defined Daily Dosage). We want to be able to find a medication statement inside our bundle and then retrieve the medication dosage quantity. We know
the unit is measured in micrograms (ug) and the medication (drug) is called Fentanyl. Here is a property getter with logic to retrieve this value.
publicint? Fentanyl
{
get
{
var dosageQuantity = _bundle.SearchMedicationStatements("http://someacme.no/fhir/MedicationStatement/")
?.GetMedicationDosageQuantity("Fentanyl", "ug");
//value is already a decimal? data type and must be parsed if (int.TryParse(dosageQuantity?.Value?.ToString(), outvar dosageQuantityParsed))
{
return dosageQuantityParsed;
}
returnnull;
}
}
We have these two extension methods to help us with retrieving the data :
publicstatic List<MedicationStatement>? SearchMedicationStatements(this Bundle bundle, string resourcePath)
{
var medicationStatementsMatching = bundle?.Entry?.Where(e => e.FullUrl.StartsWith(resourcePath))?.Select(m => m.Resource)?.OfType<MedicationStatement>()?.ToList();
return medicationStatementsMatching;
}
publicstatic Dosage? GetMedicationDosageDosage(this List<MedicationStatement> medicationStatements, string displayText)
{
//find dosage with given display text foreach (var medicationStatement in medicationStatements)
{
var medicationStatementMedication = medicationStatement?.Medication as CodeableConcept;
if (medicationStatementMedication == null)
{
continue;
}
var medicationCoding = medicationStatementMedication?.Coding?.FirstOrDefault(med => med.Display?.Equals(displayText, StringComparison.InvariantCultureIgnoreCase) == true);
if (medicationCoding != null)
{
var quantity = medicationStatement?.Dosage?.FirstOrDefault();
return quantity;
}
}
returnnull;
}
And our unit test will then be very simply with some static Fhir bundle data like this :
Now, we have used some classes here as you have seen called FhirJsonParser and MedicationStatements. These classes and functionality is available in some selected nuget packages :
This packages are licensed under BSD-3 license and are 'free' as long as you include the copyright notice. See this url for more info - it is the HL7 FHIR SDK for Microsoft .net platform.
As you see we have additional nuget packages for (de)serialization and Poco objects. We also have common interfaces and classes in the HL7.Fhir.Support and HL7.Fhir.Poco nuget packages.
This makes it way easier to work with a large bundle of Fhir data.
Now, about sample data - this is often given in a .json file by the for example another organization, that you want to integrate with. FHIR is about interoperability and a common understanding of different health information systems via a common standard.
The sample data starts with these data at the top - a bundle and a diagnosticreport. Of course, FHIR is a very large standard and which data you work against will vary a lot.
As you can see we also have something called Snomed SCT codes in our data. The meaning of these codes can be looked up online. International edition is here: https://browser.ihtsdotools.org/?
Our medication drug statment for Fentanyl is defined further into the FHIR json bundle.
As you can see, a FHIR json bundle will be quite lengthy and my sample file, which is a sample diagnostic report for coloscopy is right above 1000 lines and 33 kilobytes. To retrieve the medication dosage we need to go deep into the FHIR json structure sometimes and sometimes look at sibling nodes or further down. What helped me a lot while creating the extension methods I will mention next, was debugging and looking into the immediate window and inspect which kind of Entry it is. An Entry is a generic term which describes that our component is a general term in FHIR which can be many different types, such as a MedicationStatment and contain a Codable concept which ultimately will contain the dose and quantity of our medication (drug).
What I did was the following to make working with the FHIR bundle in a more code friendly manner :
GENERIC APPROACH - Implement a mapping from a FHIR bundle into a DOMAIN MODEL which then can be used in your SYSTEM
Use the debugger and unit tests and explore in the immediate window which kind of Entry each component in the FHIR bundle is. Identity if we can cast an Entry into a correct subtype, such as a MedicationStatement. These POCO objects are available in the noted nuget packages above.
After finding a way to retrieve the data - generalize the retrival into extension methods, which can be chained and then make use of these extension methods into property getter logic.
Use a TDD approach to retrieve the data. Each property was found in the sample doc for me (about 100 properties) and I was trying to find generic ways to find these property values
Sometimes you need fine tuned logic too to find data. FHIR contains some extensions and different FHIR bundles, although it is a standard, may vary some.
Okay, here is all the extension methods I made for this case. I have masked the real organization names here. My approach can be used in many different scenarios for retrieving FHIR bundle data.
using Hl7.Fhir.Model;
usingstatic Hl7.Fhir.Model.Observation;
namespaceSomeAcme.FhirFacade.SomeProduct.HelperMethods
{
///<summary>/// Helper methods for Fhir bundle. Generic use helper methods. ///</summary>publicstaticclassFhirHelperExtensions
{
publicstaticstring? SearchForPractitioner(this Bundle bundle, string procedurePath, string functionRole)
{
var performer = bundle.SearchForProcedure(procedurePath)?
.Performer?.FirstOrDefault(p => p?.Function?.Coding?.FirstOrDefault()?.Display?.Equals(functionRole, StringComparison.InvariantCultureIgnoreCase) == true);
return performer?.Function?.Coding?.FirstOrDefault()?.Code;
}
///<summary>/// Looks up an identifier value (e.g. hpr number or similar) of practitioner///</summary>///<param name="practitioner"></param>///<returns></returns>publicstaticstring? GetPractitionerIdentifierValue(this Practitioner practitioner)
{
return practitioner?.Identifier?.FirstOrDefault()?.Value;
}
publicstatic Organization? SearchForOrganization(this Bundle bundle, string resourcePath, bool startsWith = true)
{
if (startsWith)
{
var organizationMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Organization;
return organizationMatching;
}
var organization = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Organization;
return organization;
}
publicstatic MedicationStatement? SearchMedicationStatement(this Bundle bundle, string resourcePath, bool startsWith = true)
{
if (startsWith)
{
var medicationStatementMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as MedicationStatement;
return medicationStatementMatching;
}
var medicationStatement = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as MedicationStatement;
return medicationStatement;
}
publicstatic List<MedicationStatement>? SearchMedicationStatements(this Bundle bundle, string resourcePath)
{
var medicationStatementsMatching = bundle?.Entry?.Where(e => e.FullUrl.StartsWith(resourcePath))?.Select(m => m.Resource)?.OfType<MedicationStatement>()?.ToList();
return medicationStatementsMatching;
}
publicstatic Dosage? GetMedicationDosageDosage(this List<MedicationStatement> medicationStatements, string displayText)
{
//find dosage with given display text foreach (var medicationStatement in medicationStatements)
{
var medicationStatementMedication = medicationStatement?.Medication as CodeableConcept;
if (medicationStatementMedication == null)
{
continue;
}
var medicationCoding = medicationStatementMedication?.Coding?.FirstOrDefault(med => med.Display?.Equals(displayText, StringComparison.InvariantCultureIgnoreCase) == true);
if (medicationCoding != null)
{
var quantity = medicationStatement?.Dosage?.FirstOrDefault();
return quantity;
}
}
returnnull;
}
publicstatic Quantity? GetMedicationDosageQuantity(this List<MedicationStatement> medicationStatements, string displayText, string? expectedUnitname = null)
{
//find quantity for dosage with given display text foreach (var medicationStatement in medicationStatements)
{
var medicationStatementMedication = medicationStatement?.Medication as CodeableConcept;
if (medicationStatementMedication == null)
{
continue;
}
var medicationCoding = medicationStatementMedication?.Coding?.FirstOrDefault(med => med.Display?.Equals(displayText, StringComparison.InvariantCultureIgnoreCase) == true);
if (medicationCoding != null)
{
if (medicationStatement?.Dosage?.FirstOrDefault()?.DoseAndRate?.FirstOrDefault()?.Dose is Quantity quantity)
{
if (!string.IsNullOrWhiteSpace(expectedUnitname) && expectedUnitname.Equals(expectedUnitname, StringComparison.InvariantCultureIgnoreCase))
{
return quantity;
}
returnnull; //found the right dosage - but the unit name does not agree
}
}
}
returnnull;
}
publicstaticstring? GetOrganizationIdentifierValue(this Organization organization)
{
return organization?.Identifier?.FirstOrDefault()?.Value;
}
publicstatic Observation? SearchForObservation(this Bundle bundle, string resourcePath, bool startsWith = true)
{
if (startsWith)
{
var observationMatching = bundle?.Entry?.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Observation;
return observationMatching;
}
var observation = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Observation;
return observation;
}
publicstatic ComponentComponent? GetObservationComponent(this Observation observation, string observationComponentDisplayText)
{
//TODO : these observations is not the same as the observation in SomeProduct and must be additionally mapped (enums does not agree) foreach (var observationComponent in observation.Component)
{
if (observationComponent?.Code?.Coding?.Any() != true)
{
continue;
}
foreach (var observationEntry in observationComponent.Code.Coding)
{
if (observationEntry?.Display.Contains(observationComponentDisplayText, StringComparison.InvariantCultureIgnoreCase) == true)
{
return observationComponent;
}
}
}
returnnull;
}
publicstaticstring? GetObservationComponentCodeValue(this Observation observation, string observationComponentDisplayText)
{
//TODO : these observations is not the same as the observation in Gastronet and must be additionally mapped (enums does not agree) foreach (var observationComponent in observation.Component)
{
if (observationComponent?.Code?.Coding?.Any() != true)
{
continue;
}
foreach (var observationEntry in observationComponent.Code.Coding)
{
if (observationEntry?.Display.Contains(observationComponentDisplayText, StringComparison.InvariantCultureIgnoreCase) == true)
{
return observationEntry?.Code;
}
}
}
returnnull;
}
publicstatic Patient? SearchForPatient(this Bundle bundle, string resourcePath, bool startsWith = true)
{
if (startsWith)
{
var patientMatching = bundle?.Entry?.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Patient;
return patientMatching;
}
var patient = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Patient;
return patient;
}
publicstatic CodeableConcept? SearchForProcedureReason(this Procedure procedure, string code)
{
return procedure?.ReasonCode?.FirstOrDefault(c => c?.Coding?.FirstOrDefault()?.Code?.Equals(code, StringComparison.InvariantCultureIgnoreCase) == true);
}
publicstatic CodeableConcept? SearchForProcedureReasonViaDisplay(this Procedure procedure, string display)
{
return procedure?.ReasonCode?.FirstOrDefault(c => c?.Coding?.FirstOrDefault()?.Display?.Equals(display, StringComparison.InvariantCultureIgnoreCase) == true);
}
publicstatic Procedure? SearchForProcedure(this Bundle bundle, string resourcePath, bool startsWithMatching = true)
{
if (startsWithMatching)
{
var procedureMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Procedure;
return procedureMatching;
}
var procedure = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Procedure;
return procedure;
}
publicstaticboolSearchForProcedureComplication(this Bundle bundle, string resourcePath, string displayText)
{
var procedure = SearchForProcedure(bundle, resourcePath);
if (procedure?.Complication?.Any() == true)
{
var complications = procedure.Complication.ToList();
var complicationMatching = complications.FirstOrDefault(x => x.Coding?.FirstOrDefault()?.Display?.ToLower() == displayText);
//TODO : consider complicaitonCode here or just mere precense ? Ask Kreftreg ? string complicationCode = complicationMatching?.Coding?.FirstOrDefault()?.Code;//Other developer confirmed checking mere precense is okay. going for this then.return complicationMatching != null;
}
returnfalse;
}
publicstatic Quantity? GetObservationQuantity(this Observation observation)
{
var quantity = observation?.Value as Quantity;
return quantity;
}
publicstatic DiagnosticReport? SearchForDiagnosis(this Bundle bundle, string resourcePath, bool startsWithMatching = true)
{
if (startsWithMatching)
{
var diagnosisMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as DiagnosticReport;
return diagnosisMatching;
}
var diagnosticReport = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as DiagnosticReport;
return diagnosticReport;
}
publicstaticstring? GetDiagnosticProcedureCode(this DiagnosticReport diagnosticReport, int nthProcedureCode)
{
var codes = diagnosticReport?.Code?.Coding?.ToList();
if (codes == null || !codes.Any() || codes.Count < nthProcedureCode+1)
{
returnnull;
}
var coding = codes.ElementAt(nthProcedureCode) as Coding;
return coding.Display;
}
publicstaticstring? GetDiagnosisCode(this DiagnosticReport diagnostic, int position)
{
var diagnoses = diagnostic?.ConclusionCode?.ToList();
if (diagnoses == null)
{
returnnull;
}
if (diagnoses?.Count -1 >= position)
{
try
{
return$"{diagnoses![position]?.Coding?.First().Code}{diagnoses[position]?.Coding.First().Display}";
}
catch { returnnull; }
}
returnnull;
}
publicstatic Extension? SearchForExtensionInsideProcedure(this Procedure procedure, string extensionUrl)
{
var extension = procedure?.Extension?.FirstOrDefault(e => e?.Url == extensionUrl) as Extension;
return extension;
}
publicstatic List<Extension>? SearchForExtensionsInsideProcedure(this Procedure procedure, string extensionUrl)
{
var extensions = procedure?.Extension?.Where(e => e?.Url == extensionUrl)?.ToList();
return extensions;
}
publicstatic CodeableConcept? GetCodeableConceptInsideExtension(this Extension extension)
{
if (extension?.Value == null)
{
returnnull;
}
var codeableConcept = extension?.Value as CodeableConcept;
return codeableConcept;
}
publicstatic Extension? SearchForSubExtensionInsideExtension(this Extension extension, string extensionUrl)
{
var subExtension = extension?.Extension?.FirstOrDefault(e => e?.Url == extensionUrl);
return subExtension;
}
publicstatic Duration? GetExtensionDuration(this Extension extension)
{
var duration = extension?.Value as Duration;
return duration;
}
publicstatic FhirBoolean? GetExtensionBoolean(this Extension extension)
{
return extension?.Value as FhirBoolean;
}
publicstatic Coding? GetExtensionCodeValue(this Extension extension, string url, string system)
{
var subExtension = extension?.Extension?.FirstOrDefault(e => e?.Url == url) as Extension;
var codeContainer = subExtension?.Value as CodeableConcept;
return codeContainer?.Coding?.FirstOrDefault(c => c.System == system);
}
publicstaticstring? GetPatientIdentifier(this Patient patient)
{
return patient?.Identifier?.FirstOrDefault()?.Value;
}
publicstaticstringGetPatientName(this Patient patient)
{
var firstName = (patient?.Name?.FirstOrDefault())?.Given?.FirstOrDefault();
var lastName = patient?.Name?.FirstOrDefault()?.Family;
var middleName = (patient?.Name?.FirstOrDefault()?.Extension?.FirstOrDefault())?.Value?.ToString();
return$"{firstName}{(!string.IsNullOrWhiteSpace(middleName) ? " " + middleName + " " : " ")}{lastName}";
}
}
}
I hope you found this article helpful in case you need to extract data from a FHIR bundle document. I am not specializing into working with FHIR, I just worked 1-2 weeks on such a FHIR bundle document and found my approach to maybe be of general interest and use. At least I found my approach
scalable for mapping each fields. Also note that I made a domain model as a model object where I put logic into the getters of a property and this corresponds to a field in the FHIR JSON bundle we want to retrieve.
So I would then repeat the approach by extending the list of steps to successfully map a FHIR bundle into a domain model which THEN can be used as a better prepared model for INPUT to YOUR system. i.e. we go via a domain model that then can be input to your system where mapping will be trivial inside your system, e.g. save a domain model field into a database or similar if you want to input a FHIR json bundle and create a new POCO entity into your system and store it to a database.
GENERIC APPROACH - Implement a mapping from a FHIR bundle into a DOMAIN MODEL which then can be used in your SYSTEM
Use the debugger and unit tests and explore in the immediate window which kind of Entry each component in the FHIR bundle is. Identity if we can cast an Entry into a correct subtype, such as a MedicationStatement. These POCO objects are available in the noted nuget packages above.
After finding a way to retrieve the data - generalize the retrival into extension methods, which can be chained and then make use of these extension methods into property getter logic. These properties reside in the DOMAIN MODEL object. For example ColoscopyDomainModel .
Use a TDD approach to retrieve the data. Each property was found in the sample doc for me (about 100 properties) and I was trying to find generic ways to find these property values
Sometimes you need fine tuned logic too to find data. FHIR contains some extensions and different FHIR bundles, although it is a standard, may vary some.
+ REMEMBER - then utilize the DOMAIN MODEL which is mapped into YOUR SYSTEM and then save the fields to a database or other storage for example. Or maybe you only want to use the domain model as is without it doing anything else than represent its data from the FHIR bundle.
Of course you will need some more infrastructure around handling FHIR documents, such as a REST API for example, but this article focused on FHIR bundle parsing. And finally, FHIR supports also the formats XML and RDF. The web site of FHIR can explain more, if you want to delve into details. I found
it most helpful just to get started coding here with the GENERIC APPROACH mentioned above.
HL 7 FHIR web site:
http://hl7.org/fhir/