Health Information/FHIR

Vonk FHIR Server 구축

안녕하세요 씨앤텍 시스템즈 최홍준 연구원입니다.

이번 포스트는 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 웹 어플리케이션을 선택합니다.

그림1. 프로젝트 만들기 1

2. 프로젝트 이름과 프로젝트 위치를 지정합니다.

그림2. 프로젝트 만들기 2

3. 프로젝트 구성 선택

그림3. 프로젝트 만들기 3

해당 프로젝트는 별도의 View와 컨트롤러가 필요가 없기 때문에 비어 있음으로 선택 후 필요한 부분만 클래스 추가하여 진행하실 수 있습니다.
그리고 HTTPS에 대한 구성은 필요하신분은 체크를 하시고 만들기를 눌러주시면 됩니다.

 

4. 프로젝트 추가하기

그림4. 프로젝트 만들기 4

해당 사진과 같이 나오셨다면 추가로 하나 더 프로젝트를 생성해야합니다.

그림5. 프로젝트 만들기 5
그림6. 프로젝트 만들기 6
그림7. 프로젝트 만들기 7
그림8. 프로젝트 만들기 8

위 사진과 같은 프로젝트 형태로 만들어주시면 됩니다.

 


[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

해당 패키지를 설치하기 위해 다음과 같이 들어갑니다.

그림9. Nuget 패키지 관리

 

그림10. Nuget 패키지 관리
그림11. Nuget 패키지 설치 

위 사진과 같이 해당 프로젝트에 맞는 패키지를 설치를 진행하시면 됩니다. 설치가 끝나셨다면 "설치됨" 메뉴에서

그림12. 설치된 패키지

위 사진처럼 패키지 목록들이 나와있다면 설치가 완료되었습니다.


[3. DataBase에 FHIR Repository 만들기]

Vonk FHIR의 Repository 형태는 큰 제약을 없으므로 자신이 원하는 형태로 DB 스키마를 구성할 수 있습니다.

그래서 (주)씨앤텍시스템에서는 다음과 같은 형태로 스키마를 작성했습니다.

그림13. Vonk FHIR Repository ERD

위 사진에서 PatientId는 FK로 FHIR_Patient에 참조되는 컬럼입니다. 이러한 컬럼으로 스키마를 작성하셨다면
해당 스키마에 값을 추가합니다.

 

그림 14. Repository 값 추가

다음과 같이 끝나셨다면 이제 Vonk FHIR Server에 Model을 추가합니다.


[5. Repository 프로젝트 환경설정]

Nuget에서 패키지 다운받은 NETStandard.Library을 사용하여 프로젝트를 netstandard로 변경합니다.

그림15. Repository 프로젝트 속성 변경

<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 파일도 만들어 줍니다.

그림 16. Respository Model 추가

/// <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에 다음과 같은 파일 구조가 나옵니다.

그림17. Repository 최종 파일 구성


[10. Web App Project 환경설정]

Console App Project인 Repository 프로젝트를 종속성에 프로젝트 참조를 해야합니다.

그림18. 프로젝트 참조 추가 1
그림19. 프로젝트 참조 추가 2

vonk-license.json 파일을 해당 프로젝트 폴더안에 추가합니다.

그림20. 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가지가 있는데 펼쳐보면 다음과 같은 파일이 하나 더 있습니다.

그림21. appsettings.json 펼치기

위 사진에 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()
            ;
        }
    }
}

 이렇게 셋팅 후 실행할 경우  다음과 같은 에러를 발생합니다.

그림 22. Error Page

해당 에러는 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 방법으로 프로젝트 실행을 합니다.

그림23. IIS Express 이외 실행

해당 방법으로 실행하면 CMD창으로 출력이 됩니다.

그림24. DemoFHIR 실행화면


[12. POSTMAN으로 데이터 검색]

POSTMAN에서 다음과 같이 진행 후 FHIR에서 데이터를 검색합니다.

그림25. POSTMAN 1

 

그림26. POSTMAN 2
그림27. POSTMAN 3
그림28. POSTMAN 4

다음과 같이 POSTMAN GET방식으로 검색 후 결과 값은 FHIR Message JSON형태로 출력된걸 보실 수 있습니다.


이러한 방법으로 Table에 저장된 의료데이터를 FHIR 형태로 변환하여 출력하는 방법까지 작성해보았습니다.

현재 이 포스터에는 CRUD 기능은 빠져있고 해당 부분은 추후 REST API 구현에 올리도록 하겠습니다.

 

감사합니다.

728x90