diff --git a/Keen.NET.Test/DelegatingHandlerMock.cs b/Keen.NET.Test/DelegatingHandlerMock.cs
new file mode 100644
index 0000000..288e7f1
--- /dev/null
+++ b/Keen.NET.Test/DelegatingHandlerMock.cs
@@ -0,0 +1,29 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// Wraps an and allows for using it as a DelegatingHandler.
+ /// If the IHttpMessageHandler doesn't already have a default action set, we'll have it call
+ /// our own base SendAsync() which will forward the request down the handler chain.
+ ///
+ internal class DelegatingHandlerMock : DelegatingHandler
+ {
+ private readonly IHttpMessageHandler _handler;
+
+ internal DelegatingHandlerMock(IHttpMessageHandler handler)
+ {
+ _handler = handler;
+ _handler.DefaultAsync = (_handler.DefaultAsync ?? base.SendAsync);
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ return _handler.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/Keen.NET.Test/EventCollectionMock.cs b/Keen.NET.Test/EventCollectionMock.cs
index 8459535..1b4b035 100644
--- a/Keen.NET.Test/EventCollectionMock.cs
+++ b/Keen.NET.Test/EventCollectionMock.cs
@@ -1,7 +1,8 @@
-using System;
-using System.Threading.Tasks;
-using Keen.Core;
+using Keen.Core;
using Newtonsoft.Json.Linq;
+using System;
+using System.Threading.Tasks;
+
namespace Keen.Net.Test
{
diff --git a/Keen.NET.Test/FuncHandler.cs b/Keen.NET.Test/FuncHandler.cs
new file mode 100644
index 0000000..8ce734d
--- /dev/null
+++ b/Keen.NET.Test/FuncHandler.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// An that has pre/post/default message handlers functors,
+ /// as well as a Func<> that produces the actual HttpResponseMessage. These can all be set by
+ /// test code and will be called if available. There are defaults in place that essentially do
+ /// nothing, but client code should make sure DefaultAsync gets set, either by a wrapper or
+ /// explicitly.
+ ///
+ internal class FuncHandler : IHttpMessageHandler
+ {
+ internal Action PreProcess = (request, ct) => { };
+
+ internal Func> ProduceResultAsync =
+ (request, ct) => Task.FromResult(null);
+
+ internal Func PostProcess = (request, response) => response;
+
+ internal bool DeferToDefault { get; set; } = true;
+
+ public Func> DefaultAsync { get; set; }
+
+
+ public async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ PreProcess(request, cancellationToken);
+ HttpResponseMessage response =
+ await ProduceResultAsync(request, cancellationToken).ConfigureAwait(false);
+
+ // Pass it along down the line if we didn't create a result already.
+ if (null == response)
+ {
+ if (DeferToDefault)
+ {
+ response = await DefaultAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // Create a dummy successful response so HttpClient doesn't just throw always.
+ response = await HttpTests.CreateJsonStringResponseAsync(new { dummy = "" })
+ .ConfigureAwait(false);
+ }
+ }
+
+ PostProcess(request, response);
+
+ return response;
+ }
+ }
+}
diff --git a/Keen.NET.Test/FunnelTest.cs b/Keen.NET.Test/FunnelTest.cs
index 66c934c..6f4c96d 100644
--- a/Keen.NET.Test/FunnelTest.cs
+++ b/Keen.NET.Test/FunnelTest.cs
@@ -1,11 +1,12 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Keen.Core;
+using Keen.Core;
using Keen.Core.Query;
using Moq;
using NUnit.Framework;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
namespace Keen.Net.Test
{
@@ -52,6 +53,7 @@ public override void Setup()
public async Task Funnel_Simple_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
IEnumerable funnelsteps = new List
{
@@ -83,14 +85,13 @@ public async Task Funnel_Simple_Success()
Result = new[] { 3, 2 }
};
-
Mock queryMock = null;
if (UseMocks)
{
queryMock = new Mock();
queryMock.Setup(m => m.Funnel(
It.Is>(f => f.Equals(funnelsteps)),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is(t => t == "")
))
.Returns(Task.FromResult(expected));
@@ -98,7 +99,7 @@ public async Task Funnel_Simple_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryFunnelAsync(funnelsteps));
+ var reply = (await client.QueryFunnelAsync(funnelsteps, timeframe));
Assert.NotNull(reply);
Assert.NotNull(reply.Result);
Assert.NotNull(reply.Steps);
@@ -112,6 +113,7 @@ public async Task Funnel_Simple_Success()
public async Task Funnel_Inverted_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
IEnumerable funnelsteps = new List
{
@@ -144,14 +146,13 @@ public async Task Funnel_Inverted_Success()
Result = new [] { 3, 1 }
};
-
Mock queryMock = null;
if (UseMocks)
{
queryMock = new Mock();
queryMock.Setup(m => m.Funnel(
It.Is>(f => f.Equals(funnelsteps)),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is(t => t == "")
))
.Returns(Task.FromResult(expected));
@@ -159,7 +160,7 @@ public async Task Funnel_Inverted_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryFunnelAsync(funnelsteps));
+ var reply = (await client.QueryFunnelAsync(funnelsteps, timeframe));
Assert.NotNull(reply);
Assert.NotNull(reply.Result);
Assert.True(reply.Result.SequenceEqual(expected.Result));
@@ -174,6 +175,7 @@ public async Task Funnel_Inverted_Success()
public async Task Funnel_Optional_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
IEnumerable funnelsteps = new []
{
@@ -215,14 +217,13 @@ public async Task Funnel_Optional_Success()
Result = new [] { 3, 2, 1 }
};
-
Mock queryMock = null;
if (UseMocks)
{
queryMock = new Mock();
queryMock.Setup(m => m.Funnel(
It.Is>(f => f.Equals(funnelsteps)),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is(t => t == "")
))
.Returns(Task.FromResult(expected));
@@ -230,7 +231,7 @@ public async Task Funnel_Optional_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryFunnelAsync(funnelsteps));
+ var reply = (await client.QueryFunnelAsync(funnelsteps, timeframe));
Assert.NotNull(reply);
Assert.NotNull(reply.Result);
Assert.True(reply.Result.SequenceEqual(expected.Result));
@@ -245,6 +246,7 @@ public async Task Funnel_Optional_Success()
public async Task Funnel_ValidFilter_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
var filters = new List { new QueryFilter("id", QueryFilter.FilterOperator.GreaterThanOrEqual(), 0) };
IEnumerable funnelsteps = new []
@@ -279,14 +281,13 @@ public async Task Funnel_ValidFilter_Success()
Result = new [] { 3, 2 }
};
-
Mock queryMock = null;
if (UseMocks)
{
queryMock = new Mock();
queryMock.Setup(m => m.Funnel(
It.Is>(f => f.Equals(funnelsteps)),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is(t => t == "")
))
.Returns(Task.FromResult(expected));
@@ -294,7 +295,7 @@ public async Task Funnel_ValidFilter_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryFunnelAsync(funnelsteps));
+ var reply = (await client.QueryFunnelAsync(funnelsteps, timeframe));
Assert.NotNull(reply);
Assert.NotNull(reply.Result);
Assert.True(reply.Result.SequenceEqual(expected.Result));
@@ -309,20 +310,21 @@ public async Task Funnel_ValidFilter_Success()
public async Task Funnel_ValidTimeframe_Success()
{
var client = new KeenClient(SettingsEnv);
- var timeframe = QueryRelativeTimeframe.PreviousHour();
+ var timeframe = QueryRelativeTimeframe.ThisHour();
IEnumerable funnelsteps = new []
{
new FunnelStep
{
- EventCollection = FunnelColA,
+ EventCollection = FunnelColA,
ActorProperty = "id",
- Timeframe = timeframe,
+ Timeframe = timeframe, // Issue #50 : These are ignored.
},
new FunnelStep
{
- EventCollection = FunnelColB,
- ActorProperty = "id"
+ EventCollection = FunnelColB,
+ ActorProperty = "id",
+ Timeframe = timeframe,
},
};
@@ -332,24 +334,23 @@ public async Task Funnel_ValidTimeframe_Success()
{
new FunnelResultStep
{
- EventCollection = FunnelColA,
+ EventCollection = FunnelColA,
},
new FunnelResultStep
{
- EventCollection = FunnelColB,
+ EventCollection = FunnelColB,
},
},
Result = new [] { 3, 2 }
};
-
Mock queryMock = null;
if (UseMocks)
{
queryMock = new Mock();
queryMock.Setup(m => m.Funnel(
It.Is>(f => f.Equals(funnelsteps)),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is(t => t == "")
))
.Returns(Task.FromResult(expected));
@@ -357,7 +358,7 @@ public async Task Funnel_ValidTimeframe_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryFunnelAsync(funnelsteps));
+ var reply = (await client.QueryFunnelAsync(funnelsteps, timeframe));
Assert.NotNull(reply);
Assert.NotNull(reply.Result);
Assert.True(reply.Result.SequenceEqual(expected.Result));
@@ -372,6 +373,7 @@ public async Task Funnel_ValidTimeframe_Success()
public async Task Funnel_WithActors_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
IEnumerable funnelsteps = new []
{
@@ -413,7 +415,7 @@ public async Task Funnel_WithActors_Success()
queryMock = new Mock();
queryMock.Setup(m => m.Funnel(
It.Is>(f => f.Equals(funnelsteps)),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is(t => t == "")
))
.Returns(Task.FromResult(expected));
@@ -421,7 +423,7 @@ public async Task Funnel_WithActors_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryFunnelAsync(funnelsteps));
+ var reply = (await client.QueryFunnelAsync(funnelsteps, timeframe));
Assert.NotNull(reply);
Assert.NotNull(reply.Actors);
Assert.AreEqual(reply.Actors.Count(), 2);
diff --git a/Keen.NET.Test/HttpClientHandlerMock.cs b/Keen.NET.Test/HttpClientHandlerMock.cs
new file mode 100644
index 0000000..06b4f09
--- /dev/null
+++ b/Keen.NET.Test/HttpClientHandlerMock.cs
@@ -0,0 +1,31 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// Wraps an and allows for using it as an HttpClientHandler.
+ /// If the IHttpMessageHandler doesn't already have a default action set, we'll have it call
+ /// our own base SendAsync() which will forward the request to the actual HttpClientHandler
+ /// implementation, with all the configuration and proxies and such, which may actually go out
+ /// over the network.
+ ///
+ internal class HttpClientHandlerMock : HttpClientHandler
+ {
+ internal readonly IHttpMessageHandler _handler;
+
+ internal HttpClientHandlerMock(IHttpMessageHandler handler)
+ {
+ _handler = handler;
+ _handler.DefaultAsync = (_handler.DefaultAsync ?? base.SendAsync);
+ }
+
+ protected override Task SendAsync(HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ return _handler.SendAsync(request, cancellationToken);
+ }
+ }
+}
diff --git a/Keen.NET.Test/HttpTests.cs b/Keen.NET.Test/HttpTests.cs
new file mode 100644
index 0000000..25f5205
--- /dev/null
+++ b/Keen.NET.Test/HttpTests.cs
@@ -0,0 +1,155 @@
+using Keen.Core;
+using Keen.Core.Query;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ [TestFixture]
+ internal class HttpTests : TestBase
+ {
+ [Test]
+ public void GetSdkVersion_Success()
+ {
+ string sdkVersion = KeenUtil.GetSdkVersion();
+
+ Assert.IsNotNull(sdkVersion);
+ Assert.IsNotEmpty(sdkVersion);
+ Assert.IsTrue(sdkVersion.StartsWith(".net"));
+ }
+
+ [Test]
+ public async Task DefaultHeaders_Success()
+ {
+ object responseData = new[] { new { result = 2 } };
+
+ var handler = new FuncHandler()
+ {
+ PreProcess = (req, ct) =>
+ {
+ // Make sure the default headers are in place
+ Assert.IsTrue(req.Headers.Contains("Keen-Sdk"));
+ Assert.AreEqual(KeenUtil.GetSdkVersion(), req.Headers.GetValues("Keen-Sdk").Single());
+
+ Assert.IsTrue(req.Headers.Contains("Authorization"));
+
+ var key = req.Headers.GetValues("Authorization").Single();
+ Assert.IsTrue(SettingsEnv.ReadKey == key ||
+ SettingsEnv.WriteKey == key ||
+ SettingsEnv.MasterKey == key);
+ },
+ ProduceResultAsync = (req, ct) =>
+ {
+ return CreateJsonStringResponseAsync(responseData);
+ },
+ DeferToDefault = false
+ };
+
+ var client = new KeenClient(SettingsEnv, new TestKeenHttpClientProvider()
+ {
+ ProvideKeenHttpClient =
+ (url) => KeenHttpClientFactory.Create(url,
+ new HttpClientCache(),
+ null,
+ new DelegatingHandlerMock(handler))
+ });
+
+ // Try out all the endpoints
+ Assert.DoesNotThrow(() => client.GetSchemas());
+
+ // Remaining operations expect an object, not an array of objects
+ responseData = new { result = 2 };
+
+ var @event = new { AProperty = "AValue" };
+ Assert.DoesNotThrow(() => client.AddEvent("AddEventTest", @event));
+ Assert.DoesNotThrow(() => client.AddEvents("AddEventTest", new[] { @event }));
+
+ Assert.DoesNotThrow(() => client.DeleteCollection("DeleteColTest"));
+ Assert.IsNotNull(client.GetSchema("AddEventTest"));
+
+ // Currently all the queries/extraction go through the same KeenWebApiRequest() call.
+ var count = await client.QueryAsync(
+ QueryType.Count(),
+ "testCollection",
+ "",
+ QueryRelativeTimeframe.ThisMonth());
+
+ Assert.IsNotNull(count);
+ Assert.AreEqual("2", count);
+ }
+
+ internal static Task CreateJsonStringResponseAsync(object data)
+ {
+ return HttpTests.CreateJsonStringResponseAsync(data, HttpStatusCode.OK);
+ }
+
+ internal static Task CreateJsonStringResponseAsync(
+ object data,
+ HttpStatusCode statusCode)
+ {
+ HttpResponseMessage mockResponse = new HttpResponseMessage(statusCode);
+ var dataStr = (data is Array ? JArray.FromObject(data).ToString(Formatting.None) :
+ JObject.FromObject(data).ToString(Formatting.None));
+ var content = new StringContent(dataStr);
+ content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
+ mockResponse.Content = content;
+
+ return Task.FromResult(mockResponse);
+ }
+
+ // For communicating errors in mock responses.
+ internal static Task CreateJsonStringResponseAsync(
+ HttpStatusCode statusCode,
+ string message,
+ string errorCode)
+ {
+ var content = new StringContent(JObject.FromObject(
+ // Matches what the server returns and KeenUtil.CheckApiErrorCode() expects.
+ new
+ {
+ message = message,
+ error_code = errorCode
+ }).ToString(Formatting.None));
+ content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
+
+ HttpResponseMessage mockResponse = new HttpResponseMessage(statusCode)
+ {
+ Content = content
+ };
+
+ return Task.FromResult(mockResponse);
+ }
+
+ internal static Uri GetUriForResource(IProjectSettings projectSettings, string resource)
+ {
+ string keenUrl = projectSettings.KeenUrl;
+ string projectId = projectSettings.ProjectId;
+
+ return new Uri($"{keenUrl}projects/{projectId}/{resource}");
+ }
+
+ internal static Task ValidateRequest(HttpRequestMessage request, string expectedRequestBody)
+ {
+ return HttpTests.ValidateRequest(request, JObject.Parse(expectedRequestBody));
+ }
+
+ internal static async Task ValidateRequest(HttpRequestMessage request, JObject expectedRequestJson)
+ {
+ // Request should have a body
+ Assert.NotNull(request.Content);
+
+ string requestBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+ var requestJson = JObject.Parse(requestBody);
+
+ Assert.IsTrue(JToken.DeepEquals(expectedRequestJson, requestJson));
+ }
+ }
+}
diff --git a/Keen.NET.Test/IHttpMessageHandler.cs b/Keen.NET.Test/IHttpMessageHandler.cs
new file mode 100644
index 0000000..60e16ca
--- /dev/null
+++ b/Keen.NET.Test/IHttpMessageHandler.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// Represents the main functionality needed to override both HttpClientHandler and
+ /// DelegatingHandler. This can be useful for implementing test code in pass-through fakes
+ /// where we want to alter some behavior, but let the rest execute normally. If the test just
+ /// tests/mutates and forwards to another handler, it can implement this interface and be used
+ /// in place of either type of handler in tests.
+ ///
+ /// It's a bad idea to reuse instances of this type, since the wrappers as well as the
+ /// HttpClient and pipeline code mess with their properties. Weirdness ensues, so create a
+ /// fresh instance every time at the point where you create the wrapper or stick it in the
+ /// pipeline, and don't store a reference to it unless it's needed to check later in an
+ /// assert or some verification logic, but generally don't reuse it.
+ ///
+ internal interface IHttpMessageHandler
+ {
+ Task SendAsync(HttpRequestMessage request,
+ CancellationToken cancellationToken);
+
+ Func> DefaultAsync { get; set; }
+ }
+}
diff --git a/Keen.NET.Test/Keen.NET.Test.csproj b/Keen.NET.Test/Keen.NET.Test.csproj
index aa0eecc..0710a75 100644
--- a/Keen.NET.Test/Keen.NET.Test.csproj
+++ b/Keen.NET.Test/Keen.NET.Test.csproj
@@ -93,12 +93,20 @@
+
+
+
+
+
+
+
+
diff --git a/Keen.NET.Test/KeenClientTest.cs b/Keen.NET.Test/KeenClientTest.cs
index 0015f76..d00b2fa 100644
--- a/Keen.NET.Test/KeenClientTest.cs
+++ b/Keen.NET.Test/KeenClientTest.cs
@@ -1,15 +1,14 @@
-using System;
+using Keen.Core;
+using Keen.Core.EventCache;
+using Moq;
+using Newtonsoft.Json.Linq;
+using NUnit.Framework;
+using System;
using System.Collections.Generic;
+using System.Dynamic;
using System.Linq;
using System.Threading.Tasks;
-using Keen.Core.Query;
-using Moq;
-using NUnit.Framework;
-using Keen.Core;
-using System.Dynamic;
-using Newtonsoft.Json.Linq;
-using Keen.Core.EventCache;
namespace Keen.Net.Test
{
@@ -213,7 +212,8 @@ public void GetCollectionSchemas_Success()
[Test]
public void GetCollectionSchema_InvalidProjectId_Throws()
{
- var settings = new ProjectSettingsProvider(projectId: "X", masterKey: SettingsEnv.MasterKey);
+ var settings = new ProjectSettingsProvider(projectId: "X",
+ readKey: SettingsEnv.ReadKey);
var client = new KeenClient(settings);
if (UseMocks)
client.EventCollection = new EventCollectionMock(settings,
@@ -489,17 +489,6 @@ public void DeleteCollection_Success()
// Idempotent, does not matter if collection does not exist.
Assert.DoesNotThrow(() => client.DeleteCollection("DeleteColTest"));
}
-
- [Test]
- public void GetSdkVersion_Success()
- {
- // TODO : Decide on a better place for this when we break out tests and do a CC push.
- string sdkVersion = KeenUtil.GetSdkVersion();
-
- Assert.IsNotNull(sdkVersion);
- Assert.IsNotEmpty(sdkVersion);
- Assert.IsTrue(sdkVersion.StartsWith(".net"));
- }
}
[TestFixture]
@@ -849,6 +838,7 @@ public class AsyncTests : TestBase
public async Task Async_DeleteCollection_Success()
{
var client = new KeenClient(SettingsEnv);
+
if (UseMocks)
client.EventCollection = new EventCollectionMock(SettingsEnv,
deleteCollection: new Action((c, p) =>
@@ -898,7 +888,7 @@ public void Async_AddEvent_ValidProjectIdInvalidWriteKey_Throws()
throw new KeenInvalidApiKeyException(c);
}));
- Assert.ThrowsAsync(() => client.AddEventAsync("AddEventTest", new { AProperty = "Value" }));
+ Assert.ThrowsAsync(() => client.AddEventAsync("AddEventTest", new { AProperty = "Value" }));
}
[Test]
diff --git a/Keen.NET.Test/QueryTest.cs b/Keen.NET.Test/QueryTest.cs
index d341b5c..810ca11 100644
--- a/Keen.NET.Test/QueryTest.cs
+++ b/Keen.NET.Test/QueryTest.cs
@@ -1,16 +1,15 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
+using Keen.Core;
+using Keen.Core.Query;
using Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
-using Keen.Core;
-using Keen.Core.Query;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Dynamic;
+using System.Linq;
+using System.Threading.Tasks;
+
namespace Keen.Net.Test
{
@@ -48,7 +47,7 @@ public void ReadKeyOnly_Success()
{
// Server is required for this test
// Also, test depends on existance of collection "AddEventTest"
- Assert.DoesNotThrow(() => client.Query(QueryType.Count(), "AddEventTest", ""));
+ Assert.DoesNotThrow(() => client.Query(QueryType.Count(), "AddEventTest", "", QueryRelativeTimeframe.ThisHour()));
}
}
@@ -72,7 +71,7 @@ public async Task AvailableQueries_Success()
queryMock = new Mock();
queryMock.Setup(m=>m.AvailableQueries())
.Returns(Task.FromResult(testResult));
-
+
client.Queries = queryMock.Object;
}
@@ -231,7 +230,6 @@ public async Task Query_ValidRelativeGroupInterval_Success()
}
}
-
[Test]
public async Task Query_ValidAbsoluteInterval_Success()
{
@@ -334,6 +332,7 @@ public async Task Query_ValidFilter_Success()
{
var client = new KeenClient(SettingsEnv);
var filters = new List(){ new QueryFilter("field1", QueryFilter.FilterOperator.GreaterThan(), "1") };
+ var timeframe = QueryRelativeTimeframe.ThisHour();
Mock queryMock = null;
if (UseMocks)
@@ -343,7 +342,7 @@ public async Task Query_ValidFilter_Success()
It.Is(q => q == QueryType.Count()),
It.Is(c => c == testCol),
It.Is(p => p == ""),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is>(f => f == filters),
It.Is(z => z == "")))
.Returns(Task.FromResult("1"));
@@ -351,7 +350,7 @@ public async Task Query_ValidFilter_Success()
client.Queries = queryMock.Object;
}
- var count = await client.QueryAsync(QueryType.Count(), testCol, "", null, filters);
+ var count = await client.QueryAsync(QueryType.Count(), testCol, "", timeframe, filters);
Assert.IsNotNull(count);
if (null != queryMock)
@@ -360,7 +359,6 @@ public async Task Query_ValidFilter_Success()
}
}
-
[Test]
public async Task CountUnique_ValidAbsolute_Success()
{
@@ -423,7 +421,6 @@ public async Task Minimum_ValidAbsolute_Success()
queryMock.VerifyAll();
}
-
[Test]
public async Task Maximum_ValidAbsolute_Success()
{
@@ -485,8 +482,6 @@ public async Task Average_ValidAbsolute_Success()
queryMock.VerifyAll();
}
-
-
[Test]
public async Task Sum_ValidAbsolute_Success()
{
@@ -621,6 +616,7 @@ public async Task SelectUnique_ValidFilter_Success()
{
var client = new KeenClient(SettingsEnv);
var prop = "field1";
+ var timeframe = QueryRelativeTimeframe.ThisHour();
var filters = new List() { new QueryFilter("field1", QueryFilter.FilterOperator.GreaterThan(), "1") };
var result = "hello,goodbye,I'm late";
@@ -632,7 +628,7 @@ public async Task SelectUnique_ValidFilter_Success()
It.Is(q => q == QueryType.SelectUnique()),
It.Is(c => c == testCol),
It.Is(p => p == prop),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is>(f => f == filters),
It.Is(t => t == "")
))
@@ -641,7 +637,7 @@ public async Task SelectUnique_ValidFilter_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryAsync(QueryType.SelectUnique(), testCol, prop, null, filters)).ToList();
+ var reply = (await client.QueryAsync(QueryType.SelectUnique(), testCol, prop, timeframe, filters)).ToList();
if (null != queryMock)
queryMock.VerifyAll();
@@ -736,7 +732,6 @@ public async Task SelectUnique_ValidAbsoluteIntervalGroup_Success()
queryMock.VerifyAll();
}
-
[Test]
public async Task SelectUnique_ValidRelativeInterval_Success()
{
@@ -772,9 +767,6 @@ public async Task SelectUnique_ValidRelativeInterval_Success()
queryMock.VerifyAll();
}
-
-
-
[Test]
public async Task ExtractResource_ValidAbsolute_Success()
{
@@ -841,6 +833,7 @@ public async Task ExtractResource_ValidRelative_Success()
public async Task ExtractResource_ValidFilter_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
var filters = new List() { new QueryFilter("field1", QueryFilter.FilterOperator.GreaterThan(), "1") };
dynamic eo = new ExpandoObject();
eo.field1 = "8888";
@@ -852,7 +845,7 @@ public async Task ExtractResource_ValidFilter_Success()
queryMock = new Mock();
queryMock.Setup(m => m.Extract(
It.Is(c => c == testCol),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is>(f => f == filters),
It.Is(l => l == 0),
It.Is(t => t == "")
@@ -862,17 +855,18 @@ public async Task ExtractResource_ValidFilter_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryExtractResourceAsync(testCol, null, filters)).ToList();
+ var reply = (await client.QueryExtractResourceAsync(testCol, timeframe, filters)).ToList();
if (null != queryMock)
queryMock.VerifyAll();
}
-
[Test]
public async Task MultiAnalysis_Valid_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
+
IEnumerable param = new List()
{
new MultiAnalysisParam("first", MultiAnalysisParam.Metric.Count()),
@@ -891,7 +885,7 @@ public async Task MultiAnalysis_Valid_Success()
queryMock.Setup(m => m.MultiAnalysis(
It.Is(c => c == testCol),
It.Is>(p => p == param),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is>(f => f == null),
It.Is(tz => tz == "")
))
@@ -900,7 +894,7 @@ public async Task MultiAnalysis_Valid_Success()
client.Queries = queryMock.Object;
}
- var reply = await client.QueryMultiAnalysisAsync(testCol, param, null, null, "");
+ var reply = await client.QueryMultiAnalysisAsync(testCol, param, timeframe, null, "");
if (null != queryMock)
{
@@ -954,6 +948,7 @@ public async Task MultiAnalysis_ValidRelativeTimeFrame_Success()
public async Task MultiAnalysis_ValidGroupBy_Success()
{
var client = new KeenClient(SettingsEnv);
+ var timeframe = QueryRelativeTimeframe.ThisHour();
var groupby = "field1";
IEnumerable param = new List()
{
@@ -979,7 +974,7 @@ public async Task MultiAnalysis_ValidGroupBy_Success()
queryMock.Setup(m => m.MultiAnalysis(
It.Is(c => c == testCol),
It.Is>(p => p == param),
- It.Is(t => t == null),
+ It.Is(t => t == timeframe),
It.Is>(f => f == null),
It.Is(g => g == groupby),
It.Is(tz => tz == "")
@@ -989,7 +984,7 @@ public async Task MultiAnalysis_ValidGroupBy_Success()
client.Queries = queryMock.Object;
}
- var reply = (await client.QueryMultiAnalysisGroupAsync(testCol, param, null, null, groupby, "")).ToList();
+ var reply = (await client.QueryMultiAnalysisGroupAsync(testCol, param, timeframe, null, groupby, "")).ToList();
if (null != queryMock)
{
@@ -1109,7 +1104,6 @@ public async Task MultiAnalysis_ValidInterval_Success()
Assert.AreEqual(reply.Count(), result.Count());
}
}
-
}
[TestFixture]
@@ -1122,9 +1116,15 @@ public void Constructor_InvalidProperty_Throws()
}
[Test]
- public void Constructor_InvalidValue_Throws()
+ public void Constructor_InvalidOp_Throws()
{
- Assert.Throws(() => new QueryFilter("prop", QueryFilter.FilterOperator.Equals(), null));
+ Assert.Throws(() => new QueryFilter("prop", null, "val"));
+ }
+
+ [Test]
+ public void Constructor_NullPropertyValue_Success()
+ {
+ Assert.DoesNotThrow(() => new QueryFilter("prop", QueryFilter.FilterOperator.Equals(), null));
}
[Test]
@@ -1148,6 +1148,20 @@ public void Serialize_SimpleValue_Success()
Assert.AreEqual(expectedJson, json);
}
+ [Test]
+ public void Serialize_NullValue_Success()
+ {
+ var filter = new QueryFilter("prop", QueryFilter.FilterOperator.Equals(), null);
+
+ var json = JObject.FromObject(filter).ToString(Newtonsoft.Json.Formatting.None);
+
+ const string expectedJson = "{\"property_name\":\"prop\"," +
+ "\"operator\":\"eq\"," +
+ "\"property_value\":null}";
+
+ Assert.AreEqual(expectedJson, json);
+ }
+
[Test]
public void Serialize_GeoValue_Success()
{
@@ -1169,6 +1183,5 @@ public void Serialize_GeoValue_Success()
Assert.AreEqual(expectedJson, json);
}
-
}
}
diff --git a/Keen.NET.Test/QueryTests_Integration.cs b/Keen.NET.Test/QueryTests_Integration.cs
new file mode 100644
index 0000000..6994f2e
--- /dev/null
+++ b/Keen.NET.Test/QueryTests_Integration.cs
@@ -0,0 +1,123 @@
+using Keen.Core;
+using Keen.Core.Query;
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// Integration tests for Queries. These will exercise more than unit tests, like the
+ /// integration between KeenClient, Queries and KeenHttpClient.
+ ///
+ class QueryTests_Integration : TestBase
+ {
+ [Test]
+ public async Task QueryFilter_NotContains_Success()
+ {
+ var queriesUrl = HttpTests.GetUriForResource(SettingsEnv,
+ KeenConstants.QueriesResource);
+
+ var handler = new FuncHandler()
+ {
+ PreProcess = (req, ct) =>
+ {
+ var queryStr = req.RequestUri.Query;
+
+ // Make sure our filter properties are in the query string
+ Assert.IsTrue(queryStr.Contains("propertyName") &&
+ queryStr.Contains("four") &&
+ queryStr.Contains(QueryFilter.FilterOperator.NotContains()));
+ },
+ ProduceResultAsync = (req, ct) =>
+ {
+ return HttpTests.CreateJsonStringResponseAsync(new { result = 2 });
+ },
+ DeferToDefault = false
+ };
+
+ // NOTE : This example shows use of UrlToMessageHandler, but since we only make one
+ // request to a single endpoint, we could just directly use the FuncHandler here.
+ var urlHandler = new UrlToMessageHandler(
+ new Dictionary
+ {
+ { queriesUrl, handler }
+ })
+ { DeferToDefault = false };
+
+ var client = new KeenClient(SettingsEnv, new TestKeenHttpClientProvider()
+ {
+ ProvideKeenHttpClient =
+ (url) => KeenHttpClientFactory.Create(url,
+ new HttpClientCache(),
+ null,
+ new DelegatingHandlerMock(urlHandler))
+ });
+
+ var filters = new List
+ {
+ new QueryFilter("propertyName", QueryFilter.FilterOperator.NotContains(), "four")
+ };
+
+ var count = await client.QueryAsync(
+ QueryType.Count(),
+ "testCollection",
+ "",
+ QueryRelativeTimeframe.ThisMonth(),
+ filters);
+
+ Assert.IsNotNull(count);
+ Assert.AreEqual("2", count);
+ }
+
+ [Test]
+ public async Task QueryFilter_NullPropertyValue_Success()
+ {
+ // TODO : Consolidate this FuncHandler/KeenClient setup into a helper method.
+
+ var handler = new FuncHandler()
+ {
+ PreProcess = (req, ct) =>
+ {
+ var queryStr = req.RequestUri.Query;
+
+ // Make sure our filter properties are in the query string
+ Assert.IsTrue(queryStr.Contains("propertyName") &&
+ queryStr.Contains("null") &&
+ queryStr.Contains(QueryFilter.FilterOperator.Equals()));
+ },
+ ProduceResultAsync = (req, ct) =>
+ {
+ return HttpTests.CreateJsonStringResponseAsync(new { result = 2 });
+ },
+ DeferToDefault = false
+ };
+
+ var client = new KeenClient(SettingsEnv, new TestKeenHttpClientProvider()
+ {
+ ProvideKeenHttpClient =
+ (url) => KeenHttpClientFactory.Create(url,
+ new HttpClientCache(),
+ null,
+ new DelegatingHandlerMock(handler))
+ });
+
+ var filters = new List
+ {
+ new QueryFilter("propertyName", QueryFilter.FilterOperator.Equals(), null)
+ };
+
+ var count = await client.QueryAsync(
+ QueryType.Count(),
+ "testCollection",
+ "",
+ QueryRelativeTimeframe.ThisMonth(),
+ filters);
+
+ Assert.IsNotNull(count);
+ Assert.AreEqual("2", count);
+ }
+ }
+}
diff --git a/Keen.NET.Test/TestKeenHttpClientProvider.cs b/Keen.NET.Test/TestKeenHttpClientProvider.cs
new file mode 100644
index 0000000..c518af2
--- /dev/null
+++ b/Keen.NET.Test/TestKeenHttpClientProvider.cs
@@ -0,0 +1,23 @@
+using Keen.Core;
+using System;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// An implementation of that behaves just like
+ /// by default, but allows for easily overriding this
+ /// behavior with a Func<> for use in tests.
+ ///
+ internal class TestKeenHttpClientProvider : IKeenHttpClientProvider
+ {
+ internal Func ProvideKeenHttpClient =
+ (url) => KeenHttpClientFactory.Create(url, HttpClientCache.Instance);
+
+
+ public IKeenHttpClient GetForUrl(Uri baseUrl)
+ {
+ return ProvideKeenHttpClient(baseUrl);
+ }
+ }
+}
diff --git a/Keen.NET.Test/UrlToMessageHandler.cs b/Keen.NET.Test/UrlToMessageHandler.cs
new file mode 100644
index 0000000..5683ba9
--- /dev/null
+++ b/Keen.NET.Test/UrlToMessageHandler.cs
@@ -0,0 +1,91 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Keen.Net.Test
+{
+ ///
+ /// An that matches request URIs with other
+ /// IHttpMessageHandler instances. If configured as such, matching can be done such that when
+ /// a precise match for a URL isn't found, any base URL entry will be used. This would mean one
+ /// could match, say, any queries under a certain project ID without specifying all the
+ /// query parameters, if that's useful. Another entry could match events, for example. By
+ /// default, if no strict or loose match is found, this will try to forward to DefaultAsync, so
+ /// either make sure that is set by a wrapper or explicitly, or configure to not defer to
+ /// the default action.
+ ///
+ /// This is handy to set up some validation and return canned responses for sets of URLs.
+ ///
+ /// By sticking IHttpMessageHandler instances in this mapping, their DefaultAsync properties
+ /// get forwarded to the DefaultAsync of this handler. That means if a match is found, and the
+ /// handler used decides to call DefaultAsync, it will call this handler's default action,
+ /// bypassing DeferToDefault, so make sure the contained handlers know whether or not to call
+ /// default. TODO : Evaluate this behavior since maybe the defaults here are confusing.
+ ///
+ ///
+ internal class UrlToMessageHandler : IHttpMessageHandler
+ {
+ private readonly IDictionary _urlsToHandlers;
+
+ public Func> DefaultAsync { get; set; }
+
+ internal bool DeferToDefault { get; set; } = true;
+
+ internal bool MatchBaseUrls { get; set; } = true;
+
+ internal UrlToMessageHandler(IDictionary urlsToHandlers)
+ {
+ _urlsToHandlers = new Dictionary(urlsToHandlers);
+
+ foreach (var handler in _urlsToHandlers.Values)
+ {
+ // Lazily dispatch to whatever our default handler gets set to.
+ // Be careful because trying to reuse handler instances will lead to strange
+ // outcomes w.r.t. the default behavior of these IHttpMessageHandlers.
+ handler.DefaultAsync = (request, ct) => DefaultAsync(request, ct);
+ }
+ }
+
+ public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ HttpResponseMessage response = null;
+ IHttpMessageHandler handler = null;
+
+ // First check for a perfect match, or if there's a key that's a base url of the request
+ // url, match it if client code chooses to accept that.
+ if (_urlsToHandlers.TryGetValue(request.RequestUri, out handler) ||
+ (MatchBaseUrls && null != (handler = _urlsToHandlers.FirstOrDefault(
+ entry => entry.Key.IsBaseOf(request.RequestUri)).Value)))
+ {
+ response =
+ await handler.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+ else if (DeferToDefault)
+ {
+ response = await DefaultAsync(request, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ Console.WriteLine(string.Format("WARNING: No validator found for absolute URI: {0}",
+ request.RequestUri.AbsoluteUri));
+
+ // No handler found, so return 404
+ response = await HttpTests.CreateJsonStringResponseAsync(
+ HttpStatusCode.NotFound, "Resource not found.", "ResourceNotFoundError")
+ .ConfigureAwait(false);
+ }
+
+ return response;
+ }
+ }
+}
diff --git a/Keen.Net/ProjectSettingsProviderEnv.cs b/Keen.Net/ProjectSettingsProviderEnv.cs
index 4bd6200..ec6cc47 100644
--- a/Keen.Net/ProjectSettingsProviderEnv.cs
+++ b/Keen.Net/ProjectSettingsProviderEnv.cs
@@ -1,8 +1,6 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Keen.Core;
+using Keen.Core;
+using System;
+
namespace Keen.Net
{
@@ -14,9 +12,10 @@ public class ProjectSettingsProviderEnv : ProjectSettingsProvider
///
/// Reads the project settings from environment variables
/// Project ID should be in variable KEEN_PROJECT_ID
- /// Master Key should be in variable KEEN_MASTER_ID
- /// Write Key should be in variable KEEN_WRITE_ID
- /// ReadKey should be in variable KEEN_READ_ID
+ /// Master Key should be in variable KEEN_MASTER_KEY
+ /// Write Key should be in variable KEEN_WRITE_KEY
+ /// ReadKey should be in variable KEEN_READ_KEY
+ /// Keen.IO API url should be in variable KEEN_SERVER_URL
///
public ProjectSettingsProviderEnv()
{
diff --git a/Keen.Net/ProjectSettingsProviderFile.cs b/Keen.Net/ProjectSettingsProviderFile.cs
index 72c1c7b..97cc24f 100644
--- a/Keen.Net/ProjectSettingsProviderFile.cs
+++ b/Keen.Net/ProjectSettingsProviderFile.cs
@@ -1,10 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Keen.Core;
+using Keen.Core;
using System.IO;
+
namespace Keen.Net
{
///
@@ -15,11 +12,14 @@ public class ProjectSettingsProviderFile : ProjectSettingsProvider
///
/// Reads the project settings from a text file.
/// Each setting takes one line, in the order Project ID,
- /// Master Key, Write Key ReadKey. Unused values should be represented
+ /// Master Key, Write Key, Read Key. Unused values should be represented
/// with a blank line.
///
public ProjectSettingsProviderFile(string filePath)
{
+ // TODO : Add Keen Server URL as one of the lines, optionally.
+ // TODO : Master key maybe should be de-emphasized and not be first.
+ // TODO : Share init of properties with base class implementation.
var values = File.ReadAllLines(filePath);
if (values.Length != 4)
throw new KeenException("Invalid project settings file, file must contain exactly 4 lines: " + filePath);
diff --git a/Keen.Net/ScopedKey.cs b/Keen.Net/ScopedKey.cs
index 7628166..bb7b5e3 100644
--- a/Keen.Net/ScopedKey.cs
+++ b/Keen.Net/ScopedKey.cs
@@ -31,7 +31,7 @@ public static string Encrypt(string apiKey, object secOptions, string IV = null)
{
var secOptionsJson = JObject.FromObject(secOptions ?? new object()).ToString(Formatting.None);
- return EncryptString( apiKey, secOptionsJson, IV);
+ return EncryptString(apiKey, secOptionsJson, IV);
}
///
diff --git a/Keen/CachedEvent.cs b/Keen/CachedEvent.cs
index 9b053b7..9cc2714 100644
--- a/Keen/CachedEvent.cs
+++ b/Keen/CachedEvent.cs
@@ -1,8 +1,6 @@
using Newtonsoft.Json.Linq;
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+
namespace Keen.Core.EventCache
{
diff --git a/Keen/DataEnrichment/EventAddOn.cs b/Keen/DataEnrichment/EventAddOn.cs
index 60b3610..f6f1e1c 100644
--- a/Keen/DataEnrichment/EventAddOn.cs
+++ b/Keen/DataEnrichment/EventAddOn.cs
@@ -1,5 +1,6 @@
-using System.Collections.Generic;
-using Newtonsoft.Json;
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
namespace Keen.Core.DataEnrichment
{
@@ -46,7 +47,7 @@ public AddOn(string name, IDictionary input, string output)
///
/// Build and return an IpToGeo Data Enhancement add-on. This add-on reads
/// an IP address from the field identified by the input parameter and writes
- /// data about the geographical location to the field identfied by the output parameter.
+ /// data about the geographical location to the field identified by the output parameter.
///
/// Name of field to store the geographical information
/// Name of field containing an IP address
@@ -62,7 +63,7 @@ public static AddOn IpToGeo(string ipField, string outputField)
/// Build and return a User-Agent Data Enhancement add-on. This add-on reads
/// a user agent string from the field identified by the input parameter and parses it
/// into the device, browser, browser version, OS, and OS version fields and stores that
- /// data in the field identfied by the output parameter.
+ /// data in the field identified by the output parameter.
///
/// Name of field to store the parsed user agent field
/// Name of field containing the user agent string
diff --git a/Keen/DynamicPropertyValue.cs b/Keen/DynamicPropertyValue.cs
index 22b8378..5afe658 100644
--- a/Keen/DynamicPropertyValue.cs
+++ b/Keen/DynamicPropertyValue.cs
@@ -1,8 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
+
namespace Keen.Core
{
diff --git a/Keen/Event.cs b/Keen/Event.cs
index dd7e3a0..ec6a690 100644
--- a/Keen/Event.cs
+++ b/Keen/Event.cs
@@ -2,13 +2,10 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Text;
using System.Threading.Tasks;
+
namespace Keen.Core
{
///
@@ -16,8 +13,43 @@ namespace Keen.Core
///
internal class Event : IEvent
{
- private IProjectSettings _prjSettings;
- private string _serverUrl;
+ private readonly IKeenHttpClient _keenHttpClient;
+ private readonly string _eventsRelativeUrl;
+ private readonly string _readKey;
+ private readonly string _writeKey;
+
+
+ internal Event(IProjectSettings prjSettings,
+ IKeenHttpClientProvider keenHttpClientProvider)
+ {
+ if (null == prjSettings)
+ {
+ throw new ArgumentNullException(nameof(prjSettings),
+ "Project Settings must be provided.");
+ }
+
+ if (null == keenHttpClientProvider)
+ {
+ throw new ArgumentNullException(nameof(keenHttpClientProvider),
+ "A KeenHttpClient provider must be provided.");
+ }
+
+ if (string.IsNullOrWhiteSpace(prjSettings.KeenUrl) ||
+ !Uri.IsWellFormedUriString(prjSettings.KeenUrl, UriKind.Absolute))
+ {
+ throw new KeenException(
+ "A properly formatted KeenUrl must be provided via Project Settings.");
+ }
+
+ var serverBaseUrl = new Uri(prjSettings.KeenUrl);
+ _keenHttpClient = keenHttpClientProvider.GetForUrl(serverBaseUrl);
+ _eventsRelativeUrl = KeenHttpClient.GetRelativeUrl(prjSettings.ProjectId,
+ KeenConstants.EventsResource);
+
+ _readKey = prjSettings.ReadKey;
+ _writeKey = prjSettings.WriteKey;
+ }
+
///
/// Get details of all schemas in the project.
@@ -25,25 +57,33 @@ internal class Event : IEvent
///
public async Task GetSchemas()
{
- using (var client = new HttpClient())
+ if (string.IsNullOrWhiteSpace(_readKey))
+ {
+ throw new KeenException("An API ReadKey is required to get schemas.");
+ }
+
+ var responseMsg = await _keenHttpClient
+ .GetAsync(_eventsRelativeUrl, _readKey)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var responseString = await responseMsg
+ .Content
+ .ReadAsStringAsync()
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = JArray.Parse(responseString);
+
+ // error checking, throw an exception with information from the json
+ // response if available, then check the HTTP response.
+ KeenUtil.CheckApiErrorCode(response);
+
+ if (!responseMsg.IsSuccessStatusCode)
{
- client.DefaultRequestHeaders.Add("Authorization", _prjSettings.MasterKey);
- client.DefaultRequestHeaders.Add("Keen-Sdk", KeenUtil.GetSdkVersion());
-
- var responseMsg = await client.GetAsync(_serverUrl)
- .ConfigureAwait(continueOnCapturedContext: false);
- var responseString = await responseMsg.Content.ReadAsStringAsync()
- .ConfigureAwait(continueOnCapturedContext: false);
- dynamic response = JArray.Parse(responseString);
-
- // error checking, throw an exception with information from the json
- // response if available, then check the HTTP response.
- KeenUtil.CheckApiErrorCode(response);
- if (!responseMsg.IsSuccessStatusCode)
- throw new KeenException("GetSchemas failed with status: " + responseMsg.StatusCode);
-
- return response;
+ throw new KeenException("GetSchemas failed with status: " +
+ responseMsg.StatusCode);
}
+
+ return response;
}
///
@@ -53,60 +93,64 @@ public async Task GetSchemas()
///
public async Task> AddEvents(JObject events)
{
+ if (string.IsNullOrWhiteSpace(_writeKey))
+ {
+ throw new KeenException("An API WriteKey is required to add events.");
+ }
+
var content = events.ToString();
- using (var client = new HttpClient())
- using (var contentStream = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content))))
+ var responseMsg = await _keenHttpClient
+ .PostAsync(_eventsRelativeUrl, _writeKey, content)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var responseString = await responseMsg
+ .Content
+ .ReadAsStringAsync()
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ JObject jsonResponse = null;
+
+ try
+ {
+ // Normally the response content should be parsable JSON,
+ // but if the server returned a 404 error page or something
+ // like that, this will throw.
+ jsonResponse = JObject.Parse(responseString);
+
+ // TODO : Why do we not call KeenUtil.CheckApiErrorCode(jsonResponse); ??
+ }
+ catch (Exception)
{
- contentStream.Headers.Add("content-type", "application/json");
-
- client.DefaultRequestHeaders.Add("Authorization", _prjSettings.WriteKey);
- client.DefaultRequestHeaders.Add("Keen-Sdk", KeenUtil.GetSdkVersion());
-
- var httpResponse = await client.PostAsync(_serverUrl, contentStream)
- .ConfigureAwait(continueOnCapturedContext: false);
- var responseString = await httpResponse.Content.ReadAsStringAsync()
- .ConfigureAwait(continueOnCapturedContext: false);
- JObject jsonResponse = null;
- try
- {
- // Normally the response content should be parsable JSON,
- // but if the server returned a 404 error page or something
- // like that, this will throw.
- jsonResponse = JObject.Parse(responseString);
- }
- catch (Exception)
- { }
-
- if (!httpResponse.IsSuccessStatusCode)
- throw new KeenException("AddEvents failed with status: " + httpResponse);
- if (null == jsonResponse)
- throw new KeenException("AddEvents failed with empty response from server.");
-
- // error checking, return failed events in the list,
- // or if the HTTP response is a failure, throw.
- var failedItems = from respCols in jsonResponse.Properties()
- from eventsCols in events.Properties()
- where respCols.Name == eventsCols.Name
- let collection = respCols.Name
- let combined = eventsCols.Children().Children()
- .Zip(respCols.Children().Children(),
- (e, r) => new { eventObj = (JObject)e, result = (JObject)r })
- from e in combined
- where !(bool)(e.result.Property("success").Value)
- select new CachedEvent(collection, e.eventObj, KeenUtil.GetBulkApiError(e.result));
-
- return failedItems;
}
- }
- public Event(IProjectSettings prjSettings)
- {
- _prjSettings = prjSettings;
+ if (!responseMsg.IsSuccessStatusCode)
+ {
+ throw new KeenException("AddEvents failed with status: " + responseMsg.StatusCode);
+ }
- _serverUrl = string.Format("{0}projects/{1}/{2}",
- _prjSettings.KeenUrl, _prjSettings.ProjectId, KeenConstants.EventsResource);
- }
+ if (null == jsonResponse)
+ {
+ throw new KeenException("AddEvents failed with empty response from server.");
+ }
+ // error checking, return failed events in the list,
+ // or if the HTTP response is a failure, throw.
+ var failedItems =
+ from respCols in jsonResponse.Properties()
+ from eventsCols in events.Properties()
+ where respCols.Name == eventsCols.Name
+ let collection = respCols.Name
+ let combined = eventsCols.Children().Children()
+ .Zip(respCols.Children().Children(),
+ (e, r) => new { eventObj = (JObject)e, result = (JObject)r })
+ from e in combined
+ where !(bool)(e.result.Property("success").Value)
+ select new CachedEvent(collection,
+ e.eventObj,
+ KeenUtil.GetBulkApiError(e.result));
+
+ return failedItems;
+ }
}
}
diff --git a/Keen/EventCachePortable.cs b/Keen/EventCachePortable.cs
index a8e14df..b3b18ac 100644
--- a/Keen/EventCachePortable.cs
+++ b/Keen/EventCachePortable.cs
@@ -5,9 +5,9 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
+
namespace Keen.Core
{
///
@@ -21,8 +21,7 @@ public class EventCachePortable : IEventCache
{
private static Queue events = new Queue();
- private EventCachePortable()
- {}
+ private EventCachePortable() { }
///
/// Create, initialize and return an instance of EventCachePortable.
@@ -49,7 +48,7 @@ public static async Task NewAsync()
{
var instance = new EventCachePortable();
- var keenFolder = await getKeenFolder()
+ var keenFolder = await GetKeenFolder()
.ConfigureAwait(continueOnCapturedContext: false);
var files = (await keenFolder.GetFilesAsync().ConfigureAwait(continueOnCapturedContext: false)).ToList();
@@ -66,7 +65,7 @@ public async Task Add(CachedEvent e)
if (null == e)
throw new KeenException("Cached events may not be null");
- var keenFolder = await getKeenFolder()
+ var keenFolder = await GetKeenFolder()
.ConfigureAwait(continueOnCapturedContext: false);
IFile file;
@@ -81,7 +80,7 @@ public async Task Add(CachedEvent e)
// and generating and inserting a unique name within the lock. CreateFileAsync has
// a CreateCollisionOption.GenerateUniqueName, but it will return the same name
// multiple times when called from parallel tasks.
- // If creating and writing the file fails, loop around and
+ // If creating and writing the file fails, loop around and generate a new name.
if (string.IsNullOrEmpty(name))
lock (events)
{
@@ -115,12 +114,12 @@ await file.WriteAllTextAsync(content)
// this when the queue is read than to try to dequeue the name.
if (attempts > 100)
throw new KeenException("Persistent failure while saving file, aborting", lastErr);
- } while (!done);
+ } while (!done);
}
public async Task TryTake()
{
- var keenFolder = await getKeenFolder()
+ var keenFolder = await GetKeenFolder()
.ConfigureAwait(continueOnCapturedContext: false);
if (!events.Any())
return null;
@@ -135,7 +134,7 @@ public async Task TryTake()
.ConfigureAwait(continueOnCapturedContext: false);
var ce = JObject.Parse(content);
- var item = new CachedEvent((string)ce.SelectToken("Collection"), (JObject)ce.SelectToken("Event"), ce.SelectToken("Error").ToObject() );
+ var item = new CachedEvent((string)ce.SelectToken("Collection"), (JObject)ce.SelectToken("Event"), ce.SelectToken("Error").ToObject());
await file.DeleteAsync()
.ConfigureAwait(continueOnCapturedContext: false);
return item;
@@ -143,23 +142,22 @@ await file.DeleteAsync()
public async Task Clear()
{
- var keenFolder = await getKeenFolder()
+ var keenFolder = await GetKeenFolder()
.ConfigureAwait(continueOnCapturedContext: false);
lock(events)
events.Clear();
await keenFolder.DeleteAsync()
.ConfigureAwait(continueOnCapturedContext: false);
- await getKeenFolder()
+ await GetKeenFolder()
.ConfigureAwait(continueOnCapturedContext: false);
}
- private static async Task getKeenFolder()
+ private static Task GetKeenFolder()
{
IFolder rootFolder = FileSystem.Current.LocalStorage;
- var keenFolder = await rootFolder.CreateFolderAsync("KeenCache", CreationCollisionOption.OpenIfExists)
- .ConfigureAwait(continueOnCapturedContext: false);
- return keenFolder;
- }
+ var keenFolderTask = rootFolder.CreateFolderAsync("KeenCache", CreationCollisionOption.OpenIfExists);
+ return keenFolderTask;
+ }
}
}
diff --git a/Keen/EventCollection.cs b/Keen/EventCollection.cs
index 647a48b..eb5fc24 100644
--- a/Keen/EventCollection.cs
+++ b/Keen/EventCollection.cs
@@ -1,100 +1,161 @@
using Newtonsoft.Json.Linq;
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Text;
+using System.Threading.Tasks;
+
namespace Keen.Core
{
///
- /// EventCollection implements the IEventCollection interface which represents the Keen.IO EventCollection API methods.
+ /// EventCollection implements the IEventCollection interface which represents the Keen.IO
+ /// EventCollection API methods.
///
internal class EventCollection : IEventCollection
{
- private string _serverUrl;
- private IProjectSettings _prjSettings;
+ private readonly IKeenHttpClient _keenHttpClient;
+ private readonly string _eventsRelativeUrl;
+ private readonly string _readKey;
+ private readonly string _writeKey;
+ private readonly string _masterKey;
+
- public async System.Threading.Tasks.Task GetSchema(string collection)
+ internal EventCollection(IProjectSettings prjSettings,
+ IKeenHttpClientProvider keenHttpClientProvider)
{
- using (var client = new HttpClient())
+ if (null == prjSettings)
+ {
+ throw new ArgumentNullException(nameof(prjSettings),
+ "Project Settings must be provided.");
+ }
+
+ if (null == keenHttpClientProvider)
{
- client.DefaultRequestHeaders.Add("Authorization", _prjSettings.MasterKey);
- client.DefaultRequestHeaders.Add("Keen-Sdk", KeenUtil.GetSdkVersion());
-
- var responseMsg = await client.GetAsync(_serverUrl + collection)
- .ConfigureAwait(continueOnCapturedContext: false);
- var responseString = await responseMsg.Content.ReadAsStringAsync()
- .ConfigureAwait(continueOnCapturedContext: false);
- dynamic response = JObject.Parse(responseString);
-
- // error checking, throw an exception with information from the json
- // response if available, then check the HTTP response.
- KeenUtil.CheckApiErrorCode(response);
- if (!responseMsg.IsSuccessStatusCode)
- throw new KeenException("GetSchema failed with status: " + responseMsg.StatusCode);
-
- return response;
+ throw new ArgumentNullException(nameof(keenHttpClientProvider),
+ "A KeenHttpClient provider must be provided.");
}
+
+ if (string.IsNullOrWhiteSpace(prjSettings.KeenUrl) ||
+ !Uri.IsWellFormedUriString(prjSettings.KeenUrl, UriKind.Absolute))
+ {
+ throw new KeenException(
+ "A properly formatted KeenUrl must be provided via Project Settings.");
+ }
+
+ var serverBaseUrl = new Uri(prjSettings.KeenUrl);
+ _keenHttpClient = keenHttpClientProvider.GetForUrl(serverBaseUrl);
+ _eventsRelativeUrl = KeenHttpClient.GetRelativeUrl(prjSettings.ProjectId,
+ KeenConstants.EventsResource);
+
+ // TODO : It's possible we may want to change back to dynamically grabbing the keys
+ // from a stored IProjectSettings so client code can lazily assign keys. It creates a
+ // minor potential race condition, but will allow for scenarios like creating a
+ // KeenClient instance with only a master key in order to generate/acquire access keys
+ // to then set as the other keys. Otherwise a new KeenClient must be created or at
+ // least a new instance of the IEventCollection/IEvent/IQueries implementations.
+
+ _readKey = prjSettings.ReadKey;
+ _writeKey = prjSettings.WriteKey;
+ _masterKey = prjSettings.MasterKey;
}
- public async System.Threading.Tasks.Task DeleteCollection(string collection)
+
+ public async Task GetSchema(string collection)
{
- using (var client = new HttpClient())
+ // TODO : So much of this code, both in the constructor and in the actual message
+ // dispatch, response parsing and error checking is copy/paste across Queries, Event
+ // and EventCollection everywhere we use KeenHttpClient. We could shove some of that
+ // into shared factory functionality (for the ctor stuff) and some of it into the
+ // KeenHttpClient (for the dispatch/response portions).
+
+
+ if (string.IsNullOrWhiteSpace(_readKey))
{
- client.DefaultRequestHeaders.Add("Authorization", _prjSettings.MasterKey);
- client.DefaultRequestHeaders.Add("Keen-Sdk", KeenUtil.GetSdkVersion());
+ throw new KeenException("An API ReadKey is required to get collection schema.");
+ }
+
+ var responseMsg = await _keenHttpClient
+ .GetAsync(GetCollectionUrl(collection), _readKey)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var responseString = await responseMsg
+ .Content
+ .ReadAsStringAsync()
+ .ConfigureAwait(continueOnCapturedContext: false);
- var responseMsg = await client.DeleteAsync(_serverUrl + collection)
- .ConfigureAwait(continueOnCapturedContext: false);
- if (!responseMsg.IsSuccessStatusCode)
- throw new KeenException("DeleteCollection failed with status: " + responseMsg.StatusCode);
+ dynamic response = JObject.Parse(responseString);
+
+ // error checking, throw an exception with information from the json
+ // response if available, then check the HTTP response.
+ KeenUtil.CheckApiErrorCode(response);
+
+ if (!responseMsg.IsSuccessStatusCode)
+ {
+ throw new KeenException("GetSchema failed with status: " + responseMsg.StatusCode);
}
+
+ return response;
}
- public async System.Threading.Tasks.Task AddEvent(string collection, JObject anEvent)
+ public async Task DeleteCollection(string collection)
{
- var content = anEvent.ToString();
+ if (string.IsNullOrWhiteSpace(_masterKey))
+ {
+ throw new KeenException("An API MasterKey is required to delete a collection.");
+ }
+
+ var responseMsg = await _keenHttpClient
+ .DeleteAsync(GetCollectionUrl(collection), _masterKey)
+ .ConfigureAwait(continueOnCapturedContext: false);
- using (var client = new HttpClient())
- using (var contentStream = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content))))
+ if (!responseMsg.IsSuccessStatusCode)
{
- contentStream.Headers.Add("content-type", "application/json");
-
- client.DefaultRequestHeaders.Add("Authorization", _prjSettings.WriteKey);
- client.DefaultRequestHeaders.Add("Keen-Sdk", KeenUtil.GetSdkVersion());
-
- var httpResponse = await client.PostAsync(_serverUrl + collection, contentStream)
- .ConfigureAwait(continueOnCapturedContext: false);
- var responseString = await httpResponse.Content.ReadAsStringAsync()
- .ConfigureAwait(continueOnCapturedContext: false);
- JObject jsonResponse = null;
- try
- {
- // Normally the response content should be parsable JSON,
- // but if the server returned a 404 error page or something
- // like that, this will throw.
- jsonResponse = JObject.Parse(responseString);
- }
- catch (Exception)
- { }
-
- // error checking, throw an exception with information from the
- // json response if available, then check the HTTP response.
- KeenUtil.CheckApiErrorCode(jsonResponse);
- if (!httpResponse.IsSuccessStatusCode)
- throw new KeenException("AddEvent failed with status: " + httpResponse);
+ throw new KeenException("DeleteCollection failed with status: " + responseMsg.StatusCode);
}
}
- public EventCollection(IProjectSettings prjSettings)
+ public async Task AddEvent(string collection, JObject anEvent)
{
- _prjSettings = prjSettings;
+ if (string.IsNullOrWhiteSpace(_writeKey))
+ {
+ throw new KeenException("An API WriteKey is required to add events.");
+ }
- _serverUrl = string.Format("{0}projects/{1}/{2}/",
- _prjSettings.KeenUrl, _prjSettings.ProjectId, KeenConstants.EventsResource);
+ var content = anEvent.ToString();
+
+ var responseMsg = await _keenHttpClient
+ .PostAsync(GetCollectionUrl(collection), _writeKey, content)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var responseString = await responseMsg
+ .Content
+ .ReadAsStringAsync()
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ JObject jsonResponse = null;
+
+ try
+ {
+ // Normally the response content should be parsable JSON,
+ // but if the server returned a 404 error page or something
+ // like that, this will throw.
+ jsonResponse = JObject.Parse(responseString);
+ }
+ catch (Exception)
+ {
+ }
+
+ // error checking, throw an exception with information from the
+ // json response if available, then check the HTTP response.
+ KeenUtil.CheckApiErrorCode(jsonResponse);
+
+ if (!responseMsg.IsSuccessStatusCode)
+ {
+ throw new KeenException("AddEvent failed with status: " + responseMsg.StatusCode);
+ }
+ }
+
+ private string GetCollectionUrl(string collection)
+ {
+ return $"{_eventsRelativeUrl}/{collection}";
}
}
}
diff --git a/Keen/HttpClientCache.cs b/Keen/HttpClientCache.cs
new file mode 100644
index 0000000..3dd943c
--- /dev/null
+++ b/Keen/HttpClientCache.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+
+
+namespace Keen.Core
+{
+ ///
+ /// An implementation of that caches HttpClient instances in
+ /// a dictionary mapping the base URL to a WeakReference to the actual instance. A PRO to this
+ /// approach is that HttpClient instances will automatically be evicted when no more strong
+ /// refs exist and the GC collects. A CON to using WeakReference, besides it not being generic
+ /// in the current version of the PCL and being a fairly heavyweight class, is that rapid
+ /// creation and releasing of owning instances like the KeenClient can still allow for the GC
+ /// to aggressively clean up HttpClient instances. Recommended usage of KeenClient shouldn't
+ /// make this a common problem, but at some point this cache can evolve to be more intelligent
+ /// about keeping instances alive deliberately.
+ ///
+ internal class HttpClientCache : IHttpClientProvider
+ {
+ // A singleton cache that can optionally be used and shared. If new caches need to be
+ // created, use the internal ctor. One use case for this might be to have multiple
+ // HttpClient instances with different configurations cached for the same URL for use in
+ // different sets of client modules.
+ internal static HttpClientCache Instance { get; } = new HttpClientCache();
+
+ // Explicit static constructor, no beforefieldinit
+ static HttpClientCache() { }
+
+
+ private readonly object _cacheLock;
+
+ // NOTE : We should use ConcurrentDictionary> here. if/when we upgrade the PCL
+ // profile to something >= .NET 4.0.
+
+ // NOTE : Use WeakReference in 4.5+
+ private readonly IDictionary _httpClients;
+
+
+ // No external construction
+ internal HttpClientCache()
+ {
+ _cacheLock = new object();
+ _httpClients = new Dictionary();
+ }
+
+
+ ///
+ /// Retrieve an existing HttpClient for the given URL, or throw if it doesn't exist.
+ ///
+ /// The base URL the HttpClient is tied to.
+ /// The HttpClient which is expected to exist.
+ public HttpClient this[Uri baseUrl]
+ {
+ get
+ {
+ HttpClient httpClient = null;
+
+ lock (_cacheLock)
+ {
+ WeakReference weakRef = null;
+
+ if (!_httpClients.TryGetValue(baseUrl, out weakRef))
+ {
+ throw new KeenException(
+ string.Format("No existing HttpClient for baseUrl \"{0}\"", baseUrl));
+ }
+
+ httpClient = weakRef.Target as HttpClient;
+
+ if (null == httpClient)
+ {
+ throw new KeenException(
+ string.Format("Existing HttpClient for baseUrl \"{0}\" has been" +
+ "garbage collected.", baseUrl));
+ }
+ }
+
+ return httpClient;
+ }
+ }
+
+ ///
+ /// Retrieve an existing HttpClient for the given URL, or create one with the given
+ /// handlers and headers.
+ ///
+ /// The base URL the HttpClient is tied to.
+ /// A factory function to create a handler chain.
+ /// Any headers that all requests to this URL should add by
+ /// default.
+ /// An HttpClient configured to handle requests for the given URL.
+ public HttpClient GetOrCreateForUrl(
+ Uri baseUrl,
+ Func getHandlerChain = null,
+ IEnumerable> defaultHeaders = null)
+ {
+ Action configure = null;
+
+ if (null != defaultHeaders && Enumerable.Any(defaultHeaders))
+ {
+ configure = (httpClientToConfigure) =>
+ {
+ foreach (var header in defaultHeaders)
+ {
+ httpClientToConfigure.DefaultRequestHeaders.Add(header.Key, header.Value);
+ }
+ };
+ }
+
+
+ HttpClient httpClient = GetOrCreateForUrl(baseUrl, getHandlerChain, configure);
+
+ return httpClient;
+ }
+
+ ///
+ /// Retrieve an existing HttpClient for the given URL, or create one with the given
+ /// handlers and configuration functor.
+ ///
+ /// The base URL the HttpClient is tied to.
+ /// A factory function to create a handler chain.
+ /// An action that takes the newly created HttpClient and
+ /// configures it however needed before it is stored and/or returned.
+ /// An HttpClient configured to handle requests for the given URL.
+ public HttpClient GetOrCreateForUrl(
+ Uri baseUrl,
+ Func getHandlerChain = null,
+ Action configure = null)
+ {
+ if (null == baseUrl)
+ {
+ throw new ArgumentNullException(nameof(baseUrl),
+ string.Format("Cannot use a null {0} as a key.", nameof(baseUrl)));
+ }
+
+ HttpClient httpClient = null;
+
+ lock (_cacheLock)
+ {
+ WeakReference weakRef = null;
+
+ if (!_httpClients.TryGetValue(baseUrl, out weakRef) ||
+ null == (httpClient = weakRef.Target as HttpClient))
+ {
+ // If no handler chain is provided, a plain HttpClientHandler with no
+ // configuration whatsoever is installed.
+ httpClient = new HttpClient(getHandlerChain?.Invoke() ?? new HttpClientHandler());
+ httpClient.BaseAddress = baseUrl;
+
+ configure?.Invoke(httpClient);
+
+ // Reuse the WeakReference if we already had an entry for this url.
+ if (null == weakRef)
+ {
+ _httpClients[baseUrl] = new WeakReference(httpClient);
+ }
+ else
+ {
+ weakRef.Target = httpClient;
+ }
+ }
+ }
+
+ return httpClient;
+ }
+
+ ///
+ /// Remove any cached HttpClients associated with the given URL.
+ ///
+ /// The base URL for which any cached HttpClient instances should
+ /// be purged.
+ public void RemoveForUrl(Uri baseUrl)
+ {
+ if (null == baseUrl)
+ {
+ throw new ArgumentNullException(nameof(baseUrl),
+ string.Format("Cannot use a null {0} as a key.", nameof(baseUrl)));
+ }
+
+ lock (_cacheLock)
+ {
+ _httpClients.Remove(baseUrl);
+ }
+ }
+
+ ///
+ /// Can this provider return an HttpClient instance for the given URL? For this
+ /// implementation, we'll check if an entry exists in the cache and if the WeakReference
+ /// is still valid and a strong ref can be taken.
+ ///
+ /// The base URL for which we'd like to know if an HttpClient can be
+ /// provided.
+ /// True if this provider could return an HttpClient for the given URL, false
+ /// otherwise.
+ public bool ExistsForUrl(Uri baseUrl)
+ {
+ if (null == baseUrl)
+ {
+ // We can't have null keys, so we wouldn't ever be able to look up this URL.
+ return false;
+ }
+
+ lock (_cacheLock)
+ {
+ WeakReference weakRef = null;
+ bool exists = (_httpClients.TryGetValue(baseUrl, out weakRef) &&
+ (null != weakRef.Target as HttpClient));
+
+ return exists;
+ }
+ }
+
+ ///
+ /// Drop all HttpClient instances from the cache, no matter the URL.
+ ///
+ internal void Clear()
+ {
+ lock (_cacheLock)
+ {
+ _httpClients.Clear();
+ }
+ }
+
+ ///
+ /// Override the HttpClient provided for a given URL. This tests and replaces or inserts
+ /// all in one atomic operation. This will likely be useful for testing.
+ ///
+ /// URL to override.
+ /// HttpClient instance that will do the overriding.
+ internal void OverrideForUrl(Uri baseUrl, HttpClient httpClient)
+ {
+ lock (_cacheLock)
+ {
+ WeakReference weakRef = null;
+
+ if (!_httpClients.TryGetValue(baseUrl, out weakRef))
+ {
+ _httpClients[baseUrl] = new WeakReference(httpClient);
+ }
+ else
+ {
+ weakRef.Target = httpClient;
+ }
+ }
+ }
+ }
+}
diff --git a/Keen/IDynamicPropertyValue.cs b/Keen/IDynamicPropertyValue.cs
index 2d594fc..9015095 100644
--- a/Keen/IDynamicPropertyValue.cs
+++ b/Keen/IDynamicPropertyValue.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core
{
interface IDynamicPropertyValue
diff --git a/Keen/IEvent.cs b/Keen/IEvent.cs
index 3e85c79..568d4b2 100644
--- a/Keen/IEvent.cs
+++ b/Keen/IEvent.cs
@@ -1,11 +1,9 @@
using Keen.Core.EventCache;
using Newtonsoft.Json.Linq;
-using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
+
namespace Keen.Core
{
public interface IEvent
diff --git a/Keen/IEventCache.cs b/Keen/IEventCache.cs
index c88c54a..aaf3325 100644
--- a/Keen/IEventCache.cs
+++ b/Keen/IEventCache.cs
@@ -1,8 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
+
namespace Keen.Core.EventCache
{
diff --git a/Keen/IEventCollection.cs b/Keen/IEventCollection.cs
index 46ff892..dfb96c9 100644
--- a/Keen/IEventCollection.cs
+++ b/Keen/IEventCollection.cs
@@ -1,10 +1,7 @@
using Newtonsoft.Json.Linq;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
+
namespace Keen.Core
{
public interface IEventCollection
diff --git a/Keen/IHttpClientProvider.cs b/Keen/IHttpClientProvider.cs
new file mode 100644
index 0000000..e7db7d1
--- /dev/null
+++ b/Keen/IHttpClientProvider.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+
+
+namespace Keen.Core
+{
+ ///
+ /// Represents a type that can provide an HttpClient for a given URL. It could act as a cache
+ /// by returning pre-existing instances, or create as necessary, or always create given the
+ /// optional configuration parameters pass in.
+ ///
+ public interface IHttpClientProvider
+ {
+ ///
+ /// Retrieve an existing HttpClient for the given URL.
+ ///
+ /// The base URL the HttpClient is tied to.
+ /// The HttpClient which is expected to exist.
+ HttpClient this[Uri baseUrl] { get; }
+
+ ///
+ /// Retrieve an existing HttpClient for the given URL, or create one with the given
+ /// handlers and configuration functor.
+ ///
+ /// The base URL the HttpClient is tied to.
+ /// A factory function to create a handler chain.
+ /// Any headers that all requests to this URL should add by
+ /// default.
+ /// An HttpClient configured to handle requests for the given URL.
+ HttpClient GetOrCreateForUrl(
+ Uri baseUrl,
+ Func getHandlerChain = null,
+ IEnumerable> defaultHeaders = null
+ );
+
+ ///
+ /// Retrieve an existing HttpClient for the given URL, or create one with the given
+ /// handlers and configuration functor.
+ ///
+ /// The base URL the HttpClient is tied to.
+ /// A factory function to create a handler chain.
+ /// An action that takes the newly created HttpClient and
+ /// configures it however needed before it is stored and/or returned.
+ /// An HttpClient configured to handle requests for the given URL.
+ HttpClient GetOrCreateForUrl(Uri baseUrl,
+ Func getHandlerChain = null,
+ Action configure = null);
+
+ ///
+ /// If caching instances, remove any associated with the given URL.
+ ///
+ /// The base URL for which any cached HttpClient instances should
+ /// be purged.
+ void RemoveForUrl(Uri baseUrl);
+
+ ///
+ /// Can this provider return an HttpClient instance for the given URL?
+ ///
+ /// The base URL for which we'd like to know if an HttpClient can be
+ /// provided.
+ /// True if this provider could return an HttpClient for the given URL, false
+ /// otherwise.
+ bool ExistsForUrl(Uri baseUrl);
+ }
+}
\ No newline at end of file
diff --git a/Keen/IKeenHttpClient.cs b/Keen/IKeenHttpClient.cs
new file mode 100644
index 0000000..88d3d83
--- /dev/null
+++ b/Keen/IKeenHttpClient.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+
+namespace Keen.Core
+{
+ ///
+ /// Represents a type capable of performing HTTP operations destined for a Keen API endpoint.
+ /// This should augment and/or alter normal HttpClient behavior where appropriate taking into
+ /// consideration Keen-specific protocols.
+ ///
+ public interface IKeenHttpClient
+ {
+ ///
+ /// Create and send a GET request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to GET. Must be properly formatted as a
+ /// relative Uri.
+ /// The key to use for authenticating this request.
+ /// The response message.
+ Task GetAsync(string resource, string authKey);
+
+ ///
+ /// Create and send a GET request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to GET.
+ /// The key to use for authenticating this request.
+ /// The response message.
+ Task GetAsync(Uri resource, string authKey);
+
+ ///
+ /// Create and send a POST request with the given content to the given relative resource
+ /// using the given key for authentication.
+ ///
+ /// The relative resource to POST. Must be properly formatted as a
+ /// relative Uri.
+ /// The key to use for authenticating this request.
+ /// The POST body to send.
+ /// The response message.
+ Task PostAsync(string resource, string authKey, string content);
+
+ ///
+ /// Create and send a POST request with the given content to the given relative resource
+ /// using the given key for authentication.
+ ///
+ /// The relative resource to POST.
+ /// The key to use for authenticating this request.
+ /// The POST body to send.
+ /// The response message.
+ Task PostAsync(Uri resource, string authKey, string content);
+
+ ///
+ /// Create and send a DELETE request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to DELETE. Must be properly formatted as a
+ /// relative Uri.
+ /// The key to use for authenticating this request.
+ /// The response message.
+ Task DeleteAsync(string resource, string authKey);
+
+ ///
+ /// Create and send a DELETE request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to DELETE.
+ /// The key to use for authenticating this request.
+ /// The response message.
+ Task DeleteAsync(Uri resource, string authKey);
+ }
+}
diff --git a/Keen/IKeenHttpClientProvider.cs b/Keen/IKeenHttpClientProvider.cs
new file mode 100644
index 0000000..6212b74
--- /dev/null
+++ b/Keen/IKeenHttpClientProvider.cs
@@ -0,0 +1,23 @@
+using System;
+
+
+namespace Keen.Core
+{
+ ///
+ /// An instance of this type can provide an to be used to perform
+ /// requests against a given URL. Implement to customize how other parts of the SDK dispatch
+ /// requests to a keen IO endpoint.
+ ///
+ public interface IKeenHttpClientProvider
+ {
+ ///
+ /// Given a base URL, return an IKeenHttpClient against which requests can be made. The
+ /// intent is that all requests using this IKeenHttpClient will be to resources relative to
+ /// this base URL. It is expected that this IKeenHttpClient is thread-safe.
+ ///
+ /// The base URL, e.g. https://api.keen.io/3.0/
+ /// An IKeenHttpClient configured to handle requests to resources relative to the
+ /// given base URL.
+ IKeenHttpClient GetForUrl(Uri baseUrl);
+ }
+}
diff --git a/Keen/IProjectSettings.cs b/Keen/IProjectSettings.cs
index bea4c3c..45ed7a3 100644
--- a/Keen/IProjectSettings.cs
+++ b/Keen/IProjectSettings.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core
{
///
@@ -12,7 +8,10 @@ public interface IProjectSettings
{
///
/// The Keen.IO URL for this project. Usually this will be the
- /// server address and API version.
+ /// server address and API version. This should end with a '/'.
+ ///
+ /// - e.g. https://api.keen.io/3.0/
+ ///
///
string KeenUrl { get; }
diff --git a/Keen/Keen.csproj b/Keen/Keen.csproj
index 645e77a..f2c4092 100644
--- a/Keen/Keen.csproj
+++ b/Keen/Keen.csproj
@@ -43,10 +43,17 @@
Properties\SharedVersionInfo.cs
+
+
+
+
+
+
+
diff --git a/Keen/KeenClient.cs b/Keen/KeenClient.cs
index 1ba9557..00ba880 100644
--- a/Keen/KeenClient.cs
+++ b/Keen/KeenClient.cs
@@ -8,6 +8,7 @@
using System.Linq;
using System.Threading.Tasks;
+
namespace Keen.Core
{
///
@@ -16,7 +17,7 @@ namespace Keen.Core
public class KeenClient
{
private readonly IProjectSettings _prjSettings;
- private readonly Dictionary _globalProperties = new Dictionary();
+ private readonly IDictionary _globalProperties = new Dictionary();
///
/// EventCollection provides direct access to the Keen.IO EventCollection API methods.
@@ -35,7 +36,7 @@ public class KeenClient
///
/// EventCache provides a caching implementation allowing events to be cached locally
/// instead of being sent one at a time. It is not normally necessary to use this property.
- /// The implementation is responsible for cache maintenance policy, such as trimming
+ /// The implementation is responsible for cache maintenance policy, such as trimming
/// old entries to avoid excessive cache size.
///
public IEventCache EventCache { get; private set; }
@@ -83,11 +84,9 @@ private void ExecDynamicPropertyValue(string propName, IDynamicPropertyValue dyn
throw new KeenException(string.Format("Dynamic property \"{0}\" execution returned null", propName));
}
- ///
- ///
- ///
- /// A ProjectSettings instance containing the ProjectId and API keys
- public KeenClient(IProjectSettings prjSettings)
+ private KeenClient(IProjectSettings prjSettings,
+ IEventCache eventCache,
+ IKeenHttpClientProvider keenHttpClientProvider)
{
// Preconditions
if (null == prjSettings)
@@ -102,12 +101,30 @@ public KeenClient(IProjectSettings prjSettings)
throw new KeenException("A URL for the server address is required.");
_prjSettings = prjSettings;
- // The EventCollection and Event interface normally should not need to
- // be set by callers, so the default implementation is set up here. Users
- // may override these by injecting an implementation via the property.
- EventCollection = new EventCollection(_prjSettings);
- Event = new Event(_prjSettings);
- Queries = new Queries(_prjSettings);
+
+ if (null != eventCache)
+ {
+ EventCache = eventCache;
+ }
+
+ // Use the default provider if none was passed in.
+ keenHttpClientProvider = (keenHttpClientProvider ?? new KeenHttpClientProvider());
+
+ // These interfaces normally should not need to be set by client code, so the default
+ // implementation is set up here. These may be overridden by injecting an
+ // implementation via their respective properties.
+ EventCollection = new EventCollection(_prjSettings, keenHttpClientProvider);
+ Event = new Event(_prjSettings, keenHttpClientProvider);
+ Queries = new Queries(_prjSettings, keenHttpClientProvider);
+ }
+
+ ///
+ ///
+ ///
+ /// A ProjectSettings instance containing the ProjectId and API keys
+ public KeenClient(IProjectSettings prjSettings)
+ : this(prjSettings, null, null)
+ {
}
///
@@ -116,9 +133,14 @@ public KeenClient(IProjectSettings prjSettings)
/// A ProjectSettings instance containing the ProjectId and API keys
/// An IEventCache instance providing a caching strategy
public KeenClient(IProjectSettings prjSettings, IEventCache eventCache)
- : this(prjSettings)
+ : this(prjSettings, eventCache, null)
+ {
+ }
+
+ public KeenClient(IProjectSettings prjSettings,
+ IKeenHttpClientProvider keenHttpClientProvider)
+ : this(prjSettings, null, keenHttpClientProvider)
{
- EventCache = eventCache;
}
///
@@ -161,8 +183,8 @@ public void DeleteCollection(string collection)
public async Task GetSchemasAsync()
{
// Preconditions
- if (string.IsNullOrWhiteSpace(_prjSettings.MasterKey))
- throw new KeenException("Master API key is required for GetSchemas");
+ if (string.IsNullOrWhiteSpace(_prjSettings.ReadKey))
+ throw new KeenException("Read API key is required for GetSchemas");
return await Event.GetSchemas()
.ConfigureAwait(continueOnCapturedContext: false);
@@ -176,7 +198,7 @@ public JArray GetSchemas()
{
try
{
- return Event.GetSchemas().Result;
+ return GetSchemasAsync().Result;
}
catch (AggregateException ex)
{
@@ -193,8 +215,8 @@ public async Task GetSchemaAsync(string collection)
{
// Preconditions
KeenUtil.ValidateEventCollectionName(collection);
- if (string.IsNullOrWhiteSpace(_prjSettings.MasterKey))
- throw new KeenException("Master API key is required for GetSchema");
+ if (string.IsNullOrWhiteSpace(_prjSettings.ReadKey))
+ throw new KeenException("Read API key is required for GetSchema");
return await EventCollection.GetSchema(collection)
.ConfigureAwait(continueOnCapturedContext: false);
@@ -318,7 +340,7 @@ public async Task AddEventAsync(string collection, object eventInfo, IEnumerable
var jEvent = PrepareUserObject(eventInfo, addOns);
- // If an event cache has been provided, cache this event insead of sending it.
+ // If an event cache has been provided, cache this event instead of sending it.
if (null != EventCache)
await EventCache.Add(new CachedEvent(collection, jEvent))
.ConfigureAwait(false);
@@ -397,7 +419,7 @@ public void AddEvent(string collection, object eventInfo, IEnumerable add
}
///
- /// Submit all events found in the event cache. If an events are rejected by the server,
+ /// Submit all events found in the event cache. If any events are rejected by the server,
/// KeenCacheException will be thrown with a listing of the rejected events, each with
/// the error message it received.
///
@@ -415,7 +437,7 @@ public void SendCachedEvents()
}
///
- /// Submit all events found in the event cache. If an events are rejected by the server,
+ /// Submit all events found in the event cache. If any events are rejected by the server,
/// KeenCacheException will be thrown with a listing of the rejected events, each with
/// the error message it received.
///
@@ -472,11 +494,6 @@ public async Task>> GetQueries()
return await Queries.AvailableQueries();
}
-
-
-
-
-
///
/// Call any Keen.IO API function with the specified parameters.
///
@@ -488,7 +505,6 @@ public async Task QueryAsync(string queryName, Dictionary
/// Call any Keen.IO API function with the specified parameters. Refer to Keen API documentation for
/// details of request parameters and return type. Return type may be cast as dynamic.
@@ -954,6 +970,5 @@ public IEnumerable
+ /// Helps with performing HTTP operations destined for a Keen API endpoint. Helper methods in
+ /// this class will add appropriate headers and config to use the underlying HttpClient
+ /// in the way expected by the Keen IO API. This class should be long-lived and all public
+ /// methods are thread-safe, so ideal usage is to configure it once for a given base URL and
+ /// reuse it with relative resources to send requests for the duration of the app or module.
+ ///
+ internal class KeenHttpClient : IKeenHttpClient
+ {
+ private static readonly string JSON_CONTENT_TYPE = "application/json";
+ private static readonly string AUTH_HEADER_KEY = "Authorization";
+
+
+ // We don't destroy this manually. Whatever code provides the HttpClient directly or via an
+ // IHttpClientProvider should be sure to handle its lifetime.
+ private readonly HttpClient _httpClient = null;
+
+
+ internal KeenHttpClient(HttpClient httpClient)
+ {
+ _httpClient = httpClient;
+ }
+
+ internal static string GetRelativeUrl(string projectId, string resource)
+ {
+ return $"projects/{projectId}/{resource}";
+ }
+
+ ///
+ /// Create and send a GET request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to GET. Must be properly formatted as a
+ /// relative Uri.
+ /// The key to use for authenticating this request.
+ /// >The response message.
+ public Task GetAsync(string resource, string authKey)
+ {
+ var url = new Uri(resource, UriKind.Relative);
+
+ return GetAsync(url, authKey);
+ }
+
+ ///
+ /// Create and send a GET request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to GET.
+ /// The key to use for authenticating this request.
+ /// >The response message.
+ public Task GetAsync(Uri resource, string authKey)
+ {
+ KeenHttpClient.RequireAuthKey(authKey);
+
+ HttpRequestMessage get = CreateRequest(HttpMethod.Get, resource, authKey);
+
+ return _httpClient.SendAsync(get);
+ }
+
+ // TODO : Instead of (or in addition to) string, also accept HttpContent content and/or
+ // JObject content?
+
+ ///
+ /// Create and send a POST request with the given content to the given relative resource
+ /// using the given key for authentication.
+ ///
+ /// The relative resource to POST. Must be properly formatted as a
+ /// relative Uri.
+ /// The key to use for authenticating this request.
+ /// The POST body to send.
+ /// >The response message.
+ public Task PostAsync(string resource, string authKey, string content)
+ {
+ var url = new Uri(resource, UriKind.Relative);
+
+ return PostAsync(url, authKey, content);
+ }
+
+ ///
+ /// Create and send a POST request with the given content to the given relative resource
+ /// using the given key for authentication.
+ ///
+ /// The relative resource to POST.
+ /// The key to use for authenticating this request.
+ /// The POST body to send.
+ /// >The response message.
+ public async Task PostAsync(Uri resource,
+ string authKey,
+ string content)
+ {
+ KeenHttpClient.RequireAuthKey(authKey);
+
+ if (string.IsNullOrWhiteSpace(content))
+ {
+ // Technically, we can encode an empty string or whitespace, but why? For now
+ // we use GET for querying. If we ever need to POST with no content, we should
+ // reorganize the logic below to never create/set the content stream.
+ throw new ArgumentNullException(nameof(content), "Unexpected empty content.");
+ }
+
+ // If we switch PCL profiles, instead use MediaTypeFormatters (or ObjectContent)?,
+ // like here?: https://msdn.microsoft.com/en-us/library/system.net.http.httpclientextensions.putasjsonasync(v=vs.118).aspx
+ using (var contentStream =
+ new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content))))
+ {
+ // TODO : Amake sure this is the same as Add("content-type", "application/json")
+ contentStream.Headers.ContentType =
+ new MediaTypeHeaderValue(KeenHttpClient.JSON_CONTENT_TYPE);
+
+ HttpRequestMessage post = CreateRequest(HttpMethod.Post, resource, authKey);
+ post.Content = contentStream;
+
+ return await _httpClient.SendAsync(post).ConfigureAwait(false);
+
+ // TODO : Should we do the KeenUtil.CheckApiErrorCode() here?
+ // TODO : Should we check the if (!responseMsg.IsSuccessStatusCode) here too?
+ // TODO : If we centralize error checking in this class we could have variations
+ // of these helpers that return string or JToken or JArray or JObject. It might
+ // also be nice for those options to optionally hand back the raw
+ // HttpResponseMessage in an out param if desired?
+ // TODO : Use CallerMemberNameAttribute to print error messages?
+ // http://stackoverflow.com/questions/3095696/how-do-i-get-the-calling-method-name-and-type-using-reflection?noredirect=1&lq=1
+ }
+ }
+
+ ///
+ /// Create and send a DELETE request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to DELETE. Must be properly formatted as a
+ /// relative Uri.
+ /// The key to use for authenticating this request.
+ /// The response message.
+ public Task DeleteAsync(string resource, string authKey)
+ {
+ var url = new Uri(resource, UriKind.Relative);
+
+ return DeleteAsync(url, authKey);
+ }
+
+ ///
+ /// Create and send a DELETE request to the given relative resource using the given key for
+ /// authentication.
+ ///
+ /// The relative resource to DELETE.
+ /// The key to use for authenticating this request.
+ /// The response message.
+ public Task DeleteAsync(Uri resource, string authKey)
+ {
+ KeenHttpClient.RequireAuthKey(authKey);
+
+ HttpRequestMessage delete = CreateRequest(HttpMethod.Delete, resource, authKey);
+
+ return _httpClient.SendAsync(delete);
+ }
+
+ private static HttpRequestMessage CreateRequest(HttpMethod verb,
+ Uri resource,
+ string authKey)
+ {
+ var request = new HttpRequestMessage()
+ {
+ RequestUri = resource,
+ Method = verb
+ };
+
+ request.Headers.Add(KeenHttpClient.AUTH_HEADER_KEY, authKey);
+
+ return request;
+ }
+
+ private static void RequireAuthKey(string authKey)
+ {
+ if (string.IsNullOrWhiteSpace(authKey))
+ {
+ throw new ArgumentNullException(nameof(authKey), "Auth key is required.");
+ }
+ }
+ }
+}
diff --git a/Keen/KeenHttpClientFactory.cs b/Keen/KeenHttpClientFactory.cs
new file mode 100644
index 0000000..191029c
--- /dev/null
+++ b/Keen/KeenHttpClientFactory.cs
@@ -0,0 +1,260 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+
+namespace Keen.Core
+{
+ ///
+ /// A set of factory methods to help in creating see cref="IKeenHttpClient"/> instances. These
+ /// are useful when implementing see cref="IKeenHttpClientProvider"/> so that the constructed
+ /// instances have the right mix of default and custom configuration.
+ ///
+ public static class KeenHttpClientFactory
+ {
+ private static readonly IEnumerable> DEFAULT_HEADERS =
+ new[] { new KeyValuePair("Keen-Sdk", KeenUtil.GetSdkVersion()) };
+
+
+ private class LoggingHttpHandler : DelegatingHandler
+ {
+ protected override Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ // TODO : Log stuff before and after request, then move to its own file.
+
+ // Now dispatch to the inner handler via the base impl.
+ return base.SendAsync(request, cancellationToken);
+ }
+ }
+
+
+ private static HttpMessageHandler CreateHandlerChainInternal(
+ HttpClientHandler innerHandler,
+ IEnumerable handlers)
+ {
+ // NOTE : There is no WebProxy available to the PCL profile, so we have to create an
+ // IWebProxy implementation manually. Proxy is only supported on HttpClientHandler, and
+ // not directly on DelegatingHandler, so handle that too. Basically only support Proxy
+ // if client code does *not* give us an HttpClientHandler. Or else set the Proxy on
+ // their handler, but make sure it's not already set.
+
+ // Example of how setting Proxy works in big .NET where the WebProxy class exists:
+ //
+ // new HttpClientHandler()
+ // {
+ // Proxy = WebProxy("http://localhost:8888", false),
+ // UseProxy = true
+ // };
+
+
+ // TODO : Also, to support Proxy, we have to realize we'd be turning it on for a given
+ // HttpClientHandler already installed for the HttpClient in the cache for a given URL.
+ // Since modifications aren't allowed for HttpClients/*Handlers, we would replace the
+ // HttpClient, which would affect all future users of the cache requesting an
+ // HttpClient for that URL, when really we want an abstraction to keep that sort of
+ // setting tied to, which maybe is this KeenHttpClient?
+
+
+ HttpMessageHandler handlerChain = (innerHandler ?? new HttpClientHandler());
+
+ if (null == handlers)
+ {
+ return handlerChain;
+ }
+
+ foreach (var handler in handlers.Reverse())
+ {
+ if (null == handler)
+ {
+ throw new ArgumentNullException(nameof(handlers),
+ "One of the given DelegatingHandler params was null.");
+ }
+
+ if (null != handler.InnerHandler)
+ {
+ throw new ArgumentException("Encountered a non-null InnerHandler in handler " +
+ "chain, which would be overwritten.",
+ nameof(handlers));
+ }
+
+ // This will throw if the given handler has already started any requests.
+ // Basically all properties on all HttpClient/*Handler variations call
+ // CheckDisposedOrStarted() in any setter, so the entire HttpClient is pretty much
+ // locked down once it starts getting used.
+ handler.InnerHandler = handlerChain;
+ handlerChain = handler;
+ }
+
+ return handlerChain;
+ }
+
+ private static IEnumerable CreateDefaultDelegatingHandlers()
+ {
+ // TODO : Put more custom handlers in here, like retry/failure/proxy/logging handlers.
+
+ // Create these every time, since *Handlers can't have properties changed after they've
+ // started handling requests for an HttpClient.
+ return new[] { new LoggingHttpHandler() };
+ }
+
+ ///
+ /// Create the default handler pipeline with only Keen internal handlers installed.
+ ///
+ /// returns>The default handler chain.
+ public static HttpMessageHandler CreateDefaultHandlerChain()
+ {
+ return KeenHttpClientFactory.CreateHandlerChainInternal(
+ null,
+ KeenHttpClientFactory.CreateDefaultDelegatingHandlers());
+ }
+
+ ///
+ /// Create an HttpMessageHandler representing the handler pipeline. We will construct the
+ /// HTTP handler pipeline such that provided handlers are called in order for requests, and
+ /// receive responses in reverse order. Keen internal handlers will defer to the first
+ /// DelegatingHandler and the pipeline will terminate at our HttpClientHandler.
+ ///
+ /// Handlers to be chained in the pipeline.
+ /// returns>The entire handler chain.
+ public static HttpMessageHandler CreateHandlerChain(params DelegatingHandler[] handlers)
+ {
+ return KeenHttpClientFactory.CreateHandlerChain(null, handlers);
+ }
+
+ ///
+ /// Create an HttpMessageHandler representing the handler pipeline. We will construct the
+ /// HTTP handler pipeline such that provided handlers are called in order for requests, and
+ /// receive responses in reverse order. Keen internal handlers will defer to the first
+ /// DelegatingHandler and the pipeline will terminate at our HttpClientHandler or to the
+ /// given HttpClientHandler if present, in case client code wants to do something like use
+ /// WebRequestHandler functionality or otherwise add custom behavior.
+ ///
+ /// Terminating HttpClientHandler.
+ /// Handlers to be chained in the pipeline.
+ /// The entire handler chain.
+ public static HttpMessageHandler CreateHandlerChain(HttpClientHandler innerHandler,
+ params DelegatingHandler[] handlers)
+ {
+ // We put our handlers first. Client code can look at the final state of the request
+ // this way. Overwriting built-in handler state is shooting oneself in the foot.
+ IEnumerable intermediateHandlers =
+ KeenHttpClientFactory.CreateDefaultDelegatingHandlers().Concat(handlers);
+
+ return KeenHttpClientFactory.CreateHandlerChainInternal(innerHandler,
+ intermediateHandlers);
+ }
+
+ // NOTE : BaseUrl should have a final slash or the last Uri part is discarded. Also,
+ // relative urls can *not* start with a slash.
+
+ // Not exposed so that 3rd party code doesn't accidentally build a KeenHttpClient without
+ // our handlers installed, which wouldn't be ideal.
+ private static KeenHttpClient Create(Uri baseUrl,
+ IHttpClientProvider httpClientProvider,
+ Func getHandlerChain)
+ {
+ if (!baseUrl.IsAbsoluteUri)
+ {
+ throw new ArgumentException(
+ "The given base Url must be in the form of an absolute Uri.",
+ nameof(baseUrl));
+ }
+
+ // Delay actual creation of the handler chain by passing in a Func<> to create it. This
+ // way if HttpClient already exists, we won't bother creating/modifying handlers.
+ var httpClient = httpClientProvider.GetOrCreateForUrl(
+ baseUrl,
+ getHandlerChain,
+ KeenHttpClientFactory.DEFAULT_HEADERS);
+
+ var newClient = new KeenHttpClient(httpClient);
+
+ return newClient;
+ }
+
+ ///
+ /// Construct an IKeenHttpClient for the given base URL, configured with an HttpClient that
+ /// is retrieved and/or stored in the given IHttpClientProvider. If necessary, the
+ /// HttpClient is created and configured with the default set of HTTP handlers.
+ ///
+ ///
+ ///
+ ///
+ /// The base URL for the constructed IKeenHttpClient.
+ /// The provider used to retrieve the HttpClient.
+ /// A new IKeenHttpClient for the given base URL.
+ public static IKeenHttpClient Create(Uri baseUrl, IHttpClientProvider httpClientProvider)
+ {
+ return KeenHttpClientFactory.Create(
+ baseUrl,
+ httpClientProvider,
+ () => KeenHttpClientFactory.CreateDefaultHandlerChain());
+ }
+
+ ///
+ /// Construct an IKeenHttpClient for the given base URL, configured with an HttpClient that
+ /// is retrieved and/or stored in the given IHttpClientProvider, and if necessary,
+ /// configured with the given HTTP handlers.
+ ///
+ ///
+ ///
+ ///
+ /// The base URL for the constructed IKeenHttpClient.
+ /// The provider used to retrieve the HttpClient.
+ /// HTTP handler terminating the handler chain.
+ /// Handlers to be chained in the pipeline.
+ /// A new IKeenHttpClient for the given base URL.
+ public static IKeenHttpClient Create(Uri baseUrl,
+ IHttpClientProvider httpClientProvider,
+ HttpClientHandler innerHandler,
+ params DelegatingHandler[] handlers)
+ {
+ return KeenHttpClientFactory.Create(
+ baseUrl,
+ httpClientProvider,
+ () => KeenHttpClientFactory.CreateHandlerChain(innerHandler, handlers));
+ }
+
+ ///
+ /// Construct an IKeenHttpClient for the given base URL, configured with an HttpClient that
+ /// is retrieved and/or stored in the given IHttpClientProvider, and if necessary,
+ /// configured with the given HTTP handlers in a lazy fashion only if construction is
+ /// necessary. Note that the given handler factory function could be called under a lock,
+ /// so care should be taken in multi-threaded scenarios.
+ ///
+ ///
+ ///
+ ///
+ /// The base URL for the constructed IKeenHttpClient.
+ /// The provider used to retrieve the HttpClient.
+ /// A factory function called if construction of the HttpClient
+ /// is necessary. It should return an optional HttpClientHandler to terminate the
+ /// handler chain, as well as an optional list of intermediate HTTP handlers to be
+ /// chained in the pipeline.
+ /// A new IKeenHttpClient for the given base URL.
+ public static IKeenHttpClient Create(
+ Uri baseUrl,
+ IHttpClientProvider httpClientProvider,
+ Func>> getHandlers)
+ {
+ Func getHandlerChain = () =>
+ {
+ Tuple> handlers =
+ getHandlers?.Invoke();
+
+ return KeenHttpClientFactory.CreateHandlerChainInternal(handlers.Item1,
+ handlers.Item2);
+ };
+
+ return KeenHttpClientFactory.Create(
+ baseUrl,
+ httpClientProvider,
+ getHandlerChain);
+ }
+ }
+}
diff --git a/Keen/KeenHttpClientProvider.cs b/Keen/KeenHttpClientProvider.cs
new file mode 100644
index 0000000..2703d8b
--- /dev/null
+++ b/Keen/KeenHttpClientProvider.cs
@@ -0,0 +1,26 @@
+using System;
+
+
+namespace Keen.Core
+{
+ ///
+ /// An implementation of that uses the default
+ /// creation logic and relies on the
+ /// class as an .
+ ///
+ internal class KeenHttpClientProvider : IKeenHttpClientProvider
+ {
+ ///
+ /// Given a base URL, return an IKeenHttpClient against which requests can be made.
+ ///
+ /// The base URL, e.g. https://api.keen.io/3.0/
+ /// An IKeenHttpClient configured to handle requests to resources relative to the
+ /// given base URL.
+ public IKeenHttpClient GetForUrl(Uri baseUrl)
+ {
+ var keenHttpClient = KeenHttpClientFactory.Create(baseUrl, HttpClientCache.Instance);
+
+ return keenHttpClient;
+ }
+ }
+}
diff --git a/Keen/KeenUtil.cs b/Keen/KeenUtil.cs
index 656827f..07005ca 100644
--- a/Keen/KeenUtil.cs
+++ b/Keen/KeenUtil.cs
@@ -2,13 +2,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.IO;
using System.Linq;
-using System.Net.Http;
using System.Reflection;
-using System.Text;
using System.Text.RegularExpressions;
-using System.Threading.Tasks;
+
namespace Keen.Core
{
@@ -25,7 +22,7 @@ static KeenUtil()
///
/// Retrieve a string representing the current version of the Keen IO SDK, as defined by
- /// the AssemblyInformationVersion.
+ /// the AssemblyInformationalVersion.
///
/// The SDK version string.
public static string GetSdkVersion()
@@ -64,18 +61,6 @@ public static string ToSafeString(this object obj)
return (obj ?? string.Empty).ToString();
}
- public static int? TryGetInt(this string s)
- {
- int i;
- return int.TryParse(s, out i) ? (int?)i : null;
- }
-
- public static double? TryGetDouble(this string s)
- {
- double i;
- return double.TryParse(s, out i) ? (double?)i : null;
- }
-
///
/// Apply property name restrictions. Throws KeenException with an
/// explanation if a collection name is unacceptable.
@@ -173,7 +158,6 @@ public static Exception GetBulkApiError(JObject apiResponse)
}
}
-
///
/// Check the 'error_code' field and throw the appropriate exception if non-null.
///
@@ -181,8 +165,8 @@ public static Exception GetBulkApiError(JObject apiResponse)
public static void CheckApiErrorCode(dynamic apiResponse)
{
if (apiResponse is JArray) return;
-
- var errorCode = (string) apiResponse.SelectToken("$.error_code");
+
+ var errorCode = (string)apiResponse.SelectToken("$.error_code");
if (errorCode != null)
{
diff --git a/Keen/ProjectSettingsProvider.cs b/Keen/ProjectSettingsProvider.cs
index 1a9eb0b..9fe044c 100644
--- a/Keen/ProjectSettingsProvider.cs
+++ b/Keen/ProjectSettingsProvider.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core
{
public class ProjectSettingsProvider : IProjectSettings
@@ -38,11 +33,13 @@ public class ProjectSettingsProvider : IProjectSettings
///
/// Obtains project setting values as constructor parameters
///
- /// Base Keen.IO service URL, required
+ ///
/// Keen project id, required
- /// Master API key, required for getting schema or deleting collections
+ /// Master API key, required for certain operations, such as
+ /// getting schema or deleting collections
/// Write API key, required for inserting events
- /// Read API key
+ /// Read API key, required for performing queries
+ /// Base Keen.IO service URL
public ProjectSettingsProvider(string projectId, string masterKey = "", string writeKey = "", string readKey = "", string keenUrl = null)
{
KeenUrl = keenUrl ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/";
diff --git a/Keen/Properties/AssemblyInfo.cs b/Keen/Properties/AssemblyInfo.cs
index fd43530..66bd3a7 100644
--- a/Keen/Properties/AssemblyInfo.cs
+++ b/Keen/Properties/AssemblyInfo.cs
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
+using System.Runtime.CompilerServices;
// Friendly name and description for this assembly.
@@ -8,4 +9,6 @@
[assembly: NeutralResourcesLanguage("en")]
+[assembly: InternalsVisibleTo("Keen.Net.Test")]
+
// See also: SharedAssemblyInfo.cs and SharedVersionInfo.cs
diff --git a/Keen/Query/IQueries.cs b/Keen/Query/IQueries.cs
index 71565e8..9d8b9c2 100644
--- a/Keen/Query/IQueries.cs
+++ b/Keen/Query/IQueries.cs
@@ -1,10 +1,8 @@
using Newtonsoft.Json.Linq;
-using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
+
namespace Keen.Core.Query
{
public interface IQueries
diff --git a/Keen/Query/MultiAnalysisParam.cs b/Keen/Query/MultiAnalysisParam.cs
index 002b719..e62da35 100644
--- a/Keen/Query/MultiAnalysisParam.cs
+++ b/Keen/Query/MultiAnalysisParam.cs
@@ -1,9 +1,4 @@
-using Newtonsoft.Json;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core.Query
{
public sealed class MultiAnalysisParam
@@ -36,7 +31,7 @@ public sealed class Metric
/// A user defined string that acts as a name for the analysis.
/// This will be returned in the results so the various analyses are easily identifiable.
/// The metric type.
- public MultiAnalysisParam( string label, Metric analysis)
+ public MultiAnalysisParam(string label, Metric analysis)
{
Label = label;
Analysis = analysis;
diff --git a/Keen/Query/Queries.cs b/Keen/Query/Queries.cs
index 6e2b772..b4821e9 100644
--- a/Keen/Query/Queries.cs
+++ b/Keen/Query/Queries.cs
@@ -2,14 +2,10 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
-using System.Dynamic;
-using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Text;
using System.Threading.Tasks;
+
namespace Keen.Core.Query
{
///
@@ -17,54 +13,88 @@ namespace Keen.Core.Query
///
internal class Queries : IQueries
{
- private IProjectSettings _prjSettings;
- private string _serverUrl;
+ private readonly IKeenHttpClient _keenHttpClient;
+ private readonly string _queryRelativeUrl;
+ private readonly string _key;
+
- public Queries(IProjectSettings prjSettings)
+ internal Queries(IProjectSettings prjSettings,
+ IKeenHttpClientProvider keenHttpClientProvider)
{
- _prjSettings = prjSettings;
+ if (null == prjSettings)
+ {
+ throw new ArgumentNullException(nameof(prjSettings),
+ "Project Settings must be provided.");
+ }
- _serverUrl = string.Format("{0}projects/{1}/{2}",
- _prjSettings.KeenUrl, _prjSettings.ProjectId, KeenConstants.QueriesResource);
- }
+ if (null == keenHttpClientProvider)
+ {
+ throw new ArgumentNullException(nameof(keenHttpClientProvider),
+ "A KeenHttpClient provider must be provided.");
+ }
+ if (string.IsNullOrWhiteSpace(prjSettings.KeenUrl) ||
+ !Uri.IsWellFormedUriString(prjSettings.KeenUrl, UriKind.Absolute))
+ {
+ throw new KeenException(
+ "A properly formatted KeenUrl must be provided via Project Settings.");
+ }
+
+ var serverBaseUrl = new Uri(prjSettings.KeenUrl);
+ _keenHttpClient = keenHttpClientProvider.GetForUrl(serverBaseUrl);
+ _queryRelativeUrl = KeenHttpClient.GetRelativeUrl(prjSettings.ProjectId,
+ KeenConstants.QueriesResource);
+
+ // TODO : The Python SDK has changed to not automatically falling back, but rather
+ // throwing so that client devs learn to use the most appropriate key. So here we
+ // really could or should just demand the ReadKey.
+ _key = string.IsNullOrWhiteSpace(prjSettings.MasterKey) ?
+ prjSettings.ReadKey : prjSettings.MasterKey;
+ }
- private async Task KeenWebApiRequest(string operation = "", Dictionary parms = null)
+ private async Task KeenWebApiRequest(string operation = "",
+ Dictionary parms = null)
{
- // Either an API read key or a master key is required
- var key = string.IsNullOrWhiteSpace(_prjSettings.MasterKey) ? _prjSettings.ReadKey : _prjSettings.MasterKey;
- if (string.IsNullOrWhiteSpace(key))
- throw new KeenException("An API ReadKey or MasterKey is required");
+ if (string.IsNullOrWhiteSpace(_key))
+ {
+ throw new KeenException("An API ReadKey or MasterKey is required.");
+ }
- var parmVals = parms == null ? "" : string.Join("&", from p in parms.Keys
- where !string.IsNullOrEmpty(parms[p])
- select string.Format("{0}={1}", p, Uri.EscapeDataString(parms[p])));
+ var parmVals = (parms == null) ?
+ "" : string.Join("&", from p in parms.Keys
+ where !string.IsNullOrEmpty(parms[p])
+ select string.Format("{0}={1}",
+ p,
+ Uri.EscapeDataString(parms[p])));
var url = string.Format("{0}{1}{2}",
- _serverUrl,
- string.IsNullOrWhiteSpace(operation) ? "" : "/" + operation,
- string.IsNullOrWhiteSpace(parmVals) ? "" : "?" + parmVals);
+ _queryRelativeUrl,
+ string.IsNullOrWhiteSpace(operation) ? "" : "/" + operation,
+ string.IsNullOrWhiteSpace(parmVals) ? "" : "?" + parmVals);
- using (var client = new HttpClient())
- {
- client.DefaultRequestHeaders.Add("Authorization", key);
- client.DefaultRequestHeaders.Add("Keen-Sdk", KeenUtil.GetSdkVersion());
+ var responseMsg = await _keenHttpClient.GetAsync(url, _key).ConfigureAwait(false);
- var responseMsg = await client.GetAsync(url).ConfigureAwait(false);
- var responseString = await responseMsg.Content.ReadAsStringAsync().ConfigureAwait(false);
- var response = JObject.Parse(responseString);
+ var responseString = await responseMsg
+ .Content
+ .ReadAsStringAsync()
+ .ConfigureAwait(false);
- // error checking, throw an exception with information from the json
- // response if available, then check the HTTP response.
- KeenUtil.CheckApiErrorCode((dynamic)response);
- if (!responseMsg.IsSuccessStatusCode)
- throw new KeenException("Request failed with status: " + responseMsg.StatusCode);
+ var response = JObject.Parse(responseString);
- return response;
+ // error checking, throw an exception with information from the json
+ // response if available, then check the HTTP response.
+ KeenUtil.CheckApiErrorCode(response);
+
+ if (!responseMsg.IsSuccessStatusCode)
+ {
+ throw new KeenException("Request failed with status: " +
+ responseMsg.StatusCode);
}
+
+ return response;
}
- public async Task>> AvailableQueries()
+ public async Task>> AvailableQueries()
{
var reply = await KeenWebApiRequest().ConfigureAwait(false);
return from j in reply.Children()
@@ -75,7 +105,6 @@ public async Task>> AvailableQueries()
#region metric
-
public async Task Metric(string queryName, Dictionary parms)
{
if (string.IsNullOrEmpty(queryName))
@@ -92,7 +121,7 @@ public async Task Metric(QueryType queryType, string collection, string
throw new ArgumentNullException("queryType");
if (string.IsNullOrWhiteSpace(collection))
throw new ArgumentNullException("collection");
- if (string.IsNullOrWhiteSpace(targetProperty) && (queryType!=QueryType.Count()))
+ if (string.IsNullOrWhiteSpace(targetProperty) && (queryType != QueryType.Count()))
throw new ArgumentNullException("targetProperty");
var parms = new Dictionary();
@@ -246,24 +275,23 @@ public async Task> Extract(string collection, QueryTimeframe timeframe = null, IEnumerable filters = null, int latest = 0, string email = "")
{
var parms = new Dictionary();
- parms.Add(KeenConstants.QueryParmEventCollection, collection);
- parms.Add(KeenConstants.QueryParmTimeframe, timeframe.ToSafeString());
- parms.Add(KeenConstants.QueryParmFilters, filters == null ? "" : JArray.FromObject(filters).ToString());
- parms.Add(KeenConstants.QueryParmEmail, email);
- parms.Add(KeenConstants.QueryParmLatest, latest > 0 ? latest.ToString() : "");
+ parms.Add(KeenConstants.QueryParmEventCollection, collection);
+ parms.Add(KeenConstants.QueryParmTimeframe, timeframe.ToSafeString());
+ parms.Add(KeenConstants.QueryParmFilters, filters == null ? "" : JArray.FromObject(filters).ToString());
+ parms.Add(KeenConstants.QueryParmEmail, email);
+ parms.Add(KeenConstants.QueryParmLatest, latest > 0 ? latest.ToString() : "");
var reply = await KeenWebApiRequest(KeenConstants.QueryExtraction, parms).ConfigureAwait(false);
return from i in reply.Value("result") select (dynamic)i;
}
-
-
public async Task Funnel(IEnumerable steps,
QueryTimeframe timeframe = null, string timezone = "")
{
@@ -280,9 +308,7 @@ public async Task Funnel(IEnumerable steps,
return o;
}
-
-
- public async Task> MultiAnalysis(string collection, IEnumerable analysisParams, QueryTimeframe timeframe = null, IEnumerable filters = null, string timezone = "")
+ public async Task> MultiAnalysis(string collection, IEnumerable analysisParams, QueryTimeframe timeframe = null, IEnumerable filters = null, string timezone = "")
{
var jObs = analysisParams.Select(x =>
new JProperty( x.Label, JObject.FromObject( new {analysis_type = x.Analysis, target_property = x.TargetProperty })));
@@ -320,7 +346,7 @@ public async Task>>> Mul
var reply = await KeenWebApiRequest(KeenConstants.QueryMultiAnalysis, parms).ConfigureAwait(false);
- var result = new List>>();
+ var result = new List>>();
foreach (JObject i in reply.Value("result"))
{
var d = new Dictionary();
@@ -339,7 +365,7 @@ public async Task>>> Mul
return result;
}
- public async Task>>> MultiAnalysis(string collection, IEnumerable analysisParams, QueryTimeframe timeframe = null, QueryInterval interval = null, IEnumerable filters = null, string timezone = "")
+ public async Task>>> MultiAnalysis(string collection, IEnumerable analysisParams, QueryTimeframe timeframe = null, QueryInterval interval = null, IEnumerable filters = null, string timezone = "")
{
var jObs = analysisParams.Select(x => new JProperty(x.Label, JObject.FromObject(new { analysis_type = x.Analysis, target_property = x.TargetProperty })));
var parmsJson = JsonConvert.SerializeObject(new JObject(jObs), Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
@@ -395,7 +421,6 @@ public async Task())
{
-
if (p.Name == groupby)
grpVal = (string)p.Value;
else
diff --git a/Keen/Query/QueryAbsoluteTimeframe.cs b/Keen/Query/QueryAbsoluteTimeframe.cs
index c3ccee2..cac1a25 100644
--- a/Keen/Query/QueryAbsoluteTimeframe.cs
+++ b/Keen/Query/QueryAbsoluteTimeframe.cs
@@ -1,9 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+
namespace Keen.Core.Query
{
diff --git a/Keen/Query/QueryFilter.cs b/Keen/Query/QueryFilter.cs
index e622d8f..5e77125 100644
--- a/Keen/Query/QueryFilter.cs
+++ b/Keen/Query/QueryFilter.cs
@@ -1,6 +1,7 @@
using Newtonsoft.Json;
using System;
+
namespace Keen.Core.Query
{
///
@@ -40,7 +41,6 @@ public sealed class FilterOperator
///
public static FilterOperator Equals() { return new FilterOperator("eq"); }
-
///
/// Not equal to.
/// Use with string, number
@@ -91,12 +91,17 @@ public sealed class FilterOperator
///
public static FilterOperator Contains() { return new FilterOperator("contains"); }
+ ///
+ /// Filter on events that do not contain the specified property value.
+ /// Use with strings
+ ///
+ public static FilterOperator NotContains() { return new FilterOperator("not_contains"); }
+
///
/// Used to select events within a certain radius of the provided geo coordinate.
/// Use with geo analysis
///
public static FilterOperator Within() { return new FilterOperator("within"); }
-
}
@@ -135,15 +140,20 @@ public GeoValue(double longitude, double latitude, double maxDistanceMiles)
public QueryFilter()
{
-
+
}
public QueryFilter(string property, FilterOperator op, object value)
{
if (string.IsNullOrWhiteSpace(property))
- throw new ArgumentNullException("property");
- if (null == value)
- throw new ArgumentNullException("value");
+ {
+ throw new ArgumentNullException(nameof(property), "Property name is required.");
+ }
+
+ if (null == op)
+ {
+ throw new ArgumentNullException(nameof(op), "Filter operator is required.");
+ }
PropertyName = property;
Operator = op;
diff --git a/Keen/Query/QueryGroupValue.cs b/Keen/Query/QueryGroupValue.cs
index 6cb2bd3..6c5d906 100644
--- a/Keen/Query/QueryGroupValue.cs
+++ b/Keen/Query/QueryGroupValue.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core.Query
{
///
diff --git a/Keen/Query/QueryInterval.cs b/Keen/Query/QueryInterval.cs
index 089ec52..e6f21c3 100644
--- a/Keen/Query/QueryInterval.cs
+++ b/Keen/Query/QueryInterval.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core.Query
{
///
diff --git a/Keen/Query/QueryIntervalValue.cs b/Keen/Query/QueryIntervalValue.cs
index 8aa1d67..035b790 100644
--- a/Keen/Query/QueryIntervalValue.cs
+++ b/Keen/Query/QueryIntervalValue.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+
namespace Keen.Core.Query
{
diff --git a/Keen/Query/QueryRelativeTimeframe.cs b/Keen/Query/QueryRelativeTimeframe.cs
index 4e85f1a..6bf31f4 100644
--- a/Keen/Query/QueryRelativeTimeframe.cs
+++ b/Keen/Query/QueryRelativeTimeframe.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core.Query
{
///
diff --git a/Keen/Query/QueryTimeframe.cs b/Keen/Query/QueryTimeframe.cs
index a32fa6c..594072c 100644
--- a/Keen/Query/QueryTimeframe.cs
+++ b/Keen/Query/QueryTimeframe.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core.Query
{
public class QueryTimeframe
diff --git a/Keen/Query/QueryType.cs b/Keen/Query/QueryType.cs
index 55b91eb..f9c1079 100644
--- a/Keen/Query/QueryType.cs
+++ b/Keen/Query/QueryType.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
+
namespace Keen.Core.Query
{
public sealed class QueryType
diff --git a/README.md b/README.md
index 1491043..0b26aed 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,6 @@ Installation
The easiest way to get started with the Keen IO .NET SDK is to use the [KeenClient NuGet package](http://www.nuget.org/packages/KeenClient/).
-Note that there is currently an issue and workaround for installation of the library for .NET 3.5 targets using NuGet: [https://github.com/keenlabs/keen-sdk-net/issues/38](https://github.com/keenlabs/keen-sdk-net/issues/38).
-
Install the NuGet package by running the following command from the NuGet Package Manager Console:
```
diff --git a/SharedVersionInfo.cs b/SharedVersionInfo.cs
index ccb7bfc..f8d3a01 100644
--- a/SharedVersionInfo.cs
+++ b/SharedVersionInfo.cs
@@ -13,14 +13,14 @@
// AssemblyVersion can only contain numerical values, so no pre-release or metadata info like
// "-alpha123" can go in here.
-[assembly: AssemblyVersion("0.3.16")]
+[assembly: AssemblyVersion("0.3.17")]
// AssemblyInformationalVersion can have more information in non-numerical format. Here is
// where we could/should put pre-release and/or metadata info if we want to release a version
// as "1.2.3-beta" or similar.
-[assembly: AssemblyInformationalVersion("0.3.16")]
+[assembly: AssemblyInformationalVersion("0.3.17")]
// AssemblyFileVersion can and should differ in each assembly if we get into a situation where
// a given assembly needs to be rebuilt and we'd like to track that separately, but we don't
// intend to bump the SDK version nor the NuGet version. Leave it here until then.
-[assembly: AssemblyFileVersion("0.3.16")]
+[assembly: AssemblyFileVersion("0.3.17")]