diff --git a/asset/getseq_net.ico b/asset/getseq_net.ico
deleted file mode 100644
index 34b3b9f..0000000
Binary files a/asset/getseq_net.ico and /dev/null differ
diff --git a/example/SeqQuery/App.config b/example/SeqQuery/App.config
deleted file mode 100644
index 8277a73..0000000
--- a/example/SeqQuery/App.config
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/example/SeqQuery/Program.cs b/example/SeqQuery/Program.cs
index c80ac00..9a793a7 100644
--- a/example/SeqQuery/Program.cs
+++ b/example/SeqQuery/Program.cs
@@ -32,7 +32,7 @@ static void Main(string[] args)
var server = arguments[""].ToString();
var query = Normalize(arguments[""]);
var apiKey = Normalize(arguments["--apikey"]);
- var @from = Normalize(arguments["--from"]);
+ var from = Normalize(arguments["--from"]);
var to = Normalize(arguments["--to"]);
await Run(server, apiKey, query, from, to);
diff --git a/example/SeqQuery/SeqQuery.csproj b/example/SeqQuery/SeqQuery.csproj
index 40bca25..ea3a4f4 100644
--- a/example/SeqQuery/SeqQuery.csproj
+++ b/example/SeqQuery/SeqQuery.csproj
@@ -6,10 +6,6 @@
Exe
-
-
-
-
diff --git a/example/SeqQuery/getseq_net.ico b/example/SeqQuery/getseq_net.ico
deleted file mode 100644
index 34b3b9f..0000000
Binary files a/example/SeqQuery/getseq_net.ico and /dev/null differ
diff --git a/example/SeqTail/App.config b/example/SeqTail/App.config
deleted file mode 100644
index 8277a73..0000000
--- a/example/SeqTail/App.config
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/example/SeqTail/Program.cs b/example/SeqTail/Program.cs
index 162fdd1..0d98f85 100644
--- a/example/SeqTail/Program.cs
+++ b/example/SeqTail/Program.cs
@@ -46,7 +46,7 @@ static void Main(string[] args)
Console.WriteLine("Tailing, press Ctrl+C to exit.");
Console.CancelKeyPress += (s,a) => cancel.Cancel();
- var run = Task.Run(() => Run(server, apiKey, filter, cancel));
+ var run = Task.Run(() => Run(server, apiKey, filter, cancel), cancel.Token);
run.GetAwaiter().GetResult();
}
@@ -78,8 +78,8 @@ static async Task Run(string server, string apiKey, string filter, CancellationT
using (var stream = await connection.Events.StreamAsync(filter: strict))
{
var subscription = stream
- .Select(jObject => LogEventReader.ReadFromJObject(jObject))
- .Subscribe(evt => Log.Write(evt), () => cancel.Cancel());
+ .Select(LogEventReader.ReadFromJObject)
+ .Subscribe(Log.Write, cancel.Cancel);
cancel.Token.WaitHandle.WaitOne();
subscription.Dispose();
diff --git a/example/SeqTail/SeqTail.csproj b/example/SeqTail/SeqTail.csproj
index 3ceeb51..ac34313 100644
--- a/example/SeqTail/SeqTail.csproj
+++ b/example/SeqTail/SeqTail.csproj
@@ -6,10 +6,6 @@
Exe
-
-
-
-
diff --git a/example/SeqTail/getseq_net.ico b/example/SeqTail/getseq_net.ico
deleted file mode 100644
index 34b3b9f..0000000
Binary files a/example/SeqTail/getseq_net.ico and /dev/null differ
diff --git a/example/SignalCopy/App.config b/example/SignalCopy/App.config
deleted file mode 100644
index 8277a73..0000000
--- a/example/SignalCopy/App.config
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/example/SignalCopy/SignalCopy.csproj b/example/SignalCopy/SignalCopy.csproj
index 3f1644b..9b9700e 100644
--- a/example/SignalCopy/SignalCopy.csproj
+++ b/example/SignalCopy/SignalCopy.csproj
@@ -6,10 +6,6 @@
Exe
-
-
-
-
diff --git a/example/SignalCopy/getseq_net.ico b/example/SignalCopy/getseq_net.ico
deleted file mode 100644
index 34b3b9f..0000000
Binary files a/example/SignalCopy/getseq_net.ico and /dev/null differ
diff --git a/seq-api.sln b/seq-api.sln
index 9abcb17..0c8feca 100644
--- a/seq-api.sln
+++ b/seq-api.sln
@@ -1,17 +1,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26403.3
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29230.47
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B36BE973-6D5B-4DAA-BF42-F1CF11D6F18E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{1C66E116-DC21-4C8F-833E-A4C2DDF58487}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{233C7E7D-26DE-452A-A415-44F99CBA18E7}"
- ProjectSection(SolutionItems) = preProject
- asset\getseq_net.ico = asset\getseq_net.ico
- EndProjectSection
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{7DCED657-7E83-4E6F-AB14-5753E044304D}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
@@ -69,4 +64,7 @@ Global
{42CEBFBA-208F-40F1-AC95-13F05F6D5412} = {1C66E116-DC21-4C8F-833E-A4C2DDF58487}
{E8CDDE17-8E29-4EB4-A4BB-38BCE346A752} = {1C66E116-DC21-4C8F-833E-A4C2DDF58487}
EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {20BAB483-FB94-4373-8E4C-0F846B6DBFFC}
+ EndGlobalSection
EndGlobal
diff --git a/seq-api.sln.DotSettings b/seq-api.sln.DotSettings
index d50635d..6e98e3d 100644
--- a/seq-api.sln.DotSettings
+++ b/seq-api.sln.DotSettings
@@ -1,2 +1,16 @@
- AD
\ No newline at end of file
+ AD
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+
diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs
index 3afd2c7..f505450 100644
--- a/src/Seq.Api/Client/SeqApiClient.cs
+++ b/src/Seq.Api/Client/SeqApiClient.cs
@@ -1,7 +1,20 @@
-using System;
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@@ -12,23 +25,24 @@
using Seq.Api.Model;
using Seq.Api.Model.Root;
using Seq.Api.Serialization;
-using Tavis.UriTemplates;
using System.Threading;
using Seq.Api.Streams;
using System.Net.WebSockets;
namespace Seq.Api.Client
{
+ ///
+ /// A low-level client that provides navigation over the linked resource structure of the Seq HTTP API.
+ ///
public class SeqApiClient : IDisposable
{
readonly string _apiKey;
// Future versions of Seq may not completely support v1 features, however
// providing this as an Accept header will ensure what compatibility is available
- // can be utilised.
+ // can be utilized.
const string SeqApiV7MediaType = "application/vnd.datalust.seq.v7+json";
- readonly HttpClient _httpClient;
readonly CookieContainer _cookies = new CookieContainer();
readonly JsonSerializer _serializer = JsonSerializer.Create(
new JsonSerializerSettings
@@ -36,6 +50,12 @@ public class SeqApiClient : IDisposable
Converters = { new StringEnumConverter(), new LinkCollectionConverter() }
});
+ ///
+ /// Construct a .
+ ///
+ /// The base URL of the Seq server.
+ /// An API key to use when making requests to the server, if required.
+ /// Whether default credentials will be sent with HTTP requests; the default is true.
public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true)
{
ServerUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));
@@ -53,44 +73,108 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede
if (!baseAddress.EndsWith("/"))
baseAddress += "/";
- _httpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) };
+ HttpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) };
+ HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType));
+
+ if (_apiKey != null)
+ HttpClient.DefaultRequestHeaders.Add("X-Seq-ApiKey", _apiKey);
}
+ ///
+ /// The base URL of the Seq server.
+ ///
public string ServerUrl { get; }
- public HttpClient HttpClient => _httpClient;
-
+ ///
+ /// The HTTP client used when making requests to the Seq server. The HTTP client is configured with the server's base address, collects any cookies
+ /// sent with responses from the API, and will send the appropriate Accept and X-Seq-ApiKey headers by default.
+ ///
+ public HttpClient HttpClient { get; }
+
+ ///
+ /// Get the root entity describing the server and providing links to other resources available from the API.
+ ///
+ /// A supporting cancellation.
+ /// The root entity.
public Task GetRootAsync(CancellationToken cancellationToken = default)
{
return HttpGetAsync("api", cancellationToken);
}
+ ///
+ /// Issue a GET request returning a by following the from .
+ ///
+ /// The entity type into which the response should be deserialized.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// A supporting cancellation.
+ /// The response entity.
public Task GetAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
return HttpGetAsync(linkUri, cancellationToken);
}
+ ///
+ /// Issue a GET request returning a string by following the from .
+ ///
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// A supporting cancellation.
+ /// The response as a string.
public Task GetStringAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
return HttpGetStringAsync(linkUri, cancellationToken);
}
+ ///
+ /// Issue a GET request returning a list of by following the from .
+ ///
+ /// The entity type into which the response items should be deserialized.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// A supporting cancellation.
+ /// The response entities.
public Task> ListAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
return HttpGetAsync>(linkUri, cancellationToken);
}
+ ///
+ /// Issue a POST request accepting a serialized to a from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// A task that signals request completion.
public async Task PostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) };
var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false);
- new StreamReader(stream).ReadToEnd();
+ using (var reader = new StreamReader(stream))
+ reader.ReadToEnd();
}
+ ///
+ /// Issue a POST request accepting a serialized and returning a by following from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// The entity type into which the response should be deserialized.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// The response entity.
public async Task PostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
@@ -99,14 +183,35 @@ public async Task PostAsync(ILinked entity, strin
return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream)));
}
+ ///
+ /// Issue a POST request accepting a serialized and returning a string by following from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// The response string.
public async Task PostReadStringAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) };
var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false);
- return await new StreamReader(stream).ReadToEndAsync();
+ using (var reader = new StreamReader(stream))
+ return await reader.ReadToEndAsync();
}
+ ///
+ /// Issue a POST request accepting a serialized and returning a stream by following from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// The response stream.
public async Task PostReadStreamAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
@@ -114,22 +219,55 @@ public async Task PostReadStreamAsync(ILinked entity, string li
return await HttpSendAsync(request, cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// Issue a PUT request accepting a serialized to a from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// A task that signals request completion.
public async Task PutAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
var request = new HttpRequestMessage(HttpMethod.Put, linkUri) { Content = MakeJsonContent(content) };
var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false);
- new StreamReader(stream).ReadToEnd();
+ using (var reader = new StreamReader(stream))
+ reader.ReadToEnd();
}
+ ///
+ /// Issue a DELETE request accepting a serialized to a from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// A task that signals request completion.
public async Task DeleteAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
var request = new HttpRequestMessage(HttpMethod.Delete, linkUri) { Content = MakeJsonContent(content) };
var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false);
- new StreamReader(stream).ReadToEnd();
+ using (var reader = new StreamReader(stream))
+ reader.ReadToEnd();
}
+ ///
+ /// Issue a DELETE request accepting a serialized to a from .
+ ///
+ /// The entity type that will be serialized into the request payload.
+ /// The entity type into which the response should be deserialized.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// The request content.
+ /// A supporting cancellation.
+ /// The response entity.
public async Task DeleteAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
var linkUri = ResolveLink(entity, link, parameters);
@@ -138,11 +276,28 @@ public async Task DeleteAsync(ILinked entity, str
return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream)));
}
+ ///
+ /// Connect to a websocket at the address specified by following from .
+ ///
+ /// The type of the values received over the websocket.
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// A supporting cancellation.
+ /// A stream of values from the websocket.
public async Task> StreamAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
return await WebSocketStreamAsync(entity, link, parameters, reader => _serializer.Deserialize(new JsonTextReader(reader)), cancellationToken);
}
+ ///
+ /// Connect to a websocket at the address specified by following from .
+ ///
+ /// An entity previously retrieved from the API.
+ /// The name of the outbound link template present in 's collection.
+ /// Named parameters to substitute into the link template, if required.
+ /// A supporting cancellation.
+ /// A stream of raw messages from the websocket.
public async Task> StreamTextAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default)
{
return await WebSocketStreamAsync(entity, link, parameters, reader => reader.ReadToEnd(), cancellationToken);
@@ -173,17 +328,13 @@ async Task HttpGetStringAsync(string url, CancellationToken cancellation
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false);
- return await new StreamReader(stream).ReadToEndAsync();
+ using (var reader = new StreamReader(stream))
+ return await reader.ReadToEndAsync();
}
async Task HttpSendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
- if (_apiKey != null)
- request.Headers.Add("X-Seq-ApiKey", _apiKey);
-
- request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType));
-
- var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
@@ -200,7 +351,7 @@ async Task HttpSendAsync(HttpRequestMessage request, CancellationToken c
if (payload != null && payload.TryGetValue("Error", out var error) && error != null)
throw new SeqApiException($"{(int)response.StatusCode} - {error}", response.StatusCode);
- throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}).", response.StatusCode);
+ throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}/{response.StatusCode}).", response.StatusCode);
}
HttpContent MakeJsonContent(object content)
@@ -212,34 +363,16 @@ HttpContent MakeJsonContent(object content)
static string ResolveLink(ILinked entity, string link, IDictionary parameters = null)
{
- Link linkItem;
- if (!entity.Links.TryGetValue(link, out linkItem))
+ if (!entity.Links.TryGetValue(link, out var linkItem))
throw new NotSupportedException($"The requested link `{link}` isn't available on entity `{entity}`.");
- var expression = linkItem.GetUri();
- var template = new UriTemplate(expression);
- if (parameters != null)
- {
- var missing = parameters.Select(p => p.Key).Except(template.GetParameterNames()).ToArray();
- if (missing.Any())
- throw new ArgumentException($"The URI template `{expression}` does not contain parameter: `{string.Join("`, `", missing)}`.");
-
- foreach (var parameter in parameters)
- {
- var value = parameter.Value is DateTime time
- ? time.ToString("O")
- : parameter.Value;
-
- template.SetParameter(parameter.Key, value);
- }
- }
-
- return template.Resolve();
+ return linkItem.GetUri(parameters);
}
+ /// >
public void Dispose()
{
- _httpClient.Dispose();
+ HttpClient.Dispose();
}
}
}
\ No newline at end of file
diff --git a/src/Seq.Api/Client/SeqApiException.cs b/src/Seq.Api/Client/SeqApiException.cs
index 71d5966..fc53da9 100644
--- a/src/Seq.Api/Client/SeqApiException.cs
+++ b/src/Seq.Api/Client/SeqApiException.cs
@@ -1,4 +1,18 @@
-using System;
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
using System.Net;
namespace Seq.Api.Client
diff --git a/src/Seq.Api/Model/Entity.cs b/src/Seq.Api/Model/Entity.cs
index b31f7c3..7d3a98f 100644
--- a/src/Seq.Api/Model/Entity.cs
+++ b/src/Seq.Api/Model/Entity.cs
@@ -1,14 +1,42 @@
-namespace Seq.Api.Model
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Seq.Api.Model
{
+ ///
+ /// A uniquely-identifiable resource available from the Seq HTTP API.
+ ///
public abstract class Entity : ILinked
{
+ ///
+ /// Construct an .
+ ///
protected Entity()
{
Links = new LinkCollection();
}
+ ///
+ /// The entity's unique identifier. This will be null if a newly-instantiated
+ /// has not yet been sent to the server.
+ ///
public string Id { get; set; }
+ ///
+ /// A collection of outbound links from the entity. This will be empty if the entity
+ /// was instantiated locally and not received from the API.
+ ///
public LinkCollection Links { get; set; }
}
}
diff --git a/src/Seq.Api/Model/ILinked.cs b/src/Seq.Api/Model/ILinked.cs
index df7dc62..ae30187 100644
--- a/src/Seq.Api/Model/ILinked.cs
+++ b/src/Seq.Api/Model/ILinked.cs
@@ -1,7 +1,27 @@
-namespace Seq.Api.Model
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Seq.Api.Model
{
+ ///
+ /// A resource that carries outbound links.
+ ///
public interface ILinked
{
+ ///
+ /// Links associated with the resource.
+ ///
LinkCollection Links { get; }
}
}
\ No newline at end of file
diff --git a/src/Seq.Api/Model/Link.cs b/src/Seq.Api/Model/Link.cs
index b3a77d2..0513d68 100644
--- a/src/Seq.Api/Model/Link.cs
+++ b/src/Seq.Api/Model/Link.cs
@@ -1,50 +1,81 @@
-using System;
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reflection;
using Tavis.UriTemplates;
+// ReSharper disable PossibleMultipleEnumeration
+
namespace Seq.Api.Model
{
- public class Link
+ ///
+ /// A hypermedia link. A link is an RFC 6570 URI template that can be
+ /// parameterized in order to produce a complete URI (if the template contains no
+ /// parameters then it may also be a literal URI).
+ ///
+ public struct Link
{
- readonly bool _isLiteral;
- readonly string _href;
- readonly IDictionary _parameters;
-
- public Link(string href, bool isLiteral = true)
- : this(href, new Dictionary())
- {
- _isLiteral = isLiteral;
- }
+ ///
+ /// An empty link.
+ ///
+ public static Link Empty { get; } = default;
- public Link(string href, IDictionary parameters)
+ ///
+ /// Construct a link.
+ ///
+ /// The URI template.
+ public Link(string template)
{
- if (href == null) throw new ArgumentNullException(nameof(href));
- if (parameters == null) throw new ArgumentNullException(nameof(parameters));
- _href = href;
- _parameters = parameters;
+ Template = template ?? throw new ArgumentNullException(nameof(template));
}
- public Link(string href, object parameters)
- : this(href, parameters
- .GetType()
- .GetTypeInfo()
- .DeclaredProperties
- .ToDictionary(p => p.Name, p => p.GetValue(parameters)))
- {
- }
+ ///
+ /// Get the unprocessed URI template.
+ ///
+ public string Template { get; }
- public string GetUri()
+ ///
+ /// Parameterize the link to construct a URI.
+ ///
+ /// Parameters to substitute into the template, if any.
+ /// A constructed URI.
+ /// This method ensures that templates containing parameters cannot be accidentally
+ /// used as URIs.
+ public string GetUri(IDictionary parameters = null)
{
- if (_isLiteral) return _href;
+ if (Template == null) throw new InvalidOperationException("Attempted to process an empty URI template.");
- var template = new UriTemplate(_href);
- foreach (var parameter in _parameters)
+ var template = new UriTemplate(Template);
+ if (parameters != null)
{
- template.SetParameter(parameter.Key, parameter.Value);
+ var missing = parameters.Select(p => p.Key).Except(template.GetParameterNames());
+ if (missing.Any())
+ throw new ArgumentException($"The URI template `{Template}` does not contain parameter: `{string.Join("`, `", missing)}`.");
+
+ foreach (var parameter in parameters)
+ {
+ var value = parameter.Value is DateTime time
+ ? time.ToString("O")
+ : parameter.Value;
+
+ template.SetParameter(parameter.Key, value);
+ }
}
+
return template.Resolve();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Seq.Api/Model/LinkCollection.cs b/src/Seq.Api/Model/LinkCollection.cs
index e4afbf3..aebab05 100644
--- a/src/Seq.Api/Model/LinkCollection.cs
+++ b/src/Seq.Api/Model/LinkCollection.cs
@@ -1,18 +1,30 @@
-using System;
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Seq.Api.Serialization;
namespace Seq.Api.Model
{
+ ///
+ /// A collection of s indexed by case-insensitive name.
+ ///
[JsonConverter(typeof(LinkCollectionConverter))]
public class LinkCollection : Dictionary
{
public LinkCollection() : base(StringComparer.OrdinalIgnoreCase) { }
-
- public string GetUri(string name)
- {
- return this[name].GetUri();
- }
}
}
diff --git a/src/Seq.Api/Model/ResourceGroup.cs b/src/Seq.Api/Model/ResourceGroup.cs
index f634ddd..3837ca8 100644
--- a/src/Seq.Api/Model/ResourceGroup.cs
+++ b/src/Seq.Api/Model/ResourceGroup.cs
@@ -1,12 +1,34 @@
-namespace Seq.Api.Model
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+namespace Seq.Api.Model
{
+ ///
+ /// A resource group is a logical partitioning of the API, usually
+ /// associated with a particular type of entity.
+ ///
public class ResourceGroup : ILinked
{
+ ///
+ /// Construct a .
+ ///
public ResourceGroup()
{
Links = new LinkCollection();
}
+ ///
public LinkCollection Links { get; set; }
}
}
diff --git a/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs b/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs
index cca68e7..c8b736c 100644
--- a/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs
+++ b/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs
@@ -34,6 +34,8 @@ public async Task FindAsync(
if (id == null) throw new ArgumentNullException(nameof(id));
var parameters = new Dictionary {{"id", id}};
+ if (render) parameters.Add("render", true);
+ if (permalinkId != null) parameters.Add("permalinkId", permalinkId);
return await GroupGetAsync("Item", parameters, cancellationToken).ConfigureAwait(false);
}
diff --git a/src/Seq.Api/Seq.Api.csproj b/src/Seq.Api/Seq.Api.csproj
index 677b359..11f7d45 100644
--- a/src/Seq.Api/Seq.Api.csproj
+++ b/src/Seq.Api/Seq.Api.csproj
@@ -1,24 +1,21 @@
-
+
Client library for the Seq HTTP API.
- 5.1.0
+ 5.1.1
Datalust;Contributors
netstandard2.0
$(NoWarn);CS1591
true
true
- Seq.Api
- Seq.Api
seq
Copyright © 2014-2019 Datalust Pty Ltd and Contributors
https://datalust.co/images/seq-nuget.png
https://github.com/datalust/seq-api
- http://www.apache.org/licenses/LICENSE-2.0
- Seq.Api
+ Apache-2.0
7.3
-
\ No newline at end of file
+
diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs
index 6d3e722..900968b 100644
--- a/src/Seq.Api/SeqConnection.cs
+++ b/src/Seq.Api/SeqConnection.cs
@@ -1,5 +1,20 @@
-using System;
-using System.Collections.Concurrent;
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Seq.Api.Client;
@@ -9,67 +24,201 @@
namespace Seq.Api
{
+ ///
+ /// Exposes high-level (typed) interactions with the Seq API through various resource groups.
+ ///
public class SeqConnection : ISeqConnection
{
- readonly ConcurrentDictionary> _resourceGroups = new ConcurrentDictionary>();
- readonly Lazy> _root;
-
+ readonly object _sync = new object();
+ readonly Dictionary> _resourceGroups = new Dictionary>();
+ Task _root;
+
+ ///
+ /// Construct a .
+ ///
+ /// The base URL of the Seq server.
+ /// An API key to use when making requests to the server, if required.
+ /// Whether default credentials will be sent with HTTP requests; the default is true.
public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true)
{
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials);
-
- _root = new Lazy>(() => Client.GetRootAsync());
}
+
+ ///
+ /// Access the lower-level that can be used for resource-oriented navigation through
+ /// the HTTP API.
+ ///
+ public SeqApiClient Client { get; }
+ ///
+ /// Perform operations on API keys.
+ ///
public ApiKeysResourceGroup ApiKeys => new ApiKeysResourceGroup(this);
+ ///
+ /// Perform operations on Seq app instances.
+ ///
public AppInstancesResourceGroup AppInstances => new AppInstancesResourceGroup(this);
+ ///
+ /// Perform operations on Seq app packages.
+ ///
public AppsResourceGroup Apps => new AppsResourceGroup(this);
+ ///
+ /// Perform operations on backups.
+ ///
public BackupsResourceGroup Backups => new BackupsResourceGroup(this);
+ ///
+ /// Perform operations on dashboards.
+ ///
public DashboardsResourceGroup Dashboards => new DashboardsResourceGroup(this);
+ ///
+ /// Execute SQL-style queries against the API.
+ ///
public DataResourceGroup Data => new DataResourceGroup(this);
+ ///
+ /// Access server diagnostics.
+ ///
public DiagnosticsResourceGroup Diagnostics => new DiagnosticsResourceGroup(this);
+ ///
+ /// Read and subscribe to events from the event store.
+ ///
public EventsResourceGroup Events => new EventsResourceGroup(this);
+ ///
+ /// Perform operations on queries and filter expressions.
+ ///
public ExpressionsResourceGroup Expressions => new ExpressionsResourceGroup(this);
+ ///
+ /// Perform operations on NuGet feeds.
+ ///
public FeedsResourceGroup Feeds => new FeedsResourceGroup(this);
+ ///
+ /// Perform operations on the Seq license certificate.
+ ///
public LicensesResourceGroup Licenses => new LicensesResourceGroup(this);
+ ///
+ /// Perform operations on permalinked events.
+ ///
public PermalinksResourceGroup Permalinks => new PermalinksResourceGroup(this);
+ ///
+ /// Perform operations on retention policies.
+ ///
public RetentionPoliciesResourceGroup RetentionPolicies => new RetentionPoliciesResourceGroup(this);
+ ///
+ /// Perform operations on system settings.
+ ///
public SettingsResourceGroup Settings => new SettingsResourceGroup(this);
+ ///
+ /// Perform operations on signals.
+ ///
public SignalsResourceGroup Signals => new SignalsResourceGroup(this);
+ ///
+ /// Perform operations on saved SQL queries.
+ ///
public SqlQueriesResourceGroup SqlQueries => new SqlQueriesResourceGroup(this);
+ ///
+ /// Perform operations on known available Seq versions.
+ ///
public UpdatesResourceGroup Updates => new UpdatesResourceGroup(this);
+ ///
+ /// Perform operations on users.
+ ///
public UsersResourceGroup Users => new UsersResourceGroup(this);
+ ///
+ /// Perform operations on workspaces.
+ ///
public WorkspacesResourceGroup Workspaces => new WorkspacesResourceGroup(this);
- public async Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default)
+ ///
+ /// Check that the Seq API is available. If the initial attempt fails (i.e. the server is starting up),
+ /// the method will try every 100 milliseconds until is reached.
+ ///
+ /// The maximum amount of time to retry until giving up.
+ /// A task that will complete if the API could be reached, or fault otherwise.
+ public async Task EnsureConnected(TimeSpan timeout)
{
- return await _resourceGroups.GetOrAdd(name, s => ResourceGroupFactory(s, cancellationToken)).ConfigureAwait(false);
+ var started = DateTime.UtcNow;
+ // Fractional milliseconds are lost here, but that's fine.
+ var wait = TimeSpan.FromMilliseconds(Math.Min(100, timeout.TotalMilliseconds));
+ var deadline = started.Add(timeout);
+ while (!await ConnectAsync(DateTime.UtcNow > deadline))
+ {
+ await Task.Delay(wait);
+ }
+ }
+
+ async Task ConnectAsync(bool throwOnFailure)
+ {
+ HttpStatusCode statusCode;
+
+ try
+ {
+ statusCode = (await Client.HttpClient.GetAsync("api")).StatusCode;
+ }
+ catch
+ {
+ if (throwOnFailure)
+ throw;
+
+ return false;
+ }
+
+ if (statusCode == HttpStatusCode.OK)
+ return true;
+
+ if (!throwOnFailure)
+ return false;
+
+ throw new SeqApiException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}.", statusCode);
}
- async Task ResourceGroupFactory(string name, CancellationToken cancellationToken = default)
+ async Task ISeqConnection.LoadResourceGroupAsync(string name, CancellationToken cancellationToken)
{
- return await Client.GetAsync(await _root.Value, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false);
+ Task loadRoot;
+ lock (_sync)
+ {
+ if (_root == null || _root.IsFaulted || _root.IsCanceled)
+ _root = Client.GetRootAsync(cancellationToken);
+
+ loadRoot = _root;
+ }
+
+ var rootEntity = await loadRoot.ConfigureAwait(false);
+
+ Task loadGroup;
+ lock (_sync)
+ {
+ // ReSharper disable once InvertIf
+ if (!_resourceGroups.TryGetValue(name, out loadGroup) || loadGroup.IsFaulted || loadGroup.IsCanceled)
+ {
+ loadGroup = GetResourceGroup(rootEntity, name, cancellationToken);
+ _resourceGroups.Add(name, loadGroup);
+ }
+ }
+
+ return await loadGroup.ConfigureAwait(false);
}
- public SeqApiClient Client { get; }
+ async Task GetResourceGroup(RootEntity root, string name, CancellationToken cancellationToken)
+ {
+ return await Client.GetAsync(root, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
}
}
diff --git a/src/Seq.Api/Serialization/LinkCollectionConverter.cs b/src/Seq.Api/Serialization/LinkCollectionConverter.cs
index 01dc797..c560bed 100644
--- a/src/Seq.Api/Serialization/LinkCollectionConverter.cs
+++ b/src/Seq.Api/Serialization/LinkCollectionConverter.cs
@@ -1,4 +1,18 @@
-using System;
+// Copyright 2014-2019 Datalust and contributors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
@@ -6,12 +20,12 @@
namespace Seq.Api.Serialization
{
- public class LinkCollectionConverter : JsonConverter
+ class LinkCollectionConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var lc = (LinkCollection)value;
- var dictionary = lc.ToDictionary(kv => kv.Key, kv => kv.Value.GetUri());
+ var dictionary = lc.ToDictionary(kv => kv.Key, kv => kv.Value.Template);
serializer.Serialize(writer, dictionary);
}
diff --git a/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs b/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs
index e4380e3..f7f510b 100644
--- a/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs
+++ b/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs
@@ -12,6 +12,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
+
using System;
namespace Seq.Api.Streams
@@ -25,10 +26,8 @@ sealed class Unsubscriber : IDisposable
public Unsubscriber(ObservableStream sink, IObserver observer)
{
- if (sink == null) throw new ArgumentNullException(nameof(sink));
- if (observer == null) throw new ArgumentNullException(nameof(observer));
- _stream = sink;
- _observer = observer;
+ _stream = sink ?? throw new ArgumentNullException(nameof(sink));
+ _observer = observer ?? throw new ArgumentNullException(nameof(observer));
}
public void Dispose()
diff --git a/src/Seq.Api/Streams/ObservableStream.cs b/src/Seq.Api/Streams/ObservableStream.cs
index 0ddc419..2af4853 100644
--- a/src/Seq.Api/Streams/ObservableStream.cs
+++ b/src/Seq.Api/Streams/ObservableStream.cs
@@ -24,8 +24,10 @@
namespace Seq.Api.Streams
{
- // Some questionable synchronization in this one; probably better to take the Rx dependency
- // and use the Rx support classes here.
+ ///
+ /// A stream of values read from a .
+ ///
+ /// The type of the values.
public sealed partial class ObservableStream : IObservable, IDisposable
{
readonly object _syncRoot = new object();
@@ -42,6 +44,7 @@ internal ObservableStream(ClientWebSocket socket, Func deserializ
_socket = socket ?? throw new ArgumentNullException(nameof(socket));
}
+ ///
public IDisposable Subscribe(IObserver observer)
{
if (observer == null) throw new ArgumentNullException(nameof(observer));
@@ -182,6 +185,7 @@ static void OnError(IEnumerable exceptions)
throw new AggregateException("At least one observer failed to accept the event", exceptions);
}
+ ///
public void Dispose()
{
lock (_syncRoot)
diff --git a/test/Seq.Api.Tests/LinkTests.cs b/test/Seq.Api.Tests/LinkTests.cs
new file mode 100644
index 0000000..ae6a636
--- /dev/null
+++ b/test/Seq.Api.Tests/LinkTests.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using Seq.Api.Model;
+using Xunit;
+
+namespace Seq.Api.Tests
+{
+ public class LinkTests
+ {
+ [Fact]
+ public void ALinkWithNoParametersIsLiteral()
+ {
+ const string uri = "https://example.com";
+ var link = new Link(uri);
+ var constructed = link.GetUri();
+ Assert.Equal(uri, constructed);
+ }
+
+ [Fact]
+ public void AParameterizedLinkCanBeConstructed()
+ {
+ const string template = "https://example.com/{name}";
+ var link = new Link(template);
+ var constructed = link.GetUri(new Dictionary {["name"] = "test"});
+ Assert.Equal("https://example.com/test", constructed);
+ }
+
+ [Fact]
+ public void InvalidParametersAreDetected()
+ {
+ const string template = "https://example.com";
+ var link = new Link(template);
+ Assert.Throws(() => link.GetUri(new Dictionary {["name"] = "test"}));
+ }
+ }
+}