안녕하세요 씨앤텍 시스템즈 최홍준 연구원입니다.
이번 포스트는 Simplifier에서 개발한 Vonk FHIR Server
구축을 해 보았습니다.
해당 Vonk FHIR Server 구축에 필요한 준비사항부터 말씀드리겠습니다.
1. Visual Studio 2019 Community
2. MariaDB (MySQL) or MS-SQL
3. Vonk-license.json (무료 라이센스)
4. POSTMAN
위 준비사항이 준비 되신분들은 다음과 같은 절차에 Vonk FHIR Server를 구축 하실 수 있습니다.
[1.Visual Studio 2019 프로젝트 만들기]
1. ASP .Net Core 웹 어플리케이션을 선택합니다.
2. 프로젝트 이름과 프로젝트 위치를 지정합니다.
3. 프로젝트 구성 선택
해당 프로젝트는 별도의 View와 컨트롤러가 필요가 없기 때문에 비어 있음으로 선택 후 필요한 부분만 클래스 추가하여 진행하실 수 있습니다.
그리고 HTTPS에 대한 구성은 필요하신분은 체크를 하시고 만들기를 눌러주시면 됩니다.
4. 프로젝트 추가하기
해당 사진과 같이 나오셨다면 추가로 하나 더 프로젝트를 생성해야합니다.
위 사진과 같은 프로젝트 형태로 만들어주시면 됩니다.
[2. 필요 패키지 Nuget 다운로드]
Vonk FHIR Server를 구축하기 위해 필요한 패키지는 다음과 같습니다.
번호 | 패키지 명 | 버전 | 패키지 설치 프로젝트 |
1 | EntityFramework | 6.4.4 | Console App Project |
2 | Hl7.Fhir.Specification.STU3 | 1.9.0 | Web Application Project |
3 | NETStandard.Library | 2.0.3 | Console App Project |
4 | Pomelo.EntityFrameworkCore.MySql | 3.1.2 | Console App Project |
5 | Vonk.Core | 3.8.0 | Console App Project |
6 | Vonk.Facade.Relational | 3.8.0 | Console App Project |
7 | Vonk.Fhir.R3 | 3.8.0 | Console App Project |
8 | Vonk.Smart | 3.8.0 | Web Application Project |
해당 패키지를 설치하기 위해 다음과 같이 들어갑니다.
위 사진과 같이 해당 프로젝트에 맞는 패키지를 설치를 진행하시면 됩니다. 설치가 끝나셨다면 "설치됨" 메뉴에서
위 사진처럼 패키지 목록들이 나와있다면 설치가 완료되었습니다.
[3. DataBase에 FHIR Repository 만들기]
Vonk FHIR의 Repository 형태는 큰 제약을 없으므로 자신이 원하는 형태로 DB 스키마를 구성할 수 있습니다.
그래서 (주)씨앤텍시스템에서는 다음과 같은 형태로 스키마를 작성했습니다.
위 사진에서 PatientId는 FK로 FHIR_Patient에 참조되는 컬럼입니다. 이러한 컬럼으로 스키마를 작성하셨다면
해당 스키마에 값을 추가합니다.
다음과 같이 끝나셨다면 이제 Vonk FHIR Server에 Model을 추가합니다.
[5. Repository 프로젝트 환경설정]
Nuget에서 패키지 다운받은 NETStandard.Library을 사용하여 프로젝트를 netstandard로 변경합니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0.3</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EntityFramework" Version="6.4.4" />
<PackageReference Include="NETStandard.Library" Version="2.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
<PackageReference Include="Vonk.Core" Version="3.8.0" />
<PackageReference Include="Vonk.Facade.Relational" Version="3.8.0" />
<PackageReference Include="Vonk.Fhir.R3" Version="3.8.0" />
</ItemGroup>
</Project>
[4. Vonk FHIR Server에 Model 추가]
Console App 프로젝트에 Model이란 폴더를 만든 후 해당 폴더안에 DB에서 만든 스키마를 정의하고
DB연동을 위한 Context 파일도 만들어 줍니다.
/// <summary>
/// FHIR_Patient.cs File
/// </summary>
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Repository.Models
{
[Table("FHIR_Patient")] // DB Table Name
public partial class FHIR_Patient
{
// PatientId로 참조가 걸려있기 때문에 해당 Blood_Test와 Disases 추가
public FHIR_Patient()
{
blood_Test = new HashSet<Blood_Test>();
}
[Key] // PK이므로 Key 할당
public int Id { get; set; }
public string PatientNumber { get; set; }
public string Gender { get; set; }
public string EmailAddress { get; set; }
public string FirstName { get; set; }
public string FamilyName { get; set; }
public DateTime DateOfBirth { get; set; }
public ICollection<Blood_Test> blood_Test { get; set; }
}
}
/// <summary>
/// Blood_Test.cs File
/// </summary>
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Repository.Models
{
[Table("Blood_Test")]
public partial class Blood_Test
{
[Key]
public int Id { get; set; }
public int PatientId { get; set; }
public double HDL { get; set; }
public double LDL { get; set; } // 해당 값이 0일경우 HDL TG TC를 이용하여 계산
public double TG { get; set; }
public double Creatinine { get; set; }
public double TC { get; set; }
public FHIR_Patient patient_B { get; set; }
}
}
/// <summary>
/// Context.cs File
/// </summary>
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Pomelo.EntityFrameworkCore.MySql.Storage;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using Microsoft.Extensions.Options;
namespace Repository.Models
{
public partial class Context : DbContext
{
// DB 연동
private readonly IOptions<DbOptions> _dbOptionsAccessor;
public Context(IOptions<DbOptions> dbOptionsAccessor)
{
_dbOptionsAccessor = dbOptionsAccessor;
}
// DB에 있는 Table Setting
public DbSet<FHIR_Patient> FHIR_Patients { get; set; }
public DbSet<Blood_Test> Blood_Tests { get; set; }
// DB연결 설정
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseMySql(_dbOptionsAccessor.Value.ConnectionString,
options => options
.CharSet(CharSet.Utf8)
.CharSetBehavior(CharSetBehavior.AppendToAllColumns));
}
}
// DB Table에 있는 속성들 Type 정의
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<FHIR_Patient>(entity =>
{
entity.Property(e => e.DateOfBirth).HasColumnType("date");
entity.Property(e => e.EmailAddress).HasMaxLength(100);
entity.Property(e => e.FamilyName)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.FirstName)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.PatientNumber)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.Gender)
.IsRequired()
.HasMaxLength(45);
});
modelBuilder.Entity<Blood_Test>(entity =>
{
entity.HasOne(d => d.patient_B)
.WithMany(p => p.blood_Test)
.HasForeignKey(d => d.PatientId);
});
}
}
}
위 사진과 같이 작성을 완료하셨다면 Repository 프로젝트에 DBOptions라는 클래스에 아래 코드를 작성합니다.
// <summary>
// DBOptions.cs File
// </summary>
namespace Repository
{
public class DbOptions
{
public string ConnectionString { get; set; } // DB 연결 정보
}
}
[5. ResourceMapper 작성]
Repository에 ResourceMapper라는 클래스를 하나 생성 후 각각 Resource에 Mapping을 구현 합니다.
using Hl7.Fhir.ElementModel;
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
using Hl7.Fhir.Support;
using System;
using System.Linq;
using Repository.Models;
using Vonk.Core.Common;
using Vonk.Core.Context;
using Vonk.Fhir.R3;
using static Hl7.Fhir.Model.QuestionnaireResponse;
using System.Collections.Generic;
using Vonk.Core.ElementModel;
using Hl7.FhirPath.Sprache;
using Vonk.Core.Repository;
namespace Repository
{
public class ResourceMapper
{
public IResource MapPatient(FHIR_Patient source) // Patient Identifier Setting, Name Setting ... ( FHIR Json )
{
var patient = new Patient
{
Id = source.Id.ToString(),
Gender = (source.Gender == "남성" ? AdministrativeGender.Male : AdministrativeGender.Female),
BirthDate = source.DateOfBirth.ToFhirDate()
};
patient.Identifier.Add(new Identifier("http://cntechsystems.com", source.PatientNumber));
patient.Name.Add(new HumanName().WithGiven(source.FirstName).AndFamily(source.FamilyName));
if (source.EmailAddress != null)
patient.Telecom.Add(new ContactPoint(ContactPoint.ContactPointSystem.Email, ContactPoint.ContactPointUse.Work, source.EmailAddress));
return patient.ToIResource();
}
public FHIR_Patient MapFHIRPatient(IResource source) // Patient Data Mapping Source ( DB 저장 )
{
var fhirPatient = source.ToPoco<Patient>();
var FHIR_Patient = new FHIR_Patient();
if (source.Id != null)
{
FHIR_Patient.Id = int.Parse(source.Id);
}
FHIR_Patient.PatientNumber = fhirPatient.Identifier.FirstOrDefault(i => (i.System == "http://cntechsystems.com"))?.Value;
if (fhirPatient.Gender.Value == AdministrativeGender.Male)
{
FHIR_Patient.Gender = "남성";
}
else
{
FHIR_Patient.Gender = "여성";
}
FHIR_Patient.FirstName = fhirPatient.Name.FirstOrDefault()?.Given?.FirstOrDefault();
FHIR_Patient.FamilyName = fhirPatient.Name.FirstOrDefault()?.Family;
FHIR_Patient.EmailAddress = fhirPatient.Telecom.FirstOrDefault(t => (t.System == ContactPoint.ContactPointSystem.Email))?.Value;
FHIR_Patient.DateOfBirth = Convert.ToDateTime(fhirPatient.BirthDate);
return FHIR_Patient;
}
public IResource MapBlood_Test(Blood_Test source) // Observation Identifier Setting, Category Setting, Component Setting
{
var observation = new Observation
{
Id = source.Id.ToString(),
Subject = new ResourceReference($"Patient/{source.PatientId}"),
Status = ObservationStatus.Final
};
observation.Category.Add(new CodeableConcept("http://hl7.org/fhir/observation-category", "Laboratory", "Laboratory"));
observation.Component.Add(
new Observation.ComponentComponent()
{
Code = new CodeableConcept("http://loinc.org", "2085-9", "HDL"),
Value = new Quantity((decimal)source.HDL, "mg/dL", VonkConstants.UcumSystem)
});
observation.Component.Add(
new Observation.ComponentComponent()
{
Code = new CodeableConcept("http://loinc.org", "13457-7", "HDL"),
Value = new Quantity((decimal)source.HDL, "mg/dL", VonkConstants.UcumSystem)
});
observation.Component.Add(
new Observation.ComponentComponent()
{
Code = new CodeableConcept("http://loinc.org", "3043-7", "TG"),
Value = new Quantity((decimal)source.LDL, "mg/dL", VonkConstants.UcumSystem)
});
observation.Component.Add(
new Observation.ComponentComponent()
{
Code = new CodeableConcept("http://loinc.org", "2160-0", "Creatinine"),
Value = new Quantity((decimal)source.Creatinine, "mg/dL", VonkConstants.UcumSystem)
});
observation.Component.Add(
new Observation.ComponentComponent()
{
Code = new CodeableConcept("http://loinc.org", "2093-3", "Total Cholesterol"),
Value = new Quantity((decimal)source.TC, "mg/dL", VonkConstants.UcumSystem)
});
return observation.ToIResource();
}
public Blood_Test MapFHIRBlood_Test(IResource source)
{
var fhirObservation = source.ToPoco<Observation>();
var FHIRBlood_Test = new Blood_Test();
if (source.Id != null)
{
FHIRBlood_Test.Id = int.Parse(source.Id);
}
FHIRBlood_Test.PatientId = int.Parse(new ResourceIdentity(fhirObservation.Subject.Reference).Id);
var HDL = fhirObservation.Component.Find(c => c.Code.Coding.Exists(coding => coding.Code == "2085-9"));
//var LDL = fhirObservation.Component.Find(c => c.Code.Coding.Exists(coding => coding.Code == "13457-7"));
var TG = fhirObservation.Component.Find(c => c.Code.Coding.Exists(coding => coding.Code == "3043-7"));
var Creatinine = fhirObservation.Component.Find(c => c.Code.Coding.Exists(coding => coding.Code == "2160-0"));
var TC = fhirObservation.Component.Find(c => c.Code.Coding.Exists(coding => coding.Code == "2093-3"));
FHIRBlood_Test.HDL = Convert.ToDouble(((Quantity)HDL.Value).Value);
FHIRBlood_Test.TG = Convert.ToDouble(((Quantity)TG.Value).Value);
FHIRBlood_Test.Creatinine = Convert.ToDouble(((Quantity)Creatinine.Value).Value);
FHIRBlood_Test.TC = Convert.ToDouble(((Quantity)TC.Value).Value);
FHIRBlood_Test.LDL = (FHIRBlood_Test.TC - FHIRBlood_Test.HDL - (FHIRBlood_Test.TG / 5));
return FHIRBlood_Test;
} // Observation Data Mapping Source
}
}
위 사진과 같이 FHIR_Patient, Blood_Test는 각각 맞는 Resource에 매핑룰을 작성합니다.
메소드 명에 "Map~"으로 작성된 코드는 DB에 불러온 데이터를 FHIR형태로 변환하는 Mapper이고
"MapFHIR~"으로 작성된 코드는 DB에 저장 시 필요한 Mapper 입니다.
[6. DB QueryFactory 작성]
QueryFactory는 DML을 정의하는 파일입니다. 해당 소스코드는 Resource별로 QueryFactory를 작성합니다.
using Hl7.Fhir.Model;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using Repository.Models;
using Vonk.Core.Repository;
using Vonk.Core.Repository.ResultShaping;
using Vonk.Core.Support;
using Vonk.Facade.Relational;
using static Vonk.Core.Common.VonkConstants;
namespace Repository
{
public class PatientQuery : RelationalQuery<FHIR_Patient>
{
public PatientQuery() : base() { }
public PatientQuery(SortShape sort)
{
_sort = sort;
}
private readonly SortShape _sort; // 압축하기 전에 모양을 정렬
public override IShapeValue[] Shapes => _sort is null ? base.Shapes :
base.Shapes.SafeUnion(new[] { _sort }).ToArray();
protected override IQueryable<FHIR_Patient> HandleShapes(IQueryable<FHIR_Patient> source)
{
var sorted = _sort is null ? source :
(_sort.Direction == SortDirection.ascending ? source.OrderBy(vp => vp.Id) :
source.OrderByDescending(vp => vp.Id));
return base.HandleShapes(sorted);
}
}
public class PatientQueryFactory : RelationalQueryFactory<FHIR_Patient, PatientQuery>
{
/// <summary>
/// DB Connection Setting
/// </summary>
/// <param name="onContext"></param>
public PatientQueryFactory(DbContext onContext) : base(nameof(Patient), onContext) { }
/// <summary>
/// Patient에 PatientId로 검색
/// </summary>
/// <param name="parameterName"></param>
/// <param name="value"></param>
/// <returns></returns>
public override PatientQuery AddValueFilter(string parameterName, TokenValue value)
{
if(parameterName == ParameterNames.Id)
{
if(!long.TryParse(value.Code, out long patientId))
{
throw new ArgumentException("Patient ID는 정수형 입니다.");
}
else
{
return PredicateQuery(vp => vp.Id == patientId);
}
}
else if(parameterName == "identifier")
{
return PredicateQuery(vp => vp.PatientNumber == value.Code);
}
return base.AddValueFilter(parameterName, value);
}
public override PatientQuery ResultShape(IShapeValue shape)
{
if (shape is SortShape sort && sort.ParameterName == "_lastUpdated")
{
return new PatientQuery(sort);
}
return base.ResultShape(shape);
}
}
}
위 소스코드는 FHIR_Patient Query로 각각 테이블에 Id값으로 DML을 수행합니다.
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using Repository.Models;
using Vonk.Core.Repository;
using Vonk.Core.Repository.ResultShaping;
using Vonk.Core.Support;
using Vonk.Facade.Relational;
namespace Repository
{
public class BTQuery : RelationalQuery<Blood_Test>
{
public BTQuery() : base() { }
public BTQuery(SortShape sort)
{
_sort = sort;
}
private readonly SortShape _sort;
public override IShapeValue[] Shapes => _sort is null ? base.Shapes :
base.Shapes.SafeUnion(new[] { _sort }).ToArray();
protected override IQueryable<Blood_Test> HandleShapes(IQueryable<Blood_Test> source)
{
var sorted = _sort is null ? source :
(_sort.Direction == SortDirection.ascending ? source.OrderBy(vp => vp.Id) :
source.OrderByDescending(vp => vp.Id));
return base.HandleShapes(sorted);
}
}
/// <summary>
/// Observation 쿼리의 초기 부분을 생성하기위한 팩토리 클래스
/// </summary>
public class BTQueryFactory : RelationalQueryFactory<Blood_Test, BTQuery>
{
/// <summary>
/// DB Connection Setting
/// </summary>
/// <param name="onContext"></param>
public BTQueryFactory(DbContext onContext) : base("Observation", onContext) { }
/// <summary>
/// Observation에 Id 값으로 검색
/// GET [BaseURL]/Observation/[ObservationId]
/// </summary>
/// <param name="parameterName"></param>
/// <param name="value"></param>
/// <returns></returns>
public override BTQuery AddValueFilter(string parameterName, TokenValue value)
{
if (parameterName == "_id")
{
if (!long.TryParse(value.Code, out long bpId))
{
throw new ArgumentException("Blood Test Id must be an integer value");
}
else
{
return PredicateQuery(vp => vp.Id == bpId);
}
}
return base.AddValueFilter(parameterName, value);
}
/// <summary>
/// Observation에 PatientId로 검색
/// GET [BaseURL]/Observation?subject:Patient/[PatientId]
/// </summary>
/// <param name="parameterName"></param>
/// <param name="value"></param>
/// <returns></returns>
public override BTQuery AddValueFilter(string parameterName, ReferenceValue value)
{
if (parameterName == "subject")
{
var patIdValue = value.Reference.StripFromStart("Patient/");
if (!int.TryParse(patIdValue, out var patId))
{
throw new ArgumentException("Patient Id must be an Integer value");
}
return PredicateQuery(bp => bp.PatientId == patId);
}
return base.AddValueFilter(parameterName, value);
}
/// <summary>
/// Observation에 PatientId로 검색
/// GET [BaseURL]/Observation?subject:Patient=[PatientId]
/// </summary>
/// <param name="parameterName"></param>
/// <param name="value"></param>
/// <returns></returns>
public override BTQuery AddValueFilter(string parameterName, ReferenceToValue value)
{
if (parameterName == "subject" && value.Targets.Contains("Patient"))
{
var patientQuery = value.CreateQuery(new PatientQueryFactory(OnContext));
var patIds = patientQuery.Execute(OnContext).Select(p => p.Id);
return PredicateQuery(bp => patIds.Contains(bp.PatientId));
}
return base.AddValueFilter(parameterName, value);
}
public override BTQuery ResultShape(IShapeValue shape)
{
if (shape is SortShape sort && sort.ParameterName == "_lastUpdated")
return new BTQuery(sort);
return base.ResultShape(shape);
}
}
}
위 소스코드는 Blood_Test Query로 해당 부분에는 PatientId로 검색할 수 있는 조건문이 더 추가되어 있습니다.
[7. Search 기능 작성]
FHIR Server에 데이터를 검색할 때 필요한 기능을 작성하는 코드입니다. 해당 코드에서는 각각의 Resource에 대하여 정의 및 데이터 검색을 해야합니다.
using Hl7.Fhir.Model;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Repository.Models;
using Vonk.Core.Common;
using Vonk.Core.Context;
using Vonk.Core.Pluggability.ContextAware;
using Vonk.Core.Repository;
using Vonk.Core.Support;
using Vonk.Facade.Relational;
using System.Runtime.InteropServices;
namespace Repository
{
/// <summary>
/// Resource Search 기능으로
/// Resource Type별로 검색기능을 구현
/// QueryFactory에서 파라미터와 옵션으로 쿼리 만들기
/// </summary>
[ContextAware(InformationModels = new[] { VonkConstants.Model.FhirR3 })]
public class searchRepository : SearchRepository
{
private readonly Context _Context;
private readonly ResourceMapper _resourceMapper;
public searchRepository(QueryContext queryBuilderContext, Context context, ResourceMapper resourceMapper) : base(queryBuilderContext)
{
Check.NotNull(context, nameof(context));
Check.NotNull(resourceMapper, nameof(resourceMapper));
_Context = context;
_resourceMapper = resourceMapper;
}
protected override async Task<SearchResult> Search(string resourceType, IArgumentCollection arguments, SearchOptions options) // Resource Type별로 검색기능 나눔
{
switch (resourceType)
{
case nameof(Patient): // Resource Type이 Patient일 경우
return await SearchPatient(arguments, options);
case nameof(Observation): // Resource Type이 Observation일 경우
return await SearchObservation(arguments, options);
default: // 검색기능 중 Resource Type이 없는 경우 Error 발생
throw new NotImplementedException($"ResourceType {resourceType} is not supported.");
}
}
private async Task<SearchResult> SearchPatient(IArgumentCollection arguments, SearchOptions options) // Patient 검색 기능
{
var query = _queryContext.CreateQuery(new PatientQueryFactory(_Context), arguments, options); // Patient 옵션과 파라미터로 DB에서 검색
var count = await query.ExecuteCount(_Context);
var patientResources = new List<IResource>();
if (count > 0) // 검색결과가 있을경우 PatientResources에 추가
{
var visiPatients = await query.Execute(_Context).ToListAsync();
foreach (var visiPatient in visiPatients)
{
patientResources.Add(_resourceMapper.MapPatient(visiPatient));
}
}
return new SearchResult(patientResources, query.GetPageSize(), count, query.GetSkip());
}
private async Task<SearchResult> SearchObservation(IArgumentCollection arguments, SearchOptions options) // Observation 검색 기능
{
var query = _queryContext.CreateQuery(new BTQueryFactory(_Context), arguments, options);
var count = await query.ExecuteCount(_Context);
var observationResources = new List<IResource>();
if (count > 0)
{
var observation = await query.Execute(_Context).ToListAsync();
foreach (var observations in observation)
{
observationResources.Add(_resourceMapper.MapBlood_Test(observations));
}
}
return new SearchResult(observationResources, query.GetPageSize(), count, query.GetSkip());
}
}
}
[8. Configuration 작성]
해당 코드는 작성한 코드에 대하여 Service를 해주는 코드입니다.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Repository.Models;
using System.Runtime.CompilerServices;
using Vonk.Core.Pluggability;
using Vonk.Core.Pluggability.ContextAware;
using Vonk.Core.Repository;
namespace Repository
{
[VonkConfiguration(order: 240)]
public static class Configuration
{
public static IServiceCollection AddService(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<Context>();
services.TryAddSingleton<ResourceMapper>();
services.TryAddContextAware<ISearchRepository, searchRepository>(ServiceLifetime.Scoped);
services.Configure<DbOptions>(configuration.GetSection(nameof(DbOptions)));
return services;
}
}
}
[9. appsettings.json 작성]
해당 json파일에는 FHIR에 대한 플러그인, Resource 모델, DB 접속정보를 설정하는 파일입니다.
{
/*
These settings are the minimal set which are needed in Vonk as plugin solution. Copy this file to the base folder of Vonk.
*/
"SubscriptionEvaluatorOptions": {
"Enabled": false
},
"DbOptions": { "ConnectionString": "Server=데이터베이스 주소;User ID=계정;Password=비밀번호;Database=데이터베이스명;" },
"PipelineOptions": {
"PluginDirectory": "./plugins",
"Branches": [
{
"Path": "/",
"Include": [
"Vonk.Core.Context",
"Vonk.Core.Repository",
"Vonk.Fhir.R3",
"Vonk.Core.Operations.Search",
"Vonk.Core.Operations.Crud",
"Vonk.Core.Operations.Validation.InstanceValidationConfiguration",
"Vonk.Core.Operations.Validation.ValidationConfiguration",
"Vonk.Core.Operations.Capability.CapabilityConfiguration",
"Repository",
"Vonk.UI.Demo"
],
"Exclude": [
"Vonk.Subscriptions.Administration"
]
},
{
"Path": "/administration",
"Include": [
"Vonk.Core",
"Vonk.Fhir.R3",
"Vonk.Repository.Sqlite.SqliteAdministrationConfiguration",
"Vonk.Plugins.Terminology",
"Vonk.Administration"
],
"Exclude": [
"Vonk.Core.Operations"
]
}
]
},
"SupportedModel": {
"RestrictToResources": [ "Patient", "Observation" ],
"RestrictToSearchParameters": [
"Resource._id",
"Patient.identifier",
"Observation.subject",
"StructureDefinition.url"
]
}
}
위 json파일까지 작성했다면 Repository에 다음과 같은 파일 구조가 나옵니다.
[10. Web App Project 환경설정]
Console App Project인 Repository 프로젝트를 종속성에 프로젝트 참조를 해야합니다.
vonk-license.json 파일을 해당 프로젝트 폴더안에 추가합니다.
appsettings.json 파일에 FHIR 관련 환경설정 추가
{
"License": {
"LicenseFile": "./vonk-license.json"
},
"SupportedInteractions": {
"InstanceLevelInteractions": "read, vread, update, delete, history, conditional_delete, conditional_update, $validate",
"TypeLevelInteractions": "create, search, history, $validate, $snapshot, conditional_create",
"WholeSystemInteractions": "capabilities, batch, transaction, history, search, $validate"
},
"DbOptions": {
"ConnectionString": "Server=데이터베이스 주소;User ID=계정;Password=비밀번호;Database=데이터베이스명;"
},
"MetadataImportOptions": {
"Enabled": true,
"Sets": [
{
"Source": "Api"
}
]
},
"SupportedModel": {
"RestrictToResources": [ "Patient", "Observation" ],
"RestrictToSearchParameters": [
"Resource._id",
"Patient.identifier",
"Observation.subject",
"StructureDefinition.url"
]
},
"BundleOptions": {
"DefaultCount": 1,
"MaxCount": 50
},
"SmartAuthorizationOptions": {
"Enabled": true,
"Filters": [
{
"FilterType": "Patient", //Filter on a Patient compartment if a 'patient' launch scope is in the auth token
"FilterArgument": "identifier=#patient#" //... for the Patient that has an identifier matching the value of that 'patient' launch scope
}
],
"Authority": "https://localhost:5000",
"Audience": "vonk",
"RequireHttpsToProvider": true, //You want this set to true (the default) in a production environment!
"Protected": {
"InstanceLevelInteractions": "read, vread, update, delete, history, conditional_delete, conditional_update, $validate",
"TypeLevelInteractions": "create, search, history, conditional_create",
"WholeSystemInteractions": "batch, transaction, history, search"
}
},
"Logging": {
"Debug": {
"LogLevel": {
"Default": "Debug"
}
},
"Console": {
"IncludeScopes": false,
"LogLevel": {
"Vonk": "Information",
"Default": "Information"
}
},
"LogLevel": {
"Default": "Debug"
}
}
}
appsettings.json 파일은 총 2가지가 있는데 펼쳐보면 다음과 같은 파일이 하나 더 있습니다.
위 사진에 appsettings.Development.json파일에도 환경설정 코드를 작성해야합니다.
{
"License": {
"LicenseFile": "./vonk-license.json"
},
"DbOptions": {
"ConnectionString": "Server=데이터베이스 주소;User ID=계정;Password=비밀번호;Database=데이터베이스명;"
},
"SmartAuthorizationOptions": {
"Enabled": false //Debugging is easier without authorization
}
}
[11. Web App Project에 Service 추가]
Repository에 만들었던 Service를 Web App Project에 추가해야합니다.
각각 Program.cs, Startup.cs에 각각 알맞는 코드를 작성합니다.
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace DemoFHIR
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostContext, config) =>
{
var hostingEnv = hostContext.HostingEnvironment;
var runningEnv = hostingEnv?.EnvironmentName?.ToLower() ?? "release";
config.Sources.Clear(); // Clear default sources
config
.SetBasePath(hostContext.HostingEnvironment.ContentRootPath)
.AddJsonFile(path: "appsettings.json", reloadOnChange: true, optional: true)
.AddJsonFile(path: "appsettings.development.json", reloadOnChange: true, optional: true) //Load debug specific settings.
.AddJsonFile(path: "appsettings.instance.json", reloadOnChange: true, optional: true); //Load instance specific settings. This file is intentionally not included in the Git repository.
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>().UseUrls("http://0.0.0.0:5000")
.Build();
}
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Repository;
using Vonk.Core.Configuration;
using Vonk.Core.Operations.Crud;
using Vonk.Core.Operations.Search;
using Vonk.Core.Operations.Validation;
using Vonk.Core.Support;
using Vonk.Fhir.R3;
using Vonk.Smart;
namespace DemoFHIR
{
public class Startup
{
private readonly IConfiguration _configuration;
private readonly IWebHostEnvironment _hostingEnv;
public Startup(IConfiguration configuration, IWebHostEnvironment hostingEnv)
{
Check.NotNull(configuration, nameof(configuration));
Check.NotNull(hostingEnv, nameof(hostingEnv));
_configuration = configuration;
_hostingEnv = hostingEnv;
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddFhirR3FacadeServices(_configuration)
.AddSmartServices(_configuration, _hostingEnv)
.AddVonkMinimalServices()
.AddSearchServices()
.AddReadServices()
.AddService(_configuration)
.AddInstanceValidationServices(_configuration)
.AddValidationServices(_configuration)
;
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app
.UseVonkMinimal()
.UseSmartAuthorization()
.UseSearch()
.UseRead()
.UseValidation()
.UseInstanceValidation()
;
}
}
}
이렇게 셋팅 후 실행할 경우 다음과 같은 에러를 발생합니다.
해당 에러는 specification.zip 파일을 못찾아 뜨는 에러인데 해당 zip 파일은 Nuget 패키지 다운받은 곳에 있습니다.
해당 위치는 다음과 같습니다.
"C:\Users\자신의 계정명\.nuget\packages\hl7.fhir.specification.stu3\1.9.0\contentFiles\any\any" 위치에
specification.zip 파일이 있습니다.
해당 파일을 C드라이브 경로에 contentFiles\any\any\폴더를 만들어 해당 specification.zip을 넣어주고 해당 DemoFHIR 프로젝트 경로에도 넣어줍니다.
그리고 DemoFHIR 프로젝트에 속성 추가해야합니다.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hl7.Fhir.Specification.STU3" Version="1.9.0" />
<PackageReference Include="Vonk.Smart" Version="3.8.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Repository\Repository.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="$(PkgHl7_Fhir_Specification_STU3)\contentFiles\any\any\specification.zip">
<Link>specification_Fhir3_0.zip</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<Pack>false</Pack>
</Content>
</ItemGroup>
</Project>
그리고 프로젝트 실행을 IIS Express 방법 이외인 DemoFHIR 방법으로 프로젝트 실행을 합니다.
해당 방법으로 실행하면 CMD창으로 출력이 됩니다.
[12. POSTMAN으로 데이터 검색]
POSTMAN에서 다음과 같이 진행 후 FHIR에서 데이터를 검색합니다.
다음과 같이 POSTMAN GET방식으로 검색 후 결과 값은 FHIR Message JSON형태로 출력된걸 보실 수 있습니다.
이러한 방법으로 Table에 저장된 의료데이터를 FHIR 형태로 변환하여 출력하는 방법까지 작성해보았습니다.
현재 이 포스터에는 CRUD 기능은 빠져있고 해당 부분은 추후 REST API 구현에 올리도록 하겠습니다.
감사합니다.
'Health Information > FHIR' 카테고리의 다른 글
FHIR란? (1) | 2020.08.25 |
---|---|
FHIR Server와 FHIR Client 차이점 (1) | 2020.06.19 |
FHIR as a Meta Model to Integrate CDM: Development of a Tool and Quantitative Validation Study (0) | 2020.05.15 |
SMART on FHIR - CDS Hooks - HAPI FHIR 연동 (0) | 2019.10.29 |
CDS Hooks Sandbox (0) | 2019.08.07 |