工程

医疗保健开发人员尝试使用Redox的一天

发表于十二月17,2018
By 克里斯 Hennen

今天我’使用C#.NET,MVC5和Web API 2.0构建一个简单的患者人口统计Web应用程序。

您在此处看到的所有代码都包含在 GitHub上的Redox .NET示例应用程序。该项目的最终目标是了解在已经有某种基于最新框架构建的功能应用程序的情况下,开始使用Redox发送和接收医疗数据的难度。

I’我在大约10年前建造的计算机上执行此操作,该计算机使用廉价的仿冒式机械键盘和Logitech G5激光游戏鼠标运行Windows 10副本,该鼠标具有可选的精确称重系统,已校准为所包括的最大最大砝码数量。您可以根据需要添加它们,图像就在这里。还显示了我可以额外使用的5GB RAM’不要使用,因为我的主板中有六分之四’DIMM插槽大约四年前决定停止工作。

构建应用程序

为了获得带有附加本地数据库设置的基本应用程序,我只是从 微软 。它讲述了如何制作ASP.NET MVC项目,如何安装实体框架(Microsoft’的ORM),以及如何设置第一个表。我将我的项目命名为SampleSite,因为我没有创造力。我为患者的人口统计信息制作了一张表格,其中可以存储有关使用该医疗系统的人员的所有可能的详细信息。我的 患者 .edmx 文件看起来像这样,并提供了一个名为 患者 that 我可以 use in code when interacting with database entries:

大!现在,我有了100%完美的数据模型,它将永远不会过时或需要修改(电话号码和SSN表示为 小数 类型,SMH),我’用预期的功能制作CRUD-y 患者 Service:

using SampleSite.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using  患者 Admin =  氧化还原 Api.Models.Patientadmin;

namespace SampleSite.Services
{
    public static class  患者 Service
    {
        public static IEnumerable<Patient> GetAllPatients()
        {
            var entities = new  患者 sDBEntities();
            return entities.Patients.ToList();
        }

        public static  患者  GetPatient(int patientId)
        {
            var entities = new  患者 sDBEntities();
            return entities.Patients.SingleOrDefault(x => x.Id == patientId);
        }

        public static void SavePatient(Patient patient)
        {
            var entities = new  患者 sDBEntities();
            if (!entities.Patients.Any(x => x.First == patient.First && 
                                            x.Last == patient.Last && 
                                            x.BirthDate == patient.BirthDate && 
                                            x.SocialSecurityNumber == patient.SocialSecurityNumber))
            {
                entities.Patients.Add(patient);
                entities.SaveChanges();
            }
        }

        public static void UpdatePatient(Patient patient)
        {
            var entities = new  患者 sDBEntities();
            var dbPatient = entities.Patients.Single(x => x.Id == patient.Id);
            dbPatient.First = patient.First;
            dbPatient.Last = patient.Last;
            dbPatient.MRN = patient.MRN;
            dbPatient.Gender = patient.Gender;
            dbPatient.Email = patient.Email;
            dbPatient.HomePhone = patient.HomePhone;
            dbPatient.BirthDate = patient.BirthDate;
            dbPatient.CellPhone = patient.CellPhone;
            dbPatient.AddressLine1 = patient.AddressLine1;
            dbPatient.AddressLine2 = patient.AddressLine2;
            dbPatient.City = patient.City;
            dbPatient.State = patient.State;
            dbPatient.ZipCode = patient.ZipCode;
            dbPatient.SocialSecurityNumber = patient.SocialSecurityNumber;

            entities.SaveChanges();
        }

        public static void DeletePatient(int patientId)
        {
            var entities = new  患者 sDBEntities();
            var patient = entities.Patients.Single(x => x.Id == patientId);
            entities.Patients.Remove(patient);
            entities.SaveChanges();
        }
    }
}

接下来,我们’ll编写一个控制器来执行这些操作。一世’m使用MVC的默认路由机制,该机制主要反映在controllers文件夹中的文件上,以找出应定义的路由。通常,路线看起来像是这样的字符串 SampleSite / app_start / routeConfig.cs:

routes.MapRoute(
  name: "Default",
  url: "{controller}/{action}/{id}",
  defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

我做了一个控制器 患者 Controller.cs (在MVC和WebApi中,如果文件名不’t end with Controller.cs,那么框架不会’t think it’s控制器,因为我可以使用魔法’希望永远了解)。它具有执行CRUD功能的路由,并且由于难以记住神奇的路由规则,因此我将预期的路由置于每个功能之上。明智的.NET开发人员可能会在此处使用属性路由。

using SampleSite.Models;
using SampleSite.Services;
using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace SampleSite.Controllers
{
    public class  患者 Controller : Controller
    {
        //  得到 :  患者 
        public ActionResult Index()
        {
            var patients =  患者 Service.GetAllPatients();
            return View(patients);
        }

        //  得到 :  患者 /Details/5
        public ActionResult Details(int id)
        {
            var patient =  患者 Service.GetPatient(id);
            return View(patient);
        }

        //  得到 :  患者 /Create
        public ActionResult Create()
        {
            return View();
        }

        //  开机自检 :  患者 /Create
        [HttpPost]
        public ActionResult Create(Patient patient)
        {
            if (ModelState.IsValid)
            {
                //save a patient  这里 
                try
                {
                    PatientService.SavePatient(patient);
                    return RedirectToAction("Index");
                }
                catch (DbEntityValidationException e)
                {
                    foreach (var err in e.EntityValidationErrors)
                    {
                        foreach (var msg in err.ValidationErrors)
                        {
                            ModelState.AddModelError(msg.PropertyName, msg.ErrorMessage);
                        }
                    }
                    return View();
                }
            }
            else
            {
                return View();
            }
        }

        //  得到 :  患者 /Edit/5
        public ActionResult Edit(int id)
        {
            var patient =  患者 Service.GetPatient(id);
            return View(patient);
        }

        //  开机自检 :  患者 /Edit/5
        [HttpPost]
        public ActionResult Edit(int id,  患者  patient)
        {
            try
            {
                PatientService.UpdatePatient(patient);

                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        //  得到 :  患者 /Delete/5
        public ActionResult Delete(int id)
        {
            var patient =  患者 Service.GetPatient(id);
            return View(patient);
        }

        //  开机自检 :  患者 /Delete/5
        [HttpPost]
        public ActionResult Delete(int id,  患者  patient)
        {
            try
            {
                PatientService.DeletePatient(id);

                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }
    }
}

It’将您的控制器保持在“skinny”坚持MVC模式–控制器仅应设法接收路线,收集必要的模型并返回适当的视图。这些控制器文件可以迅速增长以包含许多路由,并且在其中维护业务逻辑或与特定视图相关的信息是一种轻松的方法,可以以非常长且难以导航的文件结束,并且许多开发人员可能会对这些更改进行冲突的更改。相同的文件。

我赢了’这里没有任何人使用单独的视图代码,它是用Razor语法编写的,既无聊又极少。它用 jQuery的 引导程序 库,使前端漂亮,甚至不适合在2016年向开发人员展示。继续前进。

该网站已完成!我可以查看患者列表,删除患者,更新患者或创建新患者。该网站具有极高的未来性,其外观设计可以与大多数当前网站相媲美,并且完全通过表单提交操作来生成其所有页面,从而使其响应速度快且性能卓越。看清各种观点;这些确实是现代网站设计可以产生的最大成就。

用作氧化还原源

下一步是将我们尖端的高科技应用程序产生的数据导入Redox。几个月前,我恰好为这部分编写了.NET客户端库,因此入门非常容易。客户端库包含在GitHub上此项目的代码中。

使用客户端库很简单–我们必须使用我们的API密钥和Redox源记录中的密钥来初始化Redox Client对象(不要’看看我的秘密):

氧化还原Client对象将通过自动请求令牌,为后续请求记住该令牌以及在必要时使用刷新令牌生成新令牌来处理对Redox的身份验证请求。 氧化还原 客户端还具有代表各种Redox数据模型的类,以及将数据模型对象直接发送到 //api.toyosteel.net.cn/endpoint。所以,我们要做的就是提供适当的“hydrated”氧化还原数据模型对象要适当的功能。

I’ll instantiate a static instance of the 氧化还原API Client in my 患者 Service.cs file, since that’s the 上 ly place we’将生成暂时应发送到Redox的数据更改:

private static  氧化还原 Client redox = new  氧化还原 Api.RedoxClient("0fa96c7a-5f80-4249-bfce-7644d483ef44", "LSjjwJDhlBVZ7W5zdJ9lBhMwz1zkhrWpMsUOkwyj9XBl5DalQ0FRRWJ19QoTVw5SGQlPRZUo");

现在,要将数据模型/事件类型作为消息发送到我们的Redox源,我们’首先从Redox .NET SDK实例化相关模型,映射我们的应用程序’只需手动设置各种属性即可将其带到病人身上。氧化还原数据模型类’命名空间遵循模式 数据模型。事件类型 并以事件类型命名。

例如,在将患者保存到数据库中之后,我们的SavePatient函数可以发送PatientAdmin ^ NewPatient消息,并进行以下代码更改:

public static void SavePatient(Patient patient)
        {
            var entities = new  患者 sDBEntities();
            if (!entities.Patients.Any(x => x.First == patient.First && 
                                            x.Last == patient.Last && 
                                            x.BirthDate == patient.BirthDate && 
                                            x.SocialSecurityNumber == patient.SocialSecurityNumber))
            {
                entities.Patients.Add(patient);
                entities.SaveChanges();

                //send the new patient to  氧化还原 
                var newPatient = new  患者 Admin.Newpatient.Newpatient();
                newPatient.Patient.Identifiers.Add( new  患者 Admin.Newpatient.Anonymous2() { ID = patient.MRN, IDType = "MRN" } );
                newPatient.Patient.Demographics.FirstName = patient.First;
                newPatient.Patient.Demographics.LastName = patient.Last;
                newPatient.Patient.Demographics.DOB = patient.BirthDate.ToString();
                newPatient.Patient.Demographics.Sex = patient.Gender;
                newPatient.Patient.Demographics.EmailAddresses.Add(patient.Email);
                newPatient.Patient.Demographics.Address.StreetAddress = patient.AddressLine1 + patient.AddressLine2;
                newPatient.Patient.Demographics.Address.City = patient.City;
                newPatient.Patient.Demographics.Address.State = patient.State;
                newPatient.Patient.Demographics.Address.ZIP = patient.ZipCode;
                newPatient.Patient.Demographics.SSN = patient.SocialSecurityNumber.ToString();
                newPatient.Patient.Demographics.PhoneNumber.Home = patient.HomePhone.ToString();
                newPatient.Patient.Demographics.PhoneNumber.Mobile = patient.CellPhone.ToString();

                var response = redox.PatientAdmin.NewPatient(newPatient);
            }
        }

在这里快速切线–Redox.NET SDK中包含的数据模型类是通过提供数据模型生成的’从文档站点生成的JSON模式文件进入 新泽西州 包。您会看到,在某些情况下,这会导致一些奇怪的命名,因为模型的JSON模式未提供某些数组中对象的名称。对于那些对如何生成模型类感兴趣的人,请查看GitHub上的模型生成器项目。

我的第一个阻止程序是在测试与Redox源的连接时出现的:Redox不支持旧版本的SSL,因为严重的黑客已经破解了较低的SSL版本!一世’m使用.NET Framework版本4.5.2,它将默认连接到SSLv3的所有连接而未协商TLS(任何版本),并且在每次请求时都会收到握手错误 http:// _ api.re doxengine.com。我不知道该错误意味着几个小时,并尝试了一些非常古怪的潜在修复程序,然后才从Slack对话中记住了我’我需要使用TLS,然后弄清楚如何强制连接使用TLS 1.2。过去,您实际上必须为.NET设置Windows注册表项才能使用TLS,但是现在您只需添加以下行即可 ServicePointManager.SecurityProtocol | = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;  给你 global.asax.cs 文件。 咄 。

嘿,那很容易。作为应用程序开发人员,最困难的部分是映射我的数据库’的Patient对象放入Redox 患者 Admin数据模型。我可能想编写一个执行此翻译的函数’我会做很多事情,或者使用AutoMapper之类的工具(如果您叫Captain Fancy Pants)。我的不是那么好处理。

应用程序作为氧化还原目的地

让’通过在同一项目中公开端点,确保Redox也可以将数据发送到我们的应用程序。我在这个真正整洁的新站点上找到了有关如何使现有MVC项目与.NET Web API 2一起使用的快速教程。 “StackOverflow”

在Web API 2.0中,关于路由的一件有趣的事是,默认情况下,完全基于URL调用控制器方法(称为动作)。这给我带来了第二个障碍:氧化还原仅将POST发布到一个URL,而我的应用程序需要能够根据POST请求的主体来确定要做什么。这意味着“Redox API”通讯方式不’期望我们的应用程序是RESTy,并且仅提供一种严格的身份验证形式。

为了解决这个问题,我可以放一个巨大的 开关 一个控制器动作中的语句,否则我可以覆盖默认值 IHttpActionSelector 看起来像身体的形状,并根据我们认为请求是数据模型还是验证请求来调用特定操作。前者构成了一个巨大的控制器,我怀疑需要经常对其进行更改以支持新功能,而后者则是一项成本较高的操作,必须为每个传入请求执行。这两种方法都不理想,但我怀疑’后者的未来维护较少,因此’s what I’m gonna do alright.

我制作了一个新的Action Selector,以防我们的API有任何非Redox终结点(大笑为什么会酰胺化)并让它选择“verify”如果身体有挑战,请采取行动,否则请选择名称为 <datamodel>_<eventtype> 根据身体’的Meta对象。也可以在这些属性上创建自定义属性和键,或者对控制器操作的名称进行硬编码,或者执行其他操作。无论如何,我把这个垃圾扔进了一个叫做 App_Start / 氧化还原 ActionSelector.cs:

using Newtonsoft.Json.Linq;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Web.Http.Controllers;

namespace SampleSite
{
    public class  氧化还原 ActionSelector : ApiControllerActionSelector
    {
        public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
        {
            if (!controllerContext.ControllerDescriptor.ControllerName.Contains(" 氧化还原 "))
            {
                return base.SelectAction(controllerContext);
            }

            var requestContent = new HttpMessageContent(controllerContext.Request);
            var json = requestContent.HttpRequestMessage.Content.ReadAsStringAsync().Result;
            var body = JObject.Parse(json);

            // If we're using the redox controller due to a route like /api/redox, let's check out what's in the body
            // 1\. This could be a dang ol' verification request
            var actions = controllerContext.ControllerDescriptor.ControllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public);
            var 挑战 = (string)body.SelectToken("挑战");
            if (!string.IsNullOrEmpty(challenge))
            {
                var action = actions.Single(x => x.Name == "Verify");
                return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, action);
            }

            // 2\. This better be a 数据模型 then
            var 数据模型 = (string)body.SelectToken("Meta.DataModel");
            var eventType = (string)body.SelectToken("Meta.EventType");
            if (!string.IsNullOrEmpty(datamodel) && !string.IsNullOrEmpty(eventType))
            {
                // Expect there to be an action with the right name
                var action = actions.SingleOrDefault(x => x.Name == 数据模型 + "_" + eventType);
                if (action != null)
                {
                    return new ReflectedHttpActionDescriptor(controllerContext.ControllerDescriptor, action);
                }
            }

            // Pft Idunno just let MVC figure it out maybe
            return base.SelectAction(controllerContext);

        }
    }
}

并告诉我的应用替换默认的操作选择器 App_Start / WebApiConfig.cs:

config.Services.Replace(typeof(IHttpActionSelector), new  氧化还原 ActionSelector());

好的,因此我们的Web API路由知道如何根据数据模型和事件类型选择正确的操作。但是,在我们甚至无法接收来自Redox的实际消息之前,我们需要“verify” our “destination”。我们应该提供一个氧化还原的终点 开机自检 我们提供的令牌和“challenge” value, 和 all it does is 校验 the token is correct 和 parrot the 挑战 in the response. The 氧化还原CLI comes with a model for the verification request body (for 开机自检 验证,因为 得到 将在EO​​Y退休)。

    public class VerifyDestinationBody
{
        [JsonProperty(PropertyName = "验证令牌")]
        public string verification_token { get; set; }
        public string 挑战 { get; set; }
    }

I’我们会很快提到,很难根据目标设置的文档页面来猜测验证请求正文;建立连接并使用调试器检查请求的主体之后,我才能够弄清楚。此外,验证令牌位于“verification-token”属性和破折号竞技场’如果类成员名允许,则需要告诉您喜欢的JSON解串器哪个属性(带短划线)对应于哪个类成员。

我做了一个RedoxController来容纳Redox最终将发送到的动作。你可以看到我’ve got 上 e action called Verify that takes the VerificationDestinationBody as a parameter 和 another action for 患者 Admin^NewPatient messages. Also, I wrote a quick helper function to determine whether the request actually came from 氧化还原by inspecting the 验证令牌 header 和 making sure it matches our provided token.

using SampleSite.Services;
using System;
using System.Collections.Generic;
using System.Data.Entity.Validation;
using System.Linq;
using System.Web.Http;

namespace SampleSite.Controllers
{
    public class  氧化还原 Controller : ApiController
    {
        public const string AUTH_SECRET = "jamesisadork"; // trololololol
        private bool IsAuthorized()
        {
            IEnumerable<string> headerValues;
            return (Request.Headers.TryGetValues("验证令牌", out headerValues) &&
                headerValues.FirstOrDefault() == AUTH_SECRET);
        }

        [Route("api/redox")]
        [HttpPost]
        public IHttpActionResult Verify([FromBody]  氧化还原 Api.Models.VerifyDestinationBody verification)
        {
            if (verification.verification_token == AUTH_SECRET)
            {
                // reply with the 挑战 value to 校验 with  氧化还原 
                return Ok(verification.challenge);
            }
            return BadRequest();
        }

        // Only support new patient for now. Other 数据模型s/event types should 404.
        [Route("api/redox")]
        [HttpPost]
        public IHttpActionResult  患者 Admin_NewPatient([FromBody]  氧化还原 Api.Models.Patientadmin.Newpatient.Newpatient patient)
        {
            // Make sure this request comes from  氧化还原 
            if (!IsAuthorized())
            {
                return BadRequest();
            }

            // Save the new patient
            try
            {
                PatientService.SaveRedoxPatient(patient);
            }
            catch (DbEntityValidationException e)
            {
                foreach (var eve in e.EntityValidationErrors)
                {
                    Console.WriteLine("Entity of type \"{0}\" in state \"{1}\" has the following validation errors:",
                        eve.Entry.Entity.GetType().Name, eve.Entry.State);
                    foreach (var ve in eve.ValidationErrors)
                    {
                        Console.WriteLine("- Property: \"{0}\", Error: \"{1}\"",
                            ve.PropertyName, ve.ErrorMessage);
                    }
                }
                return BadRequest("Validation failed");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return BadRequest("Missing required fields probably");
            }
            return Ok();
        }
    }
}

接下来的麻烦是,氧化还原显然不能将Web请求发送到本地计算机。一世’ll use 恩格罗克 设置从我的机器到公共网络上随机生成的URL的安全隧道。我的.NET应用程序生气时’t接收到适当的主机头,因此我用来设置隧道的命令是 恩格罗克 http [端口]–host-header=”localhost:[port]”) 更换 [港口] 应用在本地运行的端口。

I will set up my 目的地 with the 恩格罗克 URL,然后单击“verify 和 save” button 和 see that the 目的地 is all set up!

If I use dev tools to send a 患者 Admin^NewPatient message to my 目的地, I’在所有其他患者旁边的列表中,我的网站上会看到该患者。您可以查看我为该患者创建的实际传输 这里 。我不’没有图像来证明它确实有效并且患者在我的应用程序中’的患者列表视图,因此您’我只需要信任我或以自己的方式找到应对遗漏的方法。

因此,总而言之,似乎并没有增加一个已经精心制作的Web应用程序以使用.NET发送和接收数据。’非常棘手。由于是Redox的一名开发人员,因此我能够更快地克服一些障碍,并且花了多个小时才能使所有工作正常进行。如果您已经连接到Redox API,您的体验如何?在您选择的框架中尝试并分享经验!

克里斯’s Top 10 Takeaways:

  1. 在此,数据模型类是基于JSON模式文件自动生成的,但是有些字段的名称笨拙。我没有’无法找到解决此问题的方法。
  2. 传入请求的TLS是’起初很明显,有些语言/框架没有’t清楚地报告此错误。
  3. 在设置可以接收该请求的端点之前,很难确定验证POST请求的主体。这可能需要部署到测试或生产站点,与在本地运行相比,通常更难调试。好消息是您可以使用ngrok之类的工具将本地主机隧道传输到Internet,但是我’我不确定该工具/过程的知名度。
  4. 开机自检 正文中的验证令牌名称中带有短划线。 FML。 (氧化还原将解决此问题)
  5. 我可以’为每种数据模型/事件类型设置不同的路由,因此我必须解析请求的主体以弄清楚该如何处理。这不’NET Web API不能很好地工作,因为路由仅基于URL。
  6. 其他五个去这里。

***

鼓励Redox开发人员探索新的语言,框架,并通常扩展他们的知识。 作为一家API公司,吃自己的狗食可以成为一种有成效的锻炼。请继续关注新功能和更简单的连接到Redox的方法。直到那时, 查看更多精彩的工程内容 并浏览我们的 当前的职业机会.