From 0f7f1174dfd678b1063231a5698f585b37e4d587 Mon Sep 17 00:00:00 2001 From: naresh-vadala-lrn <153288673+naresh-vadala-lrn@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:09:54 +0530 Subject: [PATCH 1/4] Refactored code to use Http client tomake requests --- TinCan/ILRS.cs | 2 +- TinCan/IRemoteLrs.cs | 17 ++++ TinCan/LanguageMap.cs | 2 +- TinCan/RemoteLRS.cs | 231 +++++++++++++++++++++++++++++++++++------- 4 files changed, 216 insertions(+), 36 deletions(-) create mode 100644 TinCan/IRemoteLrs.cs diff --git a/TinCan/ILRS.cs b/TinCan/ILRS.cs index 2110b58..406aa62 100644 --- a/TinCan/ILRS.cs +++ b/TinCan/ILRS.cs @@ -28,7 +28,7 @@ public interface ILrs Task SaveStatementAsync(Statement statement); Task VoidStatementAsync(Guid id, Agent agent); - Task SaveStatementsAsync(List statements, string timestamp = null); + Task SaveStatementsAsync(List statements); Task RetrieveStatementAsync(Guid id); Task RetrieveVoidedStatementAsync(Guid id); Task QueryStatementsAsync(StatementsQuery query); diff --git a/TinCan/IRemoteLrs.cs b/TinCan/IRemoteLrs.cs new file mode 100644 index 0000000..ae27793 --- /dev/null +++ b/TinCan/IRemoteLrs.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace TinCan +{ + public interface IRemoteLrs : ILrs + { + Uri Endpoint { get; set; } + TCAPIVersion Version { get; set; } + string Auth { get; set; } + Dictionary Extended { get; set; } + Dictionary Headers { get; set; } + bool UseHttpClinet { get; set; } + string GetJsonStringFromStatements(List statements); + void SetAuth(string username, string password); + } +} diff --git a/TinCan/LanguageMap.cs b/TinCan/LanguageMap.cs index df76d3a..b556ef6 100644 --- a/TinCan/LanguageMap.cs +++ b/TinCan/LanguageMap.cs @@ -75,7 +75,7 @@ public override string ToString() public IEnumerator GetEnumerator() { - throw new NotImplementedException(); + return _map.GetEnumerator(); } } } diff --git a/TinCan/RemoteLRS.cs b/TinCan/RemoteLRS.cs index 3334046..38bc401 100644 --- a/TinCan/RemoteLRS.cs +++ b/TinCan/RemoteLRS.cs @@ -20,6 +20,7 @@ limitations under the License. using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Web; @@ -28,18 +29,26 @@ limitations under the License. namespace TinCan { - public class RemoteLrs : ILrs + public class RemoteLrs : IRemoteLrs { public Uri Endpoint { get; set; } public TCAPIVersion Version { get; set; } public string Auth { get; set; } public Dictionary Extended { get; set; } = new Dictionary(); public Dictionary Headers { get; set; } = new Dictionary(); + private HttpClient _httpClient { get; set; } + //Passing FeatureFlag to TinCan to control form LaunchDarkly + public bool UseHttpClinet { get; set; } public RemoteLrs() { } + public RemoteLrs(HttpClient httpClient) + { + _httpClient = httpClient; + } + public RemoteLrs(Uri endpoint, TCAPIVersion version, string username, string password) { Endpoint = endpoint; @@ -60,12 +69,15 @@ public RemoteLrs(string endpoint, string username, string password) : this(endpo private class MyHttpRequest { public string Method { get; set; } + public HttpMethod HttpMethod { get; set; } public string Resource { get; set; } public Dictionary QueryParams { get; set; } = new Dictionary(); public Dictionary Headers { get; set; } = new Dictionary(); public string ContentType { get; set; } public byte[] Content { get; set; } + public string StringContent { get; set; } + } private class MyHttpResponse @@ -73,6 +85,7 @@ private class MyHttpResponse public HttpStatusCode Status { get; } public string ContentType { get; } public byte[] Content { get; set; } + public string StringContent { get; set; } public DateTime LastModified { get; } public string Etag { get; } public Exception Ex { get; set; } @@ -101,6 +114,34 @@ public MyHttpResponse(HttpWebResponse webResp) Content = ReadFully(stream, (int)webResp.ContentLength); } } + + public MyHttpResponse(HttpResponseMessage httpResponseMessage) + + { + Status = httpResponseMessage.StatusCode; + try + { + if (httpResponseMessage.Headers.Contains("Etag")) + { + Etag = httpResponseMessage.Headers.GetValues("Etag")?.FirstOrDefault()?.ToString(); + } + + if (httpResponseMessage.Content.Headers.TryGetValues("LastModified", out var values)) + { + // Get the Last-Modified header value and parse it to DateTime + var lastModifiedString = values.ToString(); + + if (DateTime.TryParse(lastModifiedString, out DateTime lastModifiedDate)) + { + LastModified = lastModifiedDate; + } + } + } + catch + { + //sometimes will throw an exception, just ignore + } + } } private async Task MakeRequest(MyHttpRequest req) @@ -158,6 +199,68 @@ private async Task MakeRequest(MyHttpRequest req) return resp; } + private async Task MakeHttpRequest(MyHttpRequest req) + { + var httpRequestMessage = BuildHttpRequestMessage(req); + + try + { + if(_httpClient == null) _httpClient = new HttpClient(); + var httpResponseMessage = await _httpClient.SendAsync(httpRequestMessage); + var response = new MyHttpResponse(httpResponseMessage); + response.StringContent = await httpResponseMessage.Content.ReadAsStringAsync(); + return response; + + } + catch (HttpRequestException ex) + { + var httpResponseMessage = new HttpResponseMessage() + { + StatusCode = ex.StatusCode.Value, + Content = new StringContent($"Request failed: {ex.Message}"), + }; + + var response = new MyHttpResponse(httpResponseMessage); + response.Ex = ex; + return response; + } + catch (Exception ex) + { + + var httpResponseMessage = new HttpResponseMessage() + { + StatusCode = HttpStatusCode.InternalServerError, + Content = new StringContent($"Request failed: {ex.Message}"), + }; + + var response = new MyHttpResponse(httpResponseMessage); + response.Ex = ex; + return response; + } + } + + private HttpRequestMessage BuildHttpRequestMessage(MyHttpRequest req) + { + string url = GetEndpointUrl(req.Resource); + url = AppendQueryStringParamsToUrl(url, req.QueryParams); + + var httpRequestMessage = new HttpRequestMessage(); + httpRequestMessage.RequestUri = new Uri(url); + httpRequestMessage.Method = req.HttpMethod; + + AddHeadersToRequest(req.Headers, httpRequestMessage); + + req.ContentType = req.ContentType ?? "application/octet-stream"; + + if (req.StringContent != null) + { + httpRequestMessage.Content = new StringContent(req.StringContent, UTF8Encoding.UTF8, req.ContentType); + + } + + return httpRequestMessage; + } + private string GetEndpointUrl(string resource) { string url; @@ -207,6 +310,44 @@ private void AddHeadersToRequest(MyHttpRequest req, HttpWebRequest webReq) } } + private void AddHeadersToRequest(Dictionary requestHeaders, HttpRequestMessage httpRequestMessage) + { + httpRequestMessage.Headers.Clear(); + httpRequestMessage.Headers.Add("X-Experience-API-Version", Version.ToString()); + if (Auth != null) + { + httpRequestMessage.Headers.Add("Authorization", Auth); + } + + Headers.Concat(requestHeaders); + foreach (var entry in Headers) + { + httpRequestMessage.Headers.Add(entry.Key, entry.Value); + } + } + + private StatementsResultLrsResponse BuildStatementsResultLrsResponse(List statements, MyHttpResponse responseMessage) + { + var resultLrsResponse = new StatementsResultLrsResponse(); + if (!IsSuccessStatusCode(responseMessage.Status)) + { + resultLrsResponse.Success = false; + resultLrsResponse.HttpException = responseMessage.Ex; + resultLrsResponse.ErrMsg = responseMessage.StringContent; + return resultLrsResponse; + } + + var ids = JArray.Parse(responseMessage.StringContent); + for (var i = 0; i < ids.Count; i++) + { + statements[i].Id = new Guid((string)ids[i]); + } + + resultLrsResponse.Success = true; + resultLrsResponse.Content = new StatementsResult(statements); + return resultLrsResponse; + } + /// /// See http://www.yoda.arachsys.com/csharp/readbinary.html no license found /// @@ -515,47 +656,59 @@ public async Task VoidStatementAsync(Guid id, Agent agent) return await SaveStatementAsync(voidStatement); } - public async Task SaveStatementsAsync(List statements, - string timestamp = null) + public async Task SaveStatementsAsync(List statements) { - var r = new StatementsResultLrsResponse(); - - var req = new MyHttpRequest + if (UseHttpClinet) { - Resource = "statements", - Method = "POST", - ContentType = "application/json" - }; - - var jarray = new JArray(); - if (!string.IsNullOrEmpty(timestamp)) - jarray.Add(JToken.Parse(timestamp + '|')); - foreach (var st in statements) - { - jarray.Add(st.ToJObject(Version)); + var req = new MyHttpRequest + { + Resource = "statements", + ContentType = "application/json", + HttpMethod = HttpMethod.Post, + StringContent = GetJsonStringFromStatements(statements) + }; + var myHttpResponse = await MakeHttpRequest(req); + return BuildStatementsResultLrsResponse(statements, myHttpResponse); } + else + { + var r = new StatementsResultLrsResponse(); - req.Content = Encoding.UTF8.GetBytes(jarray.ToString()); + var req = new MyHttpRequest + { + Resource = "statements", + Method = "POST", + ContentType = "application/json" + }; - var res = await MakeRequest(req); - if (!IsSuccessStatusCode(res.Status)) - { - r.Success = false; - r.HttpException = res.Ex; - r.SetErrMsgFromBytes(res.Content); - return r; - } + var jarray = new JArray(); + foreach (var st in statements) + { + jarray.Add(st.ToJObject(Version)); + } - var ids = JArray.Parse(Encoding.UTF8.GetString(res.Content)); - for (var i = 0; i < ids.Count; i++) - { - statements[i].Id = new Guid((string)ids[i]); - } + req.Content = Encoding.UTF8.GetBytes(jarray.ToString()); - r.Success = true; - r.Content = new StatementsResult(statements); + var res = await MakeRequest(req); + if (!IsSuccessStatusCode(res.Status)) + { + r.Success = false; + r.HttpException = res.Ex; + r.SetErrMsgFromBytes(res.Content); + return r; + } - return r; + var ids = JArray.Parse(Encoding.UTF8.GetString(res.Content)); + for (var i = 0; i < ids.Count; i++) + { + statements[i].Id = new Guid((string)ids[i]); + } + + r.Success = true; + r.Content = new StatementsResult(statements); + + return r; + } } public async Task RetrieveStatementAsync(Guid id) @@ -875,6 +1028,16 @@ public async Task DeleteAgentProfileAsync(AgentProfileDocument prof return await DeleteDocument("agents/profile", queryParams); } + public string GetJsonStringFromStatements(List statements) + { + var jarray = new JArray(); + foreach (var st in statements) + { + jarray.Add(st.ToJObject(Version)); + } + return jarray.ToString(); + } + #endregion } } \ No newline at end of file From ee867a110b41aed9269261c2aecc925e41dace33 Mon Sep 17 00:00:00 2001 From: naresh-vadala-lrn <153288673+naresh-vadala-lrn@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:16:11 +0530 Subject: [PATCH 2/4] Added a tests for new changes --- TinCanTests/RemoteLRSResourceTest.cs | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/TinCanTests/RemoteLRSResourceTest.cs b/TinCanTests/RemoteLRSResourceTest.cs index 19b4406..c3fc093 100644 --- a/TinCanTests/RemoteLRSResourceTest.cs +++ b/TinCanTests/RemoteLRSResourceTest.cs @@ -164,6 +164,38 @@ public async Task TestSaveStatementsAsync() // TODO: check statements match and ids not null } + [Test] + public async Task TestSaveStatementsAsync_Uses_HttpClient() + { + var statement1 = new Statement + { + Actor = Support.Agent, + Verb = Support.Verb, + Target = Support.Parent + }; + + var statement2 = new Statement + { + Actor = Support.Agent, + Verb = Support.Verb, + Target = Support.Activity, + Context = Support.Context + }; + + var statements = new List + { + statement1, + statement2 + }; + + _lrs.UseHttpClinet = true; + + var lrsRes = await _lrs.SaveStatementsAsync(statements); + + Assert.IsTrue(lrsRes.Success); + // TODO: check statements match and ids not null + } + [Test] public async Task TestRetrieveStatementAsync() { From 58ac4621f8673cb10d860664481fb3a105dceb52 Mon Sep 17 00:00:00 2001 From: naresh-vadala-lrn <153288673+naresh-vadala-lrn@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:19:59 +0530 Subject: [PATCH 3/4] Readme and version changes --- README.md | 4 ++++ TinCan/TinCan.csproj | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f68503a..9f64984 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Modifications done by Naresh Vadala (Questionmark) * Add public property Headers to RemoteLrs class to add custom headers to request. * Added optional property to SaveStatementsAsync which takes timestamp parameter to add it to the payload +Modifications done by Naresh Vadala (Questionmark) +* Removed optional property to SaveStatementsAsync which takes timestamp parameter to add it to the payload +* Refactored code to use HttpClient to make request rather WebRequest as it is obsolete + # Parent Project No new updates in parent project. Last checked: 2nd July, 2022 diff --git a/TinCan/TinCan.csproj b/TinCan/TinCan.csproj index 55e7bdc..fb8af66 100644 --- a/TinCan/TinCan.csproj +++ b/TinCan/TinCan.csproj @@ -1,7 +1,7 @@  net8.0 - 1.4.0.0 + 1.5.0.0 TinCanStandard TinCanStandard TinCanCore @@ -10,8 +10,8 @@ true https://github.com/mayuragarwal-qm/TinCan.NET git - 1.4.0.0 - 1.4.0.0 + 1.5.0.0 + 1.5.0.0 LICENSE.txt From 39f9eca7fc556093576d36bec826f5b99852fccc Mon Sep 17 00:00:00 2001 From: naresh-vadala-lrn <153288673+naresh-vadala-lrn@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:57:02 +0530 Subject: [PATCH 4/4] Resolved comemnts --- TinCan/IRemoteLrs.cs | 2 +- TinCan/RemoteLRS.cs | 5 ++--- TinCanTests/RemoteLRSResourceTest.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/TinCan/IRemoteLrs.cs b/TinCan/IRemoteLrs.cs index ae27793..5f3ff8c 100644 --- a/TinCan/IRemoteLrs.cs +++ b/TinCan/IRemoteLrs.cs @@ -10,7 +10,7 @@ public interface IRemoteLrs : ILrs string Auth { get; set; } Dictionary Extended { get; set; } Dictionary Headers { get; set; } - bool UseHttpClinet { get; set; } + bool UseHttpClient { get; set; } string GetJsonStringFromStatements(List statements); void SetAuth(string username, string password); } diff --git a/TinCan/RemoteLRS.cs b/TinCan/RemoteLRS.cs index 38bc401..fc9bd55 100644 --- a/TinCan/RemoteLRS.cs +++ b/TinCan/RemoteLRS.cs @@ -37,8 +37,7 @@ public class RemoteLrs : IRemoteLrs public Dictionary Extended { get; set; } = new Dictionary(); public Dictionary Headers { get; set; } = new Dictionary(); private HttpClient _httpClient { get; set; } - //Passing FeatureFlag to TinCan to control form LaunchDarkly - public bool UseHttpClinet { get; set; } + public bool UseHttpClient { get; set; } public RemoteLrs() { @@ -658,7 +657,7 @@ public async Task VoidStatementAsync(Guid id, Agent agent) public async Task SaveStatementsAsync(List statements) { - if (UseHttpClinet) + if (UseHttpClient) { var req = new MyHttpRequest { diff --git a/TinCanTests/RemoteLRSResourceTest.cs b/TinCanTests/RemoteLRSResourceTest.cs index c3fc093..82387a8 100644 --- a/TinCanTests/RemoteLRSResourceTest.cs +++ b/TinCanTests/RemoteLRSResourceTest.cs @@ -188,7 +188,7 @@ public async Task TestSaveStatementsAsync_Uses_HttpClient() statement2 }; - _lrs.UseHttpClinet = true; + _lrs.UseHttpClient = true; var lrsRes = await _lrs.SaveStatementsAsync(statements);