diff --git a/src/Hl7.Fhir.Core.Tests/Rest/FhirClientTests.cs b/src/Hl7.Fhir.Core.Tests/Rest/FhirClientTests.cs index 55493b533c..405bcec097 100644 --- a/src/Hl7.Fhir.Core.Tests/Rest/FhirClientTests.cs +++ b/src/Hl7.Fhir.Core.Tests/Rest/FhirClientTests.cs @@ -15,6 +15,7 @@ using Hl7.Fhir.Rest; using Hl7.Fhir.Serialization; using Hl7.Fhir.Model; +using TestClient = Hl7.Fhir.Rest.Http.FhirClient; namespace Hl7.Fhir.Tests.Rest { @@ -43,7 +44,7 @@ public void TestInitialize() public static void DebugDumpBundle(Hl7.Fhir.Model.Bundle b) { System.Diagnostics.Trace.WriteLine(String.Format("--------------------------------------------\r\nBundle Type: {0} ({1} total items, {2} included)", b.Type.ToString(), b.Total, (b.Entry != null ? b.Entry.Count.ToString() : "-"))); - + if (b.Entry != null) { foreach (var item in b.Entry) @@ -64,7 +65,7 @@ public static void DebugDumpBundle(Hl7.Fhir.Model.Bundle b) } [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void FetchConformance() + public void FetchConformanceWebClient() { FhirClient client = new FhirClient(testEndpoint); client.ParserSettings.AllowUnrecognizedEnums = true; @@ -87,11 +88,42 @@ public void FetchConformance() Assert.AreEqual("200", client.LastResult.Status); Assert.IsNotNull(entry.Rest[0].Resource, "The resource property should be in the summary"); - Assert.AreNotEqual(0, entry.Rest[0].Resource.Count , "There is expected to be at least 1 resource defined in the conformance statement"); + Assert.AreNotEqual(0, entry.Rest[0].Resource.Count, "There is expected to be at least 1 resource defined in the conformance statement"); Assert.IsTrue(entry.Rest[0].Resource[0].Type.HasValue, "The resource type should be provided"); Assert.AreNotEqual(0, entry.Rest[0].Operation.Count, "operations should be listed in the summary"); // actually operations are now a part of the summary } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void FetchConformanceHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + client.ParserSettings.AllowUnrecognizedEnums = true; + + var entry = client.CapabilityStatement(); + + Assert.IsNotNull(entry.Text); + Assert.IsNotNull(entry); + Assert.IsNotNull(entry.FhirVersion); + // Assert.AreEqual("Spark.Service", c.Software.Name); // This is only for ewout's server + Assert.AreEqual(CapabilityStatement.RestfulCapabilityMode.Server, entry.Rest[0].Mode.Value); + Assert.AreEqual("200", client.LastResult.Status); + + entry = client.CapabilityStatement(SummaryType.True); + + Assert.IsNull(entry.Text); // DSTU2 has this property as not include as part of the summary (that would be with SummaryType.Text) + Assert.IsNotNull(entry); + Assert.IsNotNull(entry.FhirVersion); + Assert.AreEqual(CapabilityStatement.RestfulCapabilityMode.Server, entry.Rest[0].Mode.Value); + Assert.AreEqual("200", client.LastResult.Status); + + Assert.IsNotNull(entry.Rest[0].Resource, "The resource property should be in the summary"); + Assert.AreNotEqual(0, entry.Rest[0].Resource.Count, "There is expected to be at least 1 resource defined in the conformance statement"); + Assert.IsTrue(entry.Rest[0].Resource[0].Type.HasValue, "The resource type should be provided"); + Assert.AreNotEqual(0, entry.Rest[0].Operation.Count, "operations should be listed in the summary"); // actually operations are now a part of the summary + } + } + [TestMethod, TestCategory("FhirClient")] public void VerifyFormatParamProcessing() @@ -112,7 +144,7 @@ public void VerifyFormatParamProcessing() } [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void ReadWithFormat() + public void ReadWithFormatWebClient() { FhirClient client = new FhirClient(testEndpoint); @@ -123,9 +155,22 @@ public void ReadWithFormat() Assert.IsNotNull(loc); } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void ReadWithFormatHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + client.UseFormatParam = true; + client.PreferredFormat = ResourceFormat.Json; + + var loc = client.Read("Patient/example"); + Assert.IsNotNull(loc); + } + } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void Read() + public void ReadWebClient() { FhirClient client = new FhirClient(testEndpoint); @@ -164,9 +209,51 @@ public void Read() jsonSer.SerializeToString(loc4)); } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void ReadHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + + var loc = client.Read("Location/1"); + Assert.IsNotNull(loc); + Assert.AreEqual("Den Burg", loc.Address.City); + + Assert.AreEqual("1", loc.Id); + Assert.IsNotNull(loc.Meta.VersionId); + + var loc2 = client.Read(ResourceIdentity.Build("Location", "1", loc.Meta.VersionId)); + Assert.IsNotNull(loc2); + Assert.AreEqual(loc2.Id, loc.Id); + Assert.AreEqual(loc2.Meta.VersionId, loc.Meta.VersionId); + + try + { + var random = client.Read(new Uri("Location/45qq54", UriKind.Relative)); + Assert.Fail(); + } + catch (FhirOperationException ex) + { + Assert.AreEqual(HttpStatusCode.NotFound, ex.Status); + Assert.AreEqual("404", client.LastResult.Status); + } + + var loc3 = client.Read(ResourceIdentity.Build("Location", "1", loc.Meta.VersionId)); + Assert.IsNotNull(loc3); + var jsonSer = new FhirJsonSerializer(); + Assert.AreEqual(jsonSer.SerializeToString(loc), + jsonSer.SerializeToString(loc3)); + + var loc4 = client.Read(loc.ResourceIdentity()); + Assert.IsNotNull(loc4); + Assert.AreEqual(jsonSer.SerializeToString(loc), + jsonSer.SerializeToString(loc4)); + } + } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void ReadRelative() + public void ReadRelativeWebClient() { FhirClient client = new FhirClient(testEndpoint); @@ -180,9 +267,25 @@ public void ReadRelative() Assert.AreEqual("Den Burg", loc.Address.City); } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void ReadRelativeHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + var loc = client.Read(new Uri("Location/1", UriKind.Relative)); + Assert.IsNotNull(loc); + Assert.AreEqual("Den Burg", loc.Address.City); + + var ri = ResourceIdentity.Build(testEndpoint, "Location", "1"); + loc = client.Read(ri); + Assert.IsNotNull(loc); + Assert.AreEqual("Den Burg", loc.Address.City); + } + } + #if NO_ASYNC_ANYMORE [TestMethod, TestCategory("FhirClient")] - public void ReadRelativeAsync() + public void ReadRelativeAsyncWebClient() { FhirClient client = new FhirClient(testEndpoint); @@ -195,9 +298,25 @@ public void ReadRelativeAsync() Assert.IsNotNull(loc); Assert.AreEqual("Den Burg", loc.Resource.Address.City); } + + [TestMethod, TestCategory("FhirClient")] + public void ReadRelativeAsyncHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + var loc = client.ReadAsync(new Uri("Location/1", UriKind.Relative)).Result; + Assert.IsNotNull(loc); + Assert.AreEqual("Den Burg", loc.Resource.Address.City); + + var ri = ResourceIdentity.Build(testEndpoint, "Location", "1"); + loc = client.ReadAsync(ri).Result; + Assert.IsNotNull(loc); + Assert.AreEqual("Den Burg", loc.Resource.Address.City); + } + } #endif - public static void Compression_OnBeforeRequestGZip(object sender, BeforeRequestEventArgs e) + public static void Compression_OnBeforeWebRequestGZip(object sender, Fhir.Rest.BeforeRequestEventArgs e) { if (e.RawRequest != null) { @@ -207,7 +326,7 @@ public static void Compression_OnBeforeRequestGZip(object sender, BeforeRequestE } } - public static void Compression_OnBeforeRequestDeflate(object sender, BeforeRequestEventArgs e) + public static void Compression_OnBeforeWebRequestDeflate(object sender, Fhir.Rest.BeforeRequestEventArgs e) { if (e.RawRequest != null) { @@ -217,41 +336,71 @@ public static void Compression_OnBeforeRequestDeflate(object sender, BeforeReque } } - public static void Compression_OnBeforeRequestZipOrDeflate(object sender, BeforeRequestEventArgs e) + public static void Compression_OnBeforeWebRequestZipOrDeflate(object sender, Fhir.Rest.BeforeRequestEventArgs e) + { + if (e.RawRequest != null) + { + // e.RawRequest.AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip; + e.RawRequest.Headers.Remove("Accept-Encoding"); + e.RawRequest.Headers["Accept-Encoding"] = "gzip, deflate"; + } + } + + public static void Compression_OnBeforeHttpRequestGZip(object sender, Core.Rest.Http.BeforeRequestEventArgs e) + { + if (e.RawRequest != null) + { + // e.RawRequest.AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip; + e.RawRequest.Headers.Remove("Accept-Encoding"); + e.RawRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip"); + } + } + + public static void Compression_OnBeforeHttpRequestDeflate(object sender, Core.Rest.Http.BeforeRequestEventArgs e) { if (e.RawRequest != null) { // e.RawRequest.AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip; e.RawRequest.Headers.Remove("Accept-Encoding"); - e.RawRequest.Headers["Accept-Encoding"] = "gzip, deflate"; + e.RawRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "deflate"); + } + } + + public static void Compression_OnBeforeHttpRequestZipOrDeflate(object sender, Core.Rest.Http.BeforeRequestEventArgs e) + { + if (e.RawRequest != null) + { + // e.RawRequest.AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip; + e.RawRequest.Headers.Remove("Accept-Encoding"); + e.RawRequest.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate"); } } [TestMethod, Ignore] // Something does not work with the gzip - [TestCategory("FhirClient"), + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void Search() + public void SearchWebClient() { FhirClient client = new FhirClient(testEndpoint); Bundle result; client.CompressRequestBody = true; - client.OnBeforeRequest += Compression_OnBeforeRequestGZip; - client.OnAfterResponse += Client_OnAfterResponse; + client.OnBeforeRequest += Compression_OnBeforeWebRequestGZip; + client.OnAfterResponse += Client_OnAfterWebResponse; result = client.Search(); - client.OnAfterResponse -= Client_OnAfterResponse; + client.OnAfterResponse -= Client_OnAfterWebResponse; Assert.IsNotNull(result); Assert.IsTrue(result.Entry.Count() > 10, "Test should use testdata with more than 10 reports"); - client.OnBeforeRequest -= Compression_OnBeforeRequestZipOrDeflate; - client.OnBeforeRequest += Compression_OnBeforeRequestZipOrDeflate; + client.OnBeforeRequest -= Compression_OnBeforeWebRequestZipOrDeflate; + client.OnBeforeRequest += Compression_OnBeforeWebRequestZipOrDeflate; result = client.Search(pageSize: 10); Assert.IsNotNull(result); Assert.IsTrue(result.Entry.Count <= 10); - client.OnBeforeRequest -= Compression_OnBeforeRequestGZip; + client.OnBeforeRequest -= Compression_OnBeforeWebRequestGZip; var withSubject = result.Entry.ByResourceType().FirstOrDefault(dr => dr.Subject != null); @@ -272,7 +421,7 @@ public void Search() // typeof(Patient).GetCollectionName())); - client.OnBeforeRequest += Compression_OnBeforeRequestDeflate; + client.OnBeforeRequest += Compression_OnBeforeWebRequestDeflate; result = client.Search(new string[] { "name=Chalmers", "name=Peter" }); @@ -280,7 +429,61 @@ public void Search() Assert.IsTrue(result.Entry.Count > 0); } - private void Client_OnAfterResponse(object sender, AfterResponseEventArgs e) + [TestMethod, Ignore] // Something does not work with the gzip + [TestCategory("FhirClient"), + TestCategory("IntegrationTest")] + public void SearchHttpClient() + { + using (var handler = new Core.Rest.Http.HttpClientEventHandler()) + using (TestClient client = new TestClient(testEndpoint, messageHandler: handler)) + { + Bundle result; + + client.CompressRequestBody = true; + handler.OnBeforeRequest += Compression_OnBeforeHttpRequestGZip; + + result = client.Search(); + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count() > 10, "Test should use testdata with more than 10 reports"); + + handler.OnBeforeRequest -= Compression_OnBeforeHttpRequestZipOrDeflate; + handler.OnBeforeRequest += Compression_OnBeforeHttpRequestZipOrDeflate; + + result = client.Search(pageSize: 10); + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count <= 10); + + handler.OnBeforeRequest -= Compression_OnBeforeHttpRequestGZip; + + var withSubject = + result.Entry.ByResourceType().FirstOrDefault(dr => dr.Subject != null); + Assert.IsNotNull(withSubject, "Test should use testdata with a report with a subject"); + + ResourceIdentity ri = withSubject.ResourceIdentity(); + + // TODO: The include on Grahame's server doesn't currently work + //result = client.SearchById(ri.Id, + // includes: new string[] { "DiagnosticReport:subject" }); + //Assert.IsNotNull(result); + + //Assert.AreEqual(2, result.Entry.Count); // should have subject too + + //Assert.IsNotNull(result.Entry.Single(entry => entry.Resource.ResourceIdentity().ResourceType == + // typeof(DiagnosticReport).GetCollectionName())); + //Assert.IsNotNull(result.Entry.Single(entry => entry.Resource.ResourceIdentity().ResourceType == + // typeof(Patient).GetCollectionName())); + + + handler.OnBeforeRequest += Compression_OnBeforeHttpRequestDeflate; + + result = client.Search(new string[] { "name=Chalmers", "name=Peter" }); + + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count > 0); + } + } + + private void Client_OnAfterWebResponse(object sender, Fhir.Rest.AfterResponseEventArgs e) { // Test that the response was compressed Assert.AreEqual("gzip", e.RawResponse.Headers[HttpResponseHeader.ContentEncoding]); @@ -288,7 +491,7 @@ private void Client_OnAfterResponse(object sender, AfterResponseEventArgs e) #if NO_ASYNC_ANYMORE [TestMethod, TestCategory("FhirClient")] - public void SearchAsync() + public void SearchAsyncWebClient() { FhirClient client = new FhirClient(testEndpoint); Bundle result; @@ -323,11 +526,49 @@ public void SearchAsync() Assert.IsNotNull(result); Assert.IsTrue(result.Entry.Count > 0); } + + public void SearchAsyncHttpClient() + { + using(TestClient client = new TestClient(testEndpoint)) + { + Bundle result; + + result = client.SearchAsync().Result; + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count() > 10, "Test should use testdata with more than 10 reports"); + + result = client.SearchAsync(pageSize: 10).Result; + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count <= 10); + + var withSubject = + result.Entry.ByResourceType().FirstOrDefault(dr => dr.Resource.Subject != null); + Assert.IsNotNull(withSubject, "Test should use testdata with a report with a subject"); + + ResourceIdentity ri = new ResourceIdentity(withSubject.Id); + + result = client.SearchByIdAsync(ri.Id, + includes: new string[] { "DiagnosticReport.subject" }).Result; + Assert.IsNotNull(result); + + Assert.AreEqual(2, result.Entry.Count); // should have subject too + + Assert.IsNotNull(result.Entry.Single(entry => new ResourceIdentity(entry.Id).Collection == + typeof(DiagnosticReport).GetCollectionName())); + Assert.IsNotNull(result.Entry.Single(entry => new ResourceIdentity(entry.Id).Collection == + typeof(Patient).GetCollectionName())); + + result = client.SearchAsync(new string[] { "name=Everywoman", "name=Eve" }).Result; + + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count > 0); + } + } #endif [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void Paging() + public void PagingWebClient() { FhirClient client = new FhirClient(testEndpoint); @@ -359,7 +600,40 @@ public void Paging() } [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void PagingInJson() + public void PagingHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + var result = client.Search(pageSize: 10); + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count <= 10); + + var firstId = result.Entry.First().Resource.Id; + + // Browse forward + result = client.Continue(result); + Assert.IsNotNull(result); + var nextId = result.Entry.First().Resource.Id; + Assert.AreNotEqual(firstId, nextId); + + // Browse to first + result = client.Continue(result, PageDirection.First); + Assert.IsNotNull(result); + var prevId = result.Entry.First().Resource.Id; + Assert.AreEqual(firstId, prevId); + + // Forward, then backwards + result = client.Continue(result, PageDirection.Next); + Assert.IsNotNull(result); + result = client.Continue(result, PageDirection.Previous); + Assert.IsNotNull(result); + prevId = result.Entry.First().Resource.Id; + Assert.AreEqual(firstId, prevId); + } + } + + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void PagingInJsonWebClient() { FhirClient client = new FhirClient(testEndpoint); client.PreferredFormat = ResourceFormat.Json; @@ -391,10 +665,45 @@ public void PagingInJson() Assert.AreEqual(firstId, prevId); } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void PagingInJsonHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + client.PreferredFormat = ResourceFormat.Json; + + var result = client.Search(pageSize: 10); + Assert.IsNotNull(result); + Assert.IsTrue(result.Entry.Count <= 10); + + var firstId = result.Entry.First().Resource.Id; + + // Browse forward + result = client.Continue(result); + Assert.IsNotNull(result); + var nextId = result.Entry.First().Resource.Id; + Assert.AreNotEqual(firstId, nextId); + + // Browse to first + result = client.Continue(result, PageDirection.First); + Assert.IsNotNull(result); + var prevId = result.Entry.First().Resource.Id; + Assert.AreEqual(firstId, prevId); + + // Forward, then backwards + result = client.Continue(result, PageDirection.Next); + Assert.IsNotNull(result); + result = client.Continue(result, PageDirection.Previous); + Assert.IsNotNull(result); + prevId = result.Entry.First().Resource.Id; + Assert.AreEqual(firstId, prevId); + } + } + [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void CreateAndFullRepresentation() + public void CreateAndFullRepresentationWebClient() { FhirClient client = new FhirClient(testEndpoint); client.PreferredReturn = Prefer.ReturnRepresentation; // which is also the default @@ -420,14 +729,47 @@ public void CreateAndFullRepresentation() // Now validate this resource client.PreferredReturn = Prefer.ReturnRepresentation; // which is also the default Parameters p = new Parameters(); - // p.Add("mode", new FhirString("create")); + // p.Add("mode", new FhirString("create")); p.Add("resource", pat); OperationOutcome ooI = (OperationOutcome)client.InstanceOperation(ri.WithoutVersion(), "validate", p); Assert.IsNotNull(ooI); } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void CreateAndFullRepresentationHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + client.PreferredReturn = Prefer.ReturnRepresentation; // which is also the default + + var pat = client.Read("Patient/glossy"); + ResourceIdentity ri = pat.ResourceIdentity().WithBase(client.Endpoint); + pat.Id = null; + pat.Identifier.Clear(); + var patC = client.Create(pat); + Assert.IsNotNull(patC); + client.PreferredReturn = Prefer.ReturnMinimal; + patC = client.Create(pat); + Assert.IsNull(patC); + + if (client.LastBody != null) + { + var returned = client.LastBodyAsResource; + Assert.IsTrue(returned is OperationOutcome); + } + + // Now validate this resource + client.PreferredReturn = Prefer.ReturnRepresentation; // which is also the default + Parameters p = new Parameters(); + // p.Add("mode", new FhirString("create")); + p.Add("resource", pat); + OperationOutcome ooI = (OperationOutcome)client.InstanceOperation(ri.WithoutVersion(), "validate", p); + Assert.IsNotNull(ooI); + } + } private Uri createdTestPatientUrl = null; @@ -437,11 +779,11 @@ public void CreateAndFullRepresentation() /// [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void CreateEditDelete() + public void CreateEditDeleteWebClient() { FhirClient client = new FhirClient(testEndpoint); - client.OnBeforeRequest += Compression_OnBeforeRequestZipOrDeflate; + client.OnBeforeRequest += Compression_OnBeforeWebRequestZipOrDeflate; // client.CompressRequestBody = true; var pat = client.Read("Patient/example"); @@ -478,17 +820,74 @@ public void CreateEditDelete() fe = client.Read(fe.ResourceIdentity().WithoutVersion()); Assert.Fail(); } - catch(FhirOperationException ex) + catch (FhirOperationException ex) { Assert.AreEqual(HttpStatusCode.Gone, ex.Status, "Expected the record to be gone"); Assert.AreEqual("410", client.LastResult.Status); } } + /// + /// This test is also used as a "setup" test for the History test. + /// If you change the number of operations in here, this will make the History test fail. + /// + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void CreateEditDeleteHttpClient() + { + using (var handler = new Core.Rest.Http.HttpClientEventHandler()) + using (TestClient client = new TestClient(testEndpoint, messageHandler: handler)) + { + + handler.OnBeforeRequest += Compression_OnBeforeHttpRequestZipOrDeflate; + // client.CompressRequestBody = true; + + var pat = client.Read("Patient/example"); + pat.Id = null; + pat.Identifier.Clear(); + pat.Identifier.Add(new Identifier("http://hl7.org/test/2", "99999")); + + System.Diagnostics.Trace.WriteLine(new FhirXmlSerializer().SerializeToString(pat)); + + var fe = client.Create(pat); // Create as we are not providing the ID to be used. + Assert.IsNotNull(fe); + Assert.IsNotNull(fe.Id); + Assert.IsNotNull(fe.Meta.VersionId); + createdTestPatientUrl = fe.ResourceIdentity(); + + fe.Identifier.Add(new Identifier("http://hl7.org/test/2", "3141592")); + var fe2 = client.Update(fe); + + Assert.IsNotNull(fe2); + Assert.AreEqual(fe.Id, fe2.Id); + Assert.AreNotEqual(fe.ResourceIdentity(), fe2.ResourceIdentity()); + Assert.AreEqual(2, fe2.Identifier.Count); + + fe.Identifier.Add(new Identifier("http://hl7.org/test/3", "3141592")); + var fe3 = client.Update(fe); + Assert.IsNotNull(fe3); + Assert.AreEqual(3, fe3.Identifier.Count); + + client.Delete(fe3); + + try + { + // Get most recent version + fe = client.Read(fe.ResourceIdentity().WithoutVersion()); + Assert.Fail(); + } + catch (FhirOperationException ex) + { + Assert.AreEqual(HttpStatusCode.Gone, ex.Status, "Expected the record to be gone"); + Assert.AreEqual("410", client.LastResult.Status); + } + } + } + [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] //Test for github issue https://github.com/ewoutkramer/fhir-net-api/issues/145 - public void Create_ObservationWithValueAsSimpleQuantity_ReadReturnsValueAsQuantity() + public void Create_ObservationWithValueAsSimpleQuantity_ReadReturnsValueAsQuantityWebClient() { FhirClient client = new FhirClient(testEndpoint); var observation = new Observation(); @@ -507,13 +906,37 @@ public void Create_ObservationWithValueAsSimpleQuantity_ReadReturnsValueAsQuanti Assert.IsInstanceOfType(fe.Value, typeof(Quantity)); } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + //Test for github issue https://github.com/ewoutkramer/fhir-net-api/issues/145 + public void Create_ObservationWithValueAsSimpleQuantity_ReadReturnsValueAsQuantityHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + var observation = new Observation(); + observation.Status = ObservationStatus.Preliminary; + observation.Code = new CodeableConcept("http://loinc.org", "2164-2"); + observation.Value = new SimpleQuantity() + { + System = "http://unitsofmeasure.org", + Value = 23, + Code = "mg", + Unit = "miligram" + }; + observation.BodySite = new CodeableConcept("http://snomed.info/sct", "182756003"); + var fe = client.Create(observation); + fe = client.Read(fe.ResourceIdentity().WithoutVersion()); + Assert.IsInstanceOfType(fe.Value, typeof(Quantity)); + } + } + #if NO_ASYNC_ANYMORE /// /// This test is also used as a "setup" test for the History test. /// If you change the number of operations in here, this will make the History test fail. /// [TestMethod, TestCategory("FhirClient")] - public void CreateEditDeleteAsync() + public void CreateEditDeleteAsyncWebClient() { var furore = new Organization { @@ -567,19 +990,139 @@ public void CreateEditDeleteAsync() Assert.IsTrue(client.LastResponseDetails.Result == HttpStatusCode.Gone); } } + + /// + /// This test is also used as a "setup" test for the History test. + /// If you change the number of operations in here, this will make the History test fail. + /// + [TestMethod, TestCategory("FhirClient")] + public void CreateEditDeleteAsyncHttpClient() + { + var furore = new Organization + { + Name = "Furore", + Identifier = new List { new Identifier("http://hl7.org/test/1", "3141") }, + Telecom = new List { new Contact { System = Contact.ContactSystem.Phone, Value = "+31-20-3467171" } } + }; + + using (TestClient client = new TestClient(testEndpoint)) + { + var tags = new List { new Tag("http://nu.nl/testname", Tag.FHIRTAGSCHEME_GENERAL, "TestCreateEditDelete") }; + + var fe = client.CreateAsync(furore, tags: tags, refresh: true).Result; + + Assert.IsNotNull(furore); + Assert.IsNotNull(fe); + Assert.IsNotNull(fe.Id); + Assert.IsNotNull(fe.SelfLink); + Assert.AreNotEqual(fe.Id, fe.SelfLink); + Assert.IsNotNull(fe.Tags); + Assert.AreEqual(1, fe.Tags.Count(), "Tag count on new organization record don't match"); + Assert.AreEqual(fe.Tags.First(), tags[0]); + createdTestOrganizationUrl = fe.Id; + + fe.Resource.Identifier.Add(new Identifier("http://hl7.org/test/2", "3141592")); + var fe2 = client.UpdateAsync(fe, refresh: true).Result; + + Assert.IsNotNull(fe2); + Assert.AreEqual(fe.Id, fe2.Id); + Assert.AreNotEqual(fe.SelfLink, fe2.SelfLink); + Assert.AreEqual(2, fe2.Resource.Identifier.Count); + + Assert.IsNotNull(fe2.Tags); + Assert.AreEqual(1, fe2.Tags.Count(), "Tag count on updated organization record don't match"); + Assert.AreEqual(fe2.Tags.First(), tags[0]); + + fe.Resource.Identifier.Add(new Identifier("http://hl7.org/test/3", "3141592")); + var fe3 = client.UpdateAsync(fe2.Id, fe.Resource, refresh: true).Result; + Assert.IsNotNull(fe3); + Assert.AreEqual(3, fe3.Resource.Identifier.Count); + + client.DeleteAsync(fe3).Wait(); + + try + { + // Get most recent version + fe = client.ReadAsync(new ResourceIdentity(fe.Id)).Result; + Assert.Fail(); + } + catch + { + Assert.IsTrue(client.LastResponseDetails.Result == HttpStatusCode.Gone); + } + } + } #endif /// /// This test will fail if the system records AuditEvents /// and counts them in the WholeSystemHistory /// - [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest"),Ignore] // Keeps on failing periodically. Grahames server? - public void History() + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest"), Ignore] // Keeps on failing periodically. Grahames server? + public void HistoryWebClient() + { + System.Threading.Thread.Sleep(500); + DateTimeOffset timestampBeforeCreationAndDeletions = DateTimeOffset.Now; + + CreateEditDeleteWebClient(); // this test does a create, update, update, delete (4 operations) + + FhirClient client = new FhirClient(testEndpoint); + + System.Diagnostics.Trace.WriteLine("History of this specific patient since just before the create, update, update, delete (4 operations)"); + + Bundle history = client.History(createdTestPatientUrl); + Assert.IsNotNull(history); + DebugDumpBundle(history); + + Assert.AreEqual(4, history.Entry.Count()); + Assert.AreEqual(3, history.Entry.Where(entry => entry.Resource != null).Count()); + Assert.AreEqual(1, history.Entry.Where(entry => entry.IsDeleted()).Count()); + + //// Now, assume no one is quick enough to insert something between now and the next + //// tests.... + + + System.Diagnostics.Trace.WriteLine("\r\nHistory on the patient type"); + history = client.TypeHistory("Patient", timestampBeforeCreationAndDeletions.ToUniversalTime()); + Assert.IsNotNull(history); + DebugDumpBundle(history); + Assert.AreEqual(4, history.Entry.Count()); // there's a race condition here, sometimes this is 5. + Assert.AreEqual(3, history.Entry.Where(entry => entry.Resource != null).Count()); + Assert.AreEqual(1, history.Entry.Where(entry => entry.IsDeleted()).Count()); + + + System.Diagnostics.Trace.WriteLine("\r\nHistory on the patient type (using the generic method in the client)"); + history = client.TypeHistory(timestampBeforeCreationAndDeletions.ToUniversalTime(), summary: SummaryType.True); + Assert.IsNotNull(history); + DebugDumpBundle(history); + Assert.AreEqual(4, history.Entry.Count()); + Assert.AreEqual(3, history.Entry.Where(entry => entry.Resource != null).Count()); + Assert.AreEqual(1, history.Entry.Where(entry => entry.IsDeleted()).Count()); + + if (!testEndpoint.OriginalString.Contains("sqlonfhir-stu3")) + { + System.Diagnostics.Trace.WriteLine("\r\nWhole system history since the start of this test"); + history = client.WholeSystemHistory(timestampBeforeCreationAndDeletions.ToUniversalTime()); + Assert.IsNotNull(history); + DebugDumpBundle(history); + Assert.IsTrue(4 <= history.Entry.Count(), "Whole System history should have at least 4 new events"); + // Check that the number of patients that have been created is what we expected + Assert.AreEqual(3, history.Entry.Where(entry => entry.Resource != null && entry.Resource is Patient).Count()); + Assert.AreEqual(1, history.Entry.Where(entry => entry.IsDeleted() && entry.Request.Url.Contains("Patient")).Count()); + } + } + + /// + /// This test will fail if the system records AuditEvents + /// and counts them in the WholeSystemHistory + /// + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest"), Ignore] // Keeps on failing periodically. Grahames server? + public void HistoryHttpClient() { System.Threading.Thread.Sleep(500); DateTimeOffset timestampBeforeCreationAndDeletions = DateTimeOffset.Now; - CreateEditDelete(); // this test does a create, update, update, delete (4 operations) + CreateEditDeleteHttpClient(); // this test does a create, update, update, delete (4 operations) FhirClient client = new FhirClient(testEndpoint); @@ -590,7 +1133,7 @@ public void History() DebugDumpBundle(history); Assert.AreEqual(4, history.Entry.Count()); - Assert.AreEqual(3, history.Entry.Where(entry => entry.Resource != null).Count()); + Assert.AreEqual(3, history.Entry.Where(entry => entry.Resource != null).Count()); Assert.AreEqual(1, history.Entry.Where(entry => entry.IsDeleted()).Count()); //// Now, assume no one is quick enough to insert something between now and the next @@ -630,27 +1173,38 @@ public void History() [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void TestWithParam() + public void TestWithParamWebClient() { var client = new FhirClient(testEndpoint); var res = client.Get("ValueSet/v2-0131/$validate-code?system=http://hl7.org/fhir/v2/0131&code=ep"); Assert.IsNotNull(res); } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void TestWithParamHttpClient() + { + using (var client = new TestClient(testEndpoint)) + { + var res = client.Get("ValueSet/v2-0131/$validate-code?system=http://hl7.org/fhir/v2/0131&code=ep"); + Assert.IsNotNull(res); + } + } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void ManipulateMeta() + public void ManipulateMetaWebClient() { - FhirClient client = new FhirClient(testEndpoint); + FhirClient client = new FhirClient(testEndpoint); var pat = new Patient(); pat.Meta = new Meta(); - var key = new Random().Next(); + var key = new Random().Next(); pat.Meta.ProfileElement.Add(new FhirUri("http://someserver.org/fhir/StructureDefinition/XYZ1-" + key)); pat.Meta.Security.Add(new Coding("http://mysystem.com/sec", "1234-" + key)); pat.Meta.Tag.Add(new Coding("http://mysystem.com/tag", "sometag1-" + key)); - //Before we begin, ensure that our new tags are not actually used when doing System Meta() - var wsm = client.Meta(); + //Before we begin, ensure that our new tags are not actually used when doing System Meta() + var wsm = client.Meta(); Assert.IsNotNull(wsm); Assert.IsFalse(wsm.Profile.Contains("http://someserver.org/fhir/StructureDefinition/XYZ1-" + key)); @@ -662,74 +1216,169 @@ public void ManipulateMeta() Assert.IsFalse(wsm.Tag.Select(c => c.Code + "@" + c.System).Contains("sometag2-" + key + "@http://mysystem.com/tag")); - // First, create a patient with the first set of meta - var pat2 = client.Create(pat); - var loc = pat2.ResourceIdentity(testEndpoint); + // First, create a patient with the first set of meta + var pat2 = client.Create(pat); + var loc = pat2.ResourceIdentity(testEndpoint); - // Meta should be present on created patient + // Meta should be present on created patient verifyMeta(pat2.Meta, false, key); - // Should be present when doing instance Meta() - var par = client.Meta(loc); + // Should be present when doing instance Meta() + var par = client.Meta(loc); verifyMeta(par, false, key); - // Should be present when doing type Meta() - par = client.Meta(ResourceType.Patient); + // Should be present when doing type Meta() + par = client.Meta(ResourceType.Patient); verifyMeta(par, false, key); - // Should be present when doing System Meta() - par = client.Meta(); + // Should be present when doing System Meta() + par = client.Meta(); verifyMeta(par, false, key); - // Now add some additional meta to the patient + // Now add some additional meta to the patient - var newMeta = new Meta(); + var newMeta = new Meta(); newMeta.ProfileElement.Add(new FhirUri("http://someserver.org/fhir/StructureDefinition/XYZ2-" + key)); - newMeta.Security.Add(new Coding("http://mysystem.com/sec", "5678-" + key)); - newMeta.Tag.Add(new Coding("http://mysystem.com/tag", "sometag2-" + key)); + newMeta.Security.Add(new Coding("http://mysystem.com/sec", "5678-" + key)); + newMeta.Tag.Add(new Coding("http://mysystem.com/tag", "sometag2-" + key)); + - - client.AddMeta(loc, newMeta); - var pat3 = client.Read(loc); + client.AddMeta(loc, newMeta); + var pat3 = client.Read(loc); - // New and old meta should be present on instance - verifyMeta(pat3.Meta, true, key); + // New and old meta should be present on instance + verifyMeta(pat3.Meta, true, key); - // New and old meta should be present on Meta() - par = client.Meta(loc); + // New and old meta should be present on Meta() + par = client.Meta(loc); verifyMeta(par, true, key); - // New and old meta should be present when doing type Meta() - par = client.Meta(ResourceType.Patient); + // New and old meta should be present when doing type Meta() + par = client.Meta(ResourceType.Patient); verifyMeta(par, true, key); - // New and old meta should be present when doing system Meta() - par = client.Meta(); + // New and old meta should be present when doing system Meta() + par = client.Meta(); verifyMeta(par, true, key); - // Now, remove those new meta tags - client.DeleteMeta(loc, newMeta); + // Now, remove those new meta tags + client.DeleteMeta(loc, newMeta); - // Should no longer be present on instance - var pat4 = client.Read(loc); - verifyMeta(pat4.Meta, false, key); + // Should no longer be present on instance + var pat4 = client.Read(loc); + verifyMeta(pat4.Meta, false, key); - // Should no longer be present when doing instance Meta() - par = client.Meta(loc); + // Should no longer be present when doing instance Meta() + par = client.Meta(loc); verifyMeta(par, false, key); - // Should no longer be present when doing type Meta() - par = client.Meta(ResourceType.Patient); + // Should no longer be present when doing type Meta() + par = client.Meta(ResourceType.Patient); verifyMeta(par, false, key); - // clear out the client that we created, no point keeping it around - client.Delete(pat4); + // clear out the client that we created, no point keeping it around + client.Delete(pat4); - // Should no longer be present when doing System Meta() - par = client.Meta(); + // Should no longer be present when doing System Meta() + par = client.Meta(); verifyMeta(par, false, key); } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void ManipulateMetaHttpClient() + { + using (TestClient client = new TestClient(testEndpoint)) + { + + var pat = new Patient(); + pat.Meta = new Meta(); + var key = new Random().Next(); + pat.Meta.ProfileElement.Add(new FhirUri("http://someserver.org/fhir/StructureDefinition/XYZ1-" + key)); + pat.Meta.Security.Add(new Coding("http://mysystem.com/sec", "1234-" + key)); + pat.Meta.Tag.Add(new Coding("http://mysystem.com/tag", "sometag1-" + key)); + + //Before we begin, ensure that our new tags are not actually used when doing System Meta() + var wsm = client.Meta(); + Assert.IsNotNull(wsm); + + Assert.IsFalse(wsm.Profile.Contains("http://someserver.org/fhir/StructureDefinition/XYZ1-" + key)); + Assert.IsFalse(wsm.Security.Select(c => c.Code + "@" + c.System).Contains("1234-" + key + "@http://mysystem.com/sec")); + Assert.IsFalse(wsm.Tag.Select(c => c.Code + "@" + c.System).Contains("sometag1-" + key + "@http://mysystem.com/tag")); + + Assert.IsFalse(wsm.Profile.Contains("http://someserver.org/fhir/StructureDefinition/XYZ2-" + key)); + Assert.IsFalse(wsm.Security.Select(c => c.Code + "@" + c.System).Contains("5678-" + key + "@http://mysystem.com/sec")); + Assert.IsFalse(wsm.Tag.Select(c => c.Code + "@" + c.System).Contains("sometag2-" + key + "@http://mysystem.com/tag")); + + + // First, create a patient with the first set of meta + var pat2 = client.Create(pat); + var loc = pat2.ResourceIdentity(testEndpoint); + + // Meta should be present on created patient + verifyMeta(pat2.Meta, false, key); + + // Should be present when doing instance Meta() + var par = client.Meta(loc); + verifyMeta(par, false, key); + + // Should be present when doing type Meta() + par = client.Meta(ResourceType.Patient); + verifyMeta(par, false, key); + + // Should be present when doing System Meta() + par = client.Meta(); + verifyMeta(par, false, key); + + // Now add some additional meta to the patient + + var newMeta = new Meta(); + newMeta.ProfileElement.Add(new FhirUri("http://someserver.org/fhir/StructureDefinition/XYZ2-" + key)); + newMeta.Security.Add(new Coding("http://mysystem.com/sec", "5678-" + key)); + newMeta.Tag.Add(new Coding("http://mysystem.com/tag", "sometag2-" + key)); + + + client.AddMeta(loc, newMeta); + var pat3 = client.Read(loc); + + // New and old meta should be present on instance + verifyMeta(pat3.Meta, true, key); + + // New and old meta should be present on Meta() + par = client.Meta(loc); + verifyMeta(par, true, key); + + // New and old meta should be present when doing type Meta() + par = client.Meta(ResourceType.Patient); + verifyMeta(par, true, key); + + // New and old meta should be present when doing system Meta() + par = client.Meta(); + verifyMeta(par, true, key); + + // Now, remove those new meta tags + client.DeleteMeta(loc, newMeta); + + // Should no longer be present on instance + var pat4 = client.Read(loc); + verifyMeta(pat4.Meta, false, key); + + // Should no longer be present when doing instance Meta() + par = client.Meta(loc); + verifyMeta(par, false, key); + + // Should no longer be present when doing type Meta() + par = client.Meta(ResourceType.Patient); + verifyMeta(par, false, key); + + // clear out the client that we created, no point keeping it around + client.Delete(pat4); + + // Should no longer be present when doing System Meta() + par = client.Meta(); + verifyMeta(par, false, key); + } + } + private void verifyMeta(Meta meta, bool hasNew, int key) { @@ -755,7 +1404,7 @@ private void verifyMeta(Meta meta, bool hasNew, int key) [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void TestSearchByPersonaCode() + public void TestSearchByPersonaCodeWebClient() { var client = new FhirClient(testEndpoint); @@ -765,18 +1414,31 @@ public void TestSearchByPersonaCode() var pat = (Patient)pats.Entry.First().Resource; } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void TestSearchByPersonaCodeHttpClient() + { + using (var client = new TestClient(testEndpoint)) + { + var pats = + client.Search( + new[] { string.Format("identifier={0}|{1}", "urn:oid:1.2.36.146.595.217.0.1", "12345") }); + var pat = (Patient)pats.Entry.First().Resource; + } + } + [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void CreateDynamic() + public void CreateDynamicWebClient() { Resource furore = new Organization { Name = "Furore", Identifier = new List { new Identifier("http://hl7.org/test/1", "3141") }, - Telecom = new List { + Telecom = new List { new ContactPoint { System = ContactPoint.ContactPointSystem.Phone, Value = "+31-20-3467171", Use = ContactPoint.ContactPointUse.Work }, - new ContactPoint { System = ContactPoint.ContactPointSystem.Fax, Value = "+31-20-3467172" } + new ContactPoint { System = ContactPoint.ContactPointSystem.Fax, Value = "+31-20-3467172" } } }; @@ -789,7 +1451,30 @@ public void CreateDynamic() [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void CallsCallbacks() + public void CreateDynamicHttpClient() + { + Resource furore = new Organization + { + Name = "Furore", + Identifier = new List { new Identifier("http://hl7.org/test/1", "3141") }, + Telecom = new List { + new ContactPoint { System = ContactPoint.ContactPointSystem.Phone, Value = "+31-20-3467171", Use = ContactPoint.ContactPointUse.Work }, + new ContactPoint { System = ContactPoint.ContactPointSystem.Fax, Value = "+31-20-3467172" } + } + }; + + using (TestClient client = new TestClient(testEndpoint)) + { + System.Diagnostics.Trace.WriteLine(new FhirXmlSerializer().SerializeToString(furore)); + + var fe = client.Create(furore); + Assert.IsNotNull(fe); + } + } + + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void CallsCallbacksWebClient() { FhirClient client = new FhirClient(testEndpoint); client.ParserSettings.AllowUnrecognizedEnums = true; @@ -829,6 +1514,51 @@ public void CallsCallbacks() Assert.IsTrue(bodyText.Contains(" + { + calledBefore = true; + bodyOut = e.Body; + }; + + handler.OnAfterResponse += (sender, e) => + { + body = e.Body; + status = e.RawResponse.StatusCode; + }; + + var pat = client.Read("Patient/glossy"); + Assert.IsTrue(calledBefore); + Assert.IsNotNull(status); + Assert.IsNotNull(body); + + var bodyText = HttpToEntryExtensions.DecodeBody(body, Encoding.UTF8); + + Assert.IsTrue(bodyText.Contains(" e.RawRequest.Headers["Prefer"] = minimal ? "return=minimal" : "return=representation"; + client.OnBeforeRequest += (object s, Fhir.Rest.BeforeRequestEventArgs e) => e.RawRequest.Headers["Prefer"] = minimal ? "return=minimal" : "return=representation"; var result = client.Read("Patient/glossy"); Assert.IsNotNull(result); @@ -874,14 +1604,40 @@ public void RequestFullResource() Assert.IsNull(posted); } - void client_OnBeforeRequest(object sender, BeforeRequestEventArgs e) + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void RequestFullResourceHttpClient() + { + using (var handler = new Core.Rest.Http.HttpClientEventHandler()) + using (var client = new TestClient(testEndpoint, messageHandler: handler)) + { + + var result = client.Read("Patient/glossy"); + Assert.IsNotNull(result); + result.Id = null; + result.Meta = null; + + client.PreferredReturn = Prefer.ReturnRepresentation; + var posted = client.Create(result); + Assert.IsNotNull(posted, "Patient example not found"); + + posted = client.Create(result); + Assert.IsNotNull(posted, "Did not return a resource, even when ReturnFullResource=true"); + + client.PreferredReturn = Prefer.ReturnMinimal; + posted = client.Create(result); + Assert.IsNull(posted); + } + } + + void client_OnBeforeRequest(object sender, Fhir.Rest.BeforeRequestEventArgs e) { throw new NotImplementedException(); } [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] // Currently ignoring, as spark.furore.com returns Status 500. - public void TestReceiveHtmlIsHandled() + public void TestReceiveHtmlIsHandledWebClient() { var client = new FhirClient("http://spark.furore.com/"); // an address that returns html @@ -894,11 +1650,28 @@ public void TestReceiveHtmlIsHandled() if (!fe.Message.Contains("a valid FHIR xml/json body type was expected") && !fe.Message.Contains("not recognized as either xml or json")) Assert.Fail("Failed to recognize invalid body contents"); } - } + } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] // Currently ignoring, as spark.furore.com returns Status 500. + public void TestReceiveHtmlIsHandledHttpClient() + { + using (var client = new TestClient("http://spark.furore.com/")) // an address that returns html + { + try + { + var pat = client.Read("Patient/1"); + } + catch (FhirOperationException fe) + { + if (!fe.Message.Contains("a valid FHIR xml/json body type was expected") && !fe.Message.Contains("not recognized as either xml or json")) + Assert.Fail("Failed to recognize invalid body contents"); + } + } + } [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void TestRefresh() + public void TestRefreshWebClient() { var client = new FhirClient(testEndpoint); var result = client.Read("Patient/example"); @@ -912,9 +1685,26 @@ public void TestRefresh() Assert.AreEqual(orig, result.Name[0].FamilyElement.Value); } + [TestMethod, TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void TestRefreshHttpClient() + { + using (var client = new TestClient(testEndpoint)) + { + var result = client.Read("Patient/example"); + + var orig = result.Name[0].FamilyElement.Value; + + result.Name[0].FamilyElement.Value = "overwritten name"; + + result = client.Refresh(result); + + Assert.AreEqual(orig, result.Name[0].FamilyElement.Value); + } + } + [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void TestReceiveErrorStatusWithHtmlIsHandled() + public void TestReceiveErrorStatusWithHtmlIsHandledWebClient() { var client = new FhirClient("http://spark.furore.com/"); // an address that returns Status 500 with HTML in its body @@ -956,10 +1746,54 @@ public void TestReceiveErrorStatusWithHtmlIsHandled() } } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void TestReceiveErrorStatusWithHtmlIsHandledHttpClient() + { + using (var client = new TestClient("http://spark.furore.com/")) // an address that returns Status 500 with HTML in its body + { + try + { + var pat = client.Read("Patient/1"); + Assert.Fail("Failed to throw an Exception on status 500"); + } + catch (FhirOperationException fe) + { + // Expected exception happened + if (fe.Status != HttpStatusCode.InternalServerError) + Assert.Fail("Server response of 500 did not result in FhirOperationException with status 500."); + + if (client.LastResult == null) + Assert.Fail("LastResult not set in error case."); + + if (client.LastResult.Status != "500") + Assert.Fail("LastResult.Status is not 500."); + + if (!fe.Message.Contains("a valid FHIR xml/json body type was expected") && !fe.Message.Contains("not recognized as either xml or json")) + Assert.Fail("Failed to recognize invalid body contents"); + + // Check that LastResult is of type OperationOutcome and properly filled. + OperationOutcome operationOutcome = client.LastBodyAsResource as OperationOutcome; + Assert.IsNotNull(operationOutcome, "Returned resource is not an OperationOutcome"); + + Assert.IsTrue(operationOutcome.Issue.Count > 0, "OperationOutcome does not contain an issue"); + + Assert.IsTrue(operationOutcome.Issue[0].Severity == OperationOutcome.IssueSeverity.Error, "OperationOutcome is not of severity 'error'"); + + string message = operationOutcome.Issue[0].Diagnostics; + if (!message.Contains("a valid FHIR xml/json body type was expected") && !message.Contains("not recognized as either xml or json")) + Assert.Fail("Failed to carry error message over into OperationOutcome"); + } + catch (Exception) + { + Assert.Fail("Failed to throw FhirOperationException on status 500"); + } + } + } [TestMethod] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] - public void TestReceiveErrorStatusWithOperationOutcomeIsHandled() + public void TestReceiveErrorStatusWithOperationOutcomeIsHandledWebClient() { var client = new FhirClient("http://test.fhir.org/r3"); // an address that returns Status 404 with an OperationOutcome @@ -996,9 +1830,49 @@ public void TestReceiveErrorStatusWithOperationOutcomeIsHandled() } } + [TestMethod] + [TestCategory("FhirClient"), TestCategory("IntegrationTest")] + public void TestReceiveErrorStatusWithOperationOutcomeIsHandledHttpClient() + { + using (var client = new FhirClient("http://test.fhir.org/r3"))// an address that returns Status 404 with an OperationOutcome + { + try + { + var pat = client.Read("Patient/doesnotexist"); + Assert.Fail("Failed to throw an Exception on status 404"); + } + catch (FhirOperationException fe) + { + // Expected exception happened + if (fe.Status != HttpStatusCode.NotFound) + Assert.Fail("Server response of 404 did not result in FhirOperationException with status 404."); + + if (client.LastResult == null) + Assert.Fail("LastResult not set in error case."); + + Bundle.ResponseComponent entryComponent = client.LastResult; + + if (entryComponent.Status != "404") + Assert.Fail("LastResult.Status is not 404."); + // Check that LastResult is of type OperationOutcome and properly filled. + OperationOutcome operationOutcome = client.LastBodyAsResource as OperationOutcome; + Assert.IsNotNull(operationOutcome, "Returned resource is not an OperationOutcome"); - [TestMethod,Ignore] + Assert.IsTrue(operationOutcome.Issue.Count > 0, "OperationOutcome does not contain an issue"); + + Assert.IsTrue(operationOutcome.Issue[0].Severity == OperationOutcome.IssueSeverity.Error, "OperationOutcome is not of severity 'error'"); + } + catch (Exception e) + { + Assert.Fail("Failed to throw FhirOperationException on status 404: " + e.Message); + } + } + } + + + + [TestMethod, Ignore] [TestCategory("FhirClient"), TestCategory("IntegrationTest")] public void FhirVersionIsChecked() { @@ -1043,7 +1917,7 @@ public void FhirVersionIsChecked() client = new FhirClient(testEndpointDSTU12); client.ParserSettings.AllowUnrecognizedEnums = true; - + try { p = client.CapabilityStatement(); @@ -1057,10 +1931,10 @@ public void FhirVersionIsChecked() } [TestMethod, TestCategory("IntegrationTest"), TestCategory("FhirClient")] - public void TestAuthenticationOnBefore() + public void TestAuthenticationOnBeforeWebClient() { FhirClient validationFhirClient = new FhirClient("https://sqlonfhir.azurewebsites.net/fhir"); - validationFhirClient.OnBeforeRequest += (object sender, BeforeRequestEventArgs e) => + validationFhirClient.OnBeforeRequest += (object sender, Fhir.Rest.BeforeRequestEventArgs e) => { e.RawRequest.Headers["Authorization"] = "Bearer bad-bearer"; }; @@ -1069,14 +1943,33 @@ public void TestAuthenticationOnBefore() var output = validationFhirClient.ValidateResource(new Patient()); } - catch(FhirOperationException ex) + catch (FhirOperationException ex) { Assert.IsTrue(ex.Status == HttpStatusCode.Forbidden || ex.Status == HttpStatusCode.Unauthorized, "Excpeted a security exception"); } } [TestMethod, TestCategory("IntegrationTest"), TestCategory("FhirClient")] - public void TestOperationEverything() + public void TestAuthenticationOnBeforeHttpClient() + { + using (TestClient validationFhirClient = new TestClient("https://sqlonfhir.azurewebsites.net/fhir")) + { + validationFhirClient.RequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "bad-bearer"); + + try + { + var output = validationFhirClient.ValidateResource(new Patient()); + + } + catch (FhirOperationException ex) + { + Assert.IsTrue(ex.Status == HttpStatusCode.Forbidden || ex.Status == HttpStatusCode.Unauthorized, "Excpeted a security exception"); + } + } + } + + [TestMethod, TestCategory("IntegrationTest"), TestCategory("FhirClient")] + public void TestOperationEverythingWebClient() { FhirClient client = new FhirClient(testEndpoint) { @@ -1128,6 +2021,34 @@ public void TestOperationEverything() loc = client.TypeOperation("everything", new Parameters().Add("start", new Date(2017, 10)), useGet: false); Assert.IsNotNull(loc); } + + [TestMethod, TestCategory("IntegrationTest"), TestCategory("FhirClient")] + public void TestOperationEverythingHttpClient() + { + using (TestClient client = new TestClient(testEndpoint) + { + UseFormatParam = true, + PreferredFormat = ResourceFormat.Json + }) + { + // GET operation $everything without parameters + var loc = client.TypeOperation("everything", null, true); + Assert.IsNotNull(loc); + + // POST operation $everything without parameters + loc = client.TypeOperation("everything", null, false); + Assert.IsNotNull(loc); + + // GET operation $everything with 1 parameter + // This doesn't work yet. When an operation is used with primitive types then those parameters must be appended to the url as query parameters. + // loc = client.TypeOperation("everything", new Parameters().Add("start", new Date(2017, 10)), true); + // Assert.IsNotNull(loc); + + // POST operation $everything with 1 parameter + loc = client.TypeOperation("everything", new Parameters().Add("start", new Date(2017, 10)), false); + Assert.IsNotNull(loc); + } + } } } diff --git a/src/Hl7.Fhir.Core.Tests/Rest/OperationsTests.cs b/src/Hl7.Fhir.Core.Tests/Rest/OperationsTests.cs index ee03c198e4..f292404239 100644 --- a/src/Hl7.Fhir.Core.Tests/Rest/OperationsTests.cs +++ b/src/Hl7.Fhir.Core.Tests/Rest/OperationsTests.cs @@ -14,6 +14,7 @@ using System.Text; using System.Threading.Tasks; using Hl7.Fhir.Rest; +using TestClient = Hl7.Fhir.Rest.Http.FhirClient; namespace Hl7.Fhir.Tests.Rest { @@ -25,7 +26,7 @@ public class OperationsTests [TestMethod] [TestCategory("IntegrationTest")] - public void InvokeTestPatientGetEverything() + public void InvokeTestPatientGetEverythingWebClient() { var client = new FhirClient(testEndpoint); var start = new FhirDateTime(2014,11,1); @@ -40,18 +41,44 @@ public void InvokeTestPatientGetEverything() [TestMethod] [TestCategory("IntegrationTest")] - public void InvokeExpandExistingValueSet() + public void InvokeTestPatientGetEverythingHttpClient() + { + using (var client = new TestClient(testEndpoint)) + { + var start = new FhirDateTime(2014, 11, 1); + var end = new FhirDateTime(2015, 1, 1); + var par = new Parameters().Add("start", start).Add("end", end); + var bundle = (Bundle)client.InstanceOperation(ResourceIdentity.Build("Patient", "example"), "everything", par); + Assert.IsTrue(bundle.Entry.Any()); + + var bundle2 = client.FetchPatientRecord(ResourceIdentity.Build("Patient", "example"), start, end); + Assert.IsTrue(bundle2.Entry.Any()); + } + } + + [TestMethod] + [TestCategory("IntegrationTest")] + public void InvokeExpandExistingValueSetWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); var vs = client.ExpandValueSet(ResourceIdentity.Build("ValueSet","administrative-gender")); Assert.IsTrue(vs.Expansion.Contains.Any()); } - + [TestMethod] + [TestCategory("IntegrationTest")] + public void InvokeExpandExistingValueSetHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var vs = client.ExpandValueSet(ResourceIdentity.Build("ValueSet", "administrative-gender")); + Assert.IsTrue(vs.Expansion.Contains.Any()); + } + } [TestMethod] [TestCategory("IntegrationTest")] - public void InvokeExpandParameterValueSet() + public void InvokeExpandParameterValueSetWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); @@ -61,6 +88,19 @@ public void InvokeExpandParameterValueSet() Assert.IsTrue(vsX.Expansion.Contains.Any()); } + [TestMethod] + [TestCategory("IntegrationTest")] + public void InvokeExpandParameterValueSetHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var vs = client.Read("ValueSet/administrative-gender"); + var vsX = client.ExpandValueSet(vs); + + Assert.IsTrue(vsX.Expansion.Contains.Any()); + } + } + // [WMR 20170927] Chris Munro // https://chat.fhir.org/#narrow/stream/implementers/subject/How.20to.20expand.20ValueSets.20with.20the.20C.23.20FHIR.20API.3F //[TestMethod] @@ -79,7 +119,7 @@ public void InvokeExpandParameterValueSet() /// [TestMethod] // Server returns internal server error [TestCategory("IntegrationTest")] - public void InvokeLookupCoding() + public void InvokeLookupCodingWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); var coding = new Coding("http://hl7.org/fhir/administrative-gender", "male"); @@ -90,9 +130,27 @@ public void InvokeLookupCoding() Assert.AreEqual("Male", expansion.GetSingleValue("display").Value); } + /// + /// http://hl7.org/fhir/valueset-operations.html#lookup + /// + [TestMethod] // Server returns internal server error + [TestCategory("IntegrationTest")] + public void InvokeLookupCodingHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var coding = new Coding("http://hl7.org/fhir/administrative-gender", "male"); + + var expansion = client.ConceptLookup(coding: coding); + + // Assert.AreEqual("AdministrativeGender", expansion.GetSingleValue("name").Value); // Returns empty currently on Grahame's server + Assert.AreEqual("Male", expansion.GetSingleValue("display").Value); + } + } + [TestMethod] // Server returns internal server error [TestCategory("IntegrationTest")] - public void InvokeLookupCode() + public void InvokeLookupCodeWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); var expansion = client.ConceptLookup(code: new Code("male"), system: new FhirUri("http://hl7.org/fhir/administrative-gender")); @@ -101,9 +159,22 @@ public void InvokeLookupCode() Assert.AreEqual("Male", expansion.GetSingleValue("display").Value); } + [TestMethod] // Server returns internal server error + [TestCategory("IntegrationTest")] + public void InvokeLookupCodeHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var expansion = client.ConceptLookup(code: new Code("male"), system: new FhirUri("http://hl7.org/fhir/administrative-gender")); + + //Assert.AreEqual("male", expansion.GetSingleValue("name").Value); // Returns empty currently on Grahame's server + Assert.AreEqual("Male", expansion.GetSingleValue("display").Value); + } + } + [TestMethod] [TestCategory("IntegrationTest")] - public void InvokeValidateCodeById() + public void InvokeValidateCodeByIdWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); var coding = new Coding("http://snomed.info/sct", "4322002"); @@ -114,7 +185,20 @@ public void InvokeValidateCodeById() [TestMethod] [TestCategory("IntegrationTest")] - public void InvokeValidateCodeByCanonical() + public void InvokeValidateCodeByIdHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var coding = new Coding("http://snomed.info/sct", "4322002"); + + var result = client.ValidateCode("c80-facilitycodes", coding: coding, @abstract: new FhirBoolean(false)); + Assert.IsTrue(result.Result?.Value == true); + } + } + + [TestMethod] + [TestCategory("IntegrationTest")] + public void InvokeValidateCodeByCanonicalWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); var coding = new Coding("http://snomed.info/sct", "4322002"); @@ -126,7 +210,21 @@ public void InvokeValidateCodeByCanonical() [TestMethod] [TestCategory("IntegrationTest")] - public void InvokeValidateCodeWithVS() + public void InvokeValidateCodeByCanonicalHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var coding = new Coding("http://snomed.info/sct", "4322002"); + + var result = client.ValidateCode(url: new FhirUri("http://hl7.org/fhir/ValueSet/c80-facilitycodes"), + coding: coding, @abstract: new FhirBoolean(false)); + Assert.IsTrue(result.Result?.Value == true); + } + } + + [TestMethod] + [TestCategory("IntegrationTest")] + public void InvokeValidateCodeWithVSWebClient() { var client = new FhirClient(FhirClientTests.TerminologyEndpoint); var coding = new Coding("http://snomed.info/sct", "4322002"); @@ -138,10 +236,26 @@ public void InvokeValidateCodeWithVS() Assert.IsTrue(result.Result?.Value == true); } + [TestMethod] + [TestCategory("IntegrationTest")] + public void InvokeValidateCodeWithVSHttpClient() + { + using (var client = new TestClient(FhirClientTests.TerminologyEndpoint)) + { + var coding = new Coding("http://snomed.info/sct", "4322002"); + + var vs = client.Read("ValueSet/c80-facilitycodes"); + Assert.IsNotNull(vs); + + var result = client.ValidateCode(valueSet: vs, coding: coding); + Assert.IsTrue(result.Result?.Value == true); + } + } + [TestMethod]//returns 500: validation of slices is not done yet. [TestCategory("IntegrationTest"), Ignore] - public void InvokeResourceValidation() + public void InvokeResourceValidationWebClient() { var client = new FhirClient(testEndpoint); @@ -160,9 +274,31 @@ public void InvokeResourceValidation() } } + [TestMethod]//returns 500: validation of slices is not done yet. + [TestCategory("IntegrationTest"), Ignore] + public void InvokeResourceValidationHttpClient() + { + using (var client = new TestClient(testEndpoint)) + { + var pat = client.Read("Patient/patient-uslab-example1"); + + try + { + var vresult = client.ValidateResource(pat, null, + new FhirUri("http://hl7.org/fhir/StructureDefinition/uslab-patient")); + Assert.Fail("Should have resulted in 400"); + } + catch (FhirOperationException fe) + { + Assert.AreEqual(System.Net.HttpStatusCode.BadRequest, fe.Status); + Assert.IsTrue(fe.Outcome.Issue.Where(i => i.Severity == OperationOutcome.IssueSeverity.Error).Any()); + } + } + } + [TestMethod] [TestCategory("IntegrationTest")] - public async System.Threading.Tasks.Task InvokeTestPatientGetEverythingAsync() + public async System.Threading.Tasks.Task InvokeTestPatientGetEverythingAsyncWebClient() { string _endpoint = "https://api.hspconsortium.org/rpineda/open"; @@ -182,5 +318,30 @@ public async System.Threading.Tasks.Task InvokeTestPatientGetEverythingAsync() var bundle2 = (Bundle)bundle2Task.Result; Assert.IsTrue(bundle2.Entry.Any()); } + + [TestMethod] + [TestCategory("IntegrationTest")] + public async System.Threading.Tasks.Task InvokeTestPatientGetEverythingAsyncHttpClient() + { + string _endpoint = "https://api.hspconsortium.org/rpineda/open"; + + using (var client = new TestClient(_endpoint)) + { + var start = new FhirDateTime(2014, 11, 1); + var end = new FhirDateTime(2020, 1, 1); + var par = new Parameters().Add("start", start).Add("end", end); + + var bundleTask = client.InstanceOperationAsync(ResourceIdentity.Build("Patient", "SMART-1288992"), "everything", par); + var bundle2Task = client.FetchPatientRecordAsync(ResourceIdentity.Build("Patient", "SMART-1288992"), start, end); + + await System.Threading.Tasks.Task.WhenAll(bundleTask, bundle2Task); + + var bundle = (Bundle)bundleTask.Result; + Assert.IsTrue(bundle.Entry.Any()); + + var bundle2 = (Bundle)bundle2Task.Result; + Assert.IsTrue(bundle2.Entry.Any()); + } + } } } diff --git a/src/Hl7.Fhir.Core.Tests/Rest/ReadAsyncTests.cs b/src/Hl7.Fhir.Core.Tests/Rest/ReadAsyncTests.cs index 94fac8a41e..4ca7f78871 100644 --- a/src/Hl7.Fhir.Core.Tests/Rest/ReadAsyncTests.cs +++ b/src/Hl7.Fhir.Core.Tests/Rest/ReadAsyncTests.cs @@ -4,6 +4,7 @@ using Hl7.Fhir.Model; using Hl7.Fhir.Rest; using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestClient = Hl7.Fhir.Rest.Http.FhirClient; namespace Hl7.Fhir.Core.AsyncTests { @@ -14,7 +15,7 @@ public class ReadAsyncTests [TestMethod] [TestCategory("IntegrationTest")] - public async System.Threading.Tasks.Task Read_UsingResourceIdentity_ResultReturned() + public async System.Threading.Tasks.Task Read_UsingResourceIdentity_ResultReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -32,7 +33,26 @@ public async System.Threading.Tasks.Task Read_UsingResourceIdentity_ResultReturn [TestMethod] [TestCategory("IntegrationTest")] - public async System.Threading.Tasks.Task Read_UsingLocationString_ResultReturned() + public async System.Threading.Tasks.Task Read_UsingResourceIdentity_ResultReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + Patient p = await client.ReadAsync(new ResourceIdentity("/Patient/SMART-1288992")); + Assert.IsNotNull(p); + Assert.IsNotNull(p.Name[0].Given); + Assert.IsNotNull(p.Name[0].Family); + Console.WriteLine($"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + Console.WriteLine("Test Completed"); + } + } + + [TestMethod] + [TestCategory("IntegrationTest")] + public async System.Threading.Tasks.Task Read_UsingLocationString_ResultReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -47,5 +67,24 @@ public async System.Threading.Tasks.Task Read_UsingLocationString_ResultReturned Console.WriteLine($"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); Console.WriteLine("Test Completed"); } + + [TestMethod] + [TestCategory("IntegrationTest")] + public async System.Threading.Tasks.Task Read_UsingLocationString_ResultReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + Patient p = await client.ReadAsync("/Patient/SMART-1288992"); + Assert.IsNotNull(p); + Assert.IsNotNull(p.Name[0].Given); + Assert.IsNotNull(p.Name[0].Family); + Console.WriteLine($"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + Console.WriteLine("Test Completed"); + } + } } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Core.Tests/Rest/RequesterTests.cs b/src/Hl7.Fhir.Core.Tests/Rest/RequesterTests.cs index f3e9def4e8..f72e1ff995 100644 --- a/src/Hl7.Fhir.Core.Tests/Rest/RequesterTests.cs +++ b/src/Hl7.Fhir.Core.Tests/Rest/RequesterTests.cs @@ -7,9 +7,11 @@ */ using System; +using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Hl7.Fhir.Support; using Hl7.Fhir.Rest; +using Hl7.Fhir.Rest.Http; using Hl7.Fhir.Model; using Hl7.Fhir.Utility; @@ -37,7 +39,7 @@ public void SetsInteractionType() } [TestMethod] - public void TestPreferSetting() + public void TestPreferSettingWebRequester() { var p = new Patient(); var tx = new TransactionBuilder("http://myserver.org/fhir") @@ -69,6 +71,42 @@ public void TestPreferSetting() request = b.Entry[0].ToHttpRequest(null, Prefer.ReturnRepresentation, ResourceFormat.Json, false, false, out dummy); Assert.IsNull(request.Headers["Prefer"]); } + + [TestMethod] + public void TestPreferSettingHttpRequester() + { + var p = new Patient(); + var tx = new TransactionBuilder("http://myserver.org/fhir") + .Create(p); + var b = tx.ToBundle(); + byte[] dummy; + + var request = b.Entry[0].ToHttpRequestMessage(SearchParameterHandling.Lenient, Prefer.ReturnMinimal, ResourceFormat.Json, false, false); + Assert.AreEqual("return=minimal", request.Headers.GetValues("Prefer").FirstOrDefault()); + + request = b.Entry[0].ToHttpRequestMessage(SearchParameterHandling.Strict, Prefer.ReturnRepresentation, ResourceFormat.Json, false, false); + Assert.AreEqual("return=representation", request.Headers.GetValues("Prefer").FirstOrDefault()); + + request = b.Entry[0].ToHttpRequestMessage(SearchParameterHandling.Strict, Prefer.OperationOutcome, ResourceFormat.Json, false, false); + Assert.AreEqual("return=OperationOutcome", request.Headers.GetValues("Prefer").FirstOrDefault()); + + request = b.Entry[0].ToHttpRequestMessage(SearchParameterHandling.Strict, null, ResourceFormat.Json, false, false); + request.Headers.TryGetValues("Prefer", out var preferHeader); + Assert.IsNull(preferHeader); + + tx = new TransactionBuilder("http://myserver.org/fhir").Search(new SearchParams().Where("name=ewout"), resourceType: "Patient"); + b = tx.ToBundle(); + + request = b.Entry[0].ToHttpRequestMessage(SearchParameterHandling.Lenient, Prefer.ReturnMinimal, ResourceFormat.Json, false, false); + Assert.AreEqual("handling=lenient", request.Headers.GetValues("Prefer").FirstOrDefault()); + + request = b.Entry[0].ToHttpRequestMessage(SearchParameterHandling.Strict, Prefer.ReturnRepresentation, ResourceFormat.Json, false, false); + Assert.AreEqual("handling=strict", request.Headers.GetValues("Prefer").FirstOrDefault()); + + request = b.Entry[0].ToHttpRequestMessage(null, Prefer.ReturnRepresentation, ResourceFormat.Json, false, false); + request.Headers.TryGetValues("Prefer", out preferHeader); + Assert.IsNull(preferHeader); + } } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Core.Tests/Rest/SearchAsyncTests.cs b/src/Hl7.Fhir.Core.Tests/Rest/SearchAsyncTests.cs index c1f98dc942..8a54a437a3 100644 --- a/src/Hl7.Fhir.Core.Tests/Rest/SearchAsyncTests.cs +++ b/src/Hl7.Fhir.Core.Tests/Rest/SearchAsyncTests.cs @@ -4,6 +4,7 @@ using Hl7.Fhir.Rest; using Task = System.Threading.Tasks.Task; using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestClient = Hl7.Fhir.Rest.Http.FhirClient; namespace Hl7.Fhir.Core.AsyncTests { @@ -14,7 +15,7 @@ public class SearchAsyncTests [TestMethod] [TestCategory("IntegrationTest")] - public async Task Search_UsingSearchParams_SearchReturned() + public async Task Search_UsingSearchParams_SearchReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -42,13 +43,50 @@ public async Task Search_UsingSearchParams_SearchReturned() } result1 = client.Continue(result1, PageDirection.Next); } - + Console.WriteLine("Test Completed"); } [TestMethod] [TestCategory("IntegrationTest")] - public void SearchSync_UsingSearchParams_SearchReturned() + public async Task Search_UsingSearchParams_SearchReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + + var srch = new SearchParams() + .Where("name=Daniel") + .LimitTo(10) + .SummaryOnly() + .OrderBy("birthdate", + SortOrder.Descending); + + var result1 = await client.SearchAsync(srch); + Assert.IsTrue(result1.Entry.Count >= 1); + + while (result1 != null) + { + foreach (var e in result1.Entry) + { + Patient p = (Patient)e.Resource; + Console.WriteLine( + $"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + } + result1 = client.Continue(result1, PageDirection.Next); + } + + Console.WriteLine("Test Completed"); + } + } + + + [TestMethod] + [TestCategory("IntegrationTest")] + public void SearchSync_UsingSearchParams_SearchReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -81,10 +119,45 @@ public void SearchSync_UsingSearchParams_SearchReturned() Console.WriteLine("Test Completed"); } + [TestMethod] + [TestCategory("IntegrationTest")] + public void SearchSync_UsingSearchParams_SearchReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + var srch = new SearchParams() + .Where("name=Daniel") + .LimitTo(10) + .SummaryOnly() + .OrderBy("birthdate", + SortOrder.Descending); + + var result1 = client.Search(srch); + + Assert.IsTrue(result1.Entry.Count >= 1); + + while (result1 != null) + { + foreach (var e in result1.Entry) + { + Patient p = (Patient)e.Resource; + Console.WriteLine( + $"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + } + result1 = client.Continue(result1, PageDirection.Next); + } + + Console.WriteLine("Test Completed"); + } + } [TestMethod] [TestCategory("IntegrationTest")] - public async Task SearchMultiple_UsingSearchParams_SearchReturned() + public async Task SearchMultiple_UsingSearchParams_SearchReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -98,7 +171,7 @@ public async Task SearchMultiple_UsingSearchParams_SearchReturned() .SummaryOnly() .OrderBy("birthdate", SortOrder.Descending); - + var task1 = client.SearchAsync(srchParams); var task2 = client.SearchAsync(srchParams); var task3 = client.SearchAsync(srchParams); @@ -107,7 +180,7 @@ public async Task SearchMultiple_UsingSearchParams_SearchReturned() var result1 = task1.Result; Assert.IsTrue(result1.Entry.Count >= 1); - + while (result1 != null) { foreach (var e in result1.Entry) @@ -124,15 +197,56 @@ public async Task SearchMultiple_UsingSearchParams_SearchReturned() [TestMethod] [TestCategory("IntegrationTest")] - public async Task SearchWithCriteria_SyncContinue_SearchReturned() + public async Task SearchMultiple_UsingSearchParams_SearchReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + var srchParams = new SearchParams() + .Where("name=Daniel") + .LimitTo(10) + .SummaryOnly() + .OrderBy("birthdate", + SortOrder.Descending); + + var task1 = client.SearchAsync(srchParams); + var task2 = client.SearchAsync(srchParams); + var task3 = client.SearchAsync(srchParams); + + await Task.WhenAll(task1, task2, task3); + var result1 = task1.Result; + + Assert.IsTrue(result1.Entry.Count >= 1); + + while (result1 != null) + { + foreach (var e in result1.Entry) + { + Patient p = (Patient)e.Resource; + Console.WriteLine( + $"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + } + result1 = client.Continue(result1, PageDirection.Next); + } + + Console.WriteLine("Test Completed"); + } + } + + [TestMethod] + [TestCategory("IntegrationTest")] + public async Task SearchWithCriteria_SyncContinue_SearchReturnedWebClient() { var client = new FhirClient(_endpoint) { PreferredFormat = ResourceFormat.Json, PreferredReturn = Prefer.ReturnRepresentation }; - - var result1 = await client.SearchAsync(new []{"family=clark"}); + + var result1 = await client.SearchAsync(new[] { "family=clark" }); Assert.IsTrue(result1.Entry.Count >= 1); @@ -152,7 +266,36 @@ public async Task SearchWithCriteria_SyncContinue_SearchReturned() [TestMethod] [TestCategory("IntegrationTest")] - public async Task SearchWithCriteria_AsyncContinue_SearchReturned() + public async Task SearchWithCriteria_SyncContinue_SearchReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + var result1 = await client.SearchAsync(new[] { "family=clark" }); + + Assert.IsTrue(result1.Entry.Count >= 1); + + while (result1 != null) + { + foreach (var e in result1.Entry) + { + Patient p = (Patient)e.Resource; + Console.WriteLine( + $"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + } + result1 = client.Continue(result1, PageDirection.Next); + } + + Console.WriteLine("Test Completed"); + } + } + + [TestMethod] + [TestCategory("IntegrationTest")] + public async Task SearchWithCriteria_AsyncContinue_SearchReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -160,7 +303,7 @@ public async Task SearchWithCriteria_AsyncContinue_SearchReturned() PreferredReturn = Prefer.ReturnRepresentation }; - var result1 = await client.SearchAsync(new[] { "family=clark" },null,1); + var result1 = await client.SearchAsync(new[] { "family=clark" }, null, 1); Assert.IsTrue(result1.Entry.Count >= 1); @@ -178,5 +321,35 @@ public async Task SearchWithCriteria_AsyncContinue_SearchReturned() Console.WriteLine("Test Completed"); } + + [TestMethod] + [TestCategory("IntegrationTest")] + public async Task SearchWithCriteria_AsyncContinue_SearchReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + var result1 = await client.SearchAsync(new[] { "family=clark" }, null, 1); + + Assert.IsTrue(result1.Entry.Count >= 1); + + while (result1 != null) + { + foreach (var e in result1.Entry) + { + Patient p = (Patient)e.Resource; + Console.WriteLine( + $"NAME: {p.Name[0].Given.FirstOrDefault()} {p.Name[0].Family.FirstOrDefault()}"); + } + Console.WriteLine("Fetching more results..."); + result1 = await client.ContinueAsync(result1); + } + + Console.WriteLine("Test Completed"); + } + } } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Core.Tests/Rest/UpdateRefreshDeleteAsyncTests.cs b/src/Hl7.Fhir.Core.Tests/Rest/UpdateRefreshDeleteAsyncTests.cs index 0b0d811413..d397197145 100644 --- a/src/Hl7.Fhir.Core.Tests/Rest/UpdateRefreshDeleteAsyncTests.cs +++ b/src/Hl7.Fhir.Core.Tests/Rest/UpdateRefreshDeleteAsyncTests.cs @@ -4,6 +4,7 @@ using Hl7.Fhir.Model; using Hl7.Fhir.Rest; using Microsoft.VisualStudio.TestTools.UnitTesting; +using TestClient = Hl7.Fhir.Rest.Http.FhirClient; namespace Hl7.Fhir.Core.AsyncTests { @@ -14,7 +15,7 @@ public class UpdateRefreshDeleteAsyncTests [TestMethod] [TestCategory("IntegrationTest")] - public async System.Threading.Tasks.Task UpdateDelete_UsingResourceIdentity_ResultReturned() + public async System.Threading.Tasks.Task UpdateDelete_UsingResourceIdentity_ResultReturnedWebClient() { var client = new FhirClient(_endpoint) { @@ -55,10 +56,58 @@ public async System.Threading.Tasks.Task UpdateDelete_UsingResourceIdentity_Resu // VERIFY // Assert.ThrowsException(act, "the patient is no longer on the server"); - - + + Console.WriteLine("Test Completed"); } - + + [TestMethod] + [TestCategory("IntegrationTest")] + public async System.Threading.Tasks.Task UpdateDelete_UsingResourceIdentity_ResultReturnedHttpClient() + { + using (var client = new TestClient(_endpoint) + { + PreferredFormat = ResourceFormat.Json, + PreferredReturn = Prefer.ReturnRepresentation + }) + { + var pat = new Patient() + { + Name = new List() + { + new HumanName() + { + Given = new List() {"test_given"}, + Family = "test_family", + } + }, + Id = "async-test-patient" + }; + // Create the patient + Console.WriteLine("Creating patient..."); + Patient p = await client.UpdateAsync(pat); + Assert.IsNotNull(p); + + // Refresh the patient + Console.WriteLine("Refreshing patient..."); + await client.RefreshAsync(p); + + // Delete the patient + Console.WriteLine("Deleting patient..."); + await client.DeleteAsync(p); + + Console.WriteLine("Reading patient..."); + Func act = async () => + { + await client.ReadAsync(new ResourceIdentity("/Patient/async-test-patient")); + }; + + // VERIFY // + Assert.ThrowsException(act, "the patient is no longer on the server"); + + + Console.WriteLine("Test Completed"); + } + } } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Core/Hl7.Fhir.Core.csproj b/src/Hl7.Fhir.Core/Hl7.Fhir.Core.csproj index c73f7761b9..41cc43b822 100644 --- a/src/Hl7.Fhir.Core/Hl7.Fhir.Core.csproj +++ b/src/Hl7.Fhir.Core/Hl7.Fhir.Core.csproj @@ -30,6 +30,7 @@ + @@ -38,7 +39,6 @@ - diff --git a/src/Hl7.Fhir.Core/Rest/BaseFhirClient.cs b/src/Hl7.Fhir.Core/Rest/BaseFhirClient.cs new file mode 100644 index 0000000000..079940312a --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/BaseFhirClient.cs @@ -0,0 +1,1083 @@ +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using Hl7.Fhir.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Hl7.Fhir.Rest +{ + public abstract partial class BaseFhirClient : IDisposable, IFhirClient + { + [Obsolete] + public abstract event EventHandler OnAfterResponse; + + [Obsolete] + public abstract event EventHandler OnBeforeRequest; + + protected IRequester Requester { get; set; } + + /// + /// The default endpoint for use with operations that use discrete id/version parameters + /// instead of explicit uri endpoints. This will always have a trailing "/" + /// + public Uri Endpoint + { + get; + protected set; + } + + #region << Client Communication Defaults (PreferredFormat, UseFormatParam, Timeout, ReturnFullResource) >> + public bool VerifyFhirVersion + { + get; + set; + } + + /// + /// The preferred format of the content to be used when communicating with the FHIR server (XML or JSON) + /// + public ResourceFormat PreferredFormat + { + get { return Requester.PreferredFormat; } + set { Requester.PreferredFormat = value; } + } + + /// + /// When passing the content preference, use the _format parameter instead of the request header + /// + public bool UseFormatParam + { + get { return Requester.UseFormatParameter; } + set { Requester.UseFormatParameter = value; } + } + + /// + /// The timeout (in milliseconds) to be used when making calls to the FHIR server + /// + public int Timeout + { + get { return Requester.Timeout; } + set { Requester.Timeout = value; } + } + + + //private bool _returnFullResource = false; + + /// + /// Should calls to Create, Update and transaction operations return the whole updated content? + /// + /// Refer to specification section 2.1.0.5 (Managing Return Content) + [Obsolete("In STU3 this is no longer a true/false option, use the PreferredReturn property instead")] + public bool ReturnFullResource + { + get => Requester.PreferredReturn == Prefer.ReturnRepresentation; + set => Requester.PreferredReturn = value ? Prefer.ReturnRepresentation : Prefer.ReturnMinimal; + } + + /// + /// Should calls to Create, Update and transaction operations return the whole updated content, + /// or an OperationOutcome? + /// + /// Refer to specification section 2.1.0.5 (Managing Return Content) + + public Prefer? PreferredReturn + { + get => Requester.PreferredReturn; + set => Requester.PreferredReturn = value; + } + + /// + /// Should server return which search parameters were supported after executing a search? + /// If true, the server should return an error for any unknown or unsupported parameter, otherwise + /// the server may ignore any unknown or unsupported parameter. + /// + public SearchParameterHandling? PreferredParameterHandling + { + get => Requester.PreferredParameterHandling; + set => Requester.PreferredParameterHandling = value; + } +#endregion + + +#if NET_COMPRESSION + /// + /// This will do 2 things: + /// 1. Add the header Accept-Encoding: gzip, deflate + /// 2. decompress any responses that have Content-Encoding: gzip (or deflate) + /// + public bool PreferCompressedResponses + { + get { return Requester.PreferCompressedResponses; } + set { Requester.PreferCompressedResponses = value; } + } + /// + /// Compress any Request bodies + /// (warning, if a server does not handle compressed requests you will get a 415 response) + /// + public bool CompressRequestBody + { + get { return Requester.CompressRequestBody; } + set { Requester.CompressRequestBody = value; } + } +#endif + + + /// + /// The last transaction result that was executed on this connection to the FHIR server + /// + public Bundle.ResponseComponent LastResult => Requester.LastResult?.Response; + + public ParserSettings ParserSettings + { + get { return Requester.ParserSettings; } + set { Requester.ParserSettings = value; } + } + + public abstract byte[] LastBody { get; } + + public abstract Resource LastBodyAsResource { get; } + + public abstract string LastBodyAsText { get; } + + [Obsolete] + public virtual HttpWebRequest LastRequest { get => throw new NotImplementedException(); } + + [Obsolete] + public virtual HttpWebResponse LastResponse { get => throw new NotImplementedException(); } + + protected static Uri GetValidatedEndpoint(Uri endpoint) + { + if (endpoint == null) throw new ArgumentNullException("endpoint"); + + if (!endpoint.OriginalString.EndsWith("/")) + endpoint = new Uri(endpoint.OriginalString + "/"); + + if (!endpoint.IsAbsoluteUri) throw new ArgumentException("endpoint", "Endpoint must be absolute"); + + return endpoint; + } + + #region Read + + /// + /// Fetches a typed resource from a FHIR resource endpoint. + /// + /// The url of the Resource to fetch. This can be a Resource id url or a version-specific + /// Resource url. + /// The (weak) ETag to use in a conditional read. Optional. + /// Last modified since date in a conditional read. Optional. (refer to spec 2.1.0.5) If this is used, the client will throw an exception you need + /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown + /// + /// The requested resource. This operation will throw an exception + /// if the resource has been deleted or does not exist. + /// The specified may be relative or absolute, if it is an absolute + /// url, it must reference an address within the endpoint. + /// + /// Since ResourceLocation is a subclass of Uri, you may pass in ResourceLocations too. + /// This will occur if conditional request returns a status 304 and optionally an OperationOutcome + public Task ReadAsync(Uri location, string ifNoneMatch = null, DateTimeOffset? ifModifiedSince = null) where TResource : Resource + { + if (location == null) throw Error.ArgumentNull(nameof(location)); + + var id = verifyResourceIdentity(location, needId: true, needVid: false); + Bundle tx; + + if (!id.HasVersion) + { + var ri = new TransactionBuilder(Endpoint).Read(id.ResourceType, id.Id, ifNoneMatch, ifModifiedSince); + tx = ri.ToBundle(); + } + else + { + tx = new TransactionBuilder(Endpoint).VRead(id.ResourceType, id.Id, id.VersionId).ToBundle(); + } + + return executeAsync(tx, HttpStatusCode.OK); + } + /// + /// Fetches a typed resource from a FHIR resource endpoint. + /// + /// The url of the Resource to fetch. This can be a Resource id url or a version-specific + /// Resource url. + /// The (weak) ETag to use in a conditional read. Optional. + /// Last modified since date in a conditional read. Optional. (refer to spec 2.1.0.5) If this is used, the client will throw an exception you need + /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown + /// + /// The requested resource. This operation will throw an exception + /// if the resource has been deleted or does not exist. + /// The specified may be relative or absolute, if it is an absolute + /// url, it must reference an address within the endpoint. + /// + /// Since ResourceLocation is a subclass of Uri, you may pass in ResourceLocations too. + /// This will occur if conditional request returns a status 304 and optionally an OperationOutcome + public TResource Read(Uri location, string ifNoneMatch = null, + DateTimeOffset? ifModifiedSince = null) where TResource : Resource + { + return ReadAsync(location, ifNoneMatch, ifModifiedSince).WaitResult(); + } + + /// + /// Fetches a typed resource from a FHIR resource endpoint. + /// + /// The url of the Resource to fetch as a string. This can be a Resource id url or a version-specific + /// Resource url. + /// The (weak) ETag to use in a conditional read. Optional. + /// Last modified since date in a conditional read. Optional. + /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown + /// The requested resource + /// This operation will throw an exception + /// if the resource has been deleted or does not exist. The specified may be relative or absolute, if it is an absolute + /// url, it must reference an address within the endpoint. + public Task ReadAsync(string location, string ifNoneMatch = null, DateTimeOffset? ifModifiedSince = null) where TResource : Resource + { + return ReadAsync(new Uri(location, UriKind.RelativeOrAbsolute), ifNoneMatch, ifModifiedSince); + } + /// + /// Fetches a typed resource from a FHIR resource endpoint. + /// + /// The url of the Resource to fetch as a string. This can be a Resource id url or a version-specific + /// Resource url. + /// The (weak) ETag to use in a conditional read. Optional. + /// Last modified since date in a conditional read. Optional. + /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown + /// The requested resource + /// This operation will throw an exception + /// if the resource has been deleted or does not exist. The specified may be relative or absolute, if it is an absolute + /// url, it must reference an address within the endpoint. + public TResource Read(string location, string ifNoneMatch = null, + DateTimeOffset? ifModifiedSince = null) where TResource : Resource + { + return ReadAsync(location, ifNoneMatch, ifModifiedSince).WaitResult(); + } + + #endregion + + #region Refresh + + /// + /// Refreshes the data in the resource passed as an argument by re-reading it from the server + /// + /// + /// The resource for which you want to get the most recent version. + /// A new instance of the resource, containing the most up-to-date data + /// This function will not overwrite the argument with new data, rather it will return a new instance + /// which will have the newest data, leaving the argument intact. + public Task RefreshAsync(TResource current) where TResource : Resource + { + if (current == null) throw Error.ArgumentNull(nameof(current)); + + return ReadAsync(ResourceIdentity.Build(current.TypeName, current.Id)); + } + /// + /// Refreshes the data in the resource passed as an argument by re-reading it from the server + /// + /// + /// The resource for which you want to get the most recent version. + /// A new instance of the resource, containing the most up-to-date data + /// This function will not overwrite the argument with new data, rather it will return a new instance + /// which will have the newest data, leaving the argument intact. + public TResource Refresh(TResource current) where TResource : Resource + { + return RefreshAsync(current).WaitResult(); + } + + #endregion + + #region Update + + /// + /// Update (or create) a resource + /// + /// The resource to update + /// If true, asks the server to verify we are updating the latest version + /// The type of resource that is being updated + /// The body of the updated resource, unless ReturnFullResource is set to "false" + /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. + /// If the resource does not yet exist - and the server allows client-assigned id's - a new resource with the given id will be + /// created. + public Task UpdateAsync(TResource resource, bool versionAware = false) where TResource : Resource + { + if (resource == null) throw Error.ArgumentNull(nameof(resource)); + if (resource.Id == null) throw Error.Argument(nameof(resource), "Resource needs a non-null Id to send the update to"); + + var upd = new TransactionBuilder(Endpoint); + + if (versionAware && resource.HasVersionId) + upd.Update(resource.Id, resource, versionId: resource.VersionId); + else + upd.Update(resource.Id, resource); + + return internalUpdateAsync(resource, upd.ToBundle()); + } + /// + /// Update (or create) a resource + /// + /// The resource to update + /// If true, asks the server to verify we are updating the latest version + /// The type of resource that is being updated + /// The body of the updated resource, unless ReturnFullResource is set to "false" + /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. + /// If the resource does not yet exist - and the server allows client-assigned id's - a new resource with the given id will be + /// created. + public TResource Update(TResource resource, bool versionAware = false) where TResource : Resource + { + return UpdateAsync(resource, versionAware).WaitResult(); + } + + /// + /// Conditionally update (or create) a resource + /// + /// The resource to update + /// Criteria used to locate the resource to update + /// If true, asks the server to verify we are updating the latest version + /// The type of resource that is being updated + /// The body of the updated resource, unless ReturnFullResource is set to "false" + /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. + /// If the criteria passed in condition do not match a resource a new resource with a server assigned id will be created. + public Task UpdateAsync(TResource resource, SearchParams condition, bool versionAware = false) where TResource : Resource + { + if (resource == null) throw Error.ArgumentNull(nameof(resource)); + if (condition == null) throw Error.ArgumentNull(nameof(condition)); + + var upd = new TransactionBuilder(Endpoint); + + if (versionAware && resource.HasVersionId) + upd.Update(condition, resource, versionId: resource.VersionId); + else + upd.Update(condition, resource); + + return internalUpdateAsync(resource, upd.ToBundle()); + } + /// + /// Conditionally update (or create) a resource + /// + /// The resource to update + /// Criteria used to locate the resource to update + /// If true, asks the server to verify we are updating the latest version + /// The type of resource that is being updated + /// The body of the updated resource, unless ReturnFullResource is set to "false" + /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. + /// If the criteria passed in condition do not match a resource a new resource with a server assigned id will be created. + public TResource Update(TResource resource, SearchParams condition, bool versionAware = false) + where TResource : Resource + { + return UpdateAsync(resource, condition, versionAware).WaitResult(); + } + private Task internalUpdateAsync(TResource resource, Bundle tx) where TResource : Resource + { + resource.ResourceBase = Endpoint; + + // This might be an update of a resource that doesn't yet exist, so accept a status Created too + return executeAsync(tx, new[] { HttpStatusCode.Created, HttpStatusCode.OK }); + } + private TResource internalUpdate(TResource resource, Bundle tx) where TResource : Resource + { + return internalUpdateAsync(resource, tx).WaitResult(); + } + #endregion + + #region Delete + + /// + /// Delete a resource at the given endpoint. + /// + /// endpoint of the resource to delete + /// Throws an exception when the delete failed, though this might + /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was + /// already deleted). + public async System.Threading.Tasks.Task DeleteAsync(Uri location) + { + if (location == null) throw Error.ArgumentNull(nameof(location)); + + var id = verifyResourceIdentity(location, needId: true, needVid: false); + var tx = new TransactionBuilder(Endpoint).Delete(id.ResourceType, id.Id).ToBundle(); + + await executeAsync(tx, new[] { HttpStatusCode.OK, HttpStatusCode.NoContent }).ConfigureAwait(false); + } + /// + /// Delete a resource at the given endpoint. + /// + /// endpoint of the resource to delete + /// Throws an exception when the delete failed, though this might + /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was + /// already deleted). + public void Delete(Uri location) + { + DeleteAsync(location).WaitNoResult(); + } + /// + /// Delete a resource at the given endpoint. + /// + /// endpoint of the resource to delete + /// Throws an exception when the delete failed, though this might + /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was + /// already deleted). + public System.Threading.Tasks.Task DeleteAsync(string location) + { + return DeleteAsync(new Uri(location, UriKind.Relative)); + } + /// + /// Delete a resource at the given endpoint. + /// + /// endpoint of the resource to delete + /// Throws an exception when the delete failed, though this might + /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was + /// already deleted). + public void Delete(string location) + { + DeleteAsync(location).WaitNoResult(); + } + + + /// + /// Delete a resource + /// + /// The resource to delete + public async System.Threading.Tasks.Task DeleteAsync(Resource resource) + { + if (resource == null) throw Error.ArgumentNull(nameof(resource)); + if (resource.Id == null) throw Error.Argument(nameof(resource), "Entry must have an id"); + + await DeleteAsync(resource.ResourceIdentity(Endpoint).WithoutVersion()).ConfigureAwait(false); + } + /// + /// Delete a resource + /// + /// The resource to delete + public void Delete(Resource resource) + { + DeleteAsync(resource).WaitNoResult(); + } + + /// + /// Conditionally delete a resource + /// + /// The type of resource to delete + /// Criteria to use to match the resource to delete. + public async System.Threading.Tasks.Task DeleteAsync(string resourceType, SearchParams condition) + { + if (resourceType == null) throw Error.ArgumentNull(nameof(resourceType)); + if (condition == null) throw Error.ArgumentNull(nameof(condition)); + + var tx = new TransactionBuilder(Endpoint).Delete(resourceType, condition).ToBundle(); + await executeAsync(tx, new[] { HttpStatusCode.OK, HttpStatusCode.NoContent }).ConfigureAwait(false); + } + /// + /// Conditionally delete a resource + /// + /// The type of resource to delete + /// Criteria to use to match the resource to delete. + public void Delete(string resourceType, SearchParams condition) + { + DeleteAsync(resourceType, condition).WaitNoResult(); + } + + #endregion + + #region Create + + /// + /// Create a resource on a FHIR endpoint + /// + /// The resource instance to create + /// The resource as created on the server, or an exception if the create failed. + /// The type of resource to create + public Task CreateAsync(TResource resource) where TResource : Resource + { + if (resource == null) throw Error.ArgumentNull(nameof(resource)); + + var tx = new TransactionBuilder(Endpoint).Create(resource).ToBundle(); + + return executeAsync(tx, new[] { HttpStatusCode.Created, HttpStatusCode.OK }); + } + /// + /// Create a resource on a FHIR endpoint + /// + /// The resource instance to create + /// The resource as created on the server, or an exception if the create failed. + /// The type of resource to create + public TResource Create(TResource resource) where TResource : Resource + { + return CreateAsync(resource).WaitResult(); + } + + /// + /// Conditionally Create a resource on a FHIR endpoint + /// + /// The resource instance to create + /// The criteria + /// The resource as created on the server, or an exception if the create failed. + /// The type of resource to create + public Task CreateAsync(TResource resource, SearchParams condition) where TResource : Resource + { + if (resource == null) throw Error.ArgumentNull(nameof(resource)); + if (condition == null) throw Error.ArgumentNull(nameof(condition)); + + var tx = new TransactionBuilder(Endpoint).Create(resource, condition).ToBundle(); + + return executeAsync(tx, new[] { HttpStatusCode.Created, HttpStatusCode.OK }); + } + public TResource Create(TResource resource, SearchParams condition) where TResource : Resource + { + return CreateAsync(resource, condition).WaitResult(); + } + + #endregion + + #region Conformance + + /// + /// Get a conformance statement for the system + /// + /// A Conformance resource. Throws an exception if the operation failed. + [Obsolete("The Conformance operation has been replaced by the CapabilityStatement", false)] + public CapabilityStatement Conformance(SummaryType? summary = null) + { + return CapabilityStatement(summary); + } + + /// + /// Get a conformance statement for the system + /// + /// A Conformance resource. Throws an exception if the operation failed. + public Task CapabilityStatementAsync(SummaryType? summary = null) + { + var tx = new TransactionBuilder(Endpoint).CapabilityStatement(summary).ToBundle(); + return executeAsync(tx, HttpStatusCode.OK); + } + + /// + /// Get a conformance statement for the system + /// + /// A Conformance resource. Throws an exception if the operation failed. + public CapabilityStatement CapabilityStatement(SummaryType? summary = null) + { + return CapabilityStatementAsync(summary).WaitResult(); + } + #endregion + + #region History + + /// + /// Retrieve the version history for a specific resource type + /// + /// The type of Resource to get the history for + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Task TypeHistoryAsync(string resourceType, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return internalHistoryAsync(resourceType, null, since, pageSize, summary); + } + /// + /// Retrieve the version history for a specific resource type + /// + /// The type of Resource to get the history for + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Bundle TypeHistory(string resourceType, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return TypeHistoryAsync(resourceType, since, pageSize, summary).WaitResult(); + } + + /// + /// Retrieve the version history for a specific resource type + /// + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// The type of Resource to get the history for + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Task TypeHistoryAsync(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) where TResource : Resource, new() + { + string collection = typeof(TResource).GetCollectionName(); + return internalHistoryAsync(collection, null, since, pageSize, summary); + } + /// + /// Retrieve the version history for a specific resource type + /// + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// The type of Resource to get the history for + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Bundle TypeHistory(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) where TResource : Resource, new() + { + return TypeHistoryAsync(since, pageSize, summary).WaitResult(); + } + + /// + /// Retrieve the version history for a resource at a given location + /// + /// The address of the resource to get the history for + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Task HistoryAsync(Uri location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + if (location == null) throw Error.ArgumentNull(nameof(location)); + + var id = verifyResourceIdentity(location, needId: true, needVid: false); + return internalHistoryAsync(id.ResourceType, id.Id, since, pageSize, summary); + } + /// + /// Retrieve the version history for a resource at a given location + /// + /// The address of the resource to get the history for + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Bundle History(Uri location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return HistoryAsync(location, since, pageSize, summary).WaitResult(); + } + + /// + /// Retrieve the version history for a resource at a given location + /// + /// The address of the resource to get the history for + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Task HistoryAsync(string location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return HistoryAsync(new Uri(location, UriKind.Relative), since, pageSize, summary); + } + /// + /// Retrieve the version history for a resource at a given location + /// + /// The address of the resource to get the history for + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Optional. Asks the server to only provide the fields defined for the summary + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Bundle History(string location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return HistoryAsync(location, since, pageSize, summary).WaitResult(); + } + + /// + /// Retrieve the full version history of the server + /// + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Indicates whether the returned resources should just contain the minimal set of elements + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Task WholeSystemHistoryAsync(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return internalHistoryAsync(null, null, since, pageSize, summary); + } + /// + /// Retrieve the full version history of the server + /// + /// Optional. Returns only changes after the given date + /// Optional. Asks server to limit the number of entries per page returned + /// Indicates whether the returned resources should just contain the minimal set of elements + /// A bundle with the history for the indicated instance, may contain both + /// ResourceEntries and DeletedEntries. + public Bundle WholeSystemHistory(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + return WholeSystemHistoryAsync(since, pageSize, summary).WaitResult(); + } + private Task internalHistoryAsync(string resourceType = null, string id = null, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) + { + TransactionBuilder history; + + if (resourceType == null) + history = new TransactionBuilder(Endpoint).ServerHistory(summary, pageSize, since); + else if (id == null) + history = new TransactionBuilder(Endpoint).CollectionHistory(resourceType, summary, pageSize, since); + else + history = new TransactionBuilder(Endpoint).ResourceHistory(resourceType, id, summary, pageSize, since); + + return executeAsync(history.ToBundle(), HttpStatusCode.OK); + } + private Bundle internalHistory(string resourceType = null, string id = null, DateTimeOffset? since = null, + int? pageSize = null, SummaryType summary = SummaryType.False) + { + return internalHistoryAsync(resourceType, id, since, pageSize, summary).WaitResult(); + } + + #endregion + + #region Transaction + + /// + /// Send a set of creates, updates and deletes to the server to be processed in one transaction + /// + /// The bundled creates, updates and deleted + /// A bundle as returned by the server after it has processed the transaction, or null + /// if an error occurred. + public Task TransactionAsync(Bundle bundle) + { + if (bundle == null) throw new ArgumentNullException(nameof(bundle)); + + var tx = new TransactionBuilder(Endpoint).Transaction(bundle).ToBundle(); + return executeAsync(tx, HttpStatusCode.OK); + } + /// + /// Send a set of creates, updates and deletes to the server to be processed in one transaction + /// + /// The bundled creates, updates and deleted + /// A bundle as returned by the server after it has processed the transaction, or null + /// if an error occurred. + public Bundle Transaction(Bundle bundle) + { + return TransactionAsync(bundle).WaitResult(); + } + + #endregion + + #region Operation + + public Task WholeSystemOperationAsync(string operationName, Parameters parameters = null, bool useGet = false) + { + if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); + return internalOperationAsync(operationName, parameters: parameters, useGet: useGet); + } + + public Resource WholeSystemOperation(string operationName, Parameters parameters = null, bool useGet = false) + { + return WholeSystemOperationAsync(operationName, parameters, useGet).WaitResult(); + } + + + public Task TypeOperationAsync(string operationName, Parameters parameters = null, bool useGet = false) + where TResource : Resource + { + if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); + + // [WMR 20160421] GetResourceNameForType is obsolete + // var typeName = ModelInfo.GetResourceNameForType(typeof(TResource)); + var typeName = ModelInfo.GetFhirTypeNameForType(typeof(TResource)); + + return TypeOperationAsync(operationName, typeName, parameters, useGet: useGet); + } + public Resource TypeOperation(string operationName, Parameters parameters = null, + bool useGet = false) where TResource : Resource + { + return TypeOperationAsync(operationName, parameters, useGet).WaitResult(); + } + + + + public Task TypeOperationAsync(string operationName, string typeName, Parameters parameters = null, bool useGet = false) + { + if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); + if (typeName == null) throw Error.ArgumentNull(nameof(typeName)); + + return internalOperationAsync(operationName, typeName, parameters: parameters, useGet: useGet); + } + public Resource TypeOperation(string operationName, string typeName, Parameters parameters = null, bool useGet = false) + { + return TypeOperationAsync(operationName, typeName, parameters, useGet).WaitResult(); + } + + + + public Task InstanceOperationAsync(Uri location, string operationName, Parameters parameters = null, bool useGet = false) + { + if (location == null) throw Error.ArgumentNull(nameof(location)); + if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); + + var id = verifyResourceIdentity(location, needId: true, needVid: false); + + return internalOperationAsync(operationName, id.ResourceType, id.Id, id.VersionId, parameters, useGet); + } + public Resource InstanceOperation(Uri location, string operationName, Parameters parameters = null, bool useGet = false) + { + return InstanceOperationAsync(location, operationName, parameters, useGet).WaitResult(); + } + + + + public Task OperationAsync(Uri location, string operationName, Parameters parameters = null, bool useGet = false) + { + if (location == null) throw Error.ArgumentNull(nameof(location)); + if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); + + var tx = new TransactionBuilder(Endpoint).EndpointOperation(new RestUrl(location), operationName, parameters, useGet).ToBundle(); + + return executeAsync(tx, HttpStatusCode.OK); + } + public Resource Operation(Uri location, string operationName, Parameters parameters = null, bool useGet = false) + { + return OperationAsync(location, operationName, parameters, useGet).WaitResult(); + } + + + public Task OperationAsync(Uri operation, Parameters parameters = null, bool useGet = false) + { + if (operation == null) throw Error.ArgumentNull(nameof(operation)); + + var tx = new TransactionBuilder(Endpoint).EndpointOperation(new RestUrl(operation), parameters, useGet).ToBundle(); + + return executeAsync(tx, HttpStatusCode.OK); + } + public Resource Operation(Uri operation, Parameters parameters = null, bool useGet = false) + { + return OperationAsync(operation, parameters, useGet).WaitResult(); + } + + + + private Task internalOperationAsync(string operationName, string type = null, string id = null, string vid = null, + Parameters parameters = null, bool useGet = false) + { + // Brian: Not sure why we would create this parameters object as empty. + // I would imagine that a null parameters object is different to an empty one? + // EK: What else could we do? POST an empty body? We cannot use GET unless the caller indicates this is an + // idempotent call.... + // MV: (related to issue #419): we only provide an empty parameter when we are not performing a GET operation. In r4 it will be allowed + // to provide an empty body in POST operations. In that case the line of code can be deleted. + if (parameters == null && !useGet) parameters = new Parameters(); + + Bundle tx; + + if (type == null) + tx = new TransactionBuilder(Endpoint).ServerOperation(operationName, parameters, useGet).ToBundle(); + else if (id == null) + tx = new TransactionBuilder(Endpoint).TypeOperation(type, operationName, parameters, useGet).ToBundle(); + else + tx = new TransactionBuilder(Endpoint).ResourceOperation(type, id, vid, operationName, parameters, useGet).ToBundle(); + + return executeAsync(tx, HttpStatusCode.OK); + } + + private Resource internalOperation(string operationName, string type = null, string id = null, + string vid = null, Parameters parameters = null, bool useGet = false) + { + return internalOperationAsync(operationName, type, id, vid, parameters, useGet).WaitResult(); + } + + #endregion + + #region Get + + /// + /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception + /// + /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. + /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url + /// parameters to the method are simple, and are in the URL, and this is a GET operation + public Resource Get(Uri url) + { + return GetAsync(url).WaitResult(); + } + /// + /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception + /// + /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. + /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url + /// parameters to the method are simple, and are in the URL, and this is a GET operation + public async Task GetAsync(Uri url) + { + if (url == null) throw Error.ArgumentNull(nameof(url)); + + var tx = new TransactionBuilder(Endpoint).Get(url).ToBundle(); + return await executeAsync(tx, HttpStatusCode.OK).ConfigureAwait(false); + } + /// + /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception + /// + /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. + /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url + /// parameters to the method are simple, and are in the URL, and this is a GET operation + public Resource Get(string url) + { + return GetAsync(url).WaitResult(); + } + /// + /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception + /// + /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. + /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url + /// parameters to the method are simple, and are in the URL, and this is a GET operation + public Task GetAsync(string url) + { + if (url == null) throw Error.ArgumentNull(nameof(url)); + + return GetAsync(new Uri(url, UriKind.RelativeOrAbsolute)); + } + + #endregion + + + + + private ResourceIdentity verifyResourceIdentity(Uri location, bool needId, bool needVid) + { + var result = new ResourceIdentity(location); + + if (result.ResourceType == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the resource type in its path"); + if (needId && result.Id == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the logical id in its path"); + if (needVid && !result.HasVersion) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the version id in its path"); + + return result; + } + + + // TODO: Depending on type of response, update identity & always update lastupdated? + + private void updateIdentity(Resource resource, ResourceIdentity identity) + { + if (resource.Meta == null) resource.Meta = new Meta(); + + if (resource.Id == null) + { + resource.Id = identity.Id; + resource.VersionId = identity.VersionId; + } + } + + + private void setResourceBase(Resource resource, string baseUri) + { + resource.ResourceBase = new Uri(baseUri); + + if (resource is Bundle) + { + var bundle = resource as Bundle; + foreach (var entry in bundle.Entry.Where(e => e.Resource != null)) + entry.Resource.ResourceBase = new Uri(baseUri, UriKind.RelativeOrAbsolute); + } + } + + // Original + private TResource execute(Bundle tx, HttpStatusCode expect) where TResource : Model.Resource + { + return executeAsync(tx, new[] { expect }).WaitResult(); + } + public Task executeAsync(Model.Bundle tx, HttpStatusCode expect) where TResource : Model.Resource + { + return executeAsync(tx, new[] { expect }); + } + // Original + private TResource execute(Bundle tx, IEnumerable expect) where TResource : Resource + { + return executeAsync(tx, expect).WaitResult(); + } + + private async Task executeAsync(Bundle tx, IEnumerable expect) where TResource : Resource + { + verifyServerVersion(); + + var request = tx.Entry[0]; + var response = await Requester.ExecuteAsync(request).ConfigureAwait(false); + + if (!expect.Select(sc => ((int)sc).ToString()).Contains(response.Response.Status)) + { + Enum.TryParse(response.Response.Status, out HttpStatusCode code); + throw new FhirOperationException("Operation concluded successfully, but the return status {0} was unexpected".FormatWith(response.Response.Status), code); + } + + Resource result; + + // Special feature: if ReturnFullResource was requested (using the Prefer header), but the server did not return the resource + // (or it returned an OperationOutcome) - explicitly go out to the server to get the resource and return it. + // This behavior is only valid for PUT and POST requests, where the server may device whether or not to return the full body of the alterend resource. + var noRealBody = response.Resource == null || (response.Resource is OperationOutcome && string.IsNullOrEmpty(response.Resource.Id)); + if (noRealBody && isPostOrPut(request) + && PreferredReturn == Prefer.ReturnRepresentation && response.Response.Location != null + && new ResourceIdentity(response.Response.Location).IsRestResourceIdentity()) // Check that it isn't an operation too + { + result = await GetAsync(response.Response.Location).ConfigureAwait(false); + } + else + result = response.Resource; + + if (result == null) return null; + + // We have a success code (2xx), we have a body, but the body may not be of the type we expect. + if (!(result is TResource)) + { + // If this is an operationoutcome, that may still be allright. Keep the OperationOutcome in + // the LastResult, and return null as the result. Otherwise, throw. + if (result is OperationOutcome) + return null; + + var message = String.Format("Operation {0} on {1} expected a body of type {2} but a {3} was returned", response.Request.Method, + response.Request.Url, typeof(TResource).Name, result.GetType().Name); + throw new FhirOperationException(message, Requester.LastStatusCode); + } + else + return result as TResource; + } + private bool isPostOrPut(Bundle.EntryComponent interaction) + { + var method = interaction.Request.Method; + return method == Bundle.HTTPVerb.POST || method == Bundle.HTTPVerb.PUT; + } + + + private bool versionChecked = false; + + private void verifyServerVersion() + { + if (!VerifyFhirVersion) return; + + if (versionChecked) return; + versionChecked = true; // So we can now start calling Conformance() without getting into a loop + + CapabilityStatement conf = null; + try + { + conf = CapabilityStatement(SummaryType.True); // don't get the full version as its huge just to read the fhir version + } + catch (FormatException) + { + // Mmmm...cannot even read the body. Probably not so good. + throw Error.NotSupported("Cannot read the conformance statement of the server to verify FHIR version compatibility"); + } + + if (!conf.FhirVersion.StartsWith(ModelInfo.Version)) + { + throw Error.NotSupported("This client support FHIR version {0}, but the server uses version {1}".FormatWith(ModelInfo.Version, conf.FhirVersion)); + } + } + + #region IDisposable Support + protected bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + if(Requester is IDisposable disposableRequester) + { + disposableRequester.Dispose(); + } + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + #endregion + } +} diff --git a/src/Hl7.Fhir.Core/Rest/FhirClient.cs b/src/Hl7.Fhir.Core/Rest/FhirClient.cs index 6be14d0664..d6db4a3931 100644 --- a/src/Hl7.Fhir.Core/Rest/FhirClient.cs +++ b/src/Hl7.Fhir.Core/Rest/FhirClient.cs @@ -20,10 +20,9 @@ namespace Hl7.Fhir.Rest { - public partial class FhirClient : IFhirClient + [Obsolete] + public partial class FhirClient : BaseFhirClient { - private Requester _requester; - /// /// Creates a new client using a default endpoint /// If the endpoint does not end with a slash (/), it will be added. @@ -39,16 +38,9 @@ public partial class FhirClient : IFhirClient /// public FhirClient(Uri endpoint, bool verifyFhirVersion = false) { - if (endpoint == null) throw new ArgumentNullException("endpoint"); - - if (!endpoint.OriginalString.EndsWith("/")) - endpoint = new Uri(endpoint.OriginalString + "/"); - - if (!endpoint.IsAbsoluteUri) throw new ArgumentException("endpoint", "Endpoint must be absolute"); - - Endpoint = endpoint; + Endpoint = GetValidatedEndpoint(endpoint); - _requester = new Requester(Endpoint) + Requester = new Requester(Endpoint) { BeforeRequest = this.BeforeRequest, AfterResponse = this.AfterResponse @@ -76,959 +68,43 @@ public FhirClient(string endpoint, bool verifyFhirVersion = false) { } - #region << Client Communication Defaults (PreferredFormat, UseFormatParam, Timeout, ReturnFullResource) >> - public bool VerifyFhirVersion - { - get; - set; - } - - /// - /// The preferred format of the content to be used when communicating with the FHIR server (XML or JSON) - /// - public ResourceFormat PreferredFormat - { - get { return _requester.PreferredFormat; } - set { _requester.PreferredFormat = value; } - } - - /// - /// When passing the content preference, use the _format parameter instead of the request header - /// - public bool UseFormatParam - { - get { return _requester.UseFormatParameter; } - set { _requester.UseFormatParameter = value; } - } - - /// - /// The timeout (in milliseconds) to be used when making calls to the FHIR server - /// - public int Timeout - { - get { return _requester.Timeout; } - set { _requester.Timeout = value; } - } - - //private bool _returnFullResource = false; - - /// - /// Should calls to Create, Update and transaction operations return the whole updated content? - /// - /// Refer to specification section 2.1.0.5 (Managing Return Content) - [Obsolete("In STU3 this is no longer a true/false option, use the PreferredReturn property instead")] - public bool ReturnFullResource - { - get => _requester.PreferredReturn == Prefer.ReturnRepresentation; - set => _requester.PreferredReturn = value ? Prefer.ReturnRepresentation : Prefer.ReturnMinimal; - } - - /// - /// Should calls to Create, Update and transaction operations return the whole updated content, - /// or an OperationOutcome? - /// - /// Refer to specification section 2.1.0.5 (Managing Return Content) - - public Prefer? PreferredReturn - { - get => _requester.PreferredReturn; - set => _requester.PreferredReturn = value; - } - - /// - /// Should server return which search parameters were supported after executing a search? - /// If true, the server should return an error for any unknown or unsupported parameter, otherwise - /// the server may ignore any unknown or unsupported parameter. - /// - public SearchParameterHandling? PreferredParameterHandling - { - get => _requester.PreferredParameterHandling; - set => _requester.PreferredParameterHandling = value; - } - - -#if NET_COMPRESSION - /// - /// This will do 2 things: - /// 1. Add the header Accept-Encoding: gzip, deflate - /// 2. decompress any responses that have Content-Encoding: gzip (or deflate) - /// - public bool PreferCompressedResponses - { - get { return _requester.PreferCompressedResponses; } - set { _requester.PreferCompressedResponses = value; } - } - /// - /// Compress any Request bodies - /// (warning, if a server does not handle compressed requests you will get a 415 response) - /// - public bool CompressRequestBody - { - get { return _requester.CompressRequestBody; } - set { _requester.CompressRequestBody = value; } - } -#endif - - - /// - /// The last transaction result that was executed on this connection to the FHIR server - /// - public Bundle.ResponseComponent LastResult => _requester.LastResult?.Response; - - public ParserSettings ParserSettings - { - get { return _requester.ParserSettings; } - set { _requester.ParserSettings = value; } - } - - - public byte[] LastBody => LastResult?.GetBody(); - public string LastBodyAsText => LastResult?.GetBodyAsText(); - public Resource LastBodyAsResource => _requester.LastResult?.Resource; + public override byte[] LastBody => LastResult?.GetBody(); + public override string LastBodyAsText => LastResult?.GetBodyAsText(); + public override Resource LastBodyAsResource => Requester.LastResult?.Resource; /// /// Returns the HttpWebRequest as it was last constructed to execute a call on the FhirClient /// - public HttpWebRequest LastRequest { get { return _requester.LastRequest; } } + [Obsolete] + public override HttpWebRequest LastRequest { get { return (Requester as Requester)?.LastRequest; } } /// /// Returns the HttpWebResponse as it was last received during a call on the FhirClient /// /// Note that the FhirClient will have read the body data from the HttpWebResponse, so this is /// no longer available. Use LastBody, LastBodyAsText and LastBodyAsResource to get access to the received body (if any) - public HttpWebResponse LastResponse { get { return _requester.LastResponse; } } - - /// - /// The default endpoint for use with operations that use discrete id/version parameters - /// instead of explicit uri endpoints. This will always have a trailing "/" - /// - public Uri Endpoint - { - get; - private set; - } - - #endregion - - #region Read - - /// - /// Fetches a typed resource from a FHIR resource endpoint. - /// - /// The url of the Resource to fetch. This can be a Resource id url or a version-specific - /// Resource url. - /// The (weak) ETag to use in a conditional read. Optional. - /// Last modified since date in a conditional read. Optional. (refer to spec 2.1.0.5) If this is used, the client will throw an exception you need - /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown - /// - /// The requested resource. This operation will throw an exception - /// if the resource has been deleted or does not exist. - /// The specified may be relative or absolute, if it is an absolute - /// url, it must reference an address within the endpoint. - /// - /// Since ResourceLocation is a subclass of Uri, you may pass in ResourceLocations too. - /// This will occur if conditional request returns a status 304 and optionally an OperationOutcome - public Task ReadAsync(Uri location, string ifNoneMatch=null, DateTimeOffset? ifModifiedSince=null) where TResource : Resource - { - if (location == null) throw Error.ArgumentNull(nameof(location)); - - var id = verifyResourceIdentity(location, needId: true, needVid: false); - Bundle tx; - - if (!id.HasVersion) - { - var ri = new TransactionBuilder(Endpoint).Read(id.ResourceType, id.Id, ifNoneMatch, ifModifiedSince); - tx = ri.ToBundle(); - } - else - { - tx = new TransactionBuilder(Endpoint).VRead(id.ResourceType, id.Id, id.VersionId).ToBundle(); - } - - return executeAsync(tx, HttpStatusCode.OK); - } - /// - /// Fetches a typed resource from a FHIR resource endpoint. - /// - /// The url of the Resource to fetch. This can be a Resource id url or a version-specific - /// Resource url. - /// The (weak) ETag to use in a conditional read. Optional. - /// Last modified since date in a conditional read. Optional. (refer to spec 2.1.0.5) If this is used, the client will throw an exception you need - /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown - /// - /// The requested resource. This operation will throw an exception - /// if the resource has been deleted or does not exist. - /// The specified may be relative or absolute, if it is an absolute - /// url, it must reference an address within the endpoint. - /// - /// Since ResourceLocation is a subclass of Uri, you may pass in ResourceLocations too. - /// This will occur if conditional request returns a status 304 and optionally an OperationOutcome - public TResource Read(Uri location, string ifNoneMatch = null, - DateTimeOffset? ifModifiedSince = null) where TResource : Resource - { - return ReadAsync(location, ifNoneMatch, ifModifiedSince).WaitResult(); - } - - /// - /// Fetches a typed resource from a FHIR resource endpoint. - /// - /// The url of the Resource to fetch as a string. This can be a Resource id url or a version-specific - /// Resource url. - /// The (weak) ETag to use in a conditional read. Optional. - /// Last modified since date in a conditional read. Optional. - /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown - /// The requested resource - /// This operation will throw an exception - /// if the resource has been deleted or does not exist. The specified may be relative or absolute, if it is an absolute - /// url, it must reference an address within the endpoint. - public Task ReadAsync(string location, string ifNoneMatch = null, DateTimeOffset? ifModifiedSince = null) where TResource : Resource - { - return ReadAsync(new Uri(location, UriKind.RelativeOrAbsolute), ifNoneMatch, ifModifiedSince); - } - /// - /// Fetches a typed resource from a FHIR resource endpoint. - /// - /// The url of the Resource to fetch as a string. This can be a Resource id url or a version-specific - /// Resource url. - /// The (weak) ETag to use in a conditional read. Optional. - /// Last modified since date in a conditional read. Optional. - /// The type of resource to read. Resource or DomainResource is allowed if exact type is unknown - /// The requested resource - /// This operation will throw an exception - /// if the resource has been deleted or does not exist. The specified may be relative or absolute, if it is an absolute - /// url, it must reference an address within the endpoint. - public TResource Read(string location, string ifNoneMatch = null, - DateTimeOffset? ifModifiedSince = null) where TResource : Resource - { - return ReadAsync(location, ifNoneMatch, ifModifiedSince).WaitResult(); - } - - #endregion - - #region Refresh - - /// - /// Refreshes the data in the resource passed as an argument by re-reading it from the server - /// - /// - /// The resource for which you want to get the most recent version. - /// A new instance of the resource, containing the most up-to-date data - /// This function will not overwrite the argument with new data, rather it will return a new instance - /// which will have the newest data, leaving the argument intact. - public Task RefreshAsync(TResource current) where TResource : Resource - { - if (current == null) throw Error.ArgumentNull(nameof(current)); - - return ReadAsync(ResourceIdentity.Build(current.TypeName, current.Id)); - } - /// - /// Refreshes the data in the resource passed as an argument by re-reading it from the server - /// - /// - /// The resource for which you want to get the most recent version. - /// A new instance of the resource, containing the most up-to-date data - /// This function will not overwrite the argument with new data, rather it will return a new instance - /// which will have the newest data, leaving the argument intact. - public TResource Refresh(TResource current) where TResource : Resource - { - return RefreshAsync(current).WaitResult(); - } - - #endregion - - #region Update - - /// - /// Update (or create) a resource - /// - /// The resource to update - /// If true, asks the server to verify we are updating the latest version - /// The type of resource that is being updated - /// The body of the updated resource, unless ReturnFullResource is set to "false" - /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. - /// If the resource does not yet exist - and the server allows client-assigned id's - a new resource with the given id will be - /// created. - public Task UpdateAsync(TResource resource, bool versionAware=false) where TResource : Resource - { - if (resource == null) throw Error.ArgumentNull(nameof(resource)); - if (resource.Id == null) throw Error.Argument(nameof(resource), "Resource needs a non-null Id to send the update to"); - - var upd = new TransactionBuilder(Endpoint); - - if (versionAware && resource.HasVersionId) - upd.Update(resource.Id, resource, versionId: resource.VersionId); - else - upd.Update(resource.Id, resource); - - return internalUpdateAsync(resource, upd.ToBundle()); - } - /// - /// Update (or create) a resource - /// - /// The resource to update - /// If true, asks the server to verify we are updating the latest version - /// The type of resource that is being updated - /// The body of the updated resource, unless ReturnFullResource is set to "false" - /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. - /// If the resource does not yet exist - and the server allows client-assigned id's - a new resource with the given id will be - /// created. - public TResource Update(TResource resource, bool versionAware = false) where TResource : Resource - { - return UpdateAsync(resource, versionAware).WaitResult(); - } - - /// - /// Conditionally update (or create) a resource - /// - /// The resource to update - /// Criteria used to locate the resource to update - /// If true, asks the server to verify we are updating the latest version - /// The type of resource that is being updated - /// The body of the updated resource, unless ReturnFullResource is set to "false" - /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. - /// If the criteria passed in condition do not match a resource a new resource with a server assigned id will be created. - public Task UpdateAsync(TResource resource, SearchParams condition, bool versionAware = false) where TResource : Resource - { - if (resource == null) throw Error.ArgumentNull(nameof(resource)); - if (condition == null) throw Error.ArgumentNull(nameof(condition)); - - var upd = new TransactionBuilder(Endpoint); - - if (versionAware && resource.HasVersionId) - upd.Update(condition, resource, versionId: resource.VersionId); - else - upd.Update(condition, resource); - - return internalUpdateAsync(resource, upd.ToBundle()); - } - /// - /// Conditionally update (or create) a resource - /// - /// The resource to update - /// Criteria used to locate the resource to update - /// If true, asks the server to verify we are updating the latest version - /// The type of resource that is being updated - /// The body of the updated resource, unless ReturnFullResource is set to "false" - /// Throws an exception when the update failed, in particular when an update conflict is detected and the server returns a HTTP 409. - /// If the criteria passed in condition do not match a resource a new resource with a server assigned id will be created. - public TResource Update(TResource resource, SearchParams condition, bool versionAware = false) - where TResource : Resource - { - return UpdateAsync(resource, condition, versionAware).WaitResult(); - } - private Task internalUpdateAsync(TResource resource, Bundle tx) where TResource : Resource - { - resource.ResourceBase = Endpoint; - - // This might be an update of a resource that doesn't yet exist, so accept a status Created too - return executeAsync(tx, new[] { HttpStatusCode.Created, HttpStatusCode.OK }); - } - private TResource internalUpdate(TResource resource, Bundle tx) where TResource : Resource - { - return internalUpdateAsync(resource, tx).WaitResult(); - } - #endregion - - #region Delete - - /// - /// Delete a resource at the given endpoint. - /// - /// endpoint of the resource to delete - /// Throws an exception when the delete failed, though this might - /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was - /// already deleted). - public async System.Threading.Tasks.Task DeleteAsync(Uri location) - { - if (location == null) throw Error.ArgumentNull(nameof(location)); - - var id = verifyResourceIdentity(location, needId: true, needVid: false); - var tx = new TransactionBuilder(Endpoint).Delete(id.ResourceType, id.Id).ToBundle(); - - await executeAsync(tx, new[] { HttpStatusCode.OK, HttpStatusCode.NoContent }).ConfigureAwait(false); - } - /// - /// Delete a resource at the given endpoint. - /// - /// endpoint of the resource to delete - /// Throws an exception when the delete failed, though this might - /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was - /// already deleted). - public void Delete(Uri location) - { - DeleteAsync(location).WaitNoResult(); - } - /// - /// Delete a resource at the given endpoint. - /// - /// endpoint of the resource to delete - /// Throws an exception when the delete failed, though this might - /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was - /// already deleted). - public System.Threading.Tasks.Task DeleteAsync(string location) - { - return DeleteAsync(new Uri(location, UriKind.Relative)); - } - /// - /// Delete a resource at the given endpoint. - /// - /// endpoint of the resource to delete - /// Throws an exception when the delete failed, though this might - /// just mean the server returned 404 (the resource didn't exist before) or 410 (the resource was - /// already deleted). - public void Delete(string location) - { - DeleteAsync(location).WaitNoResult(); - } - - - /// - /// Delete a resource - /// - /// The resource to delete - public async System.Threading.Tasks.Task DeleteAsync(Resource resource) - { - if (resource == null) throw Error.ArgumentNull(nameof(resource)); - if (resource.Id == null) throw Error.Argument(nameof(resource), "Entry must have an id"); - - await DeleteAsync(resource.ResourceIdentity(Endpoint).WithoutVersion()).ConfigureAwait(false); - } - /// - /// Delete a resource - /// - /// The resource to delete - public void Delete(Resource resource) - { - DeleteAsync(resource).WaitNoResult(); - } - - /// - /// Conditionally delete a resource - /// - /// The type of resource to delete - /// Criteria to use to match the resource to delete. - public async System.Threading.Tasks.Task DeleteAsync(string resourceType, SearchParams condition) - { - if (resourceType == null) throw Error.ArgumentNull(nameof(resourceType)); - if (condition == null) throw Error.ArgumentNull(nameof(condition)); - - var tx = new TransactionBuilder(Endpoint).Delete(resourceType, condition).ToBundle(); - await executeAsync(tx,new[]{ HttpStatusCode.OK, HttpStatusCode.NoContent}).ConfigureAwait(false); - } - /// - /// Conditionally delete a resource - /// - /// The type of resource to delete - /// Criteria to use to match the resource to delete. - public void Delete(string resourceType, SearchParams condition) - { - DeleteAsync(resourceType, condition).WaitNoResult(); - } - - #endregion - - #region Create - - /// - /// Create a resource on a FHIR endpoint - /// - /// The resource instance to create - /// The resource as created on the server, or an exception if the create failed. - /// The type of resource to create - public Task CreateAsync(TResource resource) where TResource : Resource - { - if (resource == null) throw Error.ArgumentNull(nameof(resource)); - - var tx = new TransactionBuilder(Endpoint).Create(resource).ToBundle(); - - return executeAsync(tx,new[] { HttpStatusCode.Created, HttpStatusCode.OK }); - } - /// - /// Create a resource on a FHIR endpoint - /// - /// The resource instance to create - /// The resource as created on the server, or an exception if the create failed. - /// The type of resource to create - public TResource Create(TResource resource) where TResource : Resource - { - return CreateAsync(resource).WaitResult(); - } - - /// - /// Conditionally Create a resource on a FHIR endpoint - /// - /// The resource instance to create - /// The criteria - /// The resource as created on the server, or an exception if the create failed. - /// The type of resource to create - public Task CreateAsync(TResource resource, SearchParams condition) where TResource : Resource - { - if (resource == null) throw Error.ArgumentNull(nameof(resource)); - if (condition == null) throw Error.ArgumentNull(nameof(condition)); - - var tx = new TransactionBuilder(Endpoint).Create(resource, condition).ToBundle(); - - return executeAsync(tx, new[] { HttpStatusCode.Created, HttpStatusCode.OK }); - } - public TResource Create(TResource resource, SearchParams condition) where TResource : Resource - { - return CreateAsync(resource, condition).WaitResult(); - } - - #endregion - - #region Conformance - - /// - /// Get a conformance statement for the system - /// - /// A Conformance resource. Throws an exception if the operation failed. - [Obsolete("The Conformance operation has been replaced by the CapabilityStatement", false)] - public CapabilityStatement Conformance(SummaryType? summary = null) - { - return CapabilityStatement(summary); - } - - /// - /// Get a conformance statement for the system - /// - /// A Conformance resource. Throws an exception if the operation failed. - public Task CapabilityStatementAsync(SummaryType? summary = null) - { - var tx = new TransactionBuilder(Endpoint).CapabilityStatement(summary).ToBundle(); - return executeAsync(tx, HttpStatusCode.OK); - } - - /// - /// Get a conformance statement for the system - /// - /// A Conformance resource. Throws an exception if the operation failed. - public CapabilityStatement CapabilityStatement(SummaryType? summary = null) - { - return CapabilityStatementAsync(summary).WaitResult(); - } - #endregion - - #region History - - /// - /// Retrieve the version history for a specific resource type - /// - /// The type of Resource to get the history for - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Task TypeHistoryAsync(string resourceType, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return internalHistoryAsync(resourceType, null, since, pageSize, summary); - } - /// - /// Retrieve the version history for a specific resource type - /// - /// The type of Resource to get the history for - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Bundle TypeHistory(string resourceType, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return TypeHistoryAsync(resourceType, since, pageSize, summary).WaitResult(); - } - - /// - /// Retrieve the version history for a specific resource type - /// - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// The type of Resource to get the history for - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Task TypeHistoryAsync(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) where TResource : Resource, new() - { - string collection = typeof(TResource).GetCollectionName(); - return internalHistoryAsync(collection, null, since, pageSize, summary); - } - /// - /// Retrieve the version history for a specific resource type - /// - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// The type of Resource to get the history for - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Bundle TypeHistory(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) where TResource : Resource, new() - { - return TypeHistoryAsync(since, pageSize, summary).WaitResult(); - } - - /// - /// Retrieve the version history for a resource at a given location - /// - /// The address of the resource to get the history for - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Task HistoryAsync(Uri location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - if (location == null) throw Error.ArgumentNull(nameof(location)); - - var id = verifyResourceIdentity(location, needId: true, needVid: false); - return internalHistoryAsync(id.ResourceType, id.Id, since, pageSize, summary); - } - /// - /// Retrieve the version history for a resource at a given location - /// - /// The address of the resource to get the history for - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Bundle History(Uri location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return HistoryAsync(location, since, pageSize, summary).WaitResult(); - } - - /// - /// Retrieve the version history for a resource at a given location - /// - /// The address of the resource to get the history for - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Task HistoryAsync(string location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return HistoryAsync(new Uri(location, UriKind.Relative), since, pageSize, summary); - } - /// - /// Retrieve the version history for a resource at a given location - /// - /// The address of the resource to get the history for - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Optional. Asks the server to only provide the fields defined for the summary - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Bundle History(string location, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return HistoryAsync(location, since, pageSize, summary).WaitResult(); - } - - /// - /// Retrieve the full version history of the server - /// - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Indicates whether the returned resources should just contain the minimal set of elements - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Task WholeSystemHistoryAsync(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return internalHistoryAsync(null, null, since, pageSize, summary); - } - /// - /// Retrieve the full version history of the server - /// - /// Optional. Returns only changes after the given date - /// Optional. Asks server to limit the number of entries per page returned - /// Indicates whether the returned resources should just contain the minimal set of elements - /// A bundle with the history for the indicated instance, may contain both - /// ResourceEntries and DeletedEntries. - public Bundle WholeSystemHistory(DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - return WholeSystemHistoryAsync(since, pageSize, summary).WaitResult(); - } - private Task internalHistoryAsync(string resourceType = null, string id = null, DateTimeOffset? since = null, int? pageSize = null, SummaryType summary = SummaryType.False) - { - TransactionBuilder history; - - if(resourceType == null) - history = new TransactionBuilder(Endpoint).ServerHistory(summary,pageSize,since); - else if(id == null) - history = new TransactionBuilder(Endpoint).CollectionHistory(resourceType, summary,pageSize,since); - else - history = new TransactionBuilder(Endpoint).ResourceHistory(resourceType,id, summary,pageSize,since); - - return executeAsync(history.ToBundle(), HttpStatusCode.OK); - } - private Bundle internalHistory(string resourceType = null, string id = null, DateTimeOffset? since = null, - int? pageSize = null, SummaryType summary = SummaryType.False) - { - return internalHistoryAsync(resourceType, id, since, pageSize, summary).WaitResult(); - } - - #endregion - - #region Transaction - - /// - /// Send a set of creates, updates and deletes to the server to be processed in one transaction - /// - /// The bundled creates, updates and deleted - /// A bundle as returned by the server after it has processed the transaction, or null - /// if an error occurred. - public Task TransactionAsync(Bundle bundle) - { - if (bundle == null) throw new ArgumentNullException(nameof(bundle)); - - var tx = new TransactionBuilder(Endpoint).Transaction(bundle).ToBundle(); - return executeAsync(tx, HttpStatusCode.OK); - } - /// - /// Send a set of creates, updates and deletes to the server to be processed in one transaction - /// - /// The bundled creates, updates and deleted - /// A bundle as returned by the server after it has processed the transaction, or null - /// if an error occurred. - public Bundle Transaction(Bundle bundle) - { - return TransactionAsync(bundle).WaitResult(); - } - - #endregion - - #region Operation - - public Task WholeSystemOperationAsync(string operationName, Parameters parameters = null, bool useGet = false) - { - if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); - return internalOperationAsync(operationName, parameters: parameters, useGet: useGet); - } - - public Resource WholeSystemOperation(string operationName, Parameters parameters = null, bool useGet = false) - { - return WholeSystemOperationAsync(operationName, parameters, useGet).WaitResult(); - } - - - public Task TypeOperationAsync(string operationName, Parameters parameters = null, bool useGet = false) - where TResource : Resource - { - if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); - - // [WMR 20160421] GetResourceNameForType is obsolete - // var typeName = ModelInfo.GetResourceNameForType(typeof(TResource)); - var typeName = ModelInfo.GetFhirTypeNameForType(typeof(TResource)); - - return TypeOperationAsync(operationName, typeName, parameters, useGet: useGet); - } - public Resource TypeOperation(string operationName, Parameters parameters = null, - bool useGet = false) where TResource : Resource - { - return TypeOperationAsync(operationName, parameters, useGet).WaitResult(); - } - - - - public Task TypeOperationAsync(string operationName, string typeName, Parameters parameters = null, bool useGet = false) - { - if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); - if (typeName == null) throw Error.ArgumentNull(nameof(typeName)); - - return internalOperationAsync(operationName, typeName, parameters: parameters, useGet: useGet); - } - public Resource TypeOperation(string operationName, string typeName, Parameters parameters = null, bool useGet = false) - { - return TypeOperationAsync(operationName, typeName, parameters, useGet).WaitResult(); - } - - - - public Task InstanceOperationAsync(Uri location, string operationName, Parameters parameters = null, bool useGet = false) - { - if (location == null) throw Error.ArgumentNull(nameof(location)); - if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); - - var id = verifyResourceIdentity(location, needId: true, needVid: false); - - return internalOperationAsync(operationName, id.ResourceType, id.Id, id.VersionId, parameters, useGet); - } - public Resource InstanceOperation(Uri location, string operationName, Parameters parameters = null, bool useGet = false) - { - return InstanceOperationAsync(location, operationName, parameters, useGet).WaitResult(); - } - - - - public Task OperationAsync(Uri location, string operationName, Parameters parameters = null, bool useGet = false) - { - if (location == null) throw Error.ArgumentNull(nameof(location)); - if (operationName == null) throw Error.ArgumentNull(nameof(operationName)); - - var tx = new TransactionBuilder(Endpoint).EndpointOperation(new RestUrl(location), operationName, parameters, useGet).ToBundle(); - - return executeAsync(tx, HttpStatusCode.OK); - } - public Resource Operation(Uri location, string operationName, Parameters parameters = null, bool useGet = false) - { - return OperationAsync(location, operationName, parameters, useGet).WaitResult(); - } - - - public Task OperationAsync(Uri operation, Parameters parameters = null, bool useGet = false) - { - if (operation == null) throw Error.ArgumentNull(nameof(operation)); - - var tx = new TransactionBuilder(Endpoint).EndpointOperation(new RestUrl(operation), parameters, useGet).ToBundle(); - - return executeAsync(tx, HttpStatusCode.OK); - } - public Resource Operation(Uri operation, Parameters parameters = null, bool useGet = false) - { - return OperationAsync(operation, parameters, useGet).WaitResult(); - } - - - - private Task internalOperationAsync(string operationName, string type = null, string id = null, string vid = null, - Parameters parameters = null, bool useGet = false) - { - // Brian: Not sure why we would create this parameters object as empty. - // I would imagine that a null parameters object is different to an empty one? - // EK: What else could we do? POST an empty body? We cannot use GET unless the caller indicates this is an - // idempotent call.... - // MV: (related to issue #419): we only provide an empty parameter when we are not performing a GET operation. In r4 it will be allowed - // to provide an empty body in POST operations. In that case the line of code can be deleted. - if (parameters == null && !useGet) parameters = new Parameters(); - - Bundle tx; - - if (type == null) - tx = new TransactionBuilder(Endpoint).ServerOperation(operationName, parameters, useGet).ToBundle(); - else if (id == null) - tx = new TransactionBuilder(Endpoint).TypeOperation(type, operationName, parameters, useGet).ToBundle(); - else - tx = new TransactionBuilder(Endpoint).ResourceOperation(type, id, vid, operationName, parameters, useGet).ToBundle(); - - return executeAsync(tx, HttpStatusCode.OK); - } - - private Resource internalOperation(string operationName, string type = null, string id = null, - string vid = null, Parameters parameters = null, bool useGet = false) - { - return internalOperationAsync(operationName, type, id, vid, parameters, useGet).WaitResult(); - } - - #endregion - - #region Get - - /// - /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception - /// - /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. - /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url - /// parameters to the method are simple, and are in the URL, and this is a GET operation - public Resource Get(Uri url) - { - return GetAsync(url).WaitResult(); - } - /// - /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception - /// - /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. - /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url - /// parameters to the method are simple, and are in the URL, and this is a GET operation - public async Task GetAsync(Uri url) - { - if (url == null) throw Error.ArgumentNull(nameof(url)); - - var tx = new TransactionBuilder(Endpoint).Get(url).ToBundle(); - return await executeAsync(tx, HttpStatusCode.OK).ConfigureAwait(false); - } - /// - /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception - /// - /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. - /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url - /// parameters to the method are simple, and are in the URL, and this is a GET operation - public Resource Get(string url) - { - return GetAsync(url).WaitResult(); - } - /// - /// Invoke a general GET on the server. If the operation fails, then this method will throw an exception - /// - /// A relative or absolute url. If the url is absolute, it has to be located within the endpoint of the client. - /// A resource that is the outcome of the operation. The type depends on the definition of the operation at the given url - /// parameters to the method are simple, and are in the URL, and this is a GET operation - public Task GetAsync(string url) - { - if (url == null) throw Error.ArgumentNull(nameof(url)); - - return GetAsync(new Uri(url, UriKind.RelativeOrAbsolute)); - } - - #endregion - - - - - private ResourceIdentity verifyResourceIdentity(Uri location, bool needId, bool needVid) - { - var result = new ResourceIdentity(location); - - if (result.ResourceType == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the resource type in its path"); - if (needId && result.Id == null) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the logical id in its path"); - if (needVid && !result.HasVersion) throw Error.Argument(nameof(location), "Must be a FHIR REST url containing the version id in its path"); - - return result; - } - - - // TODO: Depending on type of response, update identity & always update lastupdated? - - private void updateIdentity(Resource resource, ResourceIdentity identity) - { - if (resource.Meta == null) resource.Meta = new Meta(); - - if (resource.Id == null) - { - resource.Id = identity.Id; - resource.VersionId = identity.VersionId; - } - } - - - private void setResourceBase(Resource resource, string baseUri) - { - resource.ResourceBase = new Uri(baseUri); - - if (resource is Bundle) - { - var bundle = resource as Bundle; - foreach (var entry in bundle.Entry.Where(e => e.Resource != null)) - entry.Resource.ResourceBase = new Uri(baseUri, UriKind.RelativeOrAbsolute); - } - } - + [Obsolete] + public override HttpWebResponse LastResponse { get { return (Requester as Requester)?.LastResponse; } } /// /// Called just before the Http call is done /// - public event EventHandler OnBeforeRequest; + [Obsolete] + public override event EventHandler OnBeforeRequest; /// /// Called just after the response was received /// - public event EventHandler OnAfterResponse; + [Obsolete] + public override event EventHandler OnAfterResponse; /// /// Inspect or modify the HttpWebRequest just before the FhirClient issues a call to the server /// /// The request as it is about to be sent to the server /// The data in the body of the request as it is about to be sent to the server - protected virtual void BeforeRequest(HttpWebRequest rawRequest, byte[] body) + protected virtual void BeforeRequest(HttpWebRequest rawRequest, byte[] body) { // Default implementation: call event OnBeforeRequest?.Invoke(this, new BeforeRequestEventArgs(rawRequest, body)); @@ -1044,102 +120,8 @@ protected virtual void AfterResponse(HttpWebResponse webResponse, byte[] body) // Default implementation: call event OnAfterResponse?.Invoke(this, new AfterResponseEventArgs(webResponse, body)); } - - // Original - private TResource execute(Bundle tx, HttpStatusCode expect) where TResource : Model.Resource - { - return executeAsync(tx, new[] { expect }).WaitResult(); - } - public Task executeAsync(Model.Bundle tx, HttpStatusCode expect) where TResource : Model.Resource - { - return executeAsync(tx, new[] { expect }); - } - // Original - private TResource execute(Bundle tx, IEnumerable expect) where TResource : Resource - { - return executeAsync(tx, expect).WaitResult(); - } - - private async Task executeAsync(Bundle tx, IEnumerable expect) where TResource : Resource - { - verifyServerVersion(); - - var request = tx.Entry[0]; - var response = await _requester.ExecuteAsync(request).ConfigureAwait(false); - - if (!expect.Select(sc => ((int)sc).ToString()).Contains(response.Response.Status)) - { - Enum.TryParse(response.Response.Status, out HttpStatusCode code); - throw new FhirOperationException("Operation concluded successfully, but the return status {0} was unexpected".FormatWith(response.Response.Status), code); - } - - Resource result; - - // Special feature: if ReturnFullResource was requested (using the Prefer header), but the server did not return the resource - // (or it returned an OperationOutcome) - explicitly go out to the server to get the resource and return it. - // This behavior is only valid for PUT and POST requests, where the server may device whether or not to return the full body of the alterend resource. - var noRealBody = response.Resource == null || (response.Resource is OperationOutcome && string.IsNullOrEmpty(response.Resource.Id)); - if (noRealBody && isPostOrPut(request) - && PreferredReturn == Prefer.ReturnRepresentation && response.Response.Location != null - && new ResourceIdentity(response.Response.Location).IsRestResourceIdentity()) // Check that it isn't an operation too - { - result = await GetAsync(response.Response.Location).ConfigureAwait(false); - } - else - result = response.Resource; - - if (result == null) return null; - - // We have a success code (2xx), we have a body, but the body may not be of the type we expect. - if (!(result is TResource)) - { - // If this is an operationoutcome, that may still be allright. Keep the OperationOutcome in - // the LastResult, and return null as the result. Otherwise, throw. - if (result is OperationOutcome) - return null; - - var message = String.Format("Operation {0} on {1} expected a body of type {2} but a {3} was returned", response.Request.Method, - response.Request.Url, typeof(TResource).Name, result.GetType().Name); - throw new FhirOperationException(message, _requester.LastResponse.StatusCode); - } - else - return result as TResource; - } - private bool isPostOrPut(Bundle.EntryComponent interaction) - { - var method = interaction.Request.Method; - return method == Bundle.HTTPVerb.POST || method == Bundle.HTTPVerb.PUT; - } - - - private bool versionChecked = false; - - private void verifyServerVersion() - { - if (!VerifyFhirVersion) return; - - if (versionChecked) return; - versionChecked = true; // So we can now start calling Conformance() without getting into a loop - - CapabilityStatement conf = null; - try - { - conf = CapabilityStatement(SummaryType.True); // don't get the full version as its huge just to read the fhir version - } - catch (FormatException) - { - // Mmmm...cannot even read the body. Probably not so good. - throw Error.NotSupported("Cannot read the conformance statement of the server to verify FHIR version compatibility"); - } - - if (!conf.FhirVersion.StartsWith(ModelInfo.Version)) - { - throw Error.NotSupported("This client support FHIR version {0}, but the server uses version {1}".FormatWith(ModelInfo.Version, conf.FhirVersion)); - } - } } - public class BeforeRequestEventArgs : EventArgs { public BeforeRequestEventArgs(HttpWebRequest rawRequest, byte[] body) diff --git a/src/Hl7.Fhir.Core/Rest/FhirClientOperations.cs b/src/Hl7.Fhir.Core/Rest/FhirClientOperations.cs index 384e6a033a..bcf17ac1b5 100644 --- a/src/Hl7.Fhir.Core/Rest/FhirClientOperations.cs +++ b/src/Hl7.Fhir.Core/Rest/FhirClientOperations.cs @@ -63,7 +63,7 @@ public static class FhirClientOperations { #region Validate (Create/Update/Delete/Resource) - public static async Task ValidateCreateAsync(this FhirClient client, DomainResource resource, FhirUri profile = null) + public static async Task ValidateCreateAsync(this IFhirClient client, DomainResource resource, FhirUri profile = null) { if (resource == null) throw Error.ArgumentNull(nameof(resource)); @@ -72,13 +72,13 @@ public static async Task ValidateCreateAsync(this FhirClient c return OperationResult(await client.TypeOperationAsync(RestOperation.VALIDATE_RESOURCE, resource.TypeName, par).ConfigureAwait(false)); } - public static OperationOutcome ValidateCreate(this FhirClient client, DomainResource resource, + public static OperationOutcome ValidateCreate(this IFhirClient client, DomainResource resource, FhirUri profile = null) { return ValidateCreateAsync(client, resource, profile).WaitResult(); } - public static async Task ValidateUpdateAsync(this FhirClient client, DomainResource resource, string id, FhirUri profile = null) + public static async Task ValidateUpdateAsync(this IFhirClient client, DomainResource resource, string id, FhirUri profile = null) { if (id == null) throw Error.ArgumentNull(nameof(id)); if (resource == null) throw Error.ArgumentNull(nameof(resource)); @@ -89,14 +89,14 @@ public static async Task ValidateUpdateAsync(this FhirClient c var loc = ResourceIdentity.Build(resource.TypeName, id); return OperationResult(await client.InstanceOperationAsync(loc, RestOperation.VALIDATE_RESOURCE, par).ConfigureAwait(false)); } - public static OperationOutcome ValidateUpdate(this FhirClient client, DomainResource resource, string id, + public static OperationOutcome ValidateUpdate(this IFhirClient client, DomainResource resource, string id, FhirUri profile = null) { return ValidateUpdateAsync(client, resource, id, profile).WaitResult(); } - public static async Task ValidateDeleteAsync(this FhirClient client, ResourceIdentity location) + public static async Task ValidateDeleteAsync(this IFhirClient client, ResourceIdentity location) { if (location == null) throw Error.ArgumentNull(nameof(location)); @@ -104,12 +104,12 @@ public static async Task ValidateDeleteAsync(this FhirClient c return OperationResult(await client.InstanceOperationAsync(location.WithoutVersion().MakeRelative(), RestOperation.VALIDATE_RESOURCE, par).ConfigureAwait(false)); } - public static OperationOutcome ValidateDelete(this FhirClient client, ResourceIdentity location) + public static OperationOutcome ValidateDelete(this IFhirClient client, ResourceIdentity location) { return ValidateDeleteAsync(client,location).WaitResult(); } - public static async Task ValidateResourceAsync(this FhirClient client, DomainResource resource, string id = null, FhirUri profile = null) + public static async Task ValidateResourceAsync(this IFhirClient client, DomainResource resource, string id = null, FhirUri profile = null) { if (resource == null) throw Error.ArgumentNull(nameof(resource)); @@ -127,7 +127,7 @@ public static async Task ValidateResourceAsync(this FhirClient } } - public static OperationOutcome ValidateResource(this FhirClient client, DomainResource resource, + public static OperationOutcome ValidateResource(this IFhirClient client, DomainResource resource, string id = null, FhirUri profile = null) { return ValidateResourceAsync(client, resource, id, profile).WaitResult(); @@ -137,7 +137,7 @@ public static OperationOutcome ValidateResource(this FhirClient client, DomainRe #region Fetch - public static async Task FetchPatientRecordAsync(this FhirClient client, Uri patient = null, FhirDateTime start = null, FhirDateTime end = null) + public static async Task FetchPatientRecordAsync(this IFhirClient client, Uri patient = null, FhirDateTime start = null, FhirDateTime end = null) { var par = new Parameters(); @@ -155,7 +155,7 @@ public static async Task FetchPatientRecordAsync(this FhirClient client, return OperationResult(result); } - public static Bundle FetchPatientRecord(this FhirClient client, Uri patient = null, FhirDateTime start = null, + public static Bundle FetchPatientRecord(this IFhirClient client, Uri patient = null, FhirDateTime start = null, FhirDateTime end = null) { return FetchPatientRecordAsync(client, patient, start, end).WaitResult(); @@ -166,84 +166,84 @@ public static Bundle FetchPatientRecord(this FhirClient client, Uri patient = nu #region Meta //[base]/$meta - public static async Task MetaAsync(this FhirClient client) + public static async Task MetaAsync(this IFhirClient client) { return extractMeta(OperationResult(await client.WholeSystemOperationAsync(RestOperation.META, useGet:true).ConfigureAwait(false))); } - public static Meta Meta(this FhirClient client) + public static Meta Meta(this IFhirClient client) { return MetaAsync(client).WaitResult(); } //[base]/Resource/$meta - public static async Task MetaAsync(this FhirClient client, ResourceType type) + public static async Task MetaAsync(this IFhirClient client, ResourceType type) { return extractMeta(OperationResult(await client.TypeOperationAsync(RestOperation.META, type.ToString(), useGet: true).ConfigureAwait(false))); } - public static Meta Meta(this FhirClient client, ResourceType type) + public static Meta Meta(this IFhirClient client, ResourceType type) { return MetaAsync(client, type).WaitResult(); } //[base]/Resource/id/$meta/[_history/vid] - public static async Task MetaAsync(this FhirClient client, Uri location) + public static async Task MetaAsync(this IFhirClient client, Uri location) { Resource result; result = await client.InstanceOperationAsync(location, RestOperation.META, useGet: true).ConfigureAwait(false); return extractMeta(OperationResult(result)); } - public static Meta Meta(this FhirClient client, Uri location) + public static Meta Meta(this IFhirClient client, Uri location) { return MetaAsync(client, location).WaitResult(); } - public static Task MetaAsync(this FhirClient client, string location) + public static Task MetaAsync(this IFhirClient client, string location) { return MetaAsync(client, new Uri(location, UriKind.RelativeOrAbsolute)); } - public static Meta Meta(this FhirClient client, string location) + public static Meta Meta(this IFhirClient client, string location) { return MetaAsync(client, location).WaitResult(); } - public static async Task AddMetaAsync(this FhirClient client, Uri location, Meta meta) + public static async Task AddMetaAsync(this IFhirClient client, Uri location, Meta meta) { var par = new Parameters().Add("meta", meta); return extractMeta(OperationResult(await client.InstanceOperationAsync(location, RestOperation.META_ADD, par).ConfigureAwait(false))); } - public static Meta AddMeta(this FhirClient client, Uri location, Meta meta) + public static Meta AddMeta(this IFhirClient client, Uri location, Meta meta) { return AddMetaAsync(client, location, meta).WaitResult(); } - public static Task AddMetaAsync(this FhirClient client, string location, Meta meta) + public static Task AddMetaAsync(this IFhirClient client, string location, Meta meta) { return AddMetaAsync(client, new Uri(location, UriKind.RelativeOrAbsolute), meta); } - public static Meta AddMeta(this FhirClient client, string location, Meta meta) + public static Meta AddMeta(this IFhirClient client, string location, Meta meta) { return AddMetaAsync(client, location, meta).WaitResult(); } - public static async Task DeleteMetaAsync(this FhirClient client, Uri location, Meta meta) + public static async Task DeleteMetaAsync(this IFhirClient client, Uri location, Meta meta) { var par = new Parameters().Add("meta", meta); return extractMeta(OperationResult(await client.InstanceOperationAsync(location, RestOperation.META_DELETE, par).ConfigureAwait(false))); } - public static Meta DeleteMeta(this FhirClient client, Uri location, Meta meta) + public static Meta DeleteMeta(this IFhirClient client, Uri location, Meta meta) { return DeleteMetaAsync(client, location, meta).WaitResult(); } - public static Task DeleteMetaAsync(this FhirClient client, string location, Meta meta) + public static Task DeleteMetaAsync(this IFhirClient client, string location, Meta meta) { return DeleteMetaAsync(client, new Uri(location, UriKind.RelativeOrAbsolute), meta); } - public static Meta DeleteMeta(this FhirClient client, string location, Meta meta) + public static Meta DeleteMeta(this IFhirClient client, string location, Meta meta) { return DeleteMetaAsync(client, location, meta).WaitResult(); } @@ -260,14 +260,14 @@ public class TranslateConceptDependency } - public static async Task TranslateConceptAsync(this FhirClient client, string id, Code code, FhirUri system, FhirString version, + public static async Task TranslateConceptAsync(this IFhirClient client, string id, Code code, FhirUri system, FhirString version, FhirUri valueSet, Coding coding, CodeableConcept codeableConcept, FhirUri target, IEnumerable dependencies) { Parameters par = createTranslateConceptParams(code, system, version, valueSet, coding, codeableConcept, target, dependencies); var loc = ResourceIdentity.Build("ConceptMap", id); return OperationResult(await client.InstanceOperationAsync(loc, RestOperation.TRANSLATE, par).ConfigureAwait(false)); } - public static Parameters TranslateConcept(this FhirClient client, string id, Code code, FhirUri system, + public static Parameters TranslateConcept(this IFhirClient client, string id, Code code, FhirUri system, FhirString version, FhirUri valueSet, Coding coding, CodeableConcept codeableConcept, FhirUri target, IEnumerable dependencies) @@ -277,7 +277,7 @@ public static Parameters TranslateConcept(this FhirClient client, string id, Cod } - public static async Task TranslateConceptAsync(this FhirClient client, Code code, FhirUri system, FhirString version, + public static async Task TranslateConceptAsync(this IFhirClient client, Code code, FhirUri system, FhirString version, FhirUri valueSet, Coding coding, CodeableConcept codeableConcept, FhirUri target, IEnumerable dependencies ) { Parameters par = createTranslateConceptParams(code, system, version, valueSet, coding, codeableConcept, target, dependencies); @@ -285,7 +285,7 @@ public static async Task TranslateConceptAsync(this FhirClient clien return OperationResult(await client.TypeOperationAsync(RestOperation.TRANSLATE, par).ConfigureAwait(false)); } - public static Parameters TranslateConcept(this FhirClient client, Code code, FhirUri system, FhirString version, + public static Parameters TranslateConcept(this IFhirClient client, Code code, FhirUri system, FhirString version, FhirUri valueSet, Coding coding, CodeableConcept codeableConcept, FhirUri target, IEnumerable dependencies) { diff --git a/src/Hl7.Fhir.Core/Rest/FhirClientSearch.cs b/src/Hl7.Fhir.Core/Rest/FhirClientSearch.cs index ad253753dc..393e7572c9 100644 --- a/src/Hl7.Fhir.Core/Rest/FhirClientSearch.cs +++ b/src/Hl7.Fhir.Core/Rest/FhirClientSearch.cs @@ -14,7 +14,7 @@ namespace Hl7.Fhir.Rest { - public partial class FhirClient + public abstract partial class BaseFhirClient { #region Search Execution diff --git a/src/Hl7.Fhir.Core/Rest/FhirClientTermSvcExtensions.cs b/src/Hl7.Fhir.Core/Rest/FhirClientTermSvcExtensions.cs index a0de367593..728cbf8490 100644 --- a/src/Hl7.Fhir.Core/Rest/FhirClientTermSvcExtensions.cs +++ b/src/Hl7.Fhir.Core/Rest/FhirClientTermSvcExtensions.cs @@ -59,7 +59,7 @@ public static ValidateCodeResult FromParameters(Parameters p) public static class FhirClientTermSvcExtensions { #region Expand - public static async Task ExpandValueSetAsync(this FhirClient client, Uri valueset, FhirString filter = null, FhirDateTime date = null) + public static async Task ExpandValueSetAsync(this IFhirClient client, Uri valueset, FhirString filter = null, FhirDateTime date = null) { if (valueset == null) throw Error.ArgumentNull(nameof(valueset)); @@ -74,13 +74,13 @@ public static async Task ExpandValueSetAsync(this FhirClient client, U .OperationResult(); } - public static ValueSet ExpandValueSet(this FhirClient client, Uri valueset, FhirString filter = null, + public static ValueSet ExpandValueSet(this IFhirClient client, Uri valueset, FhirString filter = null, FhirDateTime date = null) { return ExpandValueSetAsync(client, valueset, filter, date).WaitResult(); } - public static async Task ExpandValueSetAsync(this FhirClient client, FhirUri identifier, FhirString filter = null, FhirDateTime date = null) + public static async Task ExpandValueSetAsync(this IFhirClient client, FhirUri identifier, FhirString filter = null, FhirDateTime date = null) { if (identifier == null) throw Error.ArgumentNull(nameof(identifier)); @@ -94,13 +94,13 @@ public static async Task ExpandValueSetAsync(this FhirClient client, F .OperationResult(); } - public static ValueSet ExpandValueSet(this FhirClient client, FhirUri identifier, FhirString filter = null, + public static ValueSet ExpandValueSet(this IFhirClient client, FhirUri identifier, FhirString filter = null, FhirDateTime date = null) { return ExpandValueSetAsync(client, identifier, filter, date).WaitResult(); } - public static async Task ExpandValueSetAsync(this FhirClient client, ValueSet vs, FhirString filter = null, FhirDateTime date = null) + public static async Task ExpandValueSetAsync(this IFhirClient client, ValueSet vs, FhirString filter = null, FhirDateTime date = null) { if (vs == null) throw Error.ArgumentNull(nameof(vs)); @@ -112,7 +112,7 @@ public static async Task ExpandValueSetAsync(this FhirClient client, V .OperationResult(); } - public static ValueSet ExpandValueSet(this FhirClient client, ValueSet vs, FhirString filter = null, + public static ValueSet ExpandValueSet(this IFhirClient client, ValueSet vs, FhirString filter = null, FhirDateTime date = null) { return ExpandValueSetAsync(client, vs, filter, date).WaitResult(); diff --git a/src/Hl7.Fhir.Core/Rest/FhirOperationException.cs b/src/Hl7.Fhir.Core/Rest/FhirOperationException.cs index bd8525e24e..16bc429956 100644 --- a/src/Hl7.Fhir.Core/Rest/FhirOperationException.cs +++ b/src/Hl7.Fhir.Core/Rest/FhirOperationException.cs @@ -32,14 +32,14 @@ public class FhirOperationException : Exception /// /// /// - public HttpStatusCode Status { get; set; } + public HttpStatusCode? Status { get; set; } /// /// Initializes a new instance of the class with a specified error message. /// /// The message that describes the error. /// The http status code associated with the message - public FhirOperationException(string message, HttpStatusCode status) + public FhirOperationException(string message, HttpStatusCode? status) : base(message) { Status = status; @@ -52,7 +52,7 @@ public FhirOperationException(string message, HttpStatusCode status) /// The error message that explains the reason for the exception. /// The http status code associated with the message /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public FhirOperationException(string message, HttpStatusCode status, Exception inner) + public FhirOperationException(string message, HttpStatusCode? status, Exception inner) : base(message, inner) { Status = status; @@ -65,7 +65,7 @@ public FhirOperationException(string message, HttpStatusCode status, Exception i /// The http status code associated with the message /// The outcome of the operation . /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public FhirOperationException(string message, HttpStatusCode status, OperationOutcome outcome, Exception inner) + public FhirOperationException(string message, HttpStatusCode? status, OperationOutcome outcome, Exception inner) : base(message, inner) { Outcome = outcome; @@ -78,7 +78,7 @@ public FhirOperationException(string message, HttpStatusCode status, OperationOu /// The message that describes the error. /// The http status code associated with the message /// The outcome of the operation . - public FhirOperationException(string message, HttpStatusCode status, OperationOutcome outcome) + public FhirOperationException(string message, HttpStatusCode? status, OperationOutcome outcome) : base(message) { Outcome = outcome; diff --git a/src/Hl7.Fhir.Core/Rest/Http/EntryToHttpExtensions.cs b/src/Hl7.Fhir.Core/Rest/Http/EntryToHttpExtensions.cs new file mode 100644 index 0000000000..939e3a274f --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/Http/EntryToHttpExtensions.cs @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2014, Furore (info@furore.com) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/ewoutkramer/fhir-net-api/master/LICENSE + */ + +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using Hl7.Fhir.Utility; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Linq; + +namespace Hl7.Fhir.Rest.Http +{ + internal static class EntryToHttpExtensions + { + public static HttpRequestMessage ToHttpRequestMessage(this Bundle.EntryComponent entry, + SearchParameterHandling? handlingPreference, Prefer? returnPreference, ResourceFormat format, bool useFormatParameter, bool CompressRequestBody) + { + System.Diagnostics.Debug.WriteLine("{0}: {1}", entry.Request.Method, entry.Request.Url); + + var interaction = entry.Request; + + if (entry.Resource != null && !(interaction.Method == Bundle.HTTPVerb.POST || interaction.Method == Bundle.HTTPVerb.PUT)) + throw Error.InvalidOperation("Cannot have a body on an Http " + interaction.Method.ToString()); + + var location = new RestUrl(interaction.Url); + + if (useFormatParameter) + location.AddParam(HttpUtil.RESTPARAM_FORMAT, Hl7.Fhir.Rest.ContentType.BuildFormatParam(format)); + + var request = new HttpRequestMessage(getMethod(interaction.Method), location.Uri); + + if (!useFormatParameter) + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(Hl7.Fhir.Rest.ContentType.BuildContentType(format, forBundle: false))); + + if (interaction.IfMatch != null) request.Headers.Add("If-Match", interaction.IfMatch); + if (interaction.IfNoneMatch != null) request.Headers.Add("If-None-Match", interaction.IfNoneMatch); + if (interaction.IfModifiedSince != null) request.Headers.IfModifiedSince = interaction.IfModifiedSince.Value.UtcDateTime; + if (interaction.IfNoneExist != null) request.Headers.Add("If-None-Exist", interaction.IfNoneExist); + + var interactionType = entry.Annotation(); + + if (interactionType == TransactionBuilder.InteractionType.Create && returnPreference != null) + request.Headers.Add("Prefer", "return=" + PrimitiveTypeConverter.ConvertTo(returnPreference)); + else if (interactionType == TransactionBuilder.InteractionType.Search && handlingPreference != null) + request.Headers.Add("Prefer", "handling=" + PrimitiveTypeConverter.ConvertTo(handlingPreference)); + + if (entry.Resource != null) + setBodyAndContentType(request, entry.Resource, format, CompressRequestBody); + + return request; + } + + /// + /// Converts bundle http verb to corresponding . + /// + /// specified by input bundle. + /// corresponding to verb specified in input bundle. + private static HttpMethod getMethod(Bundle.HTTPVerb? verb) + { + switch(verb) + { + case Bundle.HTTPVerb.GET: + return HttpMethod.Get; + case Bundle.HTTPVerb.POST: + return HttpMethod.Post; + case Bundle.HTTPVerb.PUT: + return HttpMethod.Put; + case Bundle.HTTPVerb.DELETE: + return HttpMethod.Delete; + } + throw new HttpRequestException($"Valid HttpVerb could not be found for verb type: [{verb}]"); + } + + private static void setBodyAndContentType(HttpRequestMessage request, Resource data, ResourceFormat format, bool CompressRequestBody) + { + if (data == null) throw Error.ArgumentNull(nameof(data)); + + byte[] body; + string contentType; + + if (data is Binary bin) + { + body = bin.Content; + // This is done by the caller after the OnBeforeRequest is called so that other properties + // can be set before the content is committed + // request.WriteBody(CompressRequestBody, bin.Content); + contentType = bin.ContentType; + } + else + { + body = format == ResourceFormat.Xml ? + new FhirXmlSerializer().SerializeToBytes(data, summary: Fhir.Rest.SummaryType.False) : + new FhirJsonSerializer().SerializeToBytes(data, summary: Fhir.Rest.SummaryType.False); + + // This is done by the caller after the OnBeforeRequest is called so that other properties + // can be set before the content is committed + // request.WriteBody(CompressRequestBody, body); + contentType = Hl7.Fhir.Rest.ContentType.BuildContentType(format, forBundle: false); + } + + request.Content = new ByteArrayContent(body); + + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + } + + + } +} diff --git a/src/Hl7.Fhir.Core/Rest/Http/FhirClient.cs b/src/Hl7.Fhir.Core/Rest/Http/FhirClient.cs new file mode 100644 index 0000000000..b01b1c6ffc --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/Http/FhirClient.cs @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2014, Furore (info@furore.com) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/ewoutkramer/fhir-net-api/master/LICENSE + */ + + +using Hl7.Fhir.Model; +using Hl7.Fhir.Rest; +using Hl7.Fhir.Serialization; +using Hl7.Fhir.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + + +namespace Hl7.Fhir.Rest.Http +{ + public partial class FhirClient : BaseFhirClient + { + /// + /// Creates a new client using a default endpoint + /// If the endpoint does not end with a slash (/), it will be added. + /// + /// + /// The URL of the server to connect to.
+ /// If the trailing '/' is not present, then it will be appended automatically + /// + /// + /// + /// If parameter is set to true the first time a request is made to the server a + /// conformance check will be made to check that the FHIR versions are compatible. + /// When they are not compatible, a FhirException will be thrown. + /// + public FhirClient(Uri endpoint, bool verifyFhirVersion = false, HttpMessageHandler messageHandler = null) + { + Endpoint = GetValidatedEndpoint(endpoint); + VerifyFhirVersion = verifyFhirVersion; + + // If user does not supply message handler, add decompression strategy in default handler. + var handler = messageHandler ?? new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate + }; + + var requester = new Requester(Endpoint, handler); + Requester = requester; + + // Expose default request headers to user. + RequestHeaders = requester.Client.DefaultRequestHeaders; + } + + + /// + /// Creates a new client using a default endpoint + /// If the endpoint does not end with a slash (/), it will be added. + /// + /// + /// The URL of the server to connect to.
+ /// If the trailing '/' is not present, then it will be appended automatically + /// + /// + /// + /// If parameter is set to true the first time a request is made to the server a + /// conformance check will be made to check that the FHIR versions are compatible. + /// When they are not compatible, a FhirException will be thrown. + /// + public FhirClient(string endpoint, bool verifyFhirVersion = false, HttpMessageHandler messageHandler = null) + : this(new Uri(endpoint), verifyFhirVersion, messageHandler) + { + } + + /// + /// Default request headers that can be modified to persist default headers to internal client. + /// + public HttpRequestHeaders RequestHeaders { get; protected set; } + + public override byte[] LastBody => LastResult?.GetBody(); + public override string LastBodyAsText => LastResult?.GetBodyAsText(); + public override Resource LastBodyAsResource => Requester.LastResult?.Resource; + + /// + /// Returns the HttpRequestMessage as it was last constructed to execute a call on the FhirClient + /// + new public HttpRequestMessage LastRequest { get { return (Requester as Http.Requester)?.LastRequest; } } + + /// + /// Returns the HttpResponseMessage as it was last received during a call on the FhirClient + /// + /// Note that the FhirClient will have read the body data from the HttpResponseMessage, so this is + /// no longer available. Use LastBody, LastBodyAsText and LastBodyAsResource to get access to the received body (if any) + new public HttpResponseMessage LastResponse { get { return (Requester as Http.Requester)?.LastResponse; } } + + [Obsolete] + public override event EventHandler OnAfterResponse = (args, e) => throw new NotImplementedException(); + + [Obsolete] + public override event EventHandler OnBeforeRequest = (args, e) => throw new NotImplementedException(); + + /// + /// Override dispose in order to clean up request headers tied to disposed requester. + /// + /// + protected override void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.RequestHeaders = null; + base.Dispose(disposing); + } + + disposedValue = true; + } + } + } +} diff --git a/src/Hl7.Fhir.Core/Rest/Http/HttpClientEventHandler.cs b/src/Hl7.Fhir.Core/Rest/Http/HttpClientEventHandler.cs new file mode 100644 index 0000000000..e9a24dc95c --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/Http/HttpClientEventHandler.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Hl7.Fhir.Core.Rest.Http +{ + public class HttpClientEventHandler : HttpClientHandler + { + /// + /// Called just before the Http call is done + /// + public event EventHandler OnBeforeRequest; + + /// + /// Called just after the response was received + /// + public event EventHandler OnAfterResponse; + + /// + /// Inspect or modify the HttpRequestMessage just before the FhirClient issues a call to the server + /// + /// The request as it is about to be sent to the server + /// The data in the body of the request as it is about to be sent to the server + protected virtual void BeforeRequest(HttpRequestMessage rawRequest, byte[] body) + { + // Default implementation: call event + OnBeforeRequest?.Invoke(this, new BeforeRequestEventArgs(rawRequest, body)); + } + + /// + /// Inspect the HttpResponseMessage as it came back from the server + /// + /// You cannot read the body from the HttpResponseMessage, since it has + /// already been read by the framework. Use the body parameter instead. + protected virtual void AfterResponse(HttpResponseMessage webResponse, byte[] body) + { + // Default implementation: call event + OnAfterResponse?.Invoke(this, new AfterResponseEventArgs(webResponse, body)); + } + + + protected override async Task SendAsync(HttpRequestMessage message, CancellationToken cancellationToken) + { + var requestBody = message.Content != null ? await message.Content.ReadAsByteArrayAsync() : new byte[0]; + BeforeRequest(message, requestBody); + + var response = await base.SendAsync(message, cancellationToken); + + AfterResponse(response, (await response.Content?.ReadAsByteArrayAsync() ?? new byte[0])); + + return response; + } + } + + public class BeforeRequestEventArgs : EventArgs + { + public BeforeRequestEventArgs(HttpRequestMessage rawRequest, byte[] body) + { + this.RawRequest = rawRequest; + this.Body = body; + } + + public HttpRequestMessage RawRequest { get; internal set; } + public byte[] Body { get; internal set; } + } + + public class AfterResponseEventArgs : EventArgs + { + public AfterResponseEventArgs(HttpResponseMessage webResponse, byte[] body) + { + this.RawResponse = webResponse; + this.Body = body; + } + + public HttpResponseMessage RawResponse { get; internal set; } + public byte[] Body { get; internal set; } + } +} diff --git a/src/Hl7.Fhir.Core/Rest/Http/HttpToEntryExtensions.cs b/src/Hl7.Fhir.Core/Rest/Http/HttpToEntryExtensions.cs new file mode 100644 index 0000000000..57fcb00f70 --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/Http/HttpToEntryExtensions.cs @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014, Furore (info@furore.com) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/ewoutkramer/fhir-net-api/master/LICENSE + */ + +using Hl7.Fhir.Model; +using Hl7.Fhir.Rest; +using Hl7.Fhir.Serialization; +using Hl7.Fhir.Utility; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +namespace Hl7.Fhir.Rest.Http +{ + internal static class HttpToEntryExtensions + { + private const string USERDATA_BODY = "$body"; + private const string EXTENSION_RESPONSE_HEADER = "http://hl7.org/fhir/StructureDefinition/http-response-header"; + + internal static Bundle.EntryComponent ToBundleEntry(this HttpResponseMessage response, byte[] body, ParserSettings parserSettings, bool throwOnFormatException) + { + var result = new Bundle.EntryComponent(); + + result.Response = new Bundle.ResponseComponent(); + result.Response.Status = ((int)response.StatusCode).ToString(); + result.Response.SetHeaders(response.Headers); + + var contentType = response.Content.Headers.ContentType; + + Encoding charEncoding; + + try + { + charEncoding = Encoding.GetEncoding(response.Content.Headers.ContentType.CharSet); + } + catch (ArgumentException) + { + charEncoding = Encoding.UTF8; + } + + result.Response.Location = response.Headers.Location?.AbsoluteUri ?? response.Content.Headers.ContentLocation?.AbsoluteUri; + + result.Response.LastModified = response.Content.Headers.LastModified; + result.Response.Etag = response.Headers.ETag?.Tag.Trim('\"'); + + if (body != null && body.Length != 0) + { + result.Response.SetBody(body); + + if (Rest.HttpToEntryExtensions.IsBinaryResponse(result.Response.Location, contentType.MediaType.ToString())) + { + result.Resource = Rest.HttpToEntryExtensions.MakeBinaryResource(body, contentType.ToString()); + if (result.Response.Location != null) + { + var ri = new ResourceIdentity(result.Response.Location); + result.Resource.Id = ri.Id; + result.Resource.Meta = new Meta(); + result.Resource.Meta.VersionId = ri.VersionId; + result.Resource.ResourceBase = ri.BaseUri; + } + } + else + { + var bodyText = Rest.HttpToEntryExtensions.DecodeBody(body, charEncoding); + var resource = Rest.HttpToEntryExtensions.ParseResource(bodyText, contentType.MediaType.ToString(), parserSettings, throwOnFormatException); + result.Resource = resource; + + if (result.Response.Location != null) + result.Resource.ResourceBase = new ResourceIdentity(result.Response.Location).BaseUri; + } + } + + return result; + } + + internal static void SetHeaders(this Bundle.ResponseComponent interaction, HttpResponseHeaders headers) + { + foreach (var header in headers) + { + interaction.AddExtension(EXTENSION_RESPONSE_HEADER, new FhirString(header.Key + ":" + header.Value)); + } + } + } +} diff --git a/src/Hl7.Fhir.Core/Rest/Http/Requester.cs b/src/Hl7.Fhir.Core/Rest/Http/Requester.cs new file mode 100644 index 0000000000..d415e67956 --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/Http/Requester.cs @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2014, Furore (info@furore.com) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/ewoutkramer/fhir-net-api/master/LICENSE + */ + +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using Hl7.Fhir.Utility; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Hl7.Fhir.Rest.Http +{ + internal class Requester : IRequester, IDisposable + { + public Uri BaseUrl { get; private set; } + public HttpClient Client { get; private set; } + + public bool UseFormatParameter { get; set; } + public ResourceFormat PreferredFormat { get; set; } + public int Timeout { get; set; } // In milliseconds + + public Prefer? PreferredReturn { get; set; } + public SearchParameterHandling? PreferredParameterHandling { get; set; } + + /// + /// This will do 2 things: + /// 1. Add the header Accept-Encoding: gzip, deflate + /// 2. decompress any responses that have Content-Encoding: gzip (or deflate) + /// + public bool PreferCompressedResponses { get; set; } + + /// + /// Compress any Request bodies + /// (warning, if a server does not handle compressed requests you will get a 415 response) + /// + public bool CompressRequestBody { get; set; } + + public ParserSettings ParserSettings { get; set; } + + public Requester(Uri baseUrl, HttpMessageHandler messageHandler) + { + BaseUrl = baseUrl; + Client = new HttpClient(messageHandler); + + Client.DefaultRequestHeaders.Add("User-Agent", ".NET FhirClient for FHIR " + Model.ModelInfo.Version); + UseFormatParameter = false; + PreferredFormat = ResourceFormat.Xml; + Client.Timeout = new TimeSpan(0, 0, 100); // Default timeout is 100 seconds + PreferredReturn = Rest.Prefer.ReturnRepresentation; + PreferredParameterHandling = null; + ParserSettings = Hl7.Fhir.Serialization.ParserSettings.Default; + } + + + public Bundle.EntryComponent LastResult { get; private set; } + public HttpStatusCode? LastStatusCode => LastResponse?.StatusCode; + public HttpResponseMessage LastResponse { get; private set; } + public HttpRequestMessage LastRequest { get; private set; } + + public Bundle.EntryComponent Execute(Bundle.EntryComponent interaction) + { + return ExecuteAsync(interaction).WaitResult(); + } + + public async Task ExecuteAsync(Bundle.EntryComponent interaction) + { + if (interaction == null) throw Error.ArgumentNull(nameof(interaction)); + bool compressRequestBody = false; + + compressRequestBody = CompressRequestBody; // PCL doesn't support compression at the moment + + using (var requestMessage = interaction.ToHttpRequestMessage(this.PreferredParameterHandling, this.PreferredReturn, PreferredFormat, UseFormatParameter, compressRequestBody)) + { + if (PreferCompressedResponses) + { + requestMessage.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); + requestMessage.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate")); + } + + LastRequest = requestMessage; + + byte[] outgoingBody = null; + if (requestMessage.Method == HttpMethod.Post || requestMessage.Method == HttpMethod.Put) + { + outgoingBody = await requestMessage.Content.ReadAsByteArrayAsync(); + } + + using (var response = await Client.SendAsync(requestMessage).ConfigureAwait(false)) + { + try + { + var body = await response.Content.ReadAsByteArrayAsync(); + + LastResponse = response; + + // Do this call after AfterResponse, so AfterResponse will be called, even if exceptions are thrown by ToBundleEntry() + try + { + LastResult = null; + + if (response.IsSuccessStatusCode) + { + LastResult = response.ToBundleEntry(body, ParserSettings, throwOnFormatException: true); + return LastResult; + } + else + { + LastResult = response.ToBundleEntry(body, ParserSettings, throwOnFormatException: false); + throw buildFhirOperationException(response.StatusCode, LastResult.Resource); + } + } + catch (UnsupportedBodyTypeException bte) + { + // The server responded with HTML code. Still build a FhirOperationException and set a LastResult. + // Build a very minimal LastResult + var errorResult = new Bundle.EntryComponent(); + errorResult.Response = new Bundle.ResponseComponent(); + errorResult.Response.Status = ((int)response.StatusCode).ToString(); + + OperationOutcome operationOutcome = OperationOutcome.ForException(bte, OperationOutcome.IssueType.Invalid); + + errorResult.Resource = operationOutcome; + LastResult = errorResult; + + throw buildFhirOperationException(response.StatusCode, operationOutcome); + } + } + catch (AggregateException ae) + { + //EK: This code looks weird. Is this correct? + if (ae.GetBaseException() is WebException) + { + } + throw ae.GetBaseException(); + } + } + } + } + + private static Exception buildFhirOperationException(HttpStatusCode status, Resource body) + { + string message; + + if (status.IsInformational()) + message = $"Operation resulted in an informational response ({status})"; + else if (status.IsRedirection()) + message = $"Operation resulted in a redirection response ({status})"; + else if (status.IsClientError()) + message = $"Operation was unsuccessful because of a client error ({status})"; + else + message = $"Operation was unsuccessful, and returned status {status}"; + + if (body is OperationOutcome outcome) + return new FhirOperationException($"{message}. OperationOutcome: {outcome.ToString()}.", status, outcome); + else if (body != null) + return new FhirOperationException($"{message}. Body contains a {body.TypeName}.", status); + else + return new FhirOperationException($"{message}. Body has no content.", status); + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.Client.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + #endregion + } +} diff --git a/src/Hl7.Fhir.Core/Rest/HttpToEntryExtensions.cs b/src/Hl7.Fhir.Core/Rest/HttpToEntryExtensions.cs index 3b982a1312..4efbe4d3a2 100644 --- a/src/Hl7.Fhir.Core/Rest/HttpToEntryExtensions.cs +++ b/src/Hl7.Fhir.Core/Rest/HttpToEntryExtensions.cs @@ -58,7 +58,7 @@ internal static Bundle.EntryComponent ToBundleEntry(this HttpWebResponse respons if (IsBinaryResponse(response.ResponseUri.OriginalString, contentType)) { - result.Resource = makeBinaryResource(body, contentType); + result.Resource = MakeBinaryResource(body, contentType); if (result.Response.Location != null) { var ri = new ResourceIdentity(result.Response.Location); @@ -71,7 +71,7 @@ internal static Bundle.EntryComponent ToBundleEntry(this HttpWebResponse respons else { var bodyText = DecodeBody(body, charEncoding); - var resource = parseResource(bodyText, contentType, parserSettings, throwOnFormatException); + var resource = ParseResource(bodyText, contentType, parserSettings, throwOnFormatException); result.Resource = resource; if (result.Response.Location != null) @@ -120,7 +120,7 @@ private static Encoding getCharacterEncoding(HttpWebResponse response) return result; } - private static Resource parseResource(string bodyText, string contentType, ParserSettings settings, bool throwOnFormatException) + internal static Resource ParseResource(string bodyText, string contentType, ParserSettings settings, bool throwOnFormatException) { Resource result= null; @@ -189,7 +189,7 @@ internal static string DecodeBody(byte[] body, Encoding enc) } } - private static Binary makeBinaryResource(byte[] data, string contentType) + internal static Binary MakeBinaryResource(byte[] data, string contentType) { var binary = new Binary(); diff --git a/src/Hl7.Fhir.Core/Rest/IFhirClient.cs b/src/Hl7.Fhir.Core/Rest/IFhirClient.cs index ae81cb88ca..bbeaf1b04e 100644 --- a/src/Hl7.Fhir.Core/Rest/IFhirClient.cs +++ b/src/Hl7.Fhir.Core/Rest/IFhirClient.cs @@ -8,18 +8,13 @@ namespace Hl7.Fhir.Rest { public interface IFhirClient { + #if NET_COMPRESSION bool PreferCompressedResponses { get; set; } bool CompressRequestBody { get; set; } #endif - Uri Endpoint { get; } - byte[] LastBody { get; } - Resource LastBodyAsResource { get; } - string LastBodyAsText { get; } - HttpWebRequest LastRequest { get; } - HttpWebResponse LastResponse { get; } - Bundle.ResponseComponent LastResult { get; } + ParserSettings ParserSettings { get; set; } ResourceFormat PreferredFormat { get; set; } bool ReturnFullResource { get; set; } @@ -27,8 +22,23 @@ public interface IFhirClient bool UseFormatParam { get; set; } bool VerifyFhirVersion { get; set; } + byte[] LastBody { get; } + Resource LastBodyAsResource { get; } + string LastBodyAsText { get; } + + Bundle.ResponseComponent LastResult { get; } + + [Obsolete] + HttpWebRequest LastRequest { get; } + + [Obsolete] + HttpWebResponse LastResponse { get; } + + [Obsolete] event EventHandler OnAfterResponse; - event EventHandler OnBeforeRequest; + + [Obsolete] + event EventHandler OnBeforeRequest; CapabilityStatement CapabilityStatement(SummaryType? summary = default(SummaryType?)); Task CapabilityStatementAsync(SummaryType? summary = default(SummaryType?)); diff --git a/src/Hl7.Fhir.Core/Rest/IRequester.cs b/src/Hl7.Fhir.Core/Rest/IRequester.cs new file mode 100644 index 0000000000..57ce646ad4 --- /dev/null +++ b/src/Hl7.Fhir.Core/Rest/IRequester.cs @@ -0,0 +1,40 @@ +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Hl7.Fhir.Rest +{ + public interface IRequester + { + Bundle.EntryComponent Execute(Bundle.EntryComponent interaction); + Task ExecuteAsync(Bundle.EntryComponent interaction); + + Bundle.EntryComponent LastResult { get; } + bool UseFormatParameter { get; set; } + ResourceFormat PreferredFormat { get; set; } + int Timeout { get; set; } // In milliseconds + + Prefer? PreferredReturn { get; set; } + SearchParameterHandling? PreferredParameterHandling { get; set; } + + /// + /// This will do 2 things: + /// 1. Add the header Accept-Encoding: gzip, deflate + /// 2. decompress any responses that have Content-Encoding: gzip (or deflate) + /// + bool PreferCompressedResponses { get; set; } + /// + /// Compress any Request bodies + /// (warning, if a server does not handle compressed requests you will get a 415 response) + /// + bool CompressRequestBody { get; set; } + + ParserSettings ParserSettings { get; set; } + HttpStatusCode? LastStatusCode { get; } + } +} diff --git a/src/Hl7.Fhir.Core/Rest/Requester.cs b/src/Hl7.Fhir.Core/Rest/Requester.cs index a5a4ab5772..2da1522bb5 100644 --- a/src/Hl7.Fhir.Core/Rest/Requester.cs +++ b/src/Hl7.Fhir.Core/Rest/Requester.cs @@ -17,7 +17,7 @@ namespace Hl7.Fhir.Rest { - internal class Requester + internal class Requester : IRequester { public Uri BaseUrl { get; private set; } @@ -55,6 +55,7 @@ public Requester(Uri baseUrl) public Bundle.EntryComponent LastResult { get; private set; } + public HttpStatusCode? LastStatusCode => LastResponse?.StatusCode; public HttpWebResponse LastResponse { get; private set; } public HttpWebRequest LastRequest { get; private set; } public Action BeforeRequest { get; set; } diff --git a/src/Hl7.Fhir.Specification.Tests/Source/TerminologyTests.cs b/src/Hl7.Fhir.Specification.Tests/Source/TerminologyTests.cs index c2dc1fa999..6f80d2a336 100644 --- a/src/Hl7.Fhir.Specification.Tests/Source/TerminologyTests.cs +++ b/src/Hl7.Fhir.Specification.Tests/Source/TerminologyTests.cs @@ -7,6 +7,8 @@ using System.Linq; using Xunit; +using TestClient = Hl7.Fhir.Rest.Http.FhirClient; + namespace Hl7.Fhir.Specification.Tests { public class TerminologyTests : IClassFixture @@ -167,7 +169,7 @@ public void LocalTermServiceValidateCodeTest() } [Fact, Trait("TestCategory", "IntegrationTest")] - public void ExternalServiceValidateCodeTest() + public void ExternalServiceValidateCodeTestWebClient() { var client = new FhirClient("http://ontoserver.csiro.au/stu3-latest"); var svc = new ExternalTerminologyService(client); @@ -181,7 +183,23 @@ public void ExternalServiceValidateCodeTest() } [Fact, Trait("TestCategory", "IntegrationTest")] - public void FallbackServiceValidateCodeTest() + public void ExternalServiceValidateCodeTestHttpClient() + { + using (var client = new TestClient("http://ontoserver.csiro.au/stu3-latest")) + { + var svc = new ExternalTerminologyService(client); + + // Do common tests for service + testService(svc); + + // Any good external service should be able to handle this one + var result = svc.ValidateCode("http://hl7.org/fhir/ValueSet/substance-code", code: "1166006", system: "http://snomed.info/sct"); + Assert.True(result.Success); + } + } + + [Fact, Trait("TestCategory", "IntegrationTest")] + public void FallbackServiceValidateCodeTestWebClient() { var client = new FhirClient("http://ontoserver.csiro.au/stu3-latest"); var external = new ExternalTerminologyService(client); @@ -196,7 +214,24 @@ public void FallbackServiceValidateCodeTest() } [Fact, Trait("TestCategory", "IntegrationTest")] - public void FallbackServiceValidateCodeTestWithVS() + public void FallbackServiceValidateCodeTestHttpClient() + { + using (var client = new TestClient("http://ontoserver.csiro.au/stu3-latest")) + { + var external = new ExternalTerminologyService(client); + var local = new LocalTerminologyService(_resolver); + var svc = new FallbackTerminologyService(local, external); + + testService(svc); + + // Now, this should fall back + var result = svc.ValidateCode("http://hl7.org/fhir/ValueSet/substance-code", code: "1166006", system: "http://snomed.info/sct"); + Assert.True(result.Success); + } + } + + [Fact, Trait("TestCategory", "IntegrationTest")] + public void FallbackServiceValidateCodeTestWithVSWebClient() { var client = new FhirClient("http://ontoserver.csiro.au/stu3-latest"); var service = new ExternalTerminologyService(client); @@ -213,6 +248,26 @@ public void FallbackServiceValidateCodeTestWithVS() Assert.True(result.Success); } + [Fact, Trait("TestCategory", "IntegrationTest")] + public void FallbackServiceValidateCodeTestWithVSHttpClient() + { + using (var client = new TestClient("http://ontoserver.csiro.au/stu3-latest")) + { + var service = new ExternalTerminologyService(client); + var vs = _resolver.FindValueSet("http://hl7.org/fhir/ValueSet/substance-code"); + Assert.NotNull(vs); + + // Override the canonical with something the remote server cannot know + vs.Url = "http://furore.com/fhir/ValueSet/testVS"; + var local = new LocalTerminologyService(new IKnowOnlyMyTestVSResolver(vs)); + var fallback = new FallbackTerminologyService(local, service); + + // Now, this should fall back to external + send our vs (that the server cannot know about) + var result = fallback.ValidateCode("http://furore.com/fhir/ValueSet/testVS", code: "1166006", system: "http://snomed.info/sct"); + Assert.True(result.Success); + } + } + private class IKnowOnlyMyTestVSResolver : IResourceResolver { public ValueSet _myOnlyVS;