diff --git a/Neo4jClient.Tests/Cypher/CypherFluentQueryCustomHeaderTests.cs b/Neo4jClient.Tests/Cypher/CypherFluentQueryCustomHeaderTests.cs new file mode 100644 index 000000000..bfc42b5df --- /dev/null +++ b/Neo4jClient.Tests/Cypher/CypherFluentQueryCustomHeaderTests.cs @@ -0,0 +1,64 @@ +using System.Collections.Specialized; +using Neo4jClient.Cypher; +using NSubstitute; +using NUnit.Framework; + +namespace Neo4jClient.Test.Cypher +{ + [TestFixture] + public class CypherFluentQueryCustomHeaderTests + { + [Test] + public void SetsMaxExecutionTimeAndCustomHeader_WhenUsingAReturnTypeQuery() + { + const string headerName = "HeaderName"; + const string headerValue = "TestHeaderValue"; + var client = Substitute.For(); + var customHeaders = new NameValueCollection {{headerName, headerValue}}; + + var query = new CypherFluentQuery(client) + .MaxExecutionTime(100) + .CustomHeaders(customHeaders) + .Match("n") + .Return("n") + .Query; + + Assert.AreEqual(100, query.MaxExecutionTime); + Assert.AreEqual(customHeaders, query.CustomHeaders); + } + + [Test] + public void SetsCustomHeader_WhenUsingAReturnTypeQuery() + { + const string headerName = "HeaderName"; + const string headerValue = "TestHeaderValue"; + var client = Substitute.For(); + var customHeaders = new NameValueCollection { { headerName, headerValue } }; + + var query = new CypherFluentQuery(client) + .CustomHeaders(customHeaders) + .Match("n") + .Return("n") + .Query; + + Assert.AreEqual(customHeaders, query.CustomHeaders); + } + + [Test] + public void SetsCustomHeader_WhenUsingANonReturnTypeQuery() + { + const string headerName = "HeaderName"; + const string headerValue = "TestHeaderValue"; + var customHeaders = new NameValueCollection { { headerName, headerValue } }; + + var client = Substitute.For(); + var query = new CypherFluentQuery(client) + .CustomHeaders(customHeaders) + .Match("n") + .Set("n.Value = 'value'") + .Query; + + Assert.AreEqual(customHeaders, query.CustomHeaders); + } + } +} diff --git a/Neo4jClient.Tests/Cypher/CypherFluentQueryMaxExecutionTimeTests.cs b/Neo4jClient.Tests/Cypher/CypherFluentQueryMaxExecutionTimeTests.cs new file mode 100644 index 000000000..3de5c9aa9 --- /dev/null +++ b/Neo4jClient.Tests/Cypher/CypherFluentQueryMaxExecutionTimeTests.cs @@ -0,0 +1,37 @@ +using NSubstitute; +using NUnit.Framework; +using Neo4jClient.Cypher; + +namespace Neo4jClient.Test.Cypher +{ + [TestFixture] + public class CypherFluentQueryMaxExecutionTimeTests + { + [Test] + public void SetsMaxExecutionTime_WhenUsingAReturnTypeQuery() + { + var client = Substitute.For(); + var query = new CypherFluentQuery(client) + .MaxExecutionTime(100) + .Match("n") + .Return("n") + .Query; + + Assert.AreEqual(100, query.MaxExecutionTime); + } + + [Test] + public void SetsMaxExecutionTime_WhenUsingANonReturnTypeQuery() + { + var client = Substitute.For(); + var query = new CypherFluentQuery(client) + .MaxExecutionTime(100) + .Match("n") + .Set("n.Value = 'value'") + .Query; + + Assert.AreEqual(100, query.MaxExecutionTime); + } + + } +} \ No newline at end of file diff --git a/Neo4jClient.Tests/GraphClientTests/ConnectTests.cs b/Neo4jClient.Tests/GraphClientTests/ConnectTests.cs index b793b88bd..61b58bad8 100644 --- a/Neo4jClient.Tests/GraphClientTests/ConnectTests.cs +++ b/Neo4jClient.Tests/GraphClientTests/ConnectTests.cs @@ -37,7 +37,6 @@ public void ShouldRetrieveApiEndpoints() using (var testHarness = new RestTestHarness()) { var graphClient = (GraphClient)testHarness.CreateAndConnectGraphClient(); - Assert.AreEqual("/node", graphClient.RootApiResponse.Node); Assert.AreEqual("/index/node", graphClient.RootApiResponse.NodeIndex); Assert.AreEqual("/index/relationship", graphClient.RootApiResponse.RelationshipIndex); @@ -186,7 +185,7 @@ public void PassesCorrectStreamHeader_WhenUseStreamIsTrue() .Returns(callInfo => { throw new NotImplementedException(); }); var graphClient = new GraphClient(new Uri("http://username:password@foo/db/data"), httpClient); - + try { graphClient.Connect(); diff --git a/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteCypherTests.cs b/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteCypherTests.cs index e372e0127..119df2273 100644 --- a/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteCypherTests.cs +++ b/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteCypherTests.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Linq; using System.Net; +using System.Net.Http; using NUnit.Framework; using Neo4jClient.ApiModels.Cypher; using Neo4jClient.Cypher; +using NSubstitute; namespace Neo4jClient.Test.GraphClientTests.Cypher { @@ -191,5 +196,162 @@ public void WhenExecuteCypherFails_ShouldRaiseCompletedWithException() Assert.AreEqual(-1, eventArgs.ResourcesReturned); } } + + /// + /// #75 + /// + [Test] + public void SendsCommandWithCorrectTimeout() + { + const string queryText = "MATCH n SET n.Value = 'value'"; + const int expectedMaxExecutionTime = 100; + + var cypherQuery = new CypherQuery(queryText, new Dictionary(), CypherResultMode.Set,CypherResultFormat.DependsOnEnvironment , maxExecutionTime: expectedMaxExecutionTime); + var cypherApiQuery = new CypherApiQuery(cypherQuery); + + using (var testHarness = new RestTestHarness + { + { + MockRequest.Get(""), + MockResponse.NeoRoot() + }, + { + MockRequest.PostObjectAsJson("/cypher", cypherApiQuery), + MockResponse.Http((int) HttpStatusCode.OK) + } + }) + { + var httpClient = testHarness.GenerateHttpClient(testHarness.BaseUri); + var graphClient = new GraphClient(new Uri(testHarness.BaseUri), httpClient); + graphClient.Connect(); + + httpClient.ClearReceivedCalls(); + ((IRawGraphClient)graphClient).ExecuteCypher(cypherQuery); + + var call = httpClient.ReceivedCalls().Single(); + var requestMessage = (HttpRequestMessage)call.GetArguments()[0]; + var maxExecutionTimeHeader = requestMessage.Headers.Single(h => h.Key == "max-execution-time"); + Assert.AreEqual(expectedMaxExecutionTime.ToString(CultureInfo.InvariantCulture), maxExecutionTimeHeader.Value.Single()); + } + } + + /// + /// #75 + /// + [Test] + public void DoesntSetMaxExecutionTime_WhenNotSet() + { + const string queryText = "MATCH n SET n.Value = 'value'"; + + var cypherQuery = new CypherQuery(queryText, new Dictionary(), CypherResultMode.Set); + var cypherApiQuery = new CypherApiQuery(cypherQuery); + + using (var testHarness = new RestTestHarness + { + { + MockRequest.Get(""), + MockResponse.NeoRoot() + }, + { + MockRequest.PostObjectAsJson("/cypher", cypherApiQuery), + MockResponse.Http((int) HttpStatusCode.OK) + } + }) + { + var httpClient = testHarness.GenerateHttpClient(testHarness.BaseUri); + var graphClient = new GraphClient(new Uri(testHarness.BaseUri), httpClient); + graphClient.Connect(); + + httpClient.ClearReceivedCalls(); + ((IRawGraphClient)graphClient).ExecuteCypher(cypherQuery); + + var call = httpClient.ReceivedCalls().Single(); + var requestMessage = (HttpRequestMessage)call.GetArguments()[0]; + Assert.IsFalse(requestMessage.Headers.Any(h => h.Key == "max-execution-time")); + } + } + + /// + /// #141 + /// + [Test] + public void SendsCommandWithCustomHeaders() + { + const string queryText = "MATCH n SET n.Value = 'value'"; + const int expectedMaxExecutionTime = 100; + const string headerName = "MyTestHeader"; + const string headerValue = "myTestHeaderValue"; + var customHeaders = new NameValueCollection(); + customHeaders.Add(headerName, headerValue); + + var cypherQuery = new CypherQuery(queryText, new Dictionary(), CypherResultMode.Set, CypherResultFormat.DependsOnEnvironment, maxExecutionTime: expectedMaxExecutionTime, customHeaders: customHeaders); + + var cypherApiQuery = new CypherApiQuery(cypherQuery); + + using (var testHarness = new RestTestHarness + { + { + MockRequest.Get(""), + MockResponse.NeoRoot() + }, + { + MockRequest.PostObjectAsJson("/cypher", cypherApiQuery), + MockResponse.Http((int) HttpStatusCode.OK) + } + }) + { + var httpClient = testHarness.GenerateHttpClient(testHarness.BaseUri); + var graphClient = new GraphClient(new Uri(testHarness.BaseUri), httpClient); + graphClient.Connect(); + + httpClient.ClearReceivedCalls(); + ((IRawGraphClient)graphClient).ExecuteCypher(cypherQuery); + + var call = httpClient.ReceivedCalls().Single(); + var requestMessage = (HttpRequestMessage)call.GetArguments()[0]; + var maxExecutionTimeHeader = requestMessage.Headers.Single(h => h.Key == "max-execution-time"); + Assert.AreEqual(expectedMaxExecutionTime.ToString(CultureInfo.InvariantCulture), maxExecutionTimeHeader.Value.Single()); + var customHeader = requestMessage.Headers.Single(h => h.Key == headerName); + Assert.IsNotNull(customHeader); + Assert.AreEqual(headerValue, customHeader.Value.Single()); + } + } + + + /// + /// #141 + /// + [Test] + public void DoesntSetHeaders_WhenNotSet() + { + const string queryText = "MATCH n SET n.Value = 'value'"; + + var cypherQuery = new CypherQuery(queryText, new Dictionary(), CypherResultMode.Set); + var cypherApiQuery = new CypherApiQuery(cypherQuery); + + using (var testHarness = new RestTestHarness + { + { + MockRequest.Get(""), + MockResponse.NeoRoot() + }, + { + MockRequest.PostObjectAsJson("/cypher", cypherApiQuery), + MockResponse.Http((int) HttpStatusCode.OK) + } + }) + { + var httpClient = testHarness.GenerateHttpClient(testHarness.BaseUri); + var graphClient = new GraphClient(new Uri(testHarness.BaseUri), httpClient); + graphClient.Connect(); + + httpClient.ClearReceivedCalls(); + ((IRawGraphClient)graphClient).ExecuteCypher(cypherQuery); + + var call = httpClient.ReceivedCalls().Single(); + var requestMessage = (HttpRequestMessage)call.GetArguments()[0]; + Assert.IsFalse(requestMessage.Headers.Any(h => h.Key == "max-execution-time")); + } + } } } diff --git a/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteGetCypherResultsTests.cs b/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteGetCypherResultsTests.cs index 49ca68763..296b6a11b 100644 --- a/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteGetCypherResultsTests.cs +++ b/Neo4jClient.Tests/GraphClientTests/Cypher/ExecuteGetCypherResultsTests.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; +using System.Net.Http; using NUnit.Framework; using Neo4jClient.ApiModels.Cypher; using Neo4jClient.Cypher; +using NSubstitute; namespace Neo4jClient.Test.GraphClientTests.Cypher { @@ -691,5 +694,125 @@ public void ShouldPromoteBadQueryResponseToNiceException() CollectionAssert.AreEqual(expectedStack, ex.NeoStackTrace); } } + + [Test] + public void SendsCommandWithCorrectTimeout() + { + const int expectedMaxExecutionTime = 100; + + const string queryText = @"START d=node({p0}), e=node({p1}) + MATCH p = allShortestPaths( d-[*..15]-e ) + RETURN p"; + + var parameters = new Dictionary + { + {"p0", 215}, + {"p1", 219} + }; + + + var cypherQuery = new CypherQuery(queryText, parameters, CypherResultMode.Set,CypherResultFormat.Transactional ,maxExecutionTime: expectedMaxExecutionTime); + var cypherApiQuery = new CypherApiQuery(cypherQuery); + + using (var testHarness = new RestTestHarness + { + { + MockRequest.Get(""), + MockResponse.NeoRoot() + }, + { + MockRequest.PostObjectAsJson("/cypher", cypherApiQuery), + MockResponse.Json(HttpStatusCode.OK, + @"{ + 'data' : [ [ { + 'start' : 'http://foo/db/data/node/215', + 'nodes' : [ 'http://foo/db/data/node/215', 'http://foo/db/data/node/0', 'http://foo/db/data/node/219' ], + 'length' : 2, + 'relationships' : [ 'http://foo/db/data/relationship/247', 'http://foo/db/data/relationship/257' ], + 'end' : 'http://foo/db/data/node/219' + } ], [ { + 'start' : 'http://foo/db/data/node/215', + 'nodes' : [ 'http://foo/db/data/node/215', 'http://foo/db/data/node/1', 'http://foo/db/data/node/219' ], + 'length' : 2, + 'relationships' : [ 'http://foo/db/data/relationship/248', 'http://foo/db/data/relationship/258' ], + 'end' : 'http://foo/db/data/node/219' + } ] ], + 'columns' : [ 'p' ] + }") + } + }) + { + var httpClient = testHarness.GenerateHttpClient(testHarness.BaseUri); + var graphClient = new GraphClient(new Uri(testHarness.BaseUri), httpClient); + graphClient.Connect(); + + httpClient.ClearReceivedCalls(); + ((IRawGraphClient)graphClient).ExecuteGetCypherResults(cypherQuery); + + var call = httpClient.ReceivedCalls().Single(); + var requestMessage = (HttpRequestMessage)call.GetArguments()[0]; + var maxExecutionTimeHeader = requestMessage.Headers.Single(h => h.Key == "max-execution-time"); + Assert.AreEqual(expectedMaxExecutionTime.ToString(CultureInfo.InvariantCulture), maxExecutionTimeHeader.Value.Single()); + } + } + + [Test] + public void DoesntSendMaxExecutionTime_WhenNotAddedToQuery() + { + const string queryText = @"START d=node({p0}), e=node({p1}) + MATCH p = allShortestPaths( d-[*..15]-e ) + RETURN p"; + + var parameters = new Dictionary + { + {"p0", 215}, + {"p1", 219} + }; + + var cypherQuery = new CypherQuery(queryText, parameters, CypherResultMode.Set); + var cypherApiQuery = new CypherApiQuery(cypherQuery); + + using (var testHarness = new RestTestHarness + { + { + MockRequest.Get(""), + MockResponse.NeoRoot() + }, + { + MockRequest.PostObjectAsJson("/cypher", cypherApiQuery), + MockResponse.Json(HttpStatusCode.OK, + @"{ + 'data' : [ [ { + 'start' : 'http://foo/db/data/node/215', + 'nodes' : [ 'http://foo/db/data/node/215', 'http://foo/db/data/node/0', 'http://foo/db/data/node/219' ], + 'length' : 2, + 'relationships' : [ 'http://foo/db/data/relationship/247', 'http://foo/db/data/relationship/257' ], + 'end' : 'http://foo/db/data/node/219' + } ], [ { + 'start' : 'http://foo/db/data/node/215', + 'nodes' : [ 'http://foo/db/data/node/215', 'http://foo/db/data/node/1', 'http://foo/db/data/node/219' ], + 'length' : 2, + 'relationships' : [ 'http://foo/db/data/relationship/248', 'http://foo/db/data/relationship/258' ], + 'end' : 'http://foo/db/data/node/219' + } ] ], + 'columns' : [ 'p' ] + }") + } + }) + { + var httpClient = testHarness.GenerateHttpClient(testHarness.BaseUri); + var graphClient = new GraphClient(new Uri(testHarness.BaseUri), httpClient); + graphClient.Connect(); + + httpClient.ClearReceivedCalls(); + ((IRawGraphClient)graphClient).ExecuteGetCypherResults(cypherQuery); + + var call = httpClient.ReceivedCalls().Single(); + var requestMessage = (HttpRequestMessage)call.GetArguments()[0]; + Assert.IsFalse(requestMessage.Headers.Any(h => h.Key == "max-execution-time")); + } + } + + } } diff --git a/Neo4jClient.Tests/Neo4jClient.Tests.csproj b/Neo4jClient.Tests/Neo4jClient.Tests.csproj index 7d891cc7a..a3252bec4 100644 --- a/Neo4jClient.Tests/Neo4jClient.Tests.csproj +++ b/Neo4jClient.Tests/Neo4jClient.Tests.csproj @@ -82,10 +82,12 @@ + + diff --git a/Neo4jClient/Cypher/CypherFluentQuery.cs b/Neo4jClient/Cypher/CypherFluentQuery.cs index a9d11873f..1b96207d4 100644 --- a/Neo4jClient/Cypher/CypherFluentQuery.cs +++ b/Neo4jClient/Cypher/CypherFluentQuery.cs @@ -6,6 +6,7 @@ using System.Linq; using Newtonsoft.Json.Serialization; using System.Collections; +using System.Collections.Specialized; using System.Threading.Tasks; namespace Neo4jClient.Cypher @@ -438,6 +439,19 @@ public ICypherFluentQuery Planner(CypherPlanner planner) } } + + public ICypherFluentQuery MaxExecutionTime(int milliseconds) + { + QueryWriter.MaxExecutionTime = milliseconds; + return this; + } + + public ICypherFluentQuery CustomHeaders(NameValueCollection headers) + { + QueryWriter.CustomHeaders = headers; + return this; + } + public static string ApplyCamelCase(bool isCamelCase, string propertyName) { return isCamelCase ? diff --git a/Neo4jClient/Cypher/CypherFluentQuery`With.cs b/Neo4jClient/Cypher/CypherFluentQuery`With.cs index 6bf965d6b..8acb1395e 100644 --- a/Neo4jClient/Cypher/CypherFluentQuery`With.cs +++ b/Neo4jClient/Cypher/CypherFluentQuery`With.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.Linq.Expressions; namespace Neo4jClient.Cypher diff --git a/Neo4jClient/Cypher/CypherQuery.cs b/Neo4jClient/Cypher/CypherQuery.cs index 96d695e62..e4e131479 100644 --- a/Neo4jClient/Cypher/CypherQuery.cs +++ b/Neo4jClient/Cypher/CypherQuery.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using Neo4jClient.Serialization; @@ -21,6 +22,8 @@ public class CypherQuery readonly CypherResultMode resultMode; readonly CypherResultFormat resultFormat; readonly IContractResolver jsonContractResolver; + readonly int? maxExecutionTime; + readonly NameValueCollection customHeaders; public CypherQuery( string queryText, @@ -36,13 +39,18 @@ public class CypherQuery IDictionary queryParameters, CypherResultMode resultMode, CypherResultFormat resultFormat, - IContractResolver contractResolver = null) + IContractResolver contractResolver = null, + int? maxExecutionTime = null, + NameValueCollection customHeaders = null + ) { this.queryText = queryText; this.queryParameters = queryParameters; this.resultMode = resultMode; this.resultFormat = resultFormat; jsonContractResolver = contractResolver ?? GraphClient.DefaultJsonContractResolver; + this.maxExecutionTime = maxExecutionTime; + this.customHeaders = customHeaders; } public IDictionary QueryParameters @@ -70,6 +78,20 @@ public IContractResolver JsonContractResolver get { return jsonContractResolver; } } + public int? MaxExecutionTime + { + get { return maxExecutionTime; } + } + + /// + /// Custom headers to add to REST calls to Neo4j server. + /// Example usage: This can be used to provide extra information to a Neo4j Loadbalancer. + /// + public NameValueCollection CustomHeaders + { + get { return customHeaders;} + } + CustomJsonSerializer BuildSerializer() { return new CustomJsonSerializer { JsonConverters = GraphClient.DefaultJsonConverters, JsonContractResolver = jsonContractResolver }; diff --git a/Neo4jClient/Cypher/ICypherFluentQuery.cs b/Neo4jClient/Cypher/ICypherFluentQuery.cs index 3de3647f3..cc9fe571b 100644 --- a/Neo4jClient/Cypher/ICypherFluentQuery.cs +++ b/Neo4jClient/Cypher/ICypherFluentQuery.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq.Expressions; using System.Threading.Tasks; @@ -20,6 +21,20 @@ public partial interface ICypherFluentQuery ICypherFluentQuery ParserVersion(Version version); ICypherFluentQuery ParserVersion(int major, int minor); + ICypherFluentQuery MaxExecutionTime(int milliseconds); + + /// + /// Custom headers to add to REST calls to Neo4j server. + /// Example usage: This can be used to provide extra information to a Neo4j Loadbalancer. + /// + /// + /// This settings is ignored when using + /// Since it could create a race-condition. + /// + /// Customheader added via: + /// + ICypherFluentQuery CustomHeaders(NameValueCollection headers); + ICypherFluentQuery Planner(string planner); ICypherFluentQuery Planner(CypherPlanner planner); ICypherFluentQuery Start(object startBits); diff --git a/Neo4jClient/Cypher/QueryWriter.cs b/Neo4jClient/Cypher/QueryWriter.cs index 7f84362b3..8e7fc39db 100644 --- a/Neo4jClient/Cypher/QueryWriter.cs +++ b/Neo4jClient/Cypher/QueryWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Text; using Newtonsoft.Json.Serialization; @@ -45,11 +46,19 @@ public CypherResultFormat ResultFormat set { resultFormat = value; } } + public int? MaxExecutionTime { get; set; } + + public NameValueCollection CustomHeaders { get; set; } + public QueryWriter Clone() { var clonedQueryTextBuilder = new StringBuilder(queryTextBuilder.ToString()); var clonedParameters = new Dictionary(queryParameters); - return new QueryWriter(clonedQueryTextBuilder, clonedParameters, resultMode, resultFormat); + return new QueryWriter(clonedQueryTextBuilder, clonedParameters, resultMode, resultFormat) + { + MaxExecutionTime = MaxExecutionTime, + CustomHeaders = CustomHeaders + }; } public CypherQuery ToCypherQuery(IContractResolver contractResolver = null) @@ -63,7 +72,10 @@ public CypherQuery ToCypherQuery(IContractResolver contractResolver = null) new Dictionary(queryParameters), resultMode, resultFormat, - contractResolver); + contractResolver, + MaxExecutionTime, + CustomHeaders + ); } public string CreateParameter(object paramValue) diff --git a/Neo4jClient/Execution/IRequestTypeBuilder.cs b/Neo4jClient/Execution/IRequestTypeBuilder.cs index 900335444..02f67308a 100644 --- a/Neo4jClient/Execution/IRequestTypeBuilder.cs +++ b/Neo4jClient/Execution/IRequestTypeBuilder.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Specialized; namespace Neo4jClient.Execution { diff --git a/Neo4jClient/Execution/Request.cs b/Neo4jClient/Execution/Request.cs index f19e16090..27dac3f01 100644 --- a/Neo4jClient/Execution/Request.cs +++ b/Neo4jClient/Execution/Request.cs @@ -1,10 +1,12 @@ -namespace Neo4jClient.Execution +using System.Collections.Specialized; + +namespace Neo4jClient.Execution { internal static class Request { - public static IRequestTypeBuilder With(ExecutionConfiguration configuration) + public static IRequestTypeBuilder With(ExecutionConfiguration configuration, NameValueCollection customerHeaders = null, int? maxExecutionTime = null) { - return new RequestTypeBuilder(configuration); + return new RequestTypeBuilder(configuration, customerHeaders, maxExecutionTime); } } } diff --git a/Neo4jClient/Execution/RequestTypeBuilder.cs b/Neo4jClient/Execution/RequestTypeBuilder.cs index 8e149ac8f..6c8e9a5cf 100644 --- a/Neo4jClient/Execution/RequestTypeBuilder.cs +++ b/Neo4jClient/Execution/RequestTypeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Net.Http; using System.Text; @@ -9,30 +10,34 @@ namespace Neo4jClient.Execution internal class RequestTypeBuilder : IRequestTypeBuilder { private readonly ExecutionConfiguration _executionConfiguration; - - public RequestTypeBuilder(ExecutionConfiguration executionConfiguration) + private readonly NameValueCollection _customHeaders; + private readonly int? _maxExecutionTime; + + public RequestTypeBuilder(ExecutionConfiguration executionConfiguration, NameValueCollection customHeaders, int? maxExecutionTime) { _executionConfiguration = executionConfiguration; + _customHeaders = customHeaders; + _maxExecutionTime = maxExecutionTime; } public IResponseBuilder Delete(Uri endpoint) { - return new ResponseBuilder(new HttpRequestMessage(HttpMethod.Delete, endpoint), _executionConfiguration); + return new ResponseBuilder(new HttpRequestMessage(HttpMethod.Delete, endpoint), _executionConfiguration, _customHeaders); } public IResponseBuilder Get(Uri endpoint) { - return new ResponseBuilder(new HttpRequestMessage(HttpMethod.Get, endpoint), _executionConfiguration); + return new ResponseBuilder(new HttpRequestMessage(HttpMethod.Get, endpoint), _executionConfiguration, _customHeaders); } public IRequestWithPendingContentBuilder Post(Uri endpoint) { - return new RequestWithPendingContentBuilder(HttpMethod.Post, endpoint, _executionConfiguration); + return new RequestWithPendingContentBuilder(HttpMethod.Post, endpoint, _executionConfiguration, _customHeaders, _maxExecutionTime); } public IRequestWithPendingContentBuilder Put(Uri endpoint) { - return new RequestWithPendingContentBuilder(HttpMethod.Put, endpoint, _executionConfiguration); + return new RequestWithPendingContentBuilder(HttpMethod.Put, endpoint, _executionConfiguration, _customHeaders, _maxExecutionTime); } } diff --git a/Neo4jClient/Execution/RequestWithPendingContentBuilder.cs b/Neo4jClient/Execution/RequestWithPendingContentBuilder.cs index 90eb9ac0e..2a9affeb4 100644 --- a/Neo4jClient/Execution/RequestWithPendingContentBuilder.cs +++ b/Neo4jClient/Execution/RequestWithPendingContentBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.Net.Http; using System.Text; @@ -9,12 +10,16 @@ internal class RequestWithPendingContentBuilder : IRequestWithPendingContentBuil private readonly HttpMethod _httpMethod; private readonly Uri _endpoint; private readonly ExecutionConfiguration _executionConfiguration; + private readonly NameValueCollection _customHeaders; + private readonly int? _maxExecutionTime; - public RequestWithPendingContentBuilder(HttpMethod httpMethod, Uri endpoint, ExecutionConfiguration executionConfiguration) + public RequestWithPendingContentBuilder(HttpMethod httpMethod, Uri endpoint, ExecutionConfiguration executionConfiguration, NameValueCollection customHeaders, int? maxExecutionTime) { _httpMethod = httpMethod; _endpoint = endpoint; _executionConfiguration = executionConfiguration; + _customHeaders = customHeaders; + _maxExecutionTime = maxExecutionTime; } public IResponseBuilder WithContent(string content) @@ -24,7 +29,9 @@ public IResponseBuilder WithContent(string content) { Content = new StringContent(content, Encoding.UTF8) }, - _executionConfiguration + _executionConfiguration, + _customHeaders, + _maxExecutionTime ); } @@ -35,7 +42,9 @@ public IResponseBuilder WithJsonContent(string jsonContent) { Content = new StringContent(jsonContent, Encoding.UTF8, "application/json") }, - _executionConfiguration + _executionConfiguration, + _customHeaders, + _maxExecutionTime ); } } diff --git a/Neo4jClient/Execution/ResponseBuilder.cs b/Neo4jClient/Execution/ResponseBuilder.cs index f516898d0..cb49aa0c4 100644 --- a/Neo4jClient/Execution/ResponseBuilder.cs +++ b/Neo4jClient/Execution/ResponseBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Net; using System.Net.Http; @@ -16,7 +17,10 @@ internal class ResponseBuilder : IResponseBuilder protected readonly ISet _expectedStatusCodes; protected readonly Func _errorCondition; protected readonly Func _errorGenerator; - protected readonly IList _errorGenerators; + protected readonly IList _errorGenerators; + protected readonly NameValueCollection _customHeaders; + protected readonly int? _maxExecutionTime; + internal const string MaxExecutionTimeHeaderKey = "max-execution-time"; public ISet ExpectedStatusCodes { @@ -28,23 +32,25 @@ public IList ErrorGenerators get { return _errorGenerators; } } - public ResponseBuilder(HttpRequestMessage request, ExecutionConfiguration executionConfiguration) - : this(request, new HashSet(), executionConfiguration) + public ResponseBuilder(HttpRequestMessage request, ExecutionConfiguration executionConfiguration, NameValueCollection nameValueCollection, int? maxExecutionTime = null) + : this(request, new HashSet(), executionConfiguration, new List(), nameValueCollection, maxExecutionTime ) { } public ResponseBuilder(HttpRequestMessage request, ISet expectedStatusCodes, ExecutionConfiguration executionConfiguration) : - this(request, expectedStatusCodes, executionConfiguration, new List()) + this(request, expectedStatusCodes, executionConfiguration, new List(), null, null) { } public ResponseBuilder(HttpRequestMessage request, ISet expectedStatusCodes, - ExecutionConfiguration executionConfiguration, IList errorGenerators) + ExecutionConfiguration executionConfiguration, IList errorGenerators, NameValueCollection customHeaders, int? maxExecutionTime = null) { _request = request; _expectedStatusCodes = expectedStatusCodes; _executionConfiguration = executionConfiguration; _errorGenerators = errorGenerators; + _customHeaders = customHeaders; + _maxExecutionTime = maxExecutionTime; } protected ISet UnionStatusCodes( @@ -60,13 +66,13 @@ IEnumerable source2 public IResponseBuilder WithExpectedStatusCodes(params HttpStatusCode[] statusCodes) { return new ResponseBuilder(_request, UnionStatusCodes(_expectedStatusCodes, statusCodes), - _executionConfiguration, _errorGenerators); + _executionConfiguration, _errorGenerators, _customHeaders, _maxExecutionTime); } public IResponseFailBuilder FailOnCondition(Func condition) { return new ResponseFailBuilder(_request, _expectedStatusCodes, _executionConfiguration, _errorGenerators, - condition); + condition, _customHeaders); } private Task PrepareAsync(TaskFactory taskFactory) @@ -82,6 +88,23 @@ private Task PrepareAsync(TaskFactory taskFactory) _request.Headers.Add("User-Agent", _executionConfiguration.UserAgent); + if (_maxExecutionTime.HasValue) + { + _request.Headers.Add(MaxExecutionTimeHeaderKey, _maxExecutionTime.Value.ToString()); + } + + if (_customHeaders != null && _customHeaders.Count > 0) + { + foreach (var customHeaderKey in _customHeaders.AllKeys) + { + var headerValue = _customHeaders.Get(customHeaderKey); + if (!string.IsNullOrWhiteSpace(headerValue)) + { + _request.Headers.Add(customHeaderKey, headerValue); + } + } + } + var userInfo = _request.RequestUri.UserInfo; if (!string.IsNullOrEmpty(userInfo)) { diff --git a/Neo4jClient/Execution/ResponseBuilder`TParse.cs b/Neo4jClient/Execution/ResponseBuilder`TParse.cs index 57e7dc780..bd660fa3a 100644 --- a/Neo4jClient/Execution/ResponseBuilder`TParse.cs +++ b/Neo4jClient/Execution/ResponseBuilder`TParse.cs @@ -17,7 +17,7 @@ internal class ResponseBuilder public ResponseBuilder(HttpRequestMessage request, ISet expectedStatusCodes, ExecutionConfiguration executionConfiguration, IList errorGenerators) - : base(request, expectedStatusCodes, executionConfiguration, errorGenerators) + : base(request, expectedStatusCodes, executionConfiguration, errorGenerators, null) { } diff --git a/Neo4jClient/Execution/ResponseFailBuilder.cs b/Neo4jClient/Execution/ResponseFailBuilder.cs index 9e0d832ae..97acea3b7 100644 --- a/Neo4jClient/Execution/ResponseFailBuilder.cs +++ b/Neo4jClient/Execution/ResponseFailBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Net; using System.Net.Http; @@ -12,16 +13,18 @@ internal class ResponseFailBuilder : IResponseFailBuilder private readonly ExecutionConfiguration _executionConfiguration; private readonly IList _errorGenerators; private readonly Func _errorCondition; + private readonly NameValueCollection _customHeaders; public ResponseFailBuilder(HttpRequestMessage request, ISet expectedStatusCodes, ExecutionConfiguration executionConfiguration, IList errorGenerators, - Func errorCondition) + Func errorCondition, NameValueCollection customHeaders) { _request = request; _expectedStatusCodes = expectedStatusCodes; _executionConfiguration = executionConfiguration; _errorGenerators = errorGenerators; _errorCondition = errorCondition; + _customHeaders = customHeaders; } public IResponseBuilder WithError(Func errorBuilder) @@ -39,7 +42,8 @@ public IResponseBuilder WithError(Func errorBuil _request, _expectedStatusCodes, _executionConfiguration, - newGenerators + newGenerators, + _customHeaders ); } @@ -59,6 +63,7 @@ public IResponseBuilder WithNull() _expectedStatusCodes, _executionConfiguration, newGenerators + ,_customHeaders ); } } diff --git a/Neo4jClient/GraphClient.cs b/Neo4jClient/GraphClient.cs index 1da3a21ff..3d999035d 100644 --- a/Neo4jClient/GraphClient.cs +++ b/Neo4jClient/GraphClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Diagnostics; @@ -26,6 +27,7 @@ public class GraphClient : IRawGraphClient, IInternalTransactionalGraphClient, I { internal const string GremlinPluginUnavailable = "You're attempting to execute a Gremlin query, however the server instance you are connected to does not have the Gremlin plugin loaded. If you've recently upgraded to Neo4j 2.0, you'll need to be aware that Gremlin no longer ships as part of the normal Neo4j distribution. Please move to equivalent (but much more powerful and readable!) Cypher."; + internal const string MaxExecutionTimeHeaderKey = "max-execution-time"; public static readonly JsonConverter[] DefaultJsonConverters = { @@ -196,6 +198,8 @@ public virtual void Connect() stopTimerAndNotifyCompleted(); } + //public NameValueCollection CustomHeaders { get; set; } + [Obsolete( "The concept of a single root node has being dropped in Neo4j 2.0. Use an alternate strategy for having known reference points in the graph, such as labels." )] @@ -942,7 +946,15 @@ private Task PrepareCypherRequest(CypherQuery quer }); } - return Request.With(ExecutionConfiguration) + int? maxExecutionTime = null; + NameValueCollection customHeaders = null; + if (query != null) + { + maxExecutionTime = query.MaxExecutionTime; + customHeaders = query.CustomHeaders; + } + + return Request.With(ExecutionConfiguration, customHeaders, maxExecutionTime) .Post(policy.BaseEndpoint) .WithJsonContent(policy.SerializeRequest(query)) .WithExpectedStatusCodes(HttpStatusCode.OK) @@ -1049,7 +1061,7 @@ void IRawGraphClient.ExecuteCypher(CypherQuery query) context.Complete(query); } - + async Task IRawGraphClient.ExecuteCypherAsync(CypherQuery query) { var context = ExecutionContext.Begin(this); @@ -1060,7 +1072,7 @@ async Task IRawGraphClient.ExecuteCypherAsync(CypherQuery query) context.Complete(query); } - void IRawGraphClient.ExecuteMultipleCypherQueriesInTransaction(IEnumerable queries) + void IRawGraphClient.ExecuteMultipleCypherQueriesInTransaction(IEnumerable queries, NameValueCollection customHeaders) { var context = ExecutionContext.Begin(this); @@ -1538,7 +1550,7 @@ public void Complete(CypherQuery query) public void Complete(CypherQuery query, int resultsCount) { - Complete(query.DebugQueryText, resultsCount, null); + Complete(query.DebugQueryText, resultsCount, null, query.CustomHeaders); } public void Complete(CypherQuery query, Exception exception) @@ -1546,14 +1558,16 @@ public void Complete(CypherQuery query, Exception exception) Complete(query.DebugQueryText, -1, exception); } - public void Complete(string queryText, int resultsCount = -1, Exception exception = null) + public void Complete(string queryText, int resultsCount = -1, Exception exception = null, NameValueCollection customHeaders = null, int? maxExecutionTime = null) { var args = new OperationCompletedEventArgs { QueryText = queryText, ResourcesReturned = resultsCount, TimeTaken = stopwatch.Elapsed, - Exception = exception + Exception = exception, + CustomHeaders = customHeaders, + MaxExecutionTime = maxExecutionTime }; owner.OnOperationCompleted(args); diff --git a/Neo4jClient/IGraphClient.cs b/Neo4jClient/IGraphClient.cs index 89fe5cef8..0b1ae11ec 100644 --- a/Neo4jClient/IGraphClient.cs +++ b/Neo4jClient/IGraphClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel.Design; using System.Threading.Tasks; using Neo4jClient.ApiModels; using Neo4jClient.Cypher; diff --git a/Neo4jClient/IRawGraphClient.cs b/Neo4jClient/IRawGraphClient.cs index 1e997cfba..a86253602 100644 --- a/Neo4jClient/IRawGraphClient.cs +++ b/Neo4jClient/IRawGraphClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Specialized; using System.Threading.Tasks; using Neo4jClient.Cypher; @@ -14,7 +15,7 @@ public interface IRawGraphClient : IGraphClient IEnumerable ExecuteGetCypherResults(CypherQuery query); Task> ExecuteGetCypherResultsAsync(CypherQuery query); void ExecuteCypher(CypherQuery query); - void ExecuteMultipleCypherQueriesInTransaction(IEnumerable queries); + void ExecuteMultipleCypherQueriesInTransaction(IEnumerable queries, NameValueCollection customHeaders = null); Task ExecuteCypherAsync(CypherQuery query); } } diff --git a/Neo4jClient/OperationCompletedEventHandler.cs b/Neo4jClient/OperationCompletedEventHandler.cs index 9db5c03fa..9b8f1f2c4 100644 --- a/Neo4jClient/OperationCompletedEventHandler.cs +++ b/Neo4jClient/OperationCompletedEventHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; namespace Neo4jClient { @@ -11,6 +12,8 @@ public class OperationCompletedEventArgs : EventArgs public TimeSpan TimeTaken { get; set; } public Exception Exception { get; set; } public bool HasException { get { return Exception != null; } } + public int? MaxExecutionTime { get; set; } + public NameValueCollection CustomHeaders { get; set; } public override string ToString() { diff --git a/Neo4jClient/Transactions/INeo4jTransaction.cs b/Neo4jClient/Transactions/INeo4jTransaction.cs index cc01af807..f3e1a2c51 100644 --- a/Neo4jClient/Transactions/INeo4jTransaction.cs +++ b/Neo4jClient/Transactions/INeo4jTransaction.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; namespace Neo4jClient.Transactions { @@ -12,5 +13,7 @@ internal interface INeo4jTransaction : ITransaction /// The Neo4j base endpoint for this transaction /// Uri Endpoint { get; set; } + + NameValueCollection CustomHeaders { get; set; } } } diff --git a/Neo4jClient/Transactions/ITransactionResourceManager.cs b/Neo4jClient/Transactions/ITransactionResourceManager.cs index 0d5219f8f..db2584d9e 100644 --- a/Neo4jClient/Transactions/ITransactionResourceManager.cs +++ b/Neo4jClient/Transactions/ITransactionResourceManager.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Neo4jClient.Transactions +namespace Neo4jClient.Transactions { internal interface ITransactionResourceManager { diff --git a/Neo4jClient/Transactions/Neo4jTransaction.cs b/Neo4jClient/Transactions/Neo4jTransaction.cs index 1771a3126..5827c59bf 100644 --- a/Neo4jClient/Transactions/Neo4jTransaction.cs +++ b/Neo4jClient/Transactions/Neo4jTransaction.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.Net; using Neo4jClient.ApiModels.Cypher; using Neo4jClient.Execution; @@ -16,6 +17,7 @@ internal class Neo4jTransaction : INeo4jTransaction public bool IsOpen { get; private set; } public Uri Endpoint { get; set; } + public NameValueCollection CustomHeaders { get; set; } internal int Id { @@ -93,7 +95,7 @@ public void Commit() return; } - DoCommit(Endpoint, _client.ExecutionConfiguration, _client.Serializer); + DoCommit(Endpoint, _client.ExecutionConfiguration, _client.Serializer, CustomHeaders); CleanupAfterClosedTransaction(); } @@ -147,8 +149,7 @@ internal void ForceKeepAlive() var transactionEndpoint = DoKeepAlive( keepAliveUri, _client.ExecutionConfiguration, - _client.Serializer, - Endpoint == null); + _client.Serializer,newTransaction: Endpoint == null); if (Endpoint != null) { @@ -157,20 +158,20 @@ internal void ForceKeepAlive() Endpoint = transactionEndpoint; } - private static void DoCommit(Uri commitUri, ExecutionConfiguration executionConfiguration, ISerializer serializer) + private static void DoCommit(Uri commitUri, ExecutionConfiguration executionConfiguration, ISerializer serializer, NameValueCollection customHeaders = null) { - Request.With(executionConfiguration) + Request.With(executionConfiguration, customHeaders) .Post(commitUri.AddPath("commit")) .WithJsonContent(serializer.Serialize(new CypherStatementList())) .WithExpectedStatusCodes(HttpStatusCode.OK) .Execute(); } - private static void DoRollback(Uri rollbackUri, ExecutionConfiguration executionConfiguration) + private static void DoRollback(Uri rollbackUri, ExecutionConfiguration executionConfiguration, NameValueCollection customHeaders) { // not found is ok because it means our transaction either was committed or the timeout was expired // and it was rolled back for us - Request.With(executionConfiguration) + Request.With(executionConfiguration, customHeaders) .Delete(rollbackUri) .WithExpectedStatusCodes(HttpStatusCode.OK, HttpStatusCode.NotFound) .Execute(); @@ -180,9 +181,10 @@ private static void DoRollback(Uri rollbackUri, ExecutionConfiguration execution Uri keepAliveUri, ExecutionConfiguration executionConfiguration, ISerializer serializer, + NameValueCollection customHeaders = null, bool newTransaction = false) { - var partialRequest = Request.With(executionConfiguration) + var partialRequest = Request.With(executionConfiguration, customHeaders) .Post(keepAliveUri) .WithJsonContent(serializer.Serialize(new CypherStatementList())); @@ -197,10 +199,12 @@ private static void DoRollback(Uri rollbackUri, ExecutionConfiguration execution /// Commits a transaction given the ID /// /// The transaction execution environment - internal static void DoCommit(ITransactionExecutionEnvironment transactionExecutionEnvironment) + /// Custom headers to sent to the neo4j server + internal static void DoCommit(ITransactionExecutionEnvironment transactionExecutionEnvironment, NameValueCollection customHeaders = null) { var commitUri = transactionExecutionEnvironment.TransactionBaseEndpoint.AddPath( transactionExecutionEnvironment.TransactionId.ToString()); + DoCommit( commitUri, new ExecutionConfiguration @@ -210,14 +214,16 @@ internal static void DoCommit(ITransactionExecutionEnvironment transactionExecut UseJsonStreaming = transactionExecutionEnvironment.UseJsonStreaming, UserAgent = transactionExecutionEnvironment.UserAgent }, - new CustomJsonSerializer()); + new CustomJsonSerializer(), + customHeaders + ); } /// /// Rolls back a transaction given the ID /// /// The transaction execution environment - internal static void DoRollback(ITransactionExecutionEnvironment transactionExecutionEnvironment) + internal static void DoRollback(ITransactionExecutionEnvironment transactionExecutionEnvironment, NameValueCollection customHeaders = null) { try { @@ -231,7 +237,8 @@ internal static void DoRollback(ITransactionExecutionEnvironment transactionExec JsonConverters = GraphClient.DefaultJsonConverters, UseJsonStreaming = transactionExecutionEnvironment.UseJsonStreaming, UserAgent = transactionExecutionEnvironment.UserAgent - }); + }, + customHeaders); } catch (Exception e) { @@ -251,12 +258,13 @@ internal static void DoKeepAlive(ITransactionExecutionEnvironment transactionExe keepAliveUri, new ExecutionConfiguration { - HttpClient = new HttpClientWrapper(transactionExecutionEnvironment.Username, transactionExecutionEnvironment.Password), + HttpClient = + new HttpClientWrapper(transactionExecutionEnvironment.Username, + transactionExecutionEnvironment.Password), JsonConverters = GraphClient.DefaultJsonConverters, UseJsonStreaming = transactionExecutionEnvironment.UseJsonStreaming, UserAgent = transactionExecutionEnvironment.UserAgent - }, - new CustomJsonSerializer()); + }, new CustomJsonSerializer(), null); } public void Dispose() diff --git a/Neo4jClient/Transactions/Neo4jTransactionResourceManager.cs b/Neo4jClient/Transactions/Neo4jTransactionResourceManager.cs index 767cb61e1..29f799112 100644 --- a/Neo4jClient/Transactions/Neo4jTransactionResourceManager.cs +++ b/Neo4jClient/Transactions/Neo4jTransactionResourceManager.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Transactions; namespace Neo4jClient.Transactions { - using Neo4jClient.Execution; /// /// When TransactionPromotableSinglePhaseNotification fails to register as PSPE, then this class will diff --git a/Neo4jClient/Transactions/TransactionContext.cs b/Neo4jClient/Transactions/TransactionContext.cs index 306a27e0c..ae6eceefa 100644 --- a/Neo4jClient/Transactions/TransactionContext.cs +++ b/Neo4jClient/Transactions/TransactionContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Specialized; using System.Net; using System.Net.Http; using System.Threading; @@ -33,6 +34,8 @@ internal class TransactionContext : INeo4jTransaction /// private BlockingCollection _taskQueue; + public NameValueCollection CustomHeaders { get; set; } + /// /// Where the cancellation token generates /// @@ -50,8 +53,9 @@ public Task EnqueueTask(string commandDescription, IGraphCl // grab the endpoint in the same thread var txBaseEndpoint = policy.BaseEndpoint; var serializedQuery = policy.SerializeRequest(query); + CustomHeaders = query.CustomHeaders; var task = new Task(() => - Request.With(client.ExecutionConfiguration) + Request.With(client.ExecutionConfiguration, query.CustomHeaders, query.MaxExecutionTime) .Post(Endpoint ?? txBaseEndpoint) .WithJsonContent(serializedQuery) // HttpStatusCode.Created may be returned when emitting the first query on a transaction diff --git a/Neo4jClient/Transactions/TransactionManager.cs b/Neo4jClient/Transactions/TransactionManager.cs index aac9332a3..2af4541f6 100644 --- a/Neo4jClient/Transactions/TransactionManager.cs +++ b/Neo4jClient/Transactions/TransactionManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -37,7 +38,7 @@ public TransactionManager(ITransactionalGraphClient client) dtcContexts = new Dictionary(); } - private TransactionContext GetOrCreateDtcTransactionContext() + private TransactionContext GetOrCreateDtcTransactionContext(NameValueCollection customHeaders = null) { // we need to lock as we could get other async requests to the same transaction var txId = Transaction.Current.TransactionInformation.LocalIdentifier; @@ -50,14 +51,18 @@ private TransactionContext GetOrCreateDtcTransactionContext() } // associate it with the ambient transaction - txContext = new TransactionContext(promotable.AmbientTransaction); + txContext = new TransactionContext(promotable.AmbientTransaction) + { + Transaction = {CustomHeaders = customHeaders}, + CustomHeaders = customHeaders + }; dtcContexts[txId] = txContext; - + return txContext; } } - private TransactionContext GetContext() + private TransactionContext GetContext(NameValueCollection customHeaders = null) { var nonDtcTransaction = CurrentInternalTransaction; if (nonDtcTransaction != null && nonDtcTransaction.Committable) @@ -66,7 +71,7 @@ private TransactionContext GetContext() } // if we are not in a native transaction get the context of our ambient transaction - return GetOrCreateDtcTransactionContext(); + return GetOrCreateDtcTransactionContext(customHeaders); } public bool InTransaction @@ -225,8 +230,8 @@ public Task EnqueueCypherRequest(string commandDescription, // we try to get the current dtc transaction. If we are in a System.Transactions transaction and it has // been "promoted" to be handled by DTC then transactionObject will be null, but it doesn't matter as // we don't care about updating the object. - var txContext = GetContext(); - + var txContext = GetContext(query.CustomHeaders); + txContext.CustomHeaders = query.CustomHeaders; // the main difference with a normal Request.With() call is that the request is associated with the // TX context. return txContext.EnqueueTask(commandDescription, graphClient, policy, query); diff --git a/Neo4jClient/Transactions/TransactionScopeProxy.cs b/Neo4jClient/Transactions/TransactionScopeProxy.cs index c11bac9f7..64a4abe6c 100644 --- a/Neo4jClient/Transactions/TransactionScopeProxy.cs +++ b/Neo4jClient/Transactions/TransactionScopeProxy.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Transactions; +using System.Collections.Specialized; namespace Neo4jClient.Transactions { @@ -38,6 +34,8 @@ public Uri Endpoint set { _transactionContext.Endpoint = value; } } + public NameValueCollection CustomHeaders { get; set; } + public virtual void Dispose() { if (_disposing)