From fef81d4b3017d7656f7c2bf55135030170654c24 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Feb 2021 17:30:23 +0200 Subject: [PATCH 01/23] Add CouchViewResult --- src/CouchDB.Driver/Types/CouchViewResult.cs | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/CouchDB.Driver/Types/CouchViewResult.cs diff --git a/src/CouchDB.Driver/Types/CouchViewResult.cs b/src/CouchDB.Driver/Types/CouchViewResult.cs new file mode 100644 index 0000000..d38b96e --- /dev/null +++ b/src/CouchDB.Driver/Types/CouchViewResult.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace CouchDB.Driver.Types +{ + /// + /// Object that stores results of a view. + /// + /// The type of the value. + public class CouchViewResult + { + /// + /// Number of documents in the database/view. + /// + [JsonProperty("total_rows")] + public int TotalRows { get; private set; } + + /// + /// Offset where the document list started. + /// + [JsonProperty("offset")] + public int Offset { get; private set; } + + /// + /// Array of view row objects. This result contains only the document ID and revision. + /// + [JsonProperty("rows")] + public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); + } + + /// + /// Object that stores results of a view. + /// + /// The type of the value. + /// The type of the doc. + public class CouchViewResult + where TDoc : CouchDocument + + { + /// + /// Number of documents in the database/view. + /// + [JsonProperty("total_rows")] + public int TotalRows { get; private set; } + + /// + /// Offset where the document list started. + /// + [JsonProperty("offset")] + public int Offset { get; private set; } + + /// + /// Array of view row objects. This result contains the document ID, revision and the documents. + /// + [JsonProperty("rows")] + public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); + } +} \ No newline at end of file From 964c39995c8a5c44ed0f45c101e00ba35c5df44e Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Feb 2021 17:30:35 +0200 Subject: [PATCH 02/23] Add CouchViewOptions --- src/CouchDB.Driver/Types/CouchViewOptions.cs | 169 +++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/CouchDB.Driver/Types/CouchViewOptions.cs diff --git a/src/CouchDB.Driver/Types/CouchViewOptions.cs b/src/CouchDB.Driver/Types/CouchViewOptions.cs new file mode 100644 index 0000000..f026930 --- /dev/null +++ b/src/CouchDB.Driver/Types/CouchViewOptions.cs @@ -0,0 +1,169 @@ +using System.Collections.Generic; +using System.Linq; + +namespace CouchDB.Driver.Types +{ + /// + /// Optional options that can be send with a view request. + /// + public class CouchViewOptions + { + /// + /// Include the associated document with each row. Default is false. + /// + public bool? IncludeDocs { get; set; } + + /// + /// Include conflicts information in response. + /// Ignored if include_docs isn’t true. Default is false. + /// + public bool? Conflicts { get; set; } + + /// + /// Return records starting with the specified key. + /// + public string? StartKey { get; set; } + + /// + /// Stop returning records when the specified key is reached. + /// + public string? EndKey { get; set; } + + /// + /// Specifies whether the specified end key should + /// be included in the result. Default is true. + /// + public bool? InclusiveEnd { get; set; } + + /// + /// Return the documents in descending order by key. Default is false. + /// + public bool? Descending { get; set; } + + /// + /// Group the results using the reduce function to a group or single row. + /// Implies reduce is true and the maximum group_level. Default is false. + /// + public bool? Group { get; set; } + + /// + /// Include encoding information in attachment stubs if include_docs is true + /// and the particular attachment is compressed. + /// Ignored if include_docs isn’t true. Default is false. + /// + public bool? AttEncodingInfo { get; set; } + + /// + /// Use the reduction function. Default is true + /// when a reduce function is defined. + /// + public bool? Reduce { get; set; } + + /// + /// Whether or not the view results should be returned + /// from a stable set of shards. Default is false. + /// + public bool? Stable { get; set; } + + /// + /// Whether to include in the response an update_seq value indicating the + /// sequence id of the database the view reflects. Default is false. + /// + public bool? UpdateSeq { get; set; } + + /// + /// Include the Base64-encoded content of + /// attachments + /// in the documents that are included if include_docs is true. + /// Ignored if include_docs isn’t true. Default is false. + /// + public bool? Attachments { get; set; } + + /// + /// Sort returned rows. Setting this to false offers a performance boost. + /// The total_rows and offset fields are not available when this is set to false. + /// Default is true. + /// + public bool? Sorted { get; set; } + + /// + /// Specify the group level to be used. Implies group is true. + /// + public int? GroupLevel { get; set; } + + /// + /// Limit the number of the returned documents to the specified number. + /// + public int? Limit { get; set; } + + /// + /// Skip this number of records before starting to return the results. + /// Default is 0. + /// + public int? Skip { get; set; } + + /// + /// Whether or not the view in question should be updated prior to responding to the user. + /// Supported values: true, false, lazy. Default is true. + /// + public string? Update { get; set; } + + /// + /// Stop returning records when the specified document ID is reached. + /// Ignored if endkey is not set. + /// + public string? EndkeyDocId { get; set; } + + /// + /// Return only documents that match the specified key. + /// + public string? Key { get; set; } + + /// + /// Return records starting with the specified document ID. + /// Ignored if startkey is not set. + /// + public string? StartkeyDocid { get; set; } + + // Error CA2227: Change 'Keys' to be read-only by removing the property setter. + // Disable this so keys can be added in with other properties on initialization. +#pragma warning disable CA2227 + /// + /// Return only documents where the key matches one of the keys specified in the array. + /// + public HashSet? Keys { get; set; } + +#pragma warning restore CA2227 + + public object ToQueryParameters() => new + { + include_docs = IncludeDocs, + conflicts = Conflicts, + startkey = TryEscape(StartKey), + endKey = TryEscape(EndKey), + inclusive_end = InclusiveEnd, + descending = Descending, + group = Group, + att_encoding_info = AttEncodingInfo, + reduce = Reduce, + stable = Stable, + update_seq = UpdateSeq, + attachments = Attachments, + sorted = Sorted, + group_level = GroupLevel, + limit = Limit, + skip = Skip, + update = TryEscape(Update), + endkey_docid = TryEscape(EndkeyDocId), + key = TryEscape(Key), + startkey_docid = TryEscape(StartkeyDocid), + keys = Keys is null ? null : $"[{string.Join(',', Keys.Select(Escape))}]" + }; + + private static string Escape(string str) => + $"\"{str}\""; + + private static string? TryEscape(string? str) => + str is null ? null : Escape(str); + } +} \ No newline at end of file From 9af27df4901b30744b71b4987e61a91de62d715a Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Feb 2021 17:30:57 +0200 Subject: [PATCH 03/23] Add CouchViewRow --- src/CouchDB.Driver/Types/CouchViewRow.cs | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/CouchDB.Driver/Types/CouchViewRow.cs diff --git a/src/CouchDB.Driver/Types/CouchViewRow.cs b/src/CouchDB.Driver/Types/CouchViewRow.cs new file mode 100644 index 0000000..9519d31 --- /dev/null +++ b/src/CouchDB.Driver/Types/CouchViewRow.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; + +namespace CouchDB.Driver.Types +{ + /// + /// The object returned from a view execution. + /// + /// The type the value will be deserialized to. + public class CouchViewRow + { + /// + /// The id of the document. + /// + [JsonProperty("id")] + public string Id { get; private set; } = null!; + + /// + /// The view key that was emmited. + /// + [JsonProperty("key")] + public string Key { get; private set; } = null!; + + /// + /// The value that the view emmited. + /// + [JsonProperty("value")] + public TValue Value { get; private set; } = default!; + } + + /// + /// The type the doc will be deserialized to. + public class CouchViewRow : CouchViewRow + where TDoc : CouchDocument + { + /// + /// The json document deserialize to . + /// + [JsonProperty("doc")] + public TDoc Doc { get; private set; } = default!; + } +} \ No newline at end of file From e0419f4d7af98590a48fd0650cb640a4dbe8141c Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Feb 2021 17:31:32 +0200 Subject: [PATCH 04/23] Add GetView methods on ICouchDatabase --- src/CouchDB.Driver/ICouchDatabase.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 73379bd..0b8725d 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -88,6 +88,31 @@ public interface ICouchDatabase: IOrderedQueryable /// A task that represents the asynchronous operation. The task result contains the elements created or updated. Task> AddOrUpdateRangeAsync(IList documents, CancellationToken cancellationToken = default); + /// + /// Execute a couchdb view and get a result with the values. + /// + /// The type of the value that will be returned. + /// The design to use. + /// The view to use. + /// Optional options to pass to the view. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the . + Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// Execute a couchdb view and get a result with the values and the docs. + /// + /// The type of the value that will be returned. + /// The type of the document that will be returned. + /// The design to use. + /// The view to use. + /// Optional options to pass to the view. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the . + /// The options IncludeDocs will always be set to true. + Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + where TDoc : CouchDocument; + /// /// Since CouchDB v3, it is deprecated (a no-op). /// From 0e9736aaa54854815bebdf111bc6b566233e222f Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 17 Feb 2021 17:31:55 +0200 Subject: [PATCH 05/23] Implement GetView Methods on CouchDatabase --- src/CouchDB.Driver/CouchDatabase.cs | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 3ecb4ac..1e1ce4d 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -500,6 +500,42 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #endregion + #region View + + /// + public Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + { + Check.NotNull(view, nameof(view)); + + IFlurlRequest request = NewRequest() + .AppendPathSegments("_design", design, "_view", view) + .SetQueryParams(options?.ToQueryParameters()); + + return request + .GetJsonAsync>(cancellationToken) + .SendRequestAsync(); + } + + /// + public Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + where TDoc : CouchDocument + { + Check.NotNull(view, nameof(view)); + + options ??= new CouchViewOptions(); + options.IncludeDocs = true; + + IFlurlRequest request = NewRequest() + .AppendPathSegments("_design", design, "_view", view) + .SetQueryParams(options.ToQueryParameters()); + + return request + .GetJsonAsync>(cancellationToken) + .SendRequestAsync(); + } + + #endregion + #region Utils /// From 207b8bb4bfca8a6bbfddbe3b32f90a84322a42f5 Mon Sep 17 00:00:00 2001 From: Bortolazzo Matteo Date: Sun, 28 Feb 2021 21:40:59 +0100 Subject: [PATCH 06/23] #112 - Fix object disposed before async method completes --- .../Extensions/FlurlRequestExtensions.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs index f83a5a4..ae48022 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -18,30 +18,31 @@ internal static class FlurlRequestExtensions /// The token to monitor for cancellation requests. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a Stream. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] public static Task PostJsonStreamAsync( this IFlurlRequest request, object data, CancellationToken cancellationToken = default, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - using var capturedJsonContent = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(data)); - return request.SendAsync(HttpMethod.Post, (HttpContent)capturedJsonContent, cancellationToken, completionOption).ReceiveStream(); + var capturedJsonContent = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(data)); + return request.SendAsync(HttpMethod.Post, capturedJsonContent, cancellationToken, completionOption).ReceiveStream(); } - - + /// Sends an asynchronous POST request. /// The IFlurlRequest instance. /// Data to parse. /// The token to monitor for cancellation requests. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a Stream. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "")] public static Task PostStringStreamAsync( this IFlurlRequest request, string data, CancellationToken cancellationToken = default, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - using var capturedStringContent = new CapturedStringContent(data); + var capturedStringContent = new CapturedStringContent(data); return request.SendAsync(HttpMethod.Post, capturedStringContent, cancellationToken, completionOption).ReceiveStream(); } From 40d93ffba1b35842e7ba12ed60a58dc9f8668079 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 1 Mar 2021 21:22:22 +0100 Subject: [PATCH 07/23] #92 - Improves FindAsync #107 - Improves IsUp --- .../CouchDB.Driver.DependencyInjection.csproj | 4 +- src/CouchDB.Driver/CouchClient.cs | 41 ++++++++++----- .../CouchClientAuthentication.cs | 29 ++++------- src/CouchDB.Driver/CouchContext.cs | 13 ++++- src/CouchDB.Driver/CouchDB.Driver.csproj | 7 ++- src/CouchDB.Driver/CouchDatabase.cs | 51 +++++++++---------- .../Extensions/FlurlRequestExtensions.cs | 11 ++++ src/CouchDB.Driver/Helpers/RequestsHelper.cs | 2 +- src/CouchDB.Driver/Indexes/IndexDefinition.cs | 4 +- src/CouchDB.Driver/Query/QueryTranslator.cs | 6 +-- .../Translators/BinaryExpressionTranslator.cs | 18 +++---- .../ConstantExpressionTranslator.cs | 6 +-- .../MethodCallExpressionTranslator.cs | 42 +++++++-------- .../Translators/UnaryExpressionTranslator.cs | 8 +-- .../Attachments_Tests.cs | 2 +- .../Authentication_Test.cs | 20 +++++--- .../CouchDB.Driver.UnitTests/Client_Tests.cs | 36 +++++++++---- .../Database_Tests.cs | 6 +-- .../Feed/GetChanges_Tests.cs | 26 +++++----- .../Feed/GetContinuousChangesAsync_Tests.cs | 40 +++++++-------- 20 files changed, 206 insertions(+), 166 deletions(-) diff --git a/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj b/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj index 64b6eec..4fc8d42 100644 --- a/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj +++ b/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj @@ -32,8 +32,8 @@ - - + + diff --git a/src/CouchDB.Driver/CouchClient.cs b/src/CouchDB.Driver/CouchClient.cs index e5357cd..a1b484d 100644 --- a/src/CouchDB.Driver/CouchClient.cs +++ b/src/CouchDB.Driver/CouchClient.cs @@ -26,8 +26,10 @@ public partial class CouchClient : ICouchClient { private DateTime? _cookieCreationDate; private string? _cookieToken; - private readonly CouchOptions _options; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "")] private readonly IFlurlClient _flurlClient; + private readonly CouchOptions _options; private readonly string[] _systemDatabases = { "_users", "_replicator", "_global_changes" }; public Uri Endpoint { get; } @@ -120,15 +122,15 @@ public async Task> CreateDatabaseAsync(string d where TSource : CouchDocument { QueryContext queryContext = NewQueryContext(database); - HttpResponseMessage response = await CreateDatabaseAsync(queryContext, shards, replicas, cancellationToken) + IFlurlResponse response = await CreateDatabaseAsync(queryContext, shards, replicas, cancellationToken) .ConfigureAwait(false); - if (response.IsSuccessStatusCode) + if (response.IsSuccessful()) { return new CouchDatabase(_flurlClient, _options, queryContext); } - if (response.StatusCode == HttpStatusCode.PreconditionFailed) + if (response.StatusCode == (int)HttpStatusCode.PreconditionFailed) { throw new CouchException($"Database with name {database} already exists."); } @@ -142,10 +144,10 @@ public async Task> GetOrCreateDatabaseAsync(str where TSource : CouchDocument { QueryContext queryContext = NewQueryContext(database); - HttpResponseMessage response = await CreateDatabaseAsync(queryContext, shards, replicas, cancellationToken) + IFlurlResponse response = await CreateDatabaseAsync(queryContext, shards, replicas, cancellationToken) .ConfigureAwait(false); - if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.PreconditionFailed) + if (response.IsSuccessful() || response.StatusCode == (int)HttpStatusCode.PreconditionFailed) { return new CouchDatabase(_flurlClient, _options, queryContext); } @@ -171,7 +173,7 @@ public async Task DeleteDatabaseAsync(string database, CancellationToken cancell } } - private Task CreateDatabaseAsync(QueryContext queryContext, + private Task CreateDatabaseAsync(QueryContext queryContext, int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) { IFlurlRequest request = NewRequest() @@ -259,12 +261,12 @@ public Task> GetOrCreateUsersDatabaseAsync(Cancella public async Task ExistsAsync(string database, CancellationToken cancellationToken = default) { QueryContext queryContext = NewQueryContext(database); - HttpResponseMessage? response = await NewRequest() + IFlurlResponse? response = await NewRequest() .AllowHttpStatus(HttpStatusCode.NotFound) .AppendPathSegment(queryContext.EscapedDatabaseName) .HeadAsync(cancellationToken) .ConfigureAwait(false); - return response.IsSuccessStatusCode; + return response.IsSuccessful(); } /// @@ -273,13 +275,14 @@ public async Task IsUpAsync(CancellationToken cancellationToken = default) try { StatusResult result = await NewRequest() + .AllowAnyHttpStatus() .AppendPathSegment("/_up") .GetJsonAsync(cancellationToken) .SendRequestAsync() .ConfigureAwait(false); - return result.Status == "ok"; + return result?.Status == "ok"; } - catch (CouchNotFoundException) + catch (CouchException) { return false; } @@ -337,11 +340,21 @@ private void CheckDatabaseName(string database) /// public async ValueTask DisposeAsync() { - if (_options.AuthenticationType == AuthenticationType.Cookie && _options.LogOutOnDispose) + await DisposeAsync(true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (disposing && _flurlClient != null) { - await LogoutAsync().ConfigureAwait(false); + if (_options.AuthenticationType == AuthenticationType.Cookie && _options.LogOutOnDispose) + { + await LogoutAsync().ConfigureAwait(false); + } + + _flurlClient.Dispose(); } - _flurlClient.Dispose(); } #endregion diff --git a/src/CouchDB.Driver/CouchClientAuthentication.cs b/src/CouchDB.Driver/CouchClientAuthentication.cs index 72ab6e1..1b04705 100644 --- a/src/CouchDB.Driver/CouchClientAuthentication.cs +++ b/src/CouchDB.Driver/CouchClientAuthentication.cs @@ -14,12 +14,12 @@ namespace CouchDB.Driver { public partial class CouchClient { - protected virtual async Task OnBeforeCallAsync(HttpCall httpCall) + protected virtual async Task OnBeforeCallAsync(FlurlCall httpCall) { Check.NotNull(httpCall, nameof(httpCall)); // If session requests no authorization needed - if (httpCall.Request?.RequestUri?.ToString()?.Contains("_session", StringComparison.InvariantCultureIgnoreCase) == true) + if (httpCall.Request?.Url?.ToString()?.Contains("_session", StringComparison.InvariantCultureIgnoreCase) == true) { return; } @@ -28,7 +28,7 @@ protected virtual async Task OnBeforeCallAsync(HttpCall httpCall) case AuthenticationType.None: break; case AuthenticationType.Basic: - httpCall.FlurlRequest = httpCall.FlurlRequest.WithBasicAuth(_options.Username, _options.Password); + httpCall.Request = httpCall.Request.WithBasicAuth(_options.Username, _options.Password); break; case AuthenticationType.Cookie: var isTokenExpired = @@ -38,14 +38,14 @@ protected virtual async Task OnBeforeCallAsync(HttpCall httpCall) { await LoginAsync().ConfigureAwait(false); } - httpCall.FlurlRequest = httpCall.FlurlRequest.EnableCookies().WithCookie("AuthSession", _cookieToken); + httpCall.Request = httpCall.Request.WithCookie("AuthSession", _cookieToken); break; case AuthenticationType.Proxy: - httpCall.FlurlRequest = httpCall.FlurlRequest.WithHeader("X-Auth-CouchDB-UserName", _options.Username) + httpCall.Request = httpCall.Request.WithHeader("X-Auth-CouchDB-UserName", _options.Username) .WithHeader("X-Auth-CouchDB-Roles", string.Join(",", _options.Roles)); if (_options.Password != null) { - httpCall.FlurlRequest = httpCall.FlurlRequest.WithHeader("X-Auth-CouchDB-Token", _options.Password); + httpCall.Request = httpCall.Request.WithHeader("X-Auth-CouchDB-Token", _options.Password); } break; case AuthenticationType.Jwt: @@ -54,7 +54,7 @@ protected virtual async Task OnBeforeCallAsync(HttpCall httpCall) throw new InvalidOperationException("JWT generation cannot be null."); } var jwt = await _options.JwtTokenGenerator().ConfigureAwait(false); - httpCall.FlurlRequest = httpCall.FlurlRequest.WithHeader("Authorization", jwt); + httpCall.Request = httpCall.Request.WithHeader("Authorization", jwt); break; default: throw new NotSupportedException($"Authentication of type {_options.AuthenticationType} is not supported."); @@ -63,7 +63,7 @@ protected virtual async Task OnBeforeCallAsync(HttpCall httpCall) private async Task LoginAsync() { - HttpResponseMessage response = await _flurlClient.Request(Endpoint) + IFlurlResponse response = await _flurlClient.Request(Endpoint) .AppendPathSegment("_session") .PostJsonAsync(new { @@ -74,20 +74,13 @@ private async Task LoginAsync() _cookieCreationDate = DateTime.Now; - if (!response.Headers.TryGetValues("Set-Cookie", out IEnumerable values)) + FlurlCookie? dirtyToken = response.Cookies.FirstOrDefault(c => c.Name == "AuthSession"); + if (dirtyToken == null) { throw new InvalidOperationException("Error while trying to log-in."); } - var dirtyToken = values.First(); - var regex = new Regex(@"^AuthSession=(.+); Version=1; .*Path=\/; HttpOnly$"); - Match match = regex.Match(dirtyToken); - if (!match.Success) - { - throw new InvalidOperationException("Error while trying to log-in."); - } - - _cookieToken = match.Groups[1].Value; + _cookieToken = dirtyToken.Value; } private async Task LogoutAsync() diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index 49cef49..d6c4592 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -55,9 +55,18 @@ protected CouchContext(CouchOptions options) } } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { - return Client.DisposeAsync(); + await DisposeAsync(true).ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (disposing && Client != null) + { + await Client.DisposeAsync().ConfigureAwait(false); + } } private async Task InitDatabasesAsync(PropertyInfo propertyInfo, CouchOptions options) diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index 1ab8f96..bb25f97 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -14,7 +14,7 @@ Library en - 8.0 + latest enable 2.0.0 icon.png @@ -32,10 +32,9 @@ - + - - + diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 3ecb4ac..2ee95be 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -23,6 +23,7 @@ using CouchDB.Driver.Options; using CouchDB.Driver.Query; using Newtonsoft.Json; +using System.Net; namespace CouchDB.Driver { @@ -30,7 +31,7 @@ namespace CouchDB.Driver /// Represents a CouchDB database. /// /// The type of database documents. - public class CouchDatabase: ICouchDatabase + public class CouchDatabase : ICouchDatabase where TSource : CouchDocument { private readonly IAsyncQueryProvider _queryProvider; @@ -69,28 +70,22 @@ internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryCont public async Task FindAsync(string docId, bool withConflicts = false, CancellationToken cancellationToken = default) { - try - { - IFlurlRequest request = NewRequest() + IFlurlRequest request = NewRequest() .AppendPathSegment(docId); - if (withConflicts) - { - request = request.SetQueryParam("conflicts", "true"); - } - - TSource document = await request - .GetJsonAsync(cancellationToken) - .SendRequestAsync() - .ConfigureAwait(false); - - InitAttachments(document); - return document; - } - catch (CouchNotFoundException) + if (withConflicts) { - return null; + request = request.SetQueryParam("conflicts", "true"); } + + IFlurlResponse? response = await request + .AllowHttpStatus(HttpStatusCode.NotFound) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return response != null && response.StatusCode == (int)HttpStatusCode.OK + ? await response.GetJsonAsync().ConfigureAwait(false) + : null; } /// @@ -120,7 +115,7 @@ public async Task> FindManyAsync(IReadOnlyCollection docId .ReceiveJson>() .SendRequestAsync() .ConfigureAwait(false); - + var documents = bulkGetResult.Results .SelectMany(r => r.Docs) .Select(d => d.Item) @@ -137,12 +132,12 @@ public async Task> FindManyAsync(IReadOnlyCollection docId return documents; } - private async Task> SendQueryAsync(Func> requestFunc) + private async Task> SendQueryAsync(Func> requestFunc) { IFlurlRequest request = NewRequest() .AppendPathSegment("_find"); - Task message = requestFunc(request); + Task message = requestFunc(request); FindResult findResult = await message .ReceiveJson>() @@ -346,7 +341,7 @@ private async Task UpdateAttachments(TSource document, CancellationToken cancell { document.Rev = response.Rev; document.Attachments.RemoveAttachment(attachment); - } + } } InitAttachments(document); @@ -399,7 +394,7 @@ public async IAsyncEnumerable> GetContinuousC .ConfigureAwait(false) : await request.QueryContinuousWithFilterAsync(_queryProvider, filter, cancellationToken) .ConfigureAwait(false); - + await foreach (var line in stream.ReadLinesAsync(cancellationToken)) { if (string.IsNullOrEmpty(line)) @@ -415,7 +410,7 @@ public async IAsyncEnumerable> GetContinuousC #endregion #region Index - + /// public async Task> GetIndexesAsync(CancellationToken cancellationToken = default) { @@ -448,7 +443,7 @@ internal async Task CreateIndexAsync(string name, var indexJson = indexDefinition.ToString(); var sb = new StringBuilder(); - sb.Append("{") + sb.Append('{') .Append($"\"index\":{indexJson},") .Append($"\"name\":\"{name}\",") .Append("\"type\":\"json\""); @@ -462,7 +457,7 @@ internal async Task CreateIndexAsync(string name, sb.Append($",\"partitioned\":{options.Partitioned.ToString().ToLowerInvariant()}"); } - sb.Append("}"); + sb.Append('}'); var request = sb.ToString(); @@ -568,7 +563,7 @@ public override string ToString() #endregion #region Helper - + /// public IFlurlRequest NewRequest() { diff --git a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs index ae48022..ec26576 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.IO; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using CouchDB.Driver.Shared; using Flurl.Http; using Flurl.Http.Content; +using Newtonsoft.Json; namespace CouchDB.Driver.Extensions { @@ -60,5 +62,14 @@ public static IFlurlRequest ApplyQueryParametersOptions(this IFlurlRequest reque return request; } + + public static bool IsSuccessful(this IFlurlResponse response) + { + return + response.StatusCode == (int)HttpStatusCode.OK || + response.StatusCode == (int)HttpStatusCode.Created || + response.StatusCode == (int)HttpStatusCode.Accepted || + response.StatusCode == (int)HttpStatusCode.NoContent; + } } } diff --git a/src/CouchDB.Driver/Helpers/RequestsHelper.cs b/src/CouchDB.Driver/Helpers/RequestsHelper.cs index 2d558f2..8124f31 100644 --- a/src/CouchDB.Driver/Helpers/RequestsHelper.cs +++ b/src/CouchDB.Driver/Helpers/RequestsHelper.cs @@ -18,7 +18,7 @@ public static async Task SendRequestAsync(this Task a { CouchError couchError = await ex.GetResponseJsonAsync().ConfigureAwait(false) ?? new CouchError(); - throw ex.Call.HttpStatus switch + throw (HttpStatusCode?)ex.StatusCode switch { HttpStatusCode.Conflict => new CouchConflictException(couchError, ex), HttpStatusCode.NotFound => new CouchNotFoundException(couchError, ex), diff --git a/src/CouchDB.Driver/Indexes/IndexDefinition.cs b/src/CouchDB.Driver/Indexes/IndexDefinition.cs index 660e5cb..888a8bb 100644 --- a/src/CouchDB.Driver/Indexes/IndexDefinition.cs +++ b/src/CouchDB.Driver/Indexes/IndexDefinition.cs @@ -19,13 +19,13 @@ public override string ToString() { var sb = new StringBuilder(); - sb.Append("{"); + sb.Append('{'); // Partial Selector if (PartialSelector != null) { sb.Append(PartialSelector); - sb.Append(","); + sb.Append(','); } // Fields diff --git a/src/CouchDB.Driver/Query/QueryTranslator.cs b/src/CouchDB.Driver/Query/QueryTranslator.cs index d4794b2..b53748c 100644 --- a/src/CouchDB.Driver/Query/QueryTranslator.cs +++ b/src/CouchDB.Driver/Query/QueryTranslator.cs @@ -20,7 +20,7 @@ public string Translate(Expression e) { _isSelectorSet = false; _sb.Clear(); - _sb.Append("{"); + _sb.Append('{'); Visit(e); // If no Where() calls @@ -30,7 +30,7 @@ public string Translate(Expression e) if (_sb.Length > 1) { _sb.Length--; - _sb.Append(","); + _sb.Append(','); } _sb.Append("\"selector\":{}"); } @@ -39,7 +39,7 @@ public string Translate(Expression e) _sb.Length--; } - _sb.Append("}"); + _sb.Append('}'); var body = _sb.ToString(); return body; } diff --git a/src/CouchDB.Driver/Query/Translators/BinaryExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/BinaryExpressionTranslator.cs index 2f609d2..864da9c 100644 --- a/src/CouchDB.Driver/Query/Translators/BinaryExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/BinaryExpressionTranslator.cs @@ -9,7 +9,7 @@ internal partial class QueryTranslator { protected override Expression VisitBinary(BinaryExpression b) { - _sb.Append("{"); + _sb.Append('{'); switch (b.NodeType) { case ExpressionType.Equal: @@ -34,7 +34,7 @@ protected override Expression VisitBinary(BinaryExpression b) Visit(mb.Left); _sb.Append(":{\"$mod\":["); Visit(mb.Right); - _sb.Append(","); + _sb.Append(','); Visit(b.Right); _sb.Append("]}}"); return b; @@ -42,7 +42,7 @@ protected override Expression VisitBinary(BinaryExpression b) } default: Visit(b.Left); - _sb.Append(":"); + _sb.Append(':'); Visit(b.Right); break; } @@ -58,7 +58,7 @@ protected override Expression VisitBinary(BinaryExpression b) VisitBinaryConditionOperator(b); break; } - _sb.Append("}"); + _sb.Append('}'); return b; } @@ -69,7 +69,7 @@ void InspectBinaryChildren(BinaryExpression e, ExpressionType nodeType) if (e.Left is BinaryExpression lb && lb.NodeType == nodeType) { InspectBinaryChildren(lb, nodeType); - _sb.Append(","); + _sb.Append(','); Visit(e.Right); return; } @@ -77,13 +77,13 @@ void InspectBinaryChildren(BinaryExpression e, ExpressionType nodeType) if (e.Right is BinaryExpression rb && rb.NodeType == nodeType) { Visit(e.Left); - _sb.Append(","); + _sb.Append(','); InspectBinaryChildren(rb, nodeType); return; } Visit(e.Left); - _sb.Append(","); + _sb.Append(','); Visit(e.Right); } @@ -100,7 +100,7 @@ void InspectBinaryChildren(BinaryExpression e, ExpressionType nodeType) } InspectBinaryChildren(b, b.NodeType); - _sb.Append("]"); + _sb.Append(']'); } private void VisitBinaryConditionOperator(BinaryExpression b) @@ -130,7 +130,7 @@ private void VisitBinaryConditionOperator(BinaryExpression b) } Visit(b.Right); - _sb.Append("}"); + _sb.Append('}'); } } } diff --git a/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs index 87da6ee..7ce8f0c 100644 --- a/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs @@ -69,18 +69,18 @@ private void HandleConstant(object constant) private void VisitIEnumerable(IEnumerable list) { - _sb.Append("["); + _sb.Append('['); var needsComma = false; foreach (var item in list) { if (needsComma) { - _sb.Append(","); + _sb.Append(','); } HandleConstant(item); needsComma = true; } - _sb.Append("]"); + _sb.Append(']'); } } } diff --git a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs index 149245a..2b6ad26 100644 --- a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs @@ -180,7 +180,7 @@ private Expression VisitWhereMethod(MethodCallExpression m) _sb.Append("\"selector\":"); Expression lambdaBody = m.GetLambdaBody(); Visit(lambdaBody); - _sb.Append(","); + _sb.Append(','); _isSelectorSet = true; return m; } @@ -208,7 +208,7 @@ void InspectOrdering(Expression e) return; } - _sb.Append(","); + _sb.Append(','); } InspectOrdering(m); @@ -235,7 +235,7 @@ void InspectOrdering(Expression e) break; case "ThenByDescending": InspectOrdering(o.Arguments[0]); - _sb.Append("{"); + _sb.Append('{'); Visit(lambdaBody); _sb.Append(":\"desc\"}"); break; @@ -243,7 +243,7 @@ void InspectOrdering(Expression e) return; } - _sb.Append(","); + _sb.Append(','); } InspectOrdering(m); @@ -274,7 +274,7 @@ private Expression VisitSelectMethod(MethodCallExpression m) foreach (Expression a in n.Arguments) { Visit(a); - _sb.Append(","); + _sb.Append(','); } _sb.Length--; } @@ -298,7 +298,7 @@ private Expression VisitSelectMethod(MethodCallExpression m) private Expression VisitAnyMethod(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[0]); _sb.Append(":{\"$elemMatch\":"); Expression lambdaBody = m.GetLambdaBody(); @@ -308,7 +308,7 @@ private Expression VisitAnyMethod(MethodCallExpression m) } private Expression VisitAllMethod(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[0]); _sb.Append(":{\"$allMatch\":"); Expression lambdaBody = m.GetLambdaBody(); @@ -326,7 +326,7 @@ private Expression VisitUseBookmarkMethod(MethodCallExpression m) Visit(m.Arguments[0]); _sb.Append("\"bookmark\":"); Visit(m.Arguments[1]); - _sb.Append(","); + _sb.Append(','); return m; } private Expression VisitWithQuorumMethod(MethodCallExpression m) @@ -334,21 +334,21 @@ private Expression VisitWithQuorumMethod(MethodCallExpression m) Visit(m.Arguments[0]); _sb.Append("\"r\":"); Visit(m.Arguments[1]); - _sb.Append(","); + _sb.Append(','); return m; } private Expression VisitWithoutIndexUpdateMethod(MethodCallExpression m) { Visit(m.Arguments[0]); _sb.Append("\"update\":false"); - _sb.Append(","); + _sb.Append(','); return m; } private Expression VisitFromStableMethod(MethodCallExpression m) { Visit(m.Arguments[0]); _sb.Append("\"stable\":true"); - _sb.Append(","); + _sb.Append(','); return m; } private Expression VisitUseIndexMethod(MethodCallExpression m) @@ -378,14 +378,14 @@ private Expression VisitUseIndexMethod(MethodCallExpression m) throw new ArgumentException("UseIndex requires 1 or 2 strings"); } - _sb.Append(","); + _sb.Append(','); return m; } public Expression VisitIncludeExecutionStatsMethod(MethodCallExpression m) { Visit(m.Arguments[0]); _sb.Append("\"execution_stats\":true"); - _sb.Append(","); + _sb.Append(','); return m; } @@ -393,7 +393,7 @@ public Expression VisitIncludeConflictsMethod(MethodCallExpression m) { Visit(m.Arguments[0]); _sb.Append("\"conflicts\":true"); - _sb.Append(","); + _sb.Append(','); return m; } @@ -410,7 +410,7 @@ public Expression VisitSelectFieldMethod(MethodCallExpression m) foreach (Expression a in fieldExpressions) { Visit(a); - _sb.Append(","); + _sb.Append(','); } _sb.Length--; @@ -446,7 +446,7 @@ public Expression VisitConvertMethod(MethodCallExpression m) private Expression VisitEnumerableContains(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[0]); _sb.Append(":{\"$all\":"); Visit(m.Arguments[1]); @@ -455,7 +455,7 @@ private Expression VisitEnumerableContains(MethodCallExpression m) } private Expression VisitInMethod(MethodCallExpression m, bool not = false) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[0]); _sb.Append(not ? ":{\"$nin\":" : ":{\"$in\":"); @@ -470,7 +470,7 @@ private Expression VisitInMethod(MethodCallExpression m, bool not = false) private Expression VisitFieldExistsMethod(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[1]); _sb.Append(":{\"$exists\":true"); _sb.Append("}}"); @@ -478,7 +478,7 @@ private Expression VisitFieldExistsMethod(MethodCallExpression m) } private Expression VisitIsCouchTypeMethod(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[0]); _sb.Append(":{\"$type\":"); ConstantExpression cExpression = m.Arguments[1] as ConstantExpression ?? throw new ArgumentException("Argument is not of type ConstantExpression."); @@ -494,7 +494,7 @@ private Expression VisitIsCouchTypeMethod(MethodCallExpression m) private Expression VisitIsMatchMethod(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Arguments[0]); _sb.Append(":{\"$regex\":"); Visit(m.Arguments[1]); @@ -508,7 +508,7 @@ private Expression VisitIsMatchMethod(MethodCallExpression m) private Expression VisitContainsMethod(MethodCallExpression m) { - _sb.Append("{"); + _sb.Append('{'); Visit(m.Object); _sb.Append(":{\"$all\":["); Visit(m.Arguments[0]); diff --git a/src/CouchDB.Driver/Query/Translators/UnaryExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/UnaryExpressionTranslator.cs index 11b8c93..847f2ba 100644 --- a/src/CouchDB.Driver/Query/Translators/UnaryExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/UnaryExpressionTranslator.cs @@ -14,18 +14,18 @@ protected override Expression VisitUnary(UnaryExpression u) switch (u.Operand) { case BinaryExpression b when (b.NodeType == ExpressionType.Or || b.NodeType == ExpressionType.OrElse): - _sb.Append("{"); + _sb.Append('{'); VisitBinaryCombinationOperator(b, true); - _sb.Append("}"); + _sb.Append('}'); break; case MethodCallExpression m when m.Method.Name == "In": VisitInMethod(m, true); break; default: - _sb.Append("{"); + _sb.Append('{'); _sb.Append("\"$not\":"); Visit(u.Operand); - _sb.Append("}"); + _sb.Append('}'); break; } break; diff --git a/tests/CouchDB.Driver.UnitTests/Attachments_Tests.cs b/tests/CouchDB.Driver.UnitTests/Attachments_Tests.cs index 55f4524..5937b54 100644 --- a/tests/CouchDB.Driver.UnitTests/Attachments_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Attachments_Tests.cs @@ -85,7 +85,7 @@ public async Task DownloadAttachment() httpTest .ShouldHaveCalled("http://localhost/rebels/1/luke.txt") .WithVerb(HttpMethod.Get) - .WithHeader("If-Match", "xxx"); + .WithHeader("If-Match", "xxx2"); Assert.Equal(@"anyfolder\luke.txt", newPath); } diff --git a/tests/CouchDB.Driver.UnitTests/Authentication_Test.cs b/tests/CouchDB.Driver.UnitTests/Authentication_Test.cs index b8cf8ec..51b2919 100644 --- a/tests/CouchDB.Driver.UnitTests/Authentication_Test.cs +++ b/tests/CouchDB.Driver.UnitTests/Authentication_Test.cs @@ -49,11 +49,15 @@ public async Task Cookie() using var httpTest = new HttpTest(); // Cookie response - var cookieResponse = new HttpResponseMessage(); - cookieResponse.Headers.Add("Content-Typ", "application/json"); - cookieResponse.Headers.Add("Set-Cookie", $"AuthSession={token}; Version=1; Path=/; HttpOnly"); - cookieResponse.Content = new StringContent("{}"); - httpTest.ResponseQueue.Enqueue(cookieResponse); + var headers = new + { + Content_Type = "application/json" + }; + var cookies = new + { + AuthSession = token + }; + httpTest.RespondWith(string.Empty, 200, headers, cookies); SetupListResponse(httpTest); await using var client = new CouchClient("http://localhost", s => s.UseCookieAuthentication("root", "relax")); @@ -61,9 +65,9 @@ public async Task Cookie() var all = await rebels.ToListAsync(); var authCookie = httpTest.CallLog - .Single(c => c.Request.RequestUri.ToString().Contains("_session")) - .FlurlRequest.Cookies.Single(c => c.Key == "AuthSession").Value; - Assert.Equal(token, authCookie.Value); + .Single(c => c.Request.Url.ToString().Contains("_session")) + .Response.Cookies.Single(c => c.Name == "AuthSession").Value; + Assert.Equal(token, authCookie); } [Fact] diff --git a/tests/CouchDB.Driver.UnitTests/Client_Tests.cs b/tests/CouchDB.Driver.UnitTests/Client_Tests.cs index e334bbb..561371e 100644 --- a/tests/CouchDB.Driver.UnitTests/Client_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Client_Tests.cs @@ -106,7 +106,7 @@ public async Task GetOrCreateDatabase_402_ReturnDatabase() { using var httpTest = new HttpTest(); // Operation result - httpTest.RespondWith((HttpContent)null, 412); + httpTest.RespondWith(string.Empty, 412); // Logout httpTest.RespondWithJson(new { ok = true }); @@ -157,7 +157,7 @@ public async Task CreateDatabase_402_ThrowsException() { using var httpTest = new HttpTest(); // Operation result - httpTest.RespondWith((HttpContent)null, 412); + httpTest.RespondWith((string)null, 412); // Logout httpTest.RespondWithJson(new { ok = true }); @@ -260,7 +260,7 @@ public async Task Exists() public async Task NotExists() { using var httpTest = new HttpTest(); - httpTest.RespondWith((HttpContent)null, 404); + httpTest.RespondWith(string.Empty, 404); // Logout httpTest.RespondWithJson(new { ok = true }); @@ -296,7 +296,7 @@ public async Task IsUp() public async Task IsNotUp() { using var httpTest = new HttpTest(); - httpTest.RespondWith((HttpContent)null, 404); + httpTest.RespondWith(string.Empty, 404); // Logout httpTest.RespondWithJson(new { ok = true }); @@ -309,6 +309,24 @@ public async Task IsNotUp() .WithVerb(HttpMethod.Get); } + [Fact] + public async Task IsNotUp_Timeout() + { + using var httpTest = new HttpTest(); + httpTest.SimulateTimeout(); + // Logout + httpTest.RespondWithJson(new { ok = true }); + + await using var client = new CouchClient("http://localhost"); + var result = await client.IsUpAsync(); + Assert.False(result); + + httpTest + .ShouldHaveCalled($"http://localhost/_up") + .WithVerb(HttpMethod.Get); + } + + [Fact] public async Task DatabaseNames() { @@ -350,7 +368,7 @@ public async Task ActiveTasks() public async Task ConflictException() { using var httpTest = new HttpTest(); - httpTest.RespondWith(status: (int)HttpStatusCode.Conflict); + httpTest.RespondWith(string.Empty, (int)HttpStatusCode.Conflict); await using var client = new CouchClient("http://localhost"); var couchException = await Assert.ThrowsAsync(() => client.CreateDatabaseAsync()); @@ -361,7 +379,7 @@ public async Task ConflictException() public async Task NotFoundException() { using var httpTest = new HttpTest(); - httpTest.RespondWith(status: (int)HttpStatusCode.NotFound); + httpTest.RespondWith(string.Empty, (int)HttpStatusCode.NotFound); await using var client = new CouchClient("http://localhost"); var couchException = await Assert.ThrowsAsync(() => client.DeleteDatabaseAsync()); @@ -390,7 +408,7 @@ public async Task GenericExceptionWithMessage() await using var client = new CouchClient("http://localhost"); var db = client.GetDatabase(); - var couchException = await Assert.ThrowsAsync(() => db.FindAsync("aoeu")); + var couchException = await Assert.ThrowsAsync(() => db.CompactAsync()); Assert.Equal(message, couchException.Message); Assert.Equal(reason, couchException.Reason); Assert.IsType(couchException.InnerException); @@ -400,11 +418,11 @@ public async Task GenericExceptionWithMessage() public async Task GenericExceptionNoMessage() { using var httpTest = new HttpTest(); - httpTest.RespondWith(status: (int)HttpStatusCode.InternalServerError); + httpTest.RespondWith(string.Empty, (int)HttpStatusCode.InternalServerError); await using var client = new CouchClient("http://localhost"); var db = client.GetDatabase(); - var couchException = await Assert.ThrowsAsync(() => db.FindAsync("aoeu")); + var couchException = await Assert.ThrowsAsync(() => db.CompactAsync()); Assert.IsType(couchException.InnerException); } diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index 4a83aba..a69348c 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -10,7 +10,7 @@ namespace CouchDB.Driver.UnitTests { - public class Database_Tests: IAsyncDisposable + public class Database_Tests : IAsyncDisposable { private readonly ICouchClient _client; private readonly ICouchDatabase _rebels; @@ -57,8 +57,8 @@ public async Task FindWithConflicts() var newR = await _rebels.FindAsync("1", true); httpTest - .ShouldHaveCalled("http://localhost/rebels/1") - .WithQueryParamValue("conflicts", "true") + .ShouldHaveCalled("http://localhost/rebels/1*") + .WithQueryParam("conflicts", "true") .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs b/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs index d98db93..91b071f 100644 --- a/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Feed/GetChanges_Tests.cs @@ -36,7 +36,7 @@ public async Task GetChangesAsync_Default() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") + .ShouldHaveCalled("http://localhost/rebels/_changes*") .WithVerb(HttpMethod.Get); } @@ -59,9 +59,9 @@ public async Task GetChangesAsync_WithOptions() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "longpoll") - .WithQueryParamValue("attachments", "true") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "longpoll") + .WithQueryParam("attachments", "true") .WithVerb(HttpMethod.Get); } @@ -85,8 +85,8 @@ public async Task GetChangesAsync_WithIdsFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("filter", "_doc_ids") + .ShouldHaveCalled("http://localhost/rebels/_changes**") + .WithQueryParam("filter", "_doc_ids") .WithJsonBody(f => f.DocumentIds.Contains(docId)) .WithVerb(HttpMethod.Post); } @@ -108,8 +108,8 @@ public async Task GetChangesAsync_WithSelectorFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("filter", "_selector") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("filter", "_selector") .WithContentType("application/json") .With(call => call.RequestBody == $"{{\"selector\":{{\"_id\":\"{docId}\"}}}}") .WithVerb(HttpMethod.Post); @@ -131,8 +131,8 @@ public async Task GetChangesAsync_WithDesignFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("filter", "_design") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("filter", "_design") .WithVerb(HttpMethod.Get); } @@ -153,9 +153,9 @@ public async Task GetChangesAsync_WithViewFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("filter", "_view") - .WithQueryParamValue("view", view) + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("filter", "_view") + .WithQueryParam("view", view) .WithVerb(HttpMethod.Get); } diff --git a/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs b/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs index 57f8830..6aa00c1 100644 --- a/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Feed/GetContinuousChangesAsync_Tests.cs @@ -45,8 +45,8 @@ public async Task GetContinuousChangesAsync_Default() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "continuous") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "continuous") .WithVerb(HttpMethod.Get); } @@ -73,9 +73,9 @@ public async Task GetContinuousChangesAsync_WithOptions() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("attachments", "true") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "continuous") + .WithQueryParam("attachments", "true") .WithVerb(HttpMethod.Get); } @@ -103,9 +103,9 @@ public async Task GetContinuousChangesAsync_WithIdsFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("filter", "_doc_ids") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "continuous") + .WithQueryParam("filter", "_doc_ids") .WithJsonBody(f => f.DocumentIds.Contains(docId)) .WithVerb(HttpMethod.Post); } @@ -131,9 +131,9 @@ public async Task GetContinuousChangesAsync_WithSelectorFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("filter", "_selector") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "continuous") + .WithQueryParam("filter", "_selector") .WithContentType("application/json") .With(call => call.RequestBody == $"{{\"selector\":{{\"_id\":\"{docId}\"}}}}") .WithVerb(HttpMethod.Post); @@ -160,9 +160,9 @@ public async Task GetContinuousChangesAsync_WithDesignFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("filter", "_design") + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "continuous") + .WithQueryParam("filter", "_design") .WithVerb(HttpMethod.Get); } @@ -188,10 +188,10 @@ public async Task GetContinuousChangesAsync_WithViewFilter() // Assert httpTest - .ShouldHaveCalled("http://localhost/rebels/_changes") - .WithQueryParamValue("feed", "continuous") - .WithQueryParamValue("filter", "_view") - .WithQueryParamValue("view", view) + .ShouldHaveCalled("http://localhost/rebels/_changes*") + .WithQueryParam("feed", "continuous") + .WithQueryParam("filter", "_view") + .WithQueryParam("view", view) .WithVerb(HttpMethod.Get); } @@ -203,9 +203,7 @@ private string SetFeedResponse(HttpTest httpTest) Id = docId }); changeJson += "\n"; - byte[] byteArray = Encoding.ASCII.GetBytes(changeJson); - MemoryStream stream = new MemoryStream(byteArray); - httpTest.RespondWith(new StreamContent(stream)); + httpTest.RespondWith(changeJson); return docId; } } From 53d8087b1b33cf0f797972f87a9f81a0fa760e2d Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 3 Mar 2021 22:58:34 +0200 Subject: [PATCH 08/23] Rename Type arguments --- src/CouchDB.Driver/Types/CouchViewResult.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/CouchDB.Driver/Types/CouchViewResult.cs b/src/CouchDB.Driver/Types/CouchViewResult.cs index d38b96e..cabbd7f 100644 --- a/src/CouchDB.Driver/Types/CouchViewResult.cs +++ b/src/CouchDB.Driver/Types/CouchViewResult.cs @@ -7,8 +7,8 @@ namespace CouchDB.Driver.Types /// /// Object that stores results of a view. /// - /// The type of the value. - public class CouchViewResult + /// The type of the value. + public class CouchViewResult { /// /// Number of documents in the database/view. @@ -26,17 +26,16 @@ public class CouchViewResult /// Array of view row objects. This result contains only the document ID and revision. /// [JsonProperty("rows")] - public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); + public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); } /// /// Object that stores results of a view. /// /// The type of the value. - /// The type of the doc. - public class CouchViewResult - where TDoc : CouchDocument - + /// The type of the doc. + public class CouchViewResult + where TSource : CouchDocument { /// /// Number of documents in the database/view. @@ -54,6 +53,6 @@ public class CouchViewResult /// Array of view row objects. This result contains the document ID, revision and the documents. /// [JsonProperty("rows")] - public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); + public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); } } \ No newline at end of file From 9d04d79ace155c6845dd031d13f02ed7bf26fcd3 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Wed, 3 Mar 2021 22:17:31 +0100 Subject: [PATCH 09/23] #106 - Implement optional discriminator in query builder --- src/CouchDB.Driver/CouchClient.cs | 25 ++++++------ src/CouchDB.Driver/CouchDB.Driver.csproj | 1 + src/CouchDB.Driver/CouchDatabase.cs | 13 ++++++- .../Helpers/MethodCallExpressionBuilder.cs | 12 ++++++ src/CouchDB.Driver/ICouchClient.cs | 15 +++++--- src/CouchDB.Driver/Options/CouchOptions.cs | 1 + src/CouchDB.Driver/Query/IQueryOptimizer.cs | 2 +- src/CouchDB.Driver/Query/QueryCompiler.cs | 11 +++--- src/CouchDB.Driver/Query/QueryOptimizer.cs | 12 +++++- .../MethodCallExpressionTranslator.cs | 1 - src/CouchDB.Driver/Types/CouchDocument.cs | 4 ++ .../Database_Tests.cs | 30 +++++++++++++++ .../Find/Find_Discriminator.cs | 38 +++++++++++++++++++ .../Find/Find_Selector_Combinations.cs | 1 - .../_Models/Battle.cs | 1 - .../_Models/NewRebel.cs | 6 +-- .../_Models/OtherRebel.cs | 5 +-- .../_Models/Species.cs | 6 +-- 18 files changed, 140 insertions(+), 44 deletions(-) create mode 100644 tests/CouchDB.Driver.UnitTests/Find/Find_Discriminator.cs diff --git a/src/CouchDB.Driver/CouchClient.cs b/src/CouchDB.Driver/CouchClient.cs index a1b484d..c30aa8a 100644 --- a/src/CouchDB.Driver/CouchClient.cs +++ b/src/CouchDB.Driver/CouchClient.cs @@ -11,7 +11,6 @@ using CouchDB.Driver.DTOs; using CouchDB.Driver.Exceptions; using Newtonsoft.Json; -using System.Net.Http; using System.Net; using System.Threading; using CouchDB.Driver.Options; @@ -109,16 +108,16 @@ private IFlurlClient GetConfiguredClient() => #region CRUD /// - public ICouchDatabase GetDatabase(string database) where TSource : CouchDocument + public ICouchDatabase GetDatabase(string database, string? discriminator = null) where TSource : CouchDocument { CheckDatabaseName(database); var queryContext = new QueryContext(Endpoint, database); - return new CouchDatabase(_flurlClient, _options, queryContext); + return new CouchDatabase(_flurlClient, _options, queryContext, discriminator); } /// public async Task> CreateDatabaseAsync(string database, - int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) + int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument { QueryContext queryContext = NewQueryContext(database); @@ -127,7 +126,7 @@ public async Task> CreateDatabaseAsync(string d if (response.IsSuccessful()) { - return new CouchDatabase(_flurlClient, _options, queryContext); + return new CouchDatabase(_flurlClient, _options, queryContext, discriminator); } if (response.StatusCode == (int)HttpStatusCode.PreconditionFailed) @@ -140,7 +139,7 @@ public async Task> CreateDatabaseAsync(string d /// public async Task> GetOrCreateDatabaseAsync(string database, - int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) + int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument { QueryContext queryContext = NewQueryContext(database); @@ -149,7 +148,7 @@ public async Task> GetOrCreateDatabaseAsync(str if (response.IsSuccessful() || response.StatusCode == (int)HttpStatusCode.PreconditionFailed) { - return new CouchDatabase(_flurlClient, _options, queryContext); + return new CouchDatabase(_flurlClient, _options, queryContext, discriminator); } throw new CouchException($"Something wrong happened while creating database {database}."); @@ -206,17 +205,17 @@ public ICouchDatabase GetDatabase() where TSource : CouchDocum } /// - public Task> CreateDatabaseAsync(int? shards = null, int? replicas = null, + public Task> CreateDatabaseAsync(int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument { - return CreateDatabaseAsync(GetClassName(), shards, replicas, cancellationToken); + return CreateDatabaseAsync(GetClassName(), shards, replicas, discriminator, cancellationToken); } /// - public Task> GetOrCreateDatabaseAsync(int? shards = null, int? replicas = null, + public Task> GetOrCreateDatabaseAsync(int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument { - return GetOrCreateDatabaseAsync(GetClassName(), shards, replicas, cancellationToken); + return GetOrCreateDatabaseAsync(GetClassName(), shards, replicas, discriminator, cancellationToken); } /// @@ -244,13 +243,13 @@ public ICouchDatabase GetUsersDatabase() where TUser : CouchUser /// public Task> GetOrCreateUsersDatabaseAsync(CancellationToken cancellationToken = default) { - return GetOrCreateDatabaseAsync(null, null, cancellationToken); + return GetOrCreateDatabaseAsync(null, null, null, cancellationToken); } /// public Task> GetOrCreateUsersDatabaseAsync(CancellationToken cancellationToken = default) where TUser : CouchUser { - return GetOrCreateDatabaseAsync(null, null, cancellationToken); + return GetOrCreateDatabaseAsync(null, null, null, cancellationToken); } #endregion diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index bb25f97..c5a6624 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -39,6 +39,7 @@ + diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 2ee95be..4b39ab3 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -38,6 +38,7 @@ public class CouchDatabase : ICouchDatabase private readonly IFlurlClient _flurlClient; private readonly CouchOptions _options; private readonly QueryContext _queryContext; + private readonly string? _discriminator; /// public string Database => _queryContext.DatabaseName; @@ -48,16 +49,17 @@ public class CouchDatabase : ICouchDatabase /// public ILocalDocuments LocalDocuments { get; } - internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryContext queryContext) + internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryContext queryContext, string? discriminator) { _flurlClient = flurlClient; _options = options; _queryContext = queryContext; + _discriminator = discriminator; var queryOptimizer = new QueryOptimizer(); var queryTranslator = new QueryTranslator(options); var querySender = new QuerySender(flurlClient, queryContext); - var queryCompiler = new QueryCompiler(queryOptimizer, queryTranslator, querySender); + var queryCompiler = new QueryCompiler(queryOptimizer, queryTranslator, querySender, _discriminator); _queryProvider = new CouchQueryProvider(queryCompiler); Security = new CouchSecurity(NewRequest); @@ -187,6 +189,7 @@ public async Task AddAsync(TSource document, bool batch = false, Cancel request = request.SetQueryParam("batch", "ok"); } + document.Discriminator = _discriminator; DocumentSaveResponse response = await request .PostJsonAsync(document, cancellationToken) .ReceiveJson() @@ -218,6 +221,7 @@ public async Task AddOrUpdateAsync(TSource document, bool batch = false request = request.SetQueryParam("batch", "ok"); } + document.Discriminator = _discriminator; DocumentSaveResponse response = await request .PutJsonAsync(document, cancellationToken) .ReceiveJson() @@ -262,6 +266,11 @@ public async Task> AddOrUpdateRangeAsync(IList doc { Check.NotNull(documents, nameof(documents)); + foreach(TSource? document in documents) + { + document.Discriminator = _discriminator; + } + DocumentSaveResponse[] response = await NewRequest() .AppendPathSegment("_bulk_docs") .PostJsonAsync(new { docs = documents }, cancellationToken) diff --git a/src/CouchDB.Driver/Helpers/MethodCallExpressionBuilder.cs b/src/CouchDB.Driver/Helpers/MethodCallExpressionBuilder.cs index a917a54..832bd86 100644 --- a/src/CouchDB.Driver/Helpers/MethodCallExpressionBuilder.cs +++ b/src/CouchDB.Driver/Helpers/MethodCallExpressionBuilder.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using CouchDB.Driver.Extensions; +using CouchDB.Driver.Types; namespace CouchDB.Driver.Helpers { @@ -108,6 +109,17 @@ public static MethodCallExpression WrapInMethodWithoutSelector(this MethodCallEx return Expression.Call(genericMethodInfo, node); } + public static MethodCallExpression WrapInDiscriminatorFilter(this Expression node, string discriminator) + where TSource : CouchDocument + { + Check.NotNull(node, nameof(node)); + + Expression> filter = (d) => d.Discriminator == discriminator; + + return Expression.Call(typeof(Queryable), nameof(Queryable.Where), + new[] { typeof(TSource) }, node, filter); + } + #endregion } } \ No newline at end of file diff --git a/src/CouchDB.Driver/ICouchClient.cs b/src/CouchDB.Driver/ICouchClient.cs index ff9c44c..b0c9c33 100644 --- a/src/CouchDB.Driver/ICouchClient.cs +++ b/src/CouchDB.Driver/ICouchClient.cs @@ -13,8 +13,9 @@ public interface ICouchClient: IAsyncDisposable /// /// The type of database documents. /// The database name. + /// Filters documents by the given discriminator. /// An instance of the CouchDB database with given name. - ICouchDatabase GetDatabase(string database) where TSource : CouchDocument; + ICouchDatabase GetDatabase(string database, string? discriminator = null) where TSource : CouchDocument; /// /// Returns an instance of the CouchDB database with the given name. @@ -25,10 +26,11 @@ public interface ICouchClient: IAsyncDisposable /// The database name. /// The number of range partitions. Default is 8, unless overridden in the cluster config. /// The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config. + /// Filters documents by the given discriminator. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the newly created CouchDB database. Task> CreateDatabaseAsync(string database, - int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) + int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument; /// @@ -40,10 +42,11 @@ Task> CreateDatabaseAsync(string database, /// The database name. /// Used when creating. The number of range partitions. Default is 8, unless overridden in the cluster config. /// Used when creating. The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config. + /// Filters documents by the given discriminator. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the newly created CouchDB database. Task> GetOrCreateDatabaseAsync(string database, - int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) + int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument; /// @@ -69,9 +72,10 @@ Task> GetOrCreateDatabaseAsync(string database, /// The type of database documents. /// The number of range partitions. Default is 8, unless overridden in the cluster config. /// The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config. + /// Filters documents by the given discriminator. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the newly created CouchDB database. - Task> CreateDatabaseAsync(int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) where TSource : CouchDocument; + Task> CreateDatabaseAsync(int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument; /// /// Returns an instance of the CouchDB database with the name type . @@ -80,9 +84,10 @@ Task> GetOrCreateDatabaseAsync(string database, /// The type of database documents. /// Used when creating. The number of range partitions. Default is 8, unless overridden in the cluster config. /// Used when creating. The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config. + /// Filters documents by the given discriminator. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the newly created CouchDB database. - Task> GetOrCreateDatabaseAsync(int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) + Task> GetOrCreateDatabaseAsync(int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument; /// diff --git a/src/CouchDB.Driver/Options/CouchOptions.cs b/src/CouchDB.Driver/Options/CouchOptions.cs index 8415c31..6dd5510 100644 --- a/src/CouchDB.Driver/Options/CouchOptions.cs +++ b/src/CouchDB.Driver/Options/CouchOptions.cs @@ -39,6 +39,7 @@ internal CouchOptions() PluralizeEntities = true; DocumentsCaseType = DocumentCaseType.UnderscoreCase; PropertiesCase = PropertyCaseType.CamelCase; + NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; LogOutOnDispose = true; } } diff --git a/src/CouchDB.Driver/Query/IQueryOptimizer.cs b/src/CouchDB.Driver/Query/IQueryOptimizer.cs index 663c135..d232bb7 100644 --- a/src/CouchDB.Driver/Query/IQueryOptimizer.cs +++ b/src/CouchDB.Driver/Query/IQueryOptimizer.cs @@ -4,6 +4,6 @@ namespace CouchDB.Driver.Query { internal interface IQueryOptimizer { - Expression Optimize(Expression e); + Expression Optimize(Expression e, string? discriminator); } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Query/QueryCompiler.cs b/src/CouchDB.Driver/Query/QueryCompiler.cs index a4b21b2..6817655 100644 --- a/src/CouchDB.Driver/Query/QueryCompiler.cs +++ b/src/CouchDB.Driver/Query/QueryCompiler.cs @@ -16,7 +16,7 @@ internal class QueryCompiler : IQueryCompiler private readonly IQueryOptimizer _queryOptimizer; private readonly IQueryTranslator _queryTranslator; private readonly IQuerySender _requestSender; - + private readonly string? _discriminator; private static readonly MethodInfo RequestSendMethod = typeof(IQuerySender).GetRuntimeMethods() .Single(m => (m.Name == nameof(IQuerySender.Send)) && m.IsGenericMethod); @@ -29,16 +29,17 @@ private static readonly MethodInfo PostProcessResultAsyncMethod = typeof(QueryCompiler).GetMethod(nameof(PostProcessResultAsync), BindingFlags.NonPublic | BindingFlags.Static); - public QueryCompiler(IQueryOptimizer queryOptimizer, IQueryTranslator queryTranslator, IQuerySender requestSender) + public QueryCompiler(IQueryOptimizer queryOptimizer, IQueryTranslator queryTranslator, IQuerySender requestSender, string? discriminator) { _queryOptimizer = queryOptimizer; _queryTranslator = queryTranslator; _requestSender = requestSender; + _discriminator = discriminator; } public string ToString(Expression query) { - Expression optimizedQuery = _queryOptimizer.Optimize(query); + Expression optimizedQuery = _queryOptimizer.Optimize(query, _discriminator); return _queryTranslator.Translate(optimizedQuery); } @@ -70,7 +71,7 @@ private TResult SendRequest(Expression query, bool async, CancellationT private TResult SendRequestWithoutFilter(Expression query, bool async, CancellationToken cancellationToken) { - Expression optimizedQuery = _queryOptimizer.Optimize(query); + Expression optimizedQuery = _queryOptimizer.Optimize(query, _discriminator); var body = _queryTranslator.Translate(optimizedQuery); return _requestSender.Send(body, async, cancellationToken); } @@ -78,7 +79,7 @@ private TResult SendRequestWithoutFilter(Expression query, bool async, private TResult SendRequestWithFilter(MethodCallExpression methodCallExpression, Expression query, bool async, CancellationToken cancellationToken) { - Expression optimizedQuery = _queryOptimizer.Optimize(query); + Expression optimizedQuery = _queryOptimizer.Optimize(query, _discriminator); if (!(optimizedQuery is MethodCallExpression optimizedMethodCall)) { diff --git a/src/CouchDB.Driver/Query/QueryOptimizer.cs b/src/CouchDB.Driver/Query/QueryOptimizer.cs index a266947..0c0ffc5 100644 --- a/src/CouchDB.Driver/Query/QueryOptimizer.cs +++ b/src/CouchDB.Driver/Query/QueryOptimizer.cs @@ -23,8 +23,18 @@ public QueryOptimizer() _nextWhereCalls = new Queue(); } - public Expression Optimize(Expression e) + public Expression Optimize(Expression e, string? discriminator) { + if (discriminator is not null) + { + Type? sourceType = e.Type.GetGenericArguments()[0]; + MethodInfo? wrapInWhere = typeof(MethodCallExpressionBuilder) + .GetMethod(nameof(MethodCallExpressionBuilder.WrapInDiscriminatorFilter)) + .MakeGenericMethod(new[] { sourceType }); + + e = (Expression)wrapInWhere.Invoke(null, new object[] { e, discriminator }); + } + e = LocalExpressions.PartialEval(e); return Visit(e); } diff --git a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs index 2b6ad26..29a70d9 100644 --- a/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Security.Authentication; using CouchDB.Driver.Extensions; -using CouchDB.Driver.Helpers; using CouchDB.Driver.Shared; #pragma warning disable IDE0058 // Expression value is never used diff --git a/src/CouchDB.Driver/Types/CouchDocument.cs b/src/CouchDB.Driver/Types/CouchDocument.cs index d4f8749..3b55d76 100644 --- a/src/CouchDB.Driver/Types/CouchDocument.cs +++ b/src/CouchDB.Driver/Types/CouchDocument.cs @@ -57,6 +57,10 @@ private Dictionary AttachmentsSetter [JsonIgnore] public CouchAttachmentsCollection Attachments { get; internal set; } + [DataMember] + [JsonProperty("_discriminator", NullValueHandling = NullValueHandling.Ignore)] + internal string Discriminator { get; set; } + [OnDeserialized] internal void OnDeserializedMethod(StreamingContext context) { diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index a69348c..8a8e2e5 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -112,6 +112,36 @@ public async Task CreateOrUpdate() .WithVerb(HttpMethod.Put); } + [Fact] + public async Task Create_Discriminator() + { + var rebels = _client.GetDatabase(database: "rebels", discriminator: "myRebels"); + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new { Id = "xxx", Ok = true, Rev = "xxx" }); + + var r = new Rebel { Name = "Luke" }; + var newR = await rebels.AddAsync(r); + Assert.Equal("myRebels", newR.Discriminator); + httpTest + .ShouldHaveCalled("http://localhost/rebels") + .WithVerb(HttpMethod.Post); + } + + [Fact] + public async Task CreateOrUpdate_Discriminator() + { + var rebels = _client.GetDatabase(database: "rebels", discriminator: "rebels"); + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new { Id = "xxx", Ok = true, Rev = "xxx" }); + + var r = new Rebel { Name = "Luke", Id = "1" }; + var newR = await rebels.AddOrUpdateAsync(r); + Assert.Equal("myRebels", newR.Discriminator); + httpTest + .ShouldHaveCalled("http://localhost/rebels/1") + .WithVerb(HttpMethod.Put); + } + [Fact] public async Task CreateOrUpdate_WithoutId() { diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Discriminator.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Discriminator.cs new file mode 100644 index 0000000..496d0ff --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Discriminator.cs @@ -0,0 +1,38 @@ +using CouchDB.Driver.UnitTests.Models; +using System.Linq; +using Xunit; + +namespace CouchDB.Driver.UnitTests.Find +{ + public class Find_Discriminator + { + private const string _databaseName = "allrebels"; + private readonly ICouchDatabase _rebels; + private readonly ICouchDatabase _simpleRebels; + + public Find_Discriminator() + { + var client = new CouchClient("http://localhost"); + _rebels = client.GetDatabase(_databaseName, nameof(Rebel)); + _simpleRebels = client.GetDatabase(_databaseName, nameof(SimpleRebel)); + } + + [Fact] + public void Discriminator_WithoutFilter() + { + var json1 = _rebels.ToString(); + var json2 = _simpleRebels.ToString(); + Assert.Equal(@"{""selector"":{""_discriminator"":""Rebel""}}", json1); + Assert.Equal(@"{""selector"":{""_discriminator"":""SimpleRebel""}}", json2); + } + + [Fact] + public void Discriminator_WithFilter() + { + var json1 = _rebels.Where(c => c.Age == 19).ToString(); + var json2 = _simpleRebels.Where(c => c.Age == 19).ToString(); + Assert.Equal(@"{""selector"":{""$and"":[{""age"":19},{""_discriminator"":""Rebel""}]}}", json1); + Assert.Equal(@"{""selector"":{""$and"":[{""age"":19},{""_discriminator"":""SimpleRebel""}]}}", json2); + } + } +} diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs index 78b93b3..f8110c6 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Selector_Combinations.cs @@ -1,7 +1,6 @@ using CouchDB.Driver.UnitTests.Models; using System; using Xunit; -using CouchDB.Driver.Extensions; using System.Linq; using CouchDB.Driver.Query.Extensions; using Flurl.Http.Testing; diff --git a/tests/CouchDB.Driver.UnitTests/_Models/Battle.cs b/tests/CouchDB.Driver.UnitTests/_Models/Battle.cs index dcccc17..e1205e2 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/Battle.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/Battle.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace CouchDB.Driver.UnitTests.Models { diff --git a/tests/CouchDB.Driver.UnitTests/_Models/NewRebel.cs b/tests/CouchDB.Driver.UnitTests/_Models/NewRebel.cs index fca1f13..42302d6 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/NewRebel.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/NewRebel.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CouchDB.Driver.UnitTests.Models +namespace CouchDB.Driver.UnitTests.Models { public class NewRebel : Rebel { diff --git a/tests/CouchDB.Driver.UnitTests/_Models/OtherRebel.cs b/tests/CouchDB.Driver.UnitTests/_Models/OtherRebel.cs index 6723f74..3d079a4 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/OtherRebel.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/OtherRebel.cs @@ -1,8 +1,5 @@ -using CouchDB.Driver.UnitTests.Models; -using Newtonsoft.Json; +using Newtonsoft.Json; using System; -using System.Collections.Generic; -using System.Text; namespace CouchDB.Driver.UnitTests.Models { diff --git a/tests/CouchDB.Driver.UnitTests/_Models/Species.cs b/tests/CouchDB.Driver.UnitTests/_Models/Species.cs index 1a7b668..f443561 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/Species.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/Species.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CouchDB.Driver.UnitTests.Models +namespace CouchDB.Driver.UnitTests.Models { public enum Species { From 653daf2142144b3704e1b8ddb4af2ab28bc3720a Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Wed, 3 Mar 2021 23:27:02 +0200 Subject: [PATCH 10/23] Refactor View Methods Methods Added/Renamed to reflect what they return. GetViewAsync returns an IList GetViewWithDocAsync returns an IList<(TRow Value, TSource Doc)> for calls that also want the selected doc Detailed returns that contain the whole result returned by CouchDB. GetDetailedViewAsync returns a CouchViewResult GetDetailedViewWithDocAsync returns a CouchViewResult for calls that also want the selected doc --- src/CouchDB.Driver/CouchDatabase.cs | 25 +++++++++++++++---- src/CouchDB.Driver/ICouchDatabase.cs | 37 ++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 1e1ce4d..439a7d5 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -503,7 +503,23 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #region View /// - public Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public async Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + { + CouchViewResult result = await GetDetailedViewAsync(design, view, options, cancellationToken).ConfigureAwait(false); + + return result.Rows.Select(x => x.Value).ToArray(); + } + + /// + public async Task> GetViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + { + CouchViewResult result = await GetDetailedViewWithDocAsync(design, view, options, cancellationToken).ConfigureAwait(false); + + return result.Rows.Select(x => (x.Value, x.Doc)).ToArray(); + } + + /// + public Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) { Check.NotNull(view, nameof(view)); @@ -512,13 +528,12 @@ public Task> GetViewAsync(string design, string .SetQueryParams(options?.ToQueryParameters()); return request - .GetJsonAsync>(cancellationToken) + .GetJsonAsync>(cancellationToken) .SendRequestAsync(); } /// - public Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) - where TDoc : CouchDocument + public Task> GetDetailedViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) { Check.NotNull(view, nameof(view)); @@ -530,7 +545,7 @@ public Task> GetViewAsync(string des .SetQueryParams(options.ToQueryParameters()); return request - .GetJsonAsync>(cancellationToken) + .GetJsonAsync>(cancellationToken) .SendRequestAsync(); } diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 0b8725d..884a9d4 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -89,29 +89,50 @@ public interface ICouchDatabase: IOrderedQueryable Task> AddOrUpdateRangeAsync(IList documents, CancellationToken cancellationToken = default); /// - /// Execute a couchdb view and get a result with the values. + /// Executes the specified view function from the specified design document. /// - /// The type of the value that will be returned. + /// The type of the value that will be returned. /// The design to use. /// The view to use. /// Optional options to pass to the view. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the . - Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// - /// Execute a couchdb view and get a result with the values and the docs. + /// Executes the specified view function from the specified design document. Additionally, it returns the document. /// - /// The type of the value that will be returned. - /// The type of the document that will be returned. + /// The type of the value that will be returned. /// The design to use. /// The view to use. /// Optional options to pass to the view. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the . /// The options IncludeDocs will always be set to true. - Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) - where TDoc : CouchDocument; + Task> GetViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// Executes the specified view function from the specified design document. + /// + /// The type of the value that will be returned. + /// The design to use. + /// The view to use. + /// Optional options to pass to the view. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the . + Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + + /// + /// Executes the specified view function from the specified design document. Additionally, it returns the document. + /// + /// The type of the value that will be returned. + /// The design to use. + /// The view to use. + /// Optional options to pass to the view. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the . + /// The options IncludeDocs will always be set to true. + Task> GetDetailedViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Since CouchDB v3, it is deprecated (a no-op). From 786995675ae3cb476fe2f8bfd5aadc24710a34c8 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Thu, 4 Mar 2021 22:33:56 +0100 Subject: [PATCH 11/23] #106 - Implement discriminator in CouchContext --- README.md | 40 ++++++--- src/CouchDB.Driver/CouchClient.cs | 6 ++ src/CouchDB.Driver/CouchContext.cs | 83 ++++++++++++++----- src/CouchDB.Driver/CouchDB.Driver.csproj | 1 - src/CouchDB.Driver/ICouchClient.cs | 7 ++ .../Options/CouchDatabaseBuilder.cs | 4 +- .../Options/CouchDocumentBuilder.cs | 30 ++----- .../Options/CouchDocumentBuilder`.cs | 44 ++++++++++ src/CouchDB.Driver/Query/QueryOptimizer.cs | 7 +- .../CouchDbContext_Tests.cs | 57 +++++++++++++ .../Database_Tests.cs | 2 +- .../Settings_Tests.cs | 2 +- 12 files changed, 219 insertions(+), 64 deletions(-) create mode 100644 src/CouchDB.Driver/Options/CouchDocumentBuilder`.cs diff --git a/README.md b/README.md index c73b623..f4835d7 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ The produced Mango JSON: * [Indexing](#indexing) * [Index Options](#index-options) * [Partial Indexes](#partial-indexes) +* [Database Splitting](#database-splitting) * [Local (non-replicating) Documents](#local-(non-replicating)-documents) * [Bookmark and Execution stats](#bookmark-and-execution-stats) * [Users](#users) @@ -487,16 +488,7 @@ public class MyDeathStarContext : CouchContext { public CouchDatabase Rebels { get; set; } - protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) - { - optionsBuilder - .UseEndpoint("http://localhost:5984/") - .UseBasicAuthentication("admin", "admin") - // If it finds a index with the same name and ddoc (or null) - // but with different fields and/or sort order, - // it will override the index - .OverrideExistingIndexes(); - } + // OnConfiguring(CouchOptionsBuilder optionsBuilder) { ... } protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) { @@ -506,6 +498,34 @@ public class MyDeathStarContext : CouchContext } ``` +## Database Splitting + +It is possible to use the same database for multiple types: +```csharp +public class MyDeathStarContext : CouchContext +{ + public CouchDatabase Rebels { get; set; } + public CouchDatabase Vehicles { get; set; } + + // OnConfiguring(CouchOptionsBuilder optionsBuilder) { ... } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder.Document().ToDatabase("troups"); + databaseBuilder.Document().ToDatabase("troups"); + } +} +``` +> When multiple `CouchDatabase` point to the same **database**, a `_discriminator` field is added on documents creation. +> +> When querying, a filter by `discriminator` is added automatically. + +If you are not using `CouchContext`, you can still use the database slit feature: +```csharp +var rebels = client.GetDatabase("troups", nameof(Rebel)); +var vehicles = client.GetDatabase("troups", nameof(Vehicle)); +``` + ## Local (non-replicating) Documents The Local (non-replicating) document interface allows you to create local documents that are not replicated to other databases. diff --git a/src/CouchDB.Driver/CouchClient.cs b/src/CouchDB.Driver/CouchClient.cs index c30aa8a..6b77538 100644 --- a/src/CouchDB.Driver/CouchClient.cs +++ b/src/CouchDB.Driver/CouchClient.cs @@ -361,6 +361,12 @@ protected virtual async Task DisposeAsync(bool disposing) private string GetClassName() { Type type = typeof(TSource); + return GetClassName(type); + } + + public string GetClassName(Type type) + { + Check.NotNull(type, nameof(type)); return type.GetName(_options); } } diff --git a/src/CouchDB.Driver/CouchContext.cs b/src/CouchDB.Driver/CouchContext.cs index d6c4592..e0a9b79 100644 --- a/src/CouchDB.Driver/CouchContext.cs +++ b/src/CouchDB.Driver/CouchContext.cs @@ -18,7 +18,7 @@ protected virtual void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) private static readonly MethodInfo InitDatabasesGenericMethod = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) - .Single(mi => mi.Name == nameof(InitDatabasesAsync)); + .Single(mi => mi.Name == nameof(InitDatabaseAsync)); private static readonly MethodInfo ApplyDatabaseChangesGenericMethod = typeof(CouchContext).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) @@ -39,20 +39,9 @@ protected CouchContext(CouchOptions options) #pragma warning restore CA2214 // Do not call overridable methods in constructors Client = new CouchClient(options); - IEnumerable databasePropertyInfos = GetDatabaseProperties(); - foreach (PropertyInfo dbProperty in databasePropertyInfos) - { - Type documentType = dbProperty.PropertyType.GetGenericArguments()[0]; - - var initDatabasesTask = (Task)InitDatabasesGenericMethod.MakeGenericMethod(documentType) - .Invoke(this, new object[] {dbProperty, options}); - initDatabasesTask.ConfigureAwait(false).GetAwaiter().GetResult(); - - var applyDatabaseChangesTask = (Task)ApplyDatabaseChangesGenericMethod.MakeGenericMethod(documentType) - .Invoke(this, new object[] { dbProperty, options, databaseBuilder }); - applyDatabaseChangesTask.ConfigureAwait(false).GetAwaiter().GetResult(); - } + SetupDiscriminators(databaseBuilder); + InitializeDatabases(options, databaseBuilder); } public async ValueTask DisposeAsync() @@ -69,26 +58,76 @@ protected virtual async Task DisposeAsync(bool disposing) } } - private async Task InitDatabasesAsync(PropertyInfo propertyInfo, CouchOptions options) + private static void SetupDiscriminators(CouchDatabaseBuilder databaseBuilder) + { + // Get all options that share the database with another one + IEnumerable>? sharedDatabase = databaseBuilder.DocumentBuilders + .Where(opt => opt.Value.Database != null) + .GroupBy(v => v.Value.Database) + .Where(g => g.Count() > 1) + .SelectMany(g => g); + foreach (KeyValuePair option in sharedDatabase) + { + option.Value.Discriminator = option.Key.Name; + } + } + + private void InitializeDatabases(CouchOptions options, CouchDatabaseBuilder databaseBuilder) + { + foreach (PropertyInfo dbProperty in GetDatabaseProperties()) + { + Type documentType = dbProperty.PropertyType.GetGenericArguments()[0]; + + var initDatabasesTask = (Task)InitDatabasesGenericMethod.MakeGenericMethod(documentType) + .Invoke(this, new object[] { dbProperty, options, databaseBuilder }); + initDatabasesTask.ConfigureAwait(false).GetAwaiter().GetResult(); + + var applyDatabaseChangesTask = (Task)ApplyDatabaseChangesGenericMethod.MakeGenericMethod(documentType) + .Invoke(this, new object[] { dbProperty, options, databaseBuilder }); + applyDatabaseChangesTask.ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + + private async Task InitDatabaseAsync(PropertyInfo propertyInfo, CouchOptions options, CouchDatabaseBuilder databaseBuilder) where TSource : CouchDocument { - ICouchDatabase database = options.CheckDatabaseExists - ? await Client.GetOrCreateDatabaseAsync().ConfigureAwait(false) - : Client.GetDatabase(); + ICouchDatabase database; + Type documentType = typeof(TSource); + + if (databaseBuilder.DocumentBuilders.ContainsKey(documentType)) + { + var documentBuilder = (CouchDocumentBuilder)databaseBuilder.DocumentBuilders[documentType]; + var databaseName = documentBuilder.Database ?? Client.GetClassName(documentType); + database = options.CheckDatabaseExists + ? await Client.GetOrCreateDatabaseAsync(databaseName, documentBuilder.Shards, documentBuilder.Replicas, documentBuilder.Discriminator).ConfigureAwait(false) + : Client.GetDatabase(databaseName, documentBuilder.Discriminator); + } + else + { + database = options.CheckDatabaseExists + ? await Client.GetOrCreateDatabaseAsync().ConfigureAwait(false) + : Client.GetDatabase(); + } propertyInfo.SetValue(this, database); } private async Task ApplyDatabaseChangesAsync(PropertyInfo propertyInfo, CouchOptions options, CouchDatabaseBuilder databaseBuilder) - where TSource: CouchDocument + where TSource : CouchDocument { - if (!databaseBuilder.DocumentBuilders.ContainsKey(typeof(TSource))) + Type documentType = typeof(TSource); + if (!databaseBuilder.DocumentBuilders.ContainsKey(documentType)) { return; } var database = (CouchDatabase)propertyInfo.GetValue(this); - var documentBuilder = (CouchDocumentBuilder)databaseBuilder.DocumentBuilders[typeof(TSource)]; + var documentBuilder = (CouchDocumentBuilder)databaseBuilder.DocumentBuilders[documentType]; + + if (!documentBuilder.IndexDefinitions.Any()) + { + return; + } List indexes = await database.GetIndexesAsync().ConfigureAwait(false); @@ -125,7 +164,7 @@ await database.CreateIndexAsync( { return; } - + IndexDefinition indexDefinition = database.NewIndexBuilder(indexSetup.IndexBuilderAction).Build(); if (!AreFieldsEqual(currentIndex.Fields, indexDefinition.Fields)) { diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index c5a6624..bb25f97 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -39,7 +39,6 @@ - diff --git a/src/CouchDB.Driver/ICouchClient.cs b/src/CouchDB.Driver/ICouchClient.cs index b0c9c33..2a61a9c 100644 --- a/src/CouchDB.Driver/ICouchClient.cs +++ b/src/CouchDB.Driver/ICouchClient.cs @@ -160,6 +160,13 @@ Task> GetOrCreateDatabaseAsync(int? shards = nu /// A task that represents the asynchronous operation. The task result contains the sequence of all active tasks. Task> GetActiveTasksAsync(CancellationToken cancellationToken = default); + /// + /// Get the database name for the given type. + /// + /// The type of database documents. + /// + string GetClassName(Type type); + /// /// URI of the CouchDB endpoint. /// diff --git a/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs b/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs index 168344e..0c758d6 100644 --- a/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchDatabaseBuilder.cs @@ -6,11 +6,11 @@ namespace CouchDB.Driver.Options { public class CouchDatabaseBuilder { - internal readonly Dictionary DocumentBuilders; + internal readonly Dictionary DocumentBuilders; internal CouchDatabaseBuilder() { - DocumentBuilders = new Dictionary(); + DocumentBuilders = new Dictionary(); } public CouchDocumentBuilder Document() diff --git a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs index 3bc9de9..0de9621 100644 --- a/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs +++ b/src/CouchDB.Driver/Options/CouchDocumentBuilder.cs @@ -1,26 +1,10 @@ -using System; -using System.Collections.Generic; -using CouchDB.Driver.Indexes; -using CouchDB.Driver.Types; - -namespace CouchDB.Driver.Options +namespace CouchDB.Driver.Options { - public class CouchDocumentBuilder - where TSource : CouchDocument + public abstract class CouchDocumentBuilder { - internal List> IndexDefinitions { get; } - - internal CouchDocumentBuilder() - { - IndexDefinitions = new List>(); - } - - public CouchDocumentBuilder HasIndex(string name, Action> indexBuilderAction, - IndexOptions? options = null) - { - var indexDefinition = new IndexSetupDefinition(name, indexBuilderAction, options); - IndexDefinitions.Add(indexDefinition); - return this; - } - } + internal string? Database { get; set; } + internal int? Shards { get; set; } + internal int? Replicas { get; set; } + internal string? Discriminator { get; set; } + } } \ No newline at end of file diff --git a/src/CouchDB.Driver/Options/CouchDocumentBuilder`.cs b/src/CouchDB.Driver/Options/CouchDocumentBuilder`.cs new file mode 100644 index 0000000..aef636a --- /dev/null +++ b/src/CouchDB.Driver/Options/CouchDocumentBuilder`.cs @@ -0,0 +1,44 @@ +using CouchDB.Driver.Indexes; +using CouchDB.Driver.Types; +using System; +using System.Collections.Generic; + +namespace CouchDB.Driver.Options +{ + public class CouchDocumentBuilder : CouchDocumentBuilder + where TSource : CouchDocument + { + internal List> IndexDefinitions { get; } + + internal CouchDocumentBuilder() + { + IndexDefinitions = new List>(); + } + + public CouchDocumentBuilder ToDatabase(string database) + { + Database = database; + return this; + } + + public CouchDocumentBuilder WithShards(int shards) + { + Shards = shards; + return this; + } + + public CouchDocumentBuilder WithReplicas(int replicas) + { + Replicas = replicas; + return this; + } + + public CouchDocumentBuilder HasIndex(string name, Action> indexBuilderAction, + IndexOptions? options = null) + { + var indexDefinition = new IndexSetupDefinition(name, indexBuilderAction, options); + IndexDefinitions.Add(indexDefinition); + return this; + } + } +} diff --git a/src/CouchDB.Driver/Query/QueryOptimizer.cs b/src/CouchDB.Driver/Query/QueryOptimizer.cs index 0c0ffc5..4ec75ac 100644 --- a/src/CouchDB.Driver/Query/QueryOptimizer.cs +++ b/src/CouchDB.Driver/Query/QueryOptimizer.cs @@ -15,6 +15,8 @@ namespace CouchDB.Driver.Query /// internal class QueryOptimizer : ExpressionVisitor, IQueryOptimizer { + private static readonly MethodInfo WrapInWhereGenericMethod + = typeof(MethodCallExpressionBuilder).GetMethod(nameof(MethodCallExpressionBuilder.WrapInDiscriminatorFilter)); private bool _isVisitingWhereMethodOrChild; private readonly Queue _nextWhereCalls; @@ -28,10 +30,7 @@ public Expression Optimize(Expression e, string? discriminator) if (discriminator is not null) { Type? sourceType = e.Type.GetGenericArguments()[0]; - MethodInfo? wrapInWhere = typeof(MethodCallExpressionBuilder) - .GetMethod(nameof(MethodCallExpressionBuilder.WrapInDiscriminatorFilter)) - .MakeGenericMethod(new[] { sourceType }); - + MethodInfo? wrapInWhere = WrapInWhereGenericMethod.MakeGenericMethod(new[] { sourceType }); e = (Expression)wrapInWhere.Invoke(null, new object[] { e, discriminator }); } diff --git a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Tests.cs b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Tests.cs index 32d5002..2c37d8a 100644 --- a/tests/CouchDB.Driver.UnitTests/CouchDbContext_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/CouchDbContext_Tests.cs @@ -12,6 +12,8 @@ public class CouchDbContext_Tests private class MyDeathStarContext: CouchContext { public CouchDatabase Rebels { get; set; } + public CouchDatabase OtherRebels { get; set; } + public CouchDatabase SimpleRebels { get; set; } protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) { @@ -19,6 +21,17 @@ protected override void OnConfiguring(CouchOptionsBuilder optionsBuilder) .UseEndpoint("http://localhost:5984/") .UseBasicAuthentication("admin", "admin"); } + + protected override void OnDatabaseCreating(CouchDatabaseBuilder databaseBuilder) + { + databaseBuilder + .Document() + .ToDatabase("shared-rebels"); + + databaseBuilder + .Document() + .ToDatabase("shared-rebels"); + } } [Fact] @@ -51,5 +64,49 @@ await context.Rebels.AddAsync(new Rebel Assert.NotEmpty(result); Assert.Equal("Luke", result[0].Name); } + + [Fact] + public async Task Context_Query_Discriminator() + { + using var httpTest = new HttpTest(); + httpTest.RespondWithJson(new + { + Id = "176694", + Ok = true, + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e" + }); + httpTest.RespondWithJson(new + { + Id = "173694", + Ok = true, + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e" + }); + httpTest.RespondWithJson(new + { + docs = new object[] { + new { + Id = "176694", + Rev = "1-54f8e950cc338d2385d9b0cda2fd918e", + Name = "Luke" + } + } + }); + + await using var context = new MyDeathStarContext(); + await context.SimpleRebels.AddAsync(new SimpleRebel + { + Name = "Leia" + }); + await context.OtherRebels.AddAsync(new OtherRebel + { + Name = "Luke" + }); + var result = await context.OtherRebels.ToListAsync(); + Assert.NotEmpty(result); + Assert.Equal("Luke", result[0].Name); + Assert.Equal(@"{""_conflicts"":[],""name"":""Leia"",""age"":0,""_discriminator"":""SimpleRebel""}", httpTest.CallLog[0].RequestBody); + Assert.Equal(@"{""_conflicts"":[],""rebel_bith_date"":""0001-01-01T00:00:00"",""name"":""Luke"",""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""_discriminator"":""OtherRebel""}", httpTest.CallLog[1].RequestBody); + Assert.Equal(@"{""selector"":{""_discriminator"":""OtherRebel""}}", httpTest.CallLog[2].RequestBody); + } } } diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index 8a8e2e5..dfff06b 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -130,7 +130,7 @@ public async Task Create_Discriminator() [Fact] public async Task CreateOrUpdate_Discriminator() { - var rebels = _client.GetDatabase(database: "rebels", discriminator: "rebels"); + var rebels = _client.GetDatabase(database: "rebels", discriminator: "myRebels"); using var httpTest = new HttpTest(); httpTest.RespondWithJson(new { Id = "xxx", Ok = true, Rev = "xxx" }); diff --git a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs index eecd192..30803be 100644 --- a/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Settings_Tests.cs @@ -206,7 +206,7 @@ public async Task PropertyNullValueHandling_NotSet() var call = httpTest.CallLog.First(); Assert.NotNull(call); - Assert.Equal(@"{""_conflicts"":[],""name"":null,""surname"":null,""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000"",""skills"":null,""battles"":null,""vehicle"":null}", call.RequestBody); + Assert.Equal(@"{""_conflicts"":[],""age"":0,""isJedi"":false,""species"":0,""guid"":""00000000-0000-0000-0000-000000000000""}", call.RequestBody); } [Fact] From 912af753a825a4df729dfdb12f3993b765f4c223 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sat, 6 Mar 2021 17:37:14 +0100 Subject: [PATCH 12/23] #113 - Boolean expressions pruning --- .../CouchDB.Driver.DependencyInjection.csproj | 2 +- src/CouchDB.Driver/CouchDB.Driver.csproj | 2 +- .../Query/Extensions/ExpressionExtensions.cs | 17 +++++++ src/CouchDB.Driver/Query/QueryOptimizer.cs | 44 ++++++++++++++++++- .../Find/Find_Miscellaneous.cs | 33 ++++++++++++++ 5 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs diff --git a/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj b/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj index 4fc8d42..b897059 100644 --- a/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj +++ b/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj @@ -16,7 +16,7 @@ en 8.0 enable - 2.0.0 + 3.0.0 icon.png LICENSE.txt diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index bb25f97..ceef1ae 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -16,7 +16,7 @@ en latest enable - 2.0.0 + 3.0.0 icon.png LICENSE.txt diff --git a/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs b/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs new file mode 100644 index 0000000..13f37dd --- /dev/null +++ b/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs @@ -0,0 +1,17 @@ +using System.Linq.Expressions; + +namespace CouchDB.Driver.Query.Extensions +{ + internal static class ExpressionExtensions + { + public static bool IsTrue(this Expression expression) + { + return expression is ConstantExpression c && c.Value is bool b && b; + } + + public static bool IsFalse(this Expression expression) + { + return expression is ConstantExpression c && c.Value is bool b && !b; + } + } +} diff --git a/src/CouchDB.Driver/Query/QueryOptimizer.cs b/src/CouchDB.Driver/Query/QueryOptimizer.cs index 4ec75ac..07e82ab 100644 --- a/src/CouchDB.Driver/Query/QueryOptimizer.cs +++ b/src/CouchDB.Driver/Query/QueryOptimizer.cs @@ -5,6 +5,7 @@ using System.Reflection; using CouchDB.Driver.Extensions; using CouchDB.Driver.Helpers; +using CouchDB.Driver.Query.Extensions; using CouchDB.Driver.Shared; namespace CouchDB.Driver.Query @@ -61,7 +62,10 @@ protected override Expression VisitMethodCall(MethodCallExpression node) _isVisitingWhereMethodOrChild = true; Expression whereNode = VisitMethodCall(node); _isVisitingWhereMethodOrChild = false; - return whereNode; + + return whereNode.IsFalse() + ? node.Arguments[0] + : whereNode; } #endregion @@ -76,6 +80,12 @@ protected override Expression VisitMethodCall(MethodCallExpression node) Expression tail = Visit(node.Arguments[0]); LambdaExpression currentLambda = node.GetLambda(); Expression conditionExpression = Visit(currentLambda.Body); + + if (conditionExpression.IsFalse()) + { + return conditionExpression; + } + _nextWhereCalls.Dequeue(); while (_nextWhereCalls.Count > 0) @@ -280,6 +290,38 @@ protected override Expression VisitMethodCall(MethodCallExpression node) protected override Expression VisitBinary(BinaryExpression expression) { + if (expression.NodeType == ExpressionType.AndAlso) + { + if (expression.Right.IsFalse() || expression.Left.IsFalse()) + { + return Expression.Constant(false); + } + if (expression.Right.IsTrue()) + { + return Visit(expression.Left); + } + if (expression.Left.IsTrue()) + { + return Visit(expression.Right); + } + } + + if (expression.NodeType == ExpressionType.OrElse) + { + if (expression.Right.IsTrue() || expression.Left.IsTrue()) + { + return Expression.Constant(false); + } + if (expression.Right.IsFalse()) + { + return Visit(expression.Left); + } + if (expression.Left.IsFalse()) + { + return Visit(expression.Right); + } + } + if (_isVisitingWhereMethodOrChild && expression.Right is ConstantExpression c && c.Type == typeof(bool) && (expression.NodeType == ExpressionType.Equal || expression.NodeType == ExpressionType.NotEqual)) { diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs index 46ea435..96dbda9 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs @@ -137,6 +137,39 @@ public void Conflicts() Assert.Equal(@"{""conflicts"":true,""selector"":{}}", json); } + [Fact] + public void Boolean_ShortCircuit_AndTrue() + { + bool useFilter = true; + var json = _rebels.Where(r => useFilter && r.Age == 19).Take(1).ToString(); + Assert.Equal(@"{""selector"":{""age"":19},""limit"":1}", json); + } + + [Fact] + public void Boolean_ShortCircuit_AndFalse() + { + bool useFilter = false; + var json = _rebels.Where(r => useFilter && r.Age == 19).Take(1).ToString(); + Assert.Equal(@"{""limit"":1,""selector"":{}}", json); + } + + [Fact] + public void Boolean_ShortCircuit_OrTrue() + { + bool useFilter = true; + var json = _rebels.Where(r => useFilter || r.Age == 19).ToString(); + Assert.Equal(@"{""selector"":{}}", json); + } + + [Fact] + public void Boolean_ShortCircuit_Complex() + { + bool useFilter1 = true; + bool useFilter2 = false; + var json = _rebels.Where(r => (useFilter1 && r.Age == 19) || useFilter2).ToString(); + Assert.Equal(@"{""selector"":{""age"":19}}", json); + } + [Fact] public void Combinations() { From ed6e123d181a00a2669ac5882506f1b73e63d47c Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sat, 6 Mar 2021 18:08:13 +0100 Subject: [PATCH 13/23] #113 - Improved pruning --- LATEST_CHANGE.md | 17 +++++++------ .../Query/Extensions/ExpressionExtensions.cs | 5 ++++ src/CouchDB.Driver/Query/QueryOptimizer.cs | 16 ++++++------- src/azure-pipelines.yaml | 2 +- .../Find/Find_Miscellaneous.cs | 24 +++++++++++++++++++ 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index 136c07d..fd2e3f7 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -1,8 +1,11 @@ -## Features -* **Indexes"**: Ability to create indexes. ([#102](https://github.com/matteobortolazzo/couchdb-net/issues/102)) -* **Null values"**: New `SetNullValueHandling` method for `CouchOptionsBuilder` to set how to handle null values. ([#101](https://github.com/matteobortolazzo/couchdb-net/issues/101)) -* **Query"**: New `Select` and `Convert` methods to select specific fields. +## Breaking Changes +* Update to [Flurl 3](https://github.com/tmenier/Flurl/releases/tag/Flurl.Http.3.0.0). There should be no differences for the end user, but keep in mind. -## Bug Fixes -* **Conflicts**: Fix the query parameter value to get conflicts. ([#100](https://github.com/matteobortolazzo/couchdb-net/issues/100)) -* **Query**: Fix queries when variables are used. ([#104](https://github.com/matteobortolazzo/couchdb-net/issues/104)) \ No newline at end of file +## Features +* **Table Splitting**: Ability to use the same database for different document with automatic filtering. ([#106](https://github.com/matteobortolazzo/couchdb-net/issues/106)) +* **Views**: Ability to get views. Thanks to [panoukos41](https://github.com/panoukos41) ([#117](https://github.com/matteobortolazzo/couchdb-net/issues/117)) + +## Improvements +* **Logical Expressions Prune**: If expressions are constant booleans, they are removed automatically keeping the query valid. ([#113](https://github.com/matteobortolazzo/couchdb-net/issues/113)) +* **IsUpAsync**: Returns false on timeout and on not successful codes. ([#107](https://github.com/matteobortolazzo/couchdb-net/issues/107)) +* **FindAsync**: Faster when document is not found. ([#92](https://github.com/matteobortolazzo/couchdb-net/issues/92)) \ No newline at end of file diff --git a/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs b/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs index 13f37dd..2b6df88 100644 --- a/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs +++ b/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs @@ -13,5 +13,10 @@ public static bool IsFalse(this Expression expression) { return expression is ConstantExpression c && c.Value is bool b && !b; } + + public static bool IsBoolean(this Expression expression) + { + return expression is ConstantExpression c && c.Value is bool; + } } } diff --git a/src/CouchDB.Driver/Query/QueryOptimizer.cs b/src/CouchDB.Driver/Query/QueryOptimizer.cs index 07e82ab..d0033fb 100644 --- a/src/CouchDB.Driver/Query/QueryOptimizer.cs +++ b/src/CouchDB.Driver/Query/QueryOptimizer.cs @@ -63,7 +63,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) Expression whereNode = VisitMethodCall(node); _isVisitingWhereMethodOrChild = false; - return whereNode.IsFalse() + return whereNode.IsBoolean() ? node.Arguments[0] : whereNode; } @@ -80,18 +80,18 @@ protected override Expression VisitMethodCall(MethodCallExpression node) Expression tail = Visit(node.Arguments[0]); LambdaExpression currentLambda = node.GetLambda(); Expression conditionExpression = Visit(currentLambda.Body); + _nextWhereCalls.Dequeue(); - if (conditionExpression.IsFalse()) + while (_nextWhereCalls.Count > 0) { - return conditionExpression; + Expression nextWhereBody = _nextWhereCalls.Dequeue().GetLambdaBody(); + conditionExpression = Expression.AndAlso(nextWhereBody, conditionExpression); + conditionExpression = Visit(conditionExpression); } - _nextWhereCalls.Dequeue(); - - while (_nextWhereCalls.Count > 0) + if (conditionExpression.IsBoolean()) { - Expression nextWhereBody = Visit(_nextWhereCalls.Dequeue().GetLambdaBody()); - conditionExpression = Expression.And(nextWhereBody, conditionExpression); + return conditionExpression; } Expression conditionLambda = conditionExpression.WrapInLambda(currentLambda.Parameters); diff --git a/src/azure-pipelines.yaml b/src/azure-pipelines.yaml index f1639b7..15f6fe1 100644 --- a/src/azure-pipelines.yaml +++ b/src/azure-pipelines.yaml @@ -1,6 +1,6 @@ variables: BuildConfiguration: Release - PackageVersion: '2.1.0' + PackageVersion: '3.0.0' trigger: branches: diff --git a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs index 96dbda9..0b488d7 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs @@ -137,6 +137,14 @@ public void Conflicts() Assert.Equal(@"{""conflicts"":true,""selector"":{}}", json); } + [Fact] + public void Boolean_ShortCircuit_Simple() + { + bool useFilter = true; + var json = _rebels.Where(_ => useFilter).ToString(); + Assert.Equal(@"{""selector"":{}}", json); + } + [Fact] public void Boolean_ShortCircuit_AndTrue() { @@ -170,6 +178,22 @@ public void Boolean_ShortCircuit_Complex() Assert.Equal(@"{""selector"":{""age"":19}}", json); } + [Fact] + public void Boolean_ShortCircuit_MultiWhere1() + { + bool useFilter = true; + var json = _rebels.Where(r => r.Age == 19).Where(_ => useFilter).ToString(); + Assert.Equal(@"{""selector"":{""age"":19}}", json); + } + + [Fact] + public void Boolean_ShortCircuit_MultiWhere2() + { + bool useFilter = true; + var json = _rebels.Where(_ => useFilter).Where(r => r.Age == 19).ToString(); + Assert.Equal(@"{""selector"":{""age"":19}}", json); + } + [Fact] public void Combinations() { From b84434b6634912ad66deab47bdd27e1091c8008e Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sat, 6 Mar 2021 22:58:49 +0200 Subject: [PATCH 14/23] Refactor CouchDB View methods Change method names and return types. --- src/CouchDB.Driver/CouchDatabase.cs | 18 +++++++++--------- src/CouchDB.Driver/ICouchDatabase.cs | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 439a7d5..1dca6ed 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -503,23 +503,23 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #region View /// - public async Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public async Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) { - CouchViewResult result = await GetDetailedViewAsync(design, view, options, cancellationToken).ConfigureAwait(false); + CouchViewResult result = await GetDetailedViewAsync(design, view, options, cancellationToken).ConfigureAwait(false); return result.Rows.Select(x => x.Value).ToArray(); } /// - public async Task> GetViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public async Task>> GetViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) { - CouchViewResult result = await GetDetailedViewWithDocAsync(design, view, options, cancellationToken).ConfigureAwait(false); + CouchViewResult result = await GetDetailedViewWithDocumentsAsync(design, view, options, cancellationToken).ConfigureAwait(false); - return result.Rows.Select(x => (x.Value, x.Doc)).ToArray(); + return result.Rows.ToArray(); } /// - public Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) { Check.NotNull(view, nameof(view)); @@ -528,12 +528,12 @@ public Task> GetDetailedViewAsync(string design, str .SetQueryParams(options?.ToQueryParameters()); return request - .GetJsonAsync>(cancellationToken) + .GetJsonAsync>(cancellationToken) .SendRequestAsync(); } /// - public Task> GetDetailedViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public Task> GetDetailedViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) { Check.NotNull(view, nameof(view)); @@ -545,7 +545,7 @@ public Task> GetDetailedViewWithDocAsync(st .SetQueryParams(options.ToQueryParameters()); return request - .GetJsonAsync>(cancellationToken) + .GetJsonAsync>(cancellationToken) .SendRequestAsync(); } diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 884a9d4..5f7a8bd 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -109,7 +109,7 @@ public interface ICouchDatabase: IOrderedQueryable /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the . /// The options IncludeDocs will always be set to true. - Task> GetViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + Task>> GetViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Executes the specified view function from the specified design document. @@ -120,7 +120,7 @@ public interface ICouchDatabase: IOrderedQueryable /// Optional options to pass to the view. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the . - Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Executes the specified view function from the specified design document. Additionally, it returns the document. @@ -132,7 +132,7 @@ public interface ICouchDatabase: IOrderedQueryable /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the . /// The options IncludeDocs will always be set to true. - Task> GetDetailedViewWithDocAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + Task> GetDetailedViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Since CouchDB v3, it is deprecated (a no-op). From 47758fba862c030999810de9bba5259427970568 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sat, 6 Mar 2021 23:00:49 +0200 Subject: [PATCH 15/23] Refactor CouchViewOptions Remove TryEscape and Escape methods since keys can be any value and not all values need to be escaped. Add some remarks on key values. --- src/CouchDB.Driver/Types/CouchViewOptions.cs | 36 ++++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/CouchDB.Driver/Types/CouchViewOptions.cs b/src/CouchDB.Driver/Types/CouchViewOptions.cs index f026930..e7305df 100644 --- a/src/CouchDB.Driver/Types/CouchViewOptions.cs +++ b/src/CouchDB.Driver/Types/CouchViewOptions.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; namespace CouchDB.Driver.Types { @@ -22,11 +21,17 @@ public class CouchViewOptions /// /// Return records starting with the specified key. /// + /// + /// String keys should start and end with '"'. This can also be any valid json. + /// public string? StartKey { get; set; } /// /// Stop returning records when the specified key is reached. /// + /// + /// String keys should start and end with '"'. This can also be any valid json. + /// public string? EndKey { get; set; } /// @@ -106,6 +111,9 @@ public class CouchViewOptions /// Whether or not the view in question should be updated prior to responding to the user. /// Supported values: true, false, lazy. Default is true. /// + /// + /// The provided values do not need to start or end with '"'. + /// public string? Update { get; set; } /// @@ -117,6 +125,9 @@ public class CouchViewOptions /// /// Return only documents that match the specified key. /// + /// + /// String keys should start and end with '"'. This can also be any valid json. + /// public string? Key { get; set; } /// @@ -131,6 +142,9 @@ public class CouchViewOptions /// /// Return only documents where the key matches one of the keys specified in the array. /// + /// + /// String keys should start and end with '"'. Keys can also be any valid json. + /// public HashSet? Keys { get; set; } #pragma warning restore CA2227 @@ -139,8 +153,8 @@ public class CouchViewOptions { include_docs = IncludeDocs, conflicts = Conflicts, - startkey = TryEscape(StartKey), - endKey = TryEscape(EndKey), + startkey = StartKey, + endKey = EndKey, inclusive_end = InclusiveEnd, descending = Descending, group = Group, @@ -153,17 +167,11 @@ public class CouchViewOptions group_level = GroupLevel, limit = Limit, skip = Skip, - update = TryEscape(Update), - endkey_docid = TryEscape(EndkeyDocId), - key = TryEscape(Key), - startkey_docid = TryEscape(StartkeyDocid), - keys = Keys is null ? null : $"[{string.Join(',', Keys.Select(Escape))}]" + update = Update, + endkey_docid = EndkeyDocId, + key = Key, + startkey_docid = StartkeyDocid, + keys = Keys is null ? null : $"[{string.Join(',', Keys)}]" }; - - private static string Escape(string str) => - $"\"{str}\""; - - private static string? TryEscape(string? str) => - str is null ? null : Escape(str); } } \ No newline at end of file From ec0468f28316e738e239930a39c14207e1ca4222 Mon Sep 17 00:00:00 2001 From: Panos Athanasiou Date: Sat, 6 Mar 2021 23:02:26 +0200 Subject: [PATCH 16/23] Do not default to null for DTOs Disable nullables for the file instead. --- src/CouchDB.Driver/Types/CouchViewRow.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/CouchDB.Driver/Types/CouchViewRow.cs b/src/CouchDB.Driver/Types/CouchViewRow.cs index 9519d31..54ce996 100644 --- a/src/CouchDB.Driver/Types/CouchViewRow.cs +++ b/src/CouchDB.Driver/Types/CouchViewRow.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +#nullable disable +using Newtonsoft.Json; namespace CouchDB.Driver.Types { @@ -12,19 +13,19 @@ public class CouchViewRow /// The id of the document. /// [JsonProperty("id")] - public string Id { get; private set; } = null!; + public string Id { get; private set; } /// /// The view key that was emmited. /// [JsonProperty("key")] - public string Key { get; private set; } = null!; + public string Key { get; private set; } /// /// The value that the view emmited. /// [JsonProperty("value")] - public TValue Value { get; private set; } = default!; + public TValue Value { get; private set; } } /// @@ -36,6 +37,7 @@ public class CouchViewRow : CouchViewRow /// The json document deserialize to . /// [JsonProperty("doc")] - public TDoc Doc { get; private set; } = default!; + public TDoc Doc { get; private set; } } -} \ No newline at end of file +} +#nullable enable \ No newline at end of file From 79b8edd17bf0603c6547a3840be5a4ac0b51d826 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Sun, 7 Mar 2021 23:49:45 +0100 Subject: [PATCH 17/23] #117 - Implement generic key view call --- .../ChangesFeed/ChangesFeedStyle.cs | 4 +- src/CouchDB.Driver/CouchDB.Driver.csproj | 1 + src/CouchDB.Driver/CouchDatabase.cs | 52 ++--- .../DTOs/IndexDefinitionInfo.cs | 2 +- .../Extensions/FlurlRequestExtensions.cs | 9 +- .../Extensions/TypeExtensions.cs | 21 +++ src/CouchDB.Driver/ICouchDatabase.cs | 41 ++-- .../Local/LocalDocumentsResult.cs | 2 +- src/CouchDB.Driver/Shared/OptionsHelper.cs | 8 +- src/CouchDB.Driver/Types/CouchViewOptions.cs | 177 ------------------ src/CouchDB.Driver/Types/CouchViewResult.cs | 58 ------ src/CouchDB.Driver/Types/CouchViewRow.cs | 43 ----- src/CouchDB.Driver/Views/CouchViewOptions.cs | 157 ++++++++++++++++ src/CouchDB.Driver/Views/CouchViewResult.cs | 36 ++++ src/CouchDB.Driver/Views/CouchViewResult`.cs | 38 ++++ src/CouchDB.Driver/Views/CouchViewRow.cs | 18 ++ src/CouchDB.Driver/Views/CouchViewRow`.cs | 14 ++ src/CouchDB.Driver/Views/UpdateStyle.cs | 35 ++++ 18 files changed, 384 insertions(+), 332 deletions(-) delete mode 100644 src/CouchDB.Driver/Types/CouchViewOptions.cs delete mode 100644 src/CouchDB.Driver/Types/CouchViewResult.cs delete mode 100644 src/CouchDB.Driver/Types/CouchViewRow.cs create mode 100644 src/CouchDB.Driver/Views/CouchViewOptions.cs create mode 100644 src/CouchDB.Driver/Views/CouchViewResult.cs create mode 100644 src/CouchDB.Driver/Views/CouchViewResult`.cs create mode 100644 src/CouchDB.Driver/Views/CouchViewRow.cs create mode 100644 src/CouchDB.Driver/Views/CouchViewRow`.cs create mode 100644 src/CouchDB.Driver/Views/UpdateStyle.cs diff --git a/src/CouchDB.Driver/ChangesFeed/ChangesFeedStyle.cs b/src/CouchDB.Driver/ChangesFeed/ChangesFeedStyle.cs index 964ea43..81579c0 100644 --- a/src/CouchDB.Driver/ChangesFeed/ChangesFeedStyle.cs +++ b/src/CouchDB.Driver/ChangesFeed/ChangesFeedStyle.cs @@ -9,12 +9,12 @@ public class ChangesFeedStyle /// /// The feed will only return the current "winning" revision; /// - public static ChangesFeedStyle MainOnly => new ChangesFeedStyle("main_only"); + public static ChangesFeedStyle MainOnly => new("main_only"); /// /// The feed will return all leaf revisions (including conflicts and deleted former conflicts). /// - public static ChangesFeedStyle AllDocs => new ChangesFeedStyle("all_docs"); + public static ChangesFeedStyle AllDocs => new("all_docs"); private ChangesFeedStyle(string value) { diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index ceef1ae..c82cbc5 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -34,6 +34,7 @@ + diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index a3f5312..4d09f8f 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -24,6 +24,7 @@ using CouchDB.Driver.Query; using Newtonsoft.Json; using System.Net; +using CouchDB.Driver.Views; namespace CouchDB.Driver { @@ -266,7 +267,7 @@ public async Task> AddOrUpdateRangeAsync(IList doc { Check.NotNull(documents, nameof(documents)); - foreach(TSource? document in documents) + foreach (TSource? document in documents) { document.Discriminator = _discriminator; } @@ -384,7 +385,7 @@ public async Task> GetChangesAsync(ChangesFeedOptio } /// - public async IAsyncEnumerable> GetContinuousChangesAsync(ChangesFeedOptions options, ChangesFeedFilter filter, + public async IAsyncEnumerable> GetContinuousChangesAsync(ChangesFeedOptions? options, ChangesFeedFilter? filter, [EnumeratorCancellation] CancellationToken cancellationToken) { var infiniteTimeout = TimeSpan.FromMilliseconds(Timeout.Infinite); @@ -507,49 +508,54 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #region View /// - public async Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public async Task>> GetViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) { - CouchViewResult result = await GetDetailedViewAsync(design, view, options, cancellationToken).ConfigureAwait(false); - - return result.Rows.Select(x => x.Value).ToArray(); + CouchViewResult result = await GetDetailedViewAsync(design, view, options, cancellationToken).ConfigureAwait(false); + return result.Rows.ToList(); } /// - public async Task>> GetViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public async Task>> GetViewWithDocumentsAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) { - CouchViewResult result = await GetDetailedViewWithDocumentsAsync(design, view, options, cancellationToken).ConfigureAwait(false); - - return result.Rows.ToArray(); + CouchViewResult result = await GetDetailedViewWithDocumentsAsync(design, view, options, cancellationToken).ConfigureAwait(false); + return result.Rows.ToList(); } /// - public Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public Task> GetDetailedViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) { + Check.NotNull(design, nameof(design)); Check.NotNull(view, nameof(view)); IFlurlRequest request = NewRequest() - .AppendPathSegments("_design", design, "_view", view) - .SetQueryParams(options?.ToQueryParameters()); + .AppendPathSegments("_design", design, "_view", view); - return request - .GetJsonAsync>(cancellationToken) - .SendRequestAsync(); + Task>? requestTask = options == null + ? request.GetJsonAsync>(cancellationToken) + : request + .PostJsonAsync(options, cancellationToken) + .ReceiveJson>(); + + return requestTask.SendRequestAsync(); } /// - public Task> GetDetailedViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + public Task> GetDetailedViewWithDocumentsAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) { + Check.NotNull(design, nameof(design)); Check.NotNull(view, nameof(view)); - options ??= new CouchViewOptions(); + options ??= new CouchViewOptions(); options.IncludeDocs = true; - IFlurlRequest request = NewRequest() + return NewRequest() .AppendPathSegments("_design", design, "_view", view) - .SetQueryParams(options.ToQueryParameters()); - - return request - .GetJsonAsync>(cancellationToken) + .PostJsonAsync(options, cancellationToken) + .ReceiveJson>() .SendRequestAsync(); } diff --git a/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs b/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs index 792409b..f640c30 100644 --- a/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs +++ b/src/CouchDB.Driver/DTOs/IndexDefinitionInfo.cs @@ -10,4 +10,4 @@ internal class IndexDefinitionInfo public Dictionary[] Fields { get; set; } } } -#nullable enable \ No newline at end of file +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs index ec26576..f09cbbf 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -7,7 +7,6 @@ using CouchDB.Driver.Shared; using Flurl.Http; using Flurl.Http.Content; -using Newtonsoft.Json; namespace CouchDB.Driver.Extensions { @@ -51,13 +50,9 @@ public static Task PostStringStreamAsync( public static IFlurlRequest ApplyQueryParametersOptions(this IFlurlRequest request, object options) { IEnumerable<(string Name, object? Value)> queryParameters = OptionsHelper.ToQueryParameters(options); - foreach ((var name, object? value) in queryParameters) + foreach (var (name, value) in queryParameters) { - object? finalValue = value?.GetType() == typeof(bool) - ? value.ToString().ToLowerInvariant() - : value; - - request = request.SetQueryParam(name, finalValue); + request = request.SetQueryParam(name, value); } return request; diff --git a/src/CouchDB.Driver/Extensions/TypeExtensions.cs b/src/CouchDB.Driver/Extensions/TypeExtensions.cs index 7ae1ab1..8d708aa 100644 --- a/src/CouchDB.Driver/Extensions/TypeExtensions.cs +++ b/src/CouchDB.Driver/Extensions/TypeExtensions.cs @@ -107,5 +107,26 @@ public static IEnumerable GetBaseTypes(this Type type) type = type.BaseType; } } + + public static bool IsNumericType(this object o) + { + switch (Type.GetTypeCode(o.GetType())) + { + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + default: + return false; + } + } } } diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 5f7a8bd..b29109f 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -9,6 +9,7 @@ using CouchDB.Driver.Local; using CouchDB.Driver.Security; using CouchDB.Driver.Types; +using CouchDB.Driver.Views; using Flurl.Http; namespace CouchDB.Driver @@ -91,48 +92,56 @@ public interface ICouchDatabase: IOrderedQueryable /// /// Executes the specified view function from the specified design document. /// - /// The type of the value that will be returned. + /// The type of the value that will be returned. + /// The type of the value that will be returned. /// The design to use. /// The view to use. - /// Optional options to pass to the view. + /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . - Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + /// A task that represents the asynchronous operation. The task result contains the . + Task>> GetViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Executes the specified view function from the specified design document. Additionally, it returns the document. /// - /// The type of the value that will be returned. + /// The type of the value that will be returned. + /// The type of the value that will be returned. /// The design to use. /// The view to use. - /// Optional options to pass to the view. + /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . + /// A task that represents the asynchronous operation. The task result contains the . /// The options IncludeDocs will always be set to true. - Task>> GetViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + Task>> GetViewWithDocumentsAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Executes the specified view function from the specified design document. /// - /// The type of the value that will be returned. + /// The type of the value that will be returned. + /// The type of the value that will be returned. /// The design to use. /// The view to use. - /// Optional options to pass to the view. + /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . - Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + /// A task that represents the asynchronous operation. The task result contains the . + Task> GetDetailedViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Executes the specified view function from the specified design document. Additionally, it returns the document. /// - /// The type of the value that will be returned. + /// The type of the value that will be returned. + /// The type of the value that will be returned. /// The design to use. /// The view to use. - /// Optional options to pass to the view. + /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . + /// A task that represents the asynchronous operation. The task result contains the . /// The options IncludeDocs will always be set to true. - Task> GetDetailedViewWithDocumentsAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default); + Task> GetDetailedViewWithDocumentsAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Since CouchDB v3, it is deprecated (a no-op). diff --git a/src/CouchDB.Driver/Local/LocalDocumentsResult.cs b/src/CouchDB.Driver/Local/LocalDocumentsResult.cs index 3d17ae3..e74e54b 100644 --- a/src/CouchDB.Driver/Local/LocalDocumentsResult.cs +++ b/src/CouchDB.Driver/Local/LocalDocumentsResult.cs @@ -10,5 +10,5 @@ internal class LocalDocumentsResult [JsonProperty("rows")] public IList Rows { get; set; } } -#nullable enable +#nullable restore } diff --git a/src/CouchDB.Driver/Shared/OptionsHelper.cs b/src/CouchDB.Driver/Shared/OptionsHelper.cs index e51d680..cb49a5f 100644 --- a/src/CouchDB.Driver/Shared/OptionsHelper.cs +++ b/src/CouchDB.Driver/Shared/OptionsHelper.cs @@ -11,7 +11,7 @@ internal static class OptionsHelper { public static IEnumerable<(string Name, object? Value)> ToQueryParameters(object options) { - static TAttribute GetAttribute(ICustomAttributeProvider propertyInfo) + static TAttribute? GetAttribute(ICustomAttributeProvider propertyInfo) { return propertyInfo .GetCustomAttributes(typeof(TAttribute), true) @@ -22,8 +22,8 @@ static TAttribute GetAttribute(ICustomAttributeProvider propertyInfo Type optionsType = options.GetType(); foreach (PropertyInfo propertyInfo in optionsType.GetProperties()) { - JsonPropertyAttribute jsonProperty = GetAttribute(propertyInfo); - DefaultValueAttribute defaultValue = GetAttribute(propertyInfo); + JsonPropertyAttribute? jsonProperty = GetAttribute(propertyInfo); + DefaultValueAttribute? defaultValue = GetAttribute(propertyInfo); if (jsonProperty == null || defaultValue == null) { continue; @@ -44,7 +44,7 @@ static TAttribute GetAttribute(ICustomAttributeProvider propertyInfo object? propertyStringValue = propertyValue?.ToString(); if (propertyInfo.PropertyType == typeof(bool)) { - propertyStringValue = propertyValue; + propertyStringValue = propertyValue?.ToString().ToLowerInvariant(); } yield return (propertyName, propertyStringValue); diff --git a/src/CouchDB.Driver/Types/CouchViewOptions.cs b/src/CouchDB.Driver/Types/CouchViewOptions.cs deleted file mode 100644 index e7305df..0000000 --- a/src/CouchDB.Driver/Types/CouchViewOptions.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System.Collections.Generic; - -namespace CouchDB.Driver.Types -{ - /// - /// Optional options that can be send with a view request. - /// - public class CouchViewOptions - { - /// - /// Include the associated document with each row. Default is false. - /// - public bool? IncludeDocs { get; set; } - - /// - /// Include conflicts information in response. - /// Ignored if include_docs isn’t true. Default is false. - /// - public bool? Conflicts { get; set; } - - /// - /// Return records starting with the specified key. - /// - /// - /// String keys should start and end with '"'. This can also be any valid json. - /// - public string? StartKey { get; set; } - - /// - /// Stop returning records when the specified key is reached. - /// - /// - /// String keys should start and end with '"'. This can also be any valid json. - /// - public string? EndKey { get; set; } - - /// - /// Specifies whether the specified end key should - /// be included in the result. Default is true. - /// - public bool? InclusiveEnd { get; set; } - - /// - /// Return the documents in descending order by key. Default is false. - /// - public bool? Descending { get; set; } - - /// - /// Group the results using the reduce function to a group or single row. - /// Implies reduce is true and the maximum group_level. Default is false. - /// - public bool? Group { get; set; } - - /// - /// Include encoding information in attachment stubs if include_docs is true - /// and the particular attachment is compressed. - /// Ignored if include_docs isn’t true. Default is false. - /// - public bool? AttEncodingInfo { get; set; } - - /// - /// Use the reduction function. Default is true - /// when a reduce function is defined. - /// - public bool? Reduce { get; set; } - - /// - /// Whether or not the view results should be returned - /// from a stable set of shards. Default is false. - /// - public bool? Stable { get; set; } - - /// - /// Whether to include in the response an update_seq value indicating the - /// sequence id of the database the view reflects. Default is false. - /// - public bool? UpdateSeq { get; set; } - - /// - /// Include the Base64-encoded content of - /// attachments - /// in the documents that are included if include_docs is true. - /// Ignored if include_docs isn’t true. Default is false. - /// - public bool? Attachments { get; set; } - - /// - /// Sort returned rows. Setting this to false offers a performance boost. - /// The total_rows and offset fields are not available when this is set to false. - /// Default is true. - /// - public bool? Sorted { get; set; } - - /// - /// Specify the group level to be used. Implies group is true. - /// - public int? GroupLevel { get; set; } - - /// - /// Limit the number of the returned documents to the specified number. - /// - public int? Limit { get; set; } - - /// - /// Skip this number of records before starting to return the results. - /// Default is 0. - /// - public int? Skip { get; set; } - - /// - /// Whether or not the view in question should be updated prior to responding to the user. - /// Supported values: true, false, lazy. Default is true. - /// - /// - /// The provided values do not need to start or end with '"'. - /// - public string? Update { get; set; } - - /// - /// Stop returning records when the specified document ID is reached. - /// Ignored if endkey is not set. - /// - public string? EndkeyDocId { get; set; } - - /// - /// Return only documents that match the specified key. - /// - /// - /// String keys should start and end with '"'. This can also be any valid json. - /// - public string? Key { get; set; } - - /// - /// Return records starting with the specified document ID. - /// Ignored if startkey is not set. - /// - public string? StartkeyDocid { get; set; } - - // Error CA2227: Change 'Keys' to be read-only by removing the property setter. - // Disable this so keys can be added in with other properties on initialization. -#pragma warning disable CA2227 - /// - /// Return only documents where the key matches one of the keys specified in the array. - /// - /// - /// String keys should start and end with '"'. Keys can also be any valid json. - /// - public HashSet? Keys { get; set; } - -#pragma warning restore CA2227 - - public object ToQueryParameters() => new - { - include_docs = IncludeDocs, - conflicts = Conflicts, - startkey = StartKey, - endKey = EndKey, - inclusive_end = InclusiveEnd, - descending = Descending, - group = Group, - att_encoding_info = AttEncodingInfo, - reduce = Reduce, - stable = Stable, - update_seq = UpdateSeq, - attachments = Attachments, - sorted = Sorted, - group_level = GroupLevel, - limit = Limit, - skip = Skip, - update = Update, - endkey_docid = EndkeyDocId, - key = Key, - startkey_docid = StartkeyDocid, - keys = Keys is null ? null : $"[{string.Join(',', Keys)}]" - }; - } -} \ No newline at end of file diff --git a/src/CouchDB.Driver/Types/CouchViewResult.cs b/src/CouchDB.Driver/Types/CouchViewResult.cs deleted file mode 100644 index cabbd7f..0000000 --- a/src/CouchDB.Driver/Types/CouchViewResult.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Collections.Immutable; - -namespace CouchDB.Driver.Types -{ - /// - /// Object that stores results of a view. - /// - /// The type of the value. - public class CouchViewResult - { - /// - /// Number of documents in the database/view. - /// - [JsonProperty("total_rows")] - public int TotalRows { get; private set; } - - /// - /// Offset where the document list started. - /// - [JsonProperty("offset")] - public int Offset { get; private set; } - - /// - /// Array of view row objects. This result contains only the document ID and revision. - /// - [JsonProperty("rows")] - public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); - } - - /// - /// Object that stores results of a view. - /// - /// The type of the value. - /// The type of the doc. - public class CouchViewResult - where TSource : CouchDocument - { - /// - /// Number of documents in the database/view. - /// - [JsonProperty("total_rows")] - public int TotalRows { get; private set; } - - /// - /// Offset where the document list started. - /// - [JsonProperty("offset")] - public int Offset { get; private set; } - - /// - /// Array of view row objects. This result contains the document ID, revision and the documents. - /// - [JsonProperty("rows")] - public IReadOnlyList> Rows { get; private set; } = ImmutableArray.Create>(); - } -} \ No newline at end of file diff --git a/src/CouchDB.Driver/Types/CouchViewRow.cs b/src/CouchDB.Driver/Types/CouchViewRow.cs deleted file mode 100644 index 54ce996..0000000 --- a/src/CouchDB.Driver/Types/CouchViewRow.cs +++ /dev/null @@ -1,43 +0,0 @@ -#nullable disable -using Newtonsoft.Json; - -namespace CouchDB.Driver.Types -{ - /// - /// The object returned from a view execution. - /// - /// The type the value will be deserialized to. - public class CouchViewRow - { - /// - /// The id of the document. - /// - [JsonProperty("id")] - public string Id { get; private set; } - - /// - /// The view key that was emmited. - /// - [JsonProperty("key")] - public string Key { get; private set; } - - /// - /// The value that the view emmited. - /// - [JsonProperty("value")] - public TValue Value { get; private set; } - } - - /// - /// The type the doc will be deserialized to. - public class CouchViewRow : CouchViewRow - where TDoc : CouchDocument - { - /// - /// The json document deserialize to . - /// - [JsonProperty("doc")] - public TDoc Doc { get; private set; } - } -} -#nullable enable \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewOptions.cs b/src/CouchDB.Driver/Views/CouchViewOptions.cs new file mode 100644 index 0000000..6cb6d78 --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewOptions.cs @@ -0,0 +1,157 @@ +#nullable disable +#pragma warning disable CA2227 // Collection properties should be read only +using System.Collections.Generic; +using System.ComponentModel; +using CouchDB.Driver.ChangesFeed; +using Newtonsoft.Json; + +namespace CouchDB.Driver.Views +{ + /// + /// Optional parameters to use when getting a view. + /// + /// The type of the key. + public class CouchViewOptions + { + /// + /// Include conflicts information in response. + /// Ignored if isn't True. Default is False. + /// + [JsonProperty("conflicts")] + public bool Conflicts { get; set; } + + /// + /// Return the documents in descending order by key. Default is False. + /// + [JsonProperty("descending")] + public bool Descending { get; set; } + + /// + /// Stop returning records when the specified key is reached. + /// + [JsonProperty("endkey")] + public TKey EndKey { get; set; } + + /// + /// Stop returning records when the specified document ID is reached. + /// Ignored if is not set. + /// + [JsonProperty("endkey_docid")] + public string EndKeyDocId { get; set; } + + /// + /// Group the results using the reduce function to a group or single row. + /// Implies reduce is True and the maximum . Default is False. + /// + [JsonProperty("group")] + public bool Group { get; set; } + + /// + /// Specify the group level to be used. Implies group is True. + /// + [JsonProperty("group_level")] + public int? GroupLevel { get; set; } + + /// + /// Include the associated document with each row. Default is False. + /// + [JsonProperty("include_docs")] + internal bool IncludeDocs { get; set; } + + /// + /// Include the Base64-encoded content of attachments in the documents that are included if is True. + /// Ignored if isn’t True. Default is False. + /// + [JsonProperty("attachments")] + public bool Attachments { get; set; } + + /// + /// Include encoding information in attachment stubs if is True and the particular attachment is compressed. + /// Ignored if isn’t True. Default is False. + /// + [JsonProperty("att_encoding_info")] + public bool AttachEncodingInfo { get; set; } + + /// + /// Specifies whether the specified end key should be included in the result. Default is True. + /// + [JsonProperty("inclusive_end")] + public bool InclusiveEnd { get; set; } = true; + + /// + /// Return only documents that match the specified key. + /// + [JsonProperty("key")] + public TKey Key { get; set; } + + /// + /// Return only documents where the key matches one of the keys specified in the array. + /// + [JsonProperty("keys")] + public IList Keys { get; set; } + + /// + /// Limit the number of the returned documents to the specified number. + /// + [JsonProperty("limit")] + public int? Limit { get; set; } + + /// + /// Use the reduction function. Default is True when a reduce function is defined. + /// + [JsonProperty("reduce")] + public bool Reduce { get; set; } = false; + + /// + /// Skip this number of records before starting to return the results. Default is 0. + /// + [JsonProperty("skip")] + public int Skip { get; set; } + + /// + /// Sort returned rows (see Sorting Returned Rows). + /// Setting this to false offers a performance boost. + /// The and fields are not available when this is set to False. + /// Default is True. + /// + [JsonProperty("sorted")] + public bool Sorted { get; set; } = true; + + /// + /// Whether or not the view results should be returned from a stable set of shards. Default is False. + /// + [JsonProperty("stable")] + public bool Stable { get; set; } + + /// + /// Return records starting with the specified key. + /// + [JsonProperty("startkey")] + public TKey StartKey { get; set; } + + /// + /// Return records starting with the specified document ID. Ignored if is not set. + /// + [JsonProperty("startkey_docid")] + public string StartKeyDocId { get; set; } + + /// + /// Whether or not the view in question should be updated prior to responding to the user. + /// Supported values: , , . Default is . + /// + [JsonIgnore] + public UpdateStyle Update { get; set; } = UpdateStyle.True; + + [JsonProperty("update")] + internal string UpdateString => Update.ToString(); + + /// + /// Whether to include in the response an value indicating the sequence id of the database the view reflects. + /// Default is False. + /// + [JsonProperty("update_seq")] + public bool UpdateSeq { get; set; } + } +} +#pragma warning restore CA2227 // Collection properties should be read only +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewResult.cs b/src/CouchDB.Driver/Views/CouchViewResult.cs new file mode 100644 index 0000000..827d14b --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewResult.cs @@ -0,0 +1,36 @@ +#nullable disable +#pragma warning disable CA2227 // Collection properties should be read only +using System.Collections.Generic; +using CouchDB.Driver.Types; +using Newtonsoft.Json; + +namespace CouchDB.Driver.Views +{ + /// + /// Result of a view query. + /// + /// The type of the value that will be returned. + /// The type of the value that will be returned. + public class CouchViewResult + { + /// + /// Number of documents in the database/view. + /// + [JsonProperty("total_rows")] + public int TotalRows { get; set; } + + /// + /// Offset where the document list started. + /// + [JsonProperty("offset")] + public int Offset { get; set; } + + /// + /// Array of view row objects. This result contains only the document ID and revision. + /// + [JsonProperty("rows")] + public IList> Rows { get; set; } + } +} +#pragma warning restore CA2227 // Collection properties should be read only +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewResult`.cs b/src/CouchDB.Driver/Views/CouchViewResult`.cs new file mode 100644 index 0000000..ad7cc9a --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewResult`.cs @@ -0,0 +1,38 @@ +#nullable disable +#pragma warning disable CA2227 // Collection properties should be read only +using System.Collections.Generic; +using CouchDB.Driver.Types; +using Newtonsoft.Json; + +namespace CouchDB.Driver.Views +{ + /// + /// Result of a view query. + /// + /// The type of the value that will be returned. + /// The type of the value that will be returned. + /// The type of the document. + public class CouchViewResult + where TDoc : CouchDocument + { + /// + /// Number of documents in the database/view. + /// + [JsonProperty("total_rows")] + public int TotalRows { get; set; } + + /// + /// Offset where the document list started. + /// + [JsonProperty("offset")] + public int Offset { get; set; } + + /// + /// Array of view row objects. This result contains the document ID, revision and the documents. + /// + [JsonProperty("rows")] + public IList> Rows { get; set; } + } +} +#pragma warning restore CA2227 // Collection properties should be read only +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewRow.cs b/src/CouchDB.Driver/Views/CouchViewRow.cs new file mode 100644 index 0000000..4912271 --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewRow.cs @@ -0,0 +1,18 @@ +#nullable disable +using Newtonsoft.Json; + +namespace CouchDB.Driver.Views +{ + public class CouchViewRow + { + [JsonProperty("id")] + public string Id { get; private set; } + + [JsonProperty("key")] + public TKey Key { get; private set; } + + [JsonProperty("value")] + public TValue Value { get; private set; } + } +} +#nullable disable \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewRow`.cs b/src/CouchDB.Driver/Views/CouchViewRow`.cs new file mode 100644 index 0000000..64a10cc --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewRow`.cs @@ -0,0 +1,14 @@ +#nullable disable +using CouchDB.Driver.Types; +using Newtonsoft.Json; + +namespace CouchDB.Driver.Views +{ + public class CouchViewRow : CouchViewRow + where TDoc : CouchDocument + { + [JsonProperty("doc")] + public TDoc Doc { get; private set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/UpdateStyle.cs b/src/CouchDB.Driver/Views/UpdateStyle.cs new file mode 100644 index 0000000..27471ea --- /dev/null +++ b/src/CouchDB.Driver/Views/UpdateStyle.cs @@ -0,0 +1,35 @@ +namespace CouchDB.Driver.Views +{ + /// + /// Whether or not the view should be updated prior to responding to the user. + /// + public class UpdateStyle + { + private readonly string _value; + + /// + /// Updates the view prior to responding to the user. + /// + public static UpdateStyle True => new("true"); + + /// + /// Doesn't the view update prior to responding to the user. + /// + public static UpdateStyle False => new("false"); + + /// + /// Updates the view lazily when responding to the user. + /// + public static UpdateStyle Lazy => new("lazy"); + + private UpdateStyle(string value) + { + _value = value; + } + + public override string ToString() + { + return _value; + } + } +} \ No newline at end of file From 6c5c73d2e50ae3b8f908d7ef039d0e3803aea61f Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Mon, 8 Mar 2021 22:42:22 +0100 Subject: [PATCH 18/23] #117 - Views refactor --- README.md | 24 ++++ src/CouchDB.Driver/CouchDB.Driver.csproj | 1 - src/CouchDB.Driver/CouchDatabase.cs | 60 ++++----- .../Extensions/TypeExtensions.cs | 21 --- .../Helpers/CouchContractResolver.cs | 14 +- src/CouchDB.Driver/ICouchDatabase.cs | 48 ++----- .../ConstantExpressionTranslator.cs | 2 +- src/CouchDB.Driver/Views/CouchView.cs | 27 ++++ .../{CouchViewResult`.cs => CouchViewList.cs} | 15 ++- src/CouchDB.Driver/Views/CouchViewOptions.cs | 28 +++- src/CouchDB.Driver/Views/CouchViewResult.cs | 23 +--- src/CouchDB.Driver/Views/CouchViewRow.cs | 13 +- src/CouchDB.Driver/Views/CouchViewRow`.cs | 14 -- .../Database_Tests.cs | 123 ++++++++++++++++++ .../_Models/RebelView.cs | 13 ++ 15 files changed, 285 insertions(+), 141 deletions(-) create mode 100644 src/CouchDB.Driver/Views/CouchView.cs rename src/CouchDB.Driver/Views/{CouchViewResult`.cs => CouchViewList.cs} (72%) delete mode 100644 src/CouchDB.Driver/Views/CouchViewRow`.cs create mode 100644 tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs diff --git a/README.md b/README.md index f4835d7..ebdab13 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ The produced Mango JSON: * [Index Options](#index-options) * [Partial Indexes](#partial-indexes) * [Database Splitting](#database-splitting) +* [Views](#views) * [Local (non-replicating) Documents](#local-(non-replicating)-documents) * [Bookmark and Execution stats](#bookmark-and-execution-stats) * [Users](#users) @@ -526,6 +527,29 @@ var rebels = client.GetDatabase("troups", nameof(Rebel)); var vehicles = client.GetDatabase("troups", nameof(Vehicle)); ``` +## Views + +To read a view create a new class that implements `CouchView` where: +* `TKey` is the type of the key; +* `TDoc` is the type of the document. +```csharp +public class RebelView : CouchView +{ + public int NumberOfBattles { get; set; } +} +``` +Then you can query the view: +```csharp +var options = new CouchViewOptions +{ + StartKey = new[] {"Luke", "Skywalker"}, + IncludeDocs = true +}; +var rebels = await _rebels.GetViewAsync("jedi", "by_name", options); +// OR +var details = await _rebels.GetDetailedViewAsync("jedi", "by_name", options); +``` + ## Local (non-replicating) Documents The Local (non-replicating) document interface allows you to create local documents that are not replicated to other databases. diff --git a/src/CouchDB.Driver/CouchDB.Driver.csproj b/src/CouchDB.Driver/CouchDB.Driver.csproj index c82cbc5..ceef1ae 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -34,7 +34,6 @@ - diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 4d09f8f..e76189b 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -25,6 +25,8 @@ using Newtonsoft.Json; using System.Net; using CouchDB.Driver.Views; +using Flurl.Http.Configuration; +using Newtonsoft.Json.Serialization; namespace CouchDB.Driver { @@ -508,55 +510,47 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #region View /// - public async Task>> GetViewAsync(string design, string view, + public async Task> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + where TView : CouchView { - CouchViewResult result = await GetDetailedViewAsync(design, view, options, cancellationToken).ConfigureAwait(false); - return result.Rows.ToList(); - } - - /// - public async Task>> GetViewWithDocumentsAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default) - { - CouchViewResult result = await GetDetailedViewWithDocumentsAsync(design, view, options, cancellationToken).ConfigureAwait(false); - return result.Rows.ToList(); + CouchViewList result = + await GetDetailedViewAsync(design, view, options, cancellationToken) + .ConfigureAwait(false); + return result.Rows; } /// - public Task> GetDetailedViewAsync(string design, string view, + public async Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) + where TView : CouchView { Check.NotNull(design, nameof(design)); Check.NotNull(view, nameof(view)); IFlurlRequest request = NewRequest() .AppendPathSegments("_design", design, "_view", view); - - Task>? requestTask = options == null - ? request.GetJsonAsync>(cancellationToken) + + Task>? requestTask = options == null + ? request.GetJsonAsync>(cancellationToken) : request .PostJsonAsync(options, cancellationToken) - .ReceiveJson>(); + .ReceiveJson>(); - return requestTask.SendRequestAsync(); - } + CouchViewResult result = await requestTask.SendRequestAsync().ConfigureAwait(false); - /// - public Task> GetDetailedViewWithDocumentsAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default) - { - Check.NotNull(design, nameof(design)); - Check.NotNull(view, nameof(view)); - - options ??= new CouchViewOptions(); - options.IncludeDocs = true; - - return NewRequest() - .AppendPathSegments("_design", design, "_view", view) - .PostJsonAsync(options, cancellationToken) - .ReceiveJson>() - .SendRequestAsync(); + return new CouchViewList + { + Offset = result.Offset, + TotalRows = result.TotalRows, + Rows = result.Rows.Select(row => + { + row.Value.Id = row.Id; + row.Value.Key = row.Key; + row.Value.Document = row.Doc; + return row.Value; + }).ToList() + }; } #endregion diff --git a/src/CouchDB.Driver/Extensions/TypeExtensions.cs b/src/CouchDB.Driver/Extensions/TypeExtensions.cs index 8d708aa..7ae1ab1 100644 --- a/src/CouchDB.Driver/Extensions/TypeExtensions.cs +++ b/src/CouchDB.Driver/Extensions/TypeExtensions.cs @@ -107,26 +107,5 @@ public static IEnumerable GetBaseTypes(this Type type) type = type.BaseType; } } - - public static bool IsNumericType(this object o) - { - switch (Type.GetTypeCode(o.GetType())) - { - case TypeCode.Byte: - case TypeCode.SByte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Decimal: - case TypeCode.Double: - case TypeCode.Single: - return true; - default: - return false; - } - } } } diff --git a/src/CouchDB.Driver/Helpers/CouchContractResolver.cs b/src/CouchDB.Driver/Helpers/CouchContractResolver.cs index f120eb1..f083543 100644 --- a/src/CouchDB.Driver/Helpers/CouchContractResolver.cs +++ b/src/CouchDB.Driver/Helpers/CouchContractResolver.cs @@ -1,4 +1,4 @@ -using System; +using System.ComponentModel; using System.Reflection; using CouchDB.Driver.Extensions; using CouchDB.Driver.Options; @@ -24,6 +24,18 @@ internal CouchContractResolver(PropertyCaseType propertyCaseType) if (property != null && !property.Ignored) { property.PropertyName = member.GetCouchPropertyName(_propertyCaseType); + + DefaultValueAttribute? defaultValueAttribute = member.GetCustomAttribute(); + if (defaultValueAttribute != null && member is PropertyInfo propertyInfo) + { + property.ShouldSerialize = + instance => + { + object? value = propertyInfo.GetValue(instance); + var shouldSerialize = !Equals(value, defaultValueAttribute.Value); + return shouldSerialize; + }; + } } return property; } diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index b29109f..a249722 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -93,55 +93,29 @@ public interface ICouchDatabase: IOrderedQueryable /// Executes the specified view function from the specified design document. /// /// The type of the value that will be returned. - /// The type of the value that will be returned. + /// The type of the view. /// The design to use. /// The view to use. /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . - Task>> GetViewAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default); - - /// - /// Executes the specified view function from the specified design document. Additionally, it returns the document. - /// - /// The type of the value that will be returned. - /// The type of the value that will be returned. - /// The design to use. - /// The view to use. - /// Options for the request. - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . - /// The options IncludeDocs will always be set to true. - Task>> GetViewWithDocumentsAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default); + /// A task that represents the asynchronous operation. The task result contains a list of . + Task> GetViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) + where TView : CouchView; /// /// Executes the specified view function from the specified design document. /// - /// The type of the value that will be returned. - /// The type of the value that will be returned. - /// The design to use. - /// The view to use. - /// Options for the request. - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . - Task> GetDetailedViewAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default); - - /// - /// Executes the specified view function from the specified design document. Additionally, it returns the document. - /// - /// The type of the value that will be returned. - /// The type of the value that will be returned. + /// The type of the key. + /// The type of the view. /// The design to use. /// The view to use. /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains the . - /// The options IncludeDocs will always be set to true. - Task> GetDetailedViewWithDocumentsAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default); + /// A task that represents the asynchronous operation. The task result contains the . + Task> GetDetailedViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) + where TView : CouchView; /// /// Since CouchDB v3, it is deprecated (a no-op). diff --git a/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs b/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs index 7ce8f0c..f20f749 100644 --- a/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs +++ b/src/CouchDB.Driver/Query/Translators/ConstantExpressionTranslator.cs @@ -17,7 +17,7 @@ protected override Expression VisitConstant(ConstantExpression c) return c; } - private void HandleConstant(object constant) + private void HandleConstant(object? constant) { if (constant is IQueryable) { diff --git a/src/CouchDB.Driver/Views/CouchView.cs b/src/CouchDB.Driver/Views/CouchView.cs new file mode 100644 index 0000000..c199790 --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchView.cs @@ -0,0 +1,27 @@ +#nullable disable +namespace CouchDB.Driver.Views +{ + /// + /// Base class for a view. + /// + /// The type of the key + /// The type of the document. + public abstract class CouchView + { + /// + /// The document ID. + /// + public string Id { get; set; } + + /// + /// The view key. + /// + public TKey Key { get; set; } + + /// + /// The document. + /// + public TDoc Document { get; set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewResult`.cs b/src/CouchDB.Driver/Views/CouchViewList.cs similarity index 72% rename from src/CouchDB.Driver/Views/CouchViewResult`.cs rename to src/CouchDB.Driver/Views/CouchViewList.cs index ad7cc9a..b2a0c83 100644 --- a/src/CouchDB.Driver/Views/CouchViewResult`.cs +++ b/src/CouchDB.Driver/Views/CouchViewList.cs @@ -1,18 +1,19 @@ -#nullable disable -#pragma warning disable CA2227 // Collection properties should be read only -using System.Collections.Generic; +using System.Collections.Generic; using CouchDB.Driver.Types; using Newtonsoft.Json; +#nullable disable +#pragma warning disable CA2227 // Collection properties should be read only namespace CouchDB.Driver.Views { /// /// Result of a view query. /// - /// The type of the value that will be returned. - /// The type of the value that will be returned. + /// The type of the key. /// The type of the document. - public class CouchViewResult + /// The type of the view. + public class CouchViewList + where TView : CouchView where TDoc : CouchDocument { /// @@ -31,7 +32,7 @@ public class CouchViewResult /// Array of view row objects. This result contains the document ID, revision and the documents. /// [JsonProperty("rows")] - public IList> Rows { get; set; } + public List Rows { get; set; } } } #pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/CouchDB.Driver/Views/CouchViewOptions.cs b/src/CouchDB.Driver/Views/CouchViewOptions.cs index 6cb6d78..444a5fc 100644 --- a/src/CouchDB.Driver/Views/CouchViewOptions.cs +++ b/src/CouchDB.Driver/Views/CouchViewOptions.cs @@ -2,7 +2,6 @@ #pragma warning disable CA2227 // Collection properties should be read only using System.Collections.Generic; using System.ComponentModel; -using CouchDB.Driver.ChangesFeed; using Newtonsoft.Json; namespace CouchDB.Driver.Views @@ -18,18 +17,21 @@ public class CouchViewOptions /// Ignored if isn't True. Default is False. /// [JsonProperty("conflicts")] + [DefaultValue(false)] public bool Conflicts { get; set; } /// /// Return the documents in descending order by key. Default is False. /// [JsonProperty("descending")] + [DefaultValue(false)] public bool Descending { get; set; } /// /// Stop returning records when the specified key is reached. /// [JsonProperty("endkey")] + [DefaultValue(null)] public TKey EndKey { get; set; } /// @@ -37,6 +39,7 @@ public class CouchViewOptions /// Ignored if is not set. /// [JsonProperty("endkey_docid")] + [DefaultValue(null)] public string EndKeyDocId { get; set; } /// @@ -44,25 +47,29 @@ public class CouchViewOptions /// Implies reduce is True and the maximum . Default is False. /// [JsonProperty("group")] + [DefaultValue(false)] public bool Group { get; set; } /// /// Specify the group level to be used. Implies group is True. /// [JsonProperty("group_level")] + [DefaultValue(null)] public int? GroupLevel { get; set; } /// /// Include the associated document with each row. Default is False. /// [JsonProperty("include_docs")] - internal bool IncludeDocs { get; set; } + [DefaultValue(false)] + public bool IncludeDocs { get; set; } /// /// Include the Base64-encoded content of attachments in the documents that are included if is True. /// Ignored if isn’t True. Default is False. /// [JsonProperty("attachments")] + [DefaultValue(false)] public bool Attachments { get; set; } /// @@ -70,69 +77,80 @@ public class CouchViewOptions /// Ignored if isn’t True. Default is False. /// [JsonProperty("att_encoding_info")] + [DefaultValue(false)] public bool AttachEncodingInfo { get; set; } /// /// Specifies whether the specified end key should be included in the result. Default is True. /// [JsonProperty("inclusive_end")] + [DefaultValue(true)] public bool InclusiveEnd { get; set; } = true; /// /// Return only documents that match the specified key. /// [JsonProperty("key")] + [DefaultValue(null)] public TKey Key { get; set; } /// /// Return only documents where the key matches one of the keys specified in the array. /// [JsonProperty("keys")] + [DefaultValue(null)] public IList Keys { get; set; } /// /// Limit the number of the returned documents to the specified number. /// [JsonProperty("limit")] + [DefaultValue(null)] public int? Limit { get; set; } /// /// Use the reduction function. Default is True when a reduce function is defined. /// [JsonProperty("reduce")] - public bool Reduce { get; set; } = false; + [DefaultValue(false)] + public bool Reduce { get; set; } /// /// Skip this number of records before starting to return the results. Default is 0. /// [JsonProperty("skip")] + [DefaultValue(0)] public int Skip { get; set; } /// /// Sort returned rows (see Sorting Returned Rows). /// Setting this to false offers a performance boost. - /// The and fields are not available when this is set to False. + /// The and fields are not available when this is set to False. /// Default is True. /// [JsonProperty("sorted")] + [DefaultValue(true)] public bool Sorted { get; set; } = true; /// /// Whether or not the view results should be returned from a stable set of shards. Default is False. /// [JsonProperty("stable")] + [DefaultValue(false)] public bool Stable { get; set; } /// /// Return records starting with the specified key. /// [JsonProperty("startkey")] + [DefaultValue(null)] public TKey StartKey { get; set; } /// /// Return records starting with the specified document ID. Ignored if is not set. /// [JsonProperty("startkey_docid")] + [DefaultValue(null)] public string StartKeyDocId { get; set; } /// @@ -143,6 +161,7 @@ public class CouchViewOptions public UpdateStyle Update { get; set; } = UpdateStyle.True; [JsonProperty("update")] + [DefaultValue("true")] internal string UpdateString => Update.ToString(); /// @@ -150,6 +169,7 @@ public class CouchViewOptions /// Default is False. /// [JsonProperty("update_seq")] + [DefaultValue(false)] public bool UpdateSeq { get; set; } } } diff --git a/src/CouchDB.Driver/Views/CouchViewResult.cs b/src/CouchDB.Driver/Views/CouchViewResult.cs index 827d14b..7e0b964 100644 --- a/src/CouchDB.Driver/Views/CouchViewResult.cs +++ b/src/CouchDB.Driver/Views/CouchViewResult.cs @@ -6,30 +6,17 @@ namespace CouchDB.Driver.Views { - /// - /// Result of a view query. - /// - /// The type of the value that will be returned. - /// The type of the value that will be returned. - public class CouchViewResult + internal class CouchViewResult + where TDoc : CouchDocument { - /// - /// Number of documents in the database/view. - /// [JsonProperty("total_rows")] public int TotalRows { get; set; } - - /// - /// Offset where the document list started. - /// + [JsonProperty("offset")] public int Offset { get; set; } - - /// - /// Array of view row objects. This result contains only the document ID and revision. - /// + [JsonProperty("rows")] - public IList> Rows { get; set; } + public List> Rows { get; set; } } } #pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/CouchDB.Driver/Views/CouchViewRow.cs b/src/CouchDB.Driver/Views/CouchViewRow.cs index 4912271..fec4d5a 100644 --- a/src/CouchDB.Driver/Views/CouchViewRow.cs +++ b/src/CouchDB.Driver/Views/CouchViewRow.cs @@ -1,18 +1,23 @@ #nullable disable +using CouchDB.Driver.Types; using Newtonsoft.Json; namespace CouchDB.Driver.Views { - public class CouchViewRow + internal class CouchViewRow + where TDoc : CouchDocument { [JsonProperty("id")] public string Id { get; private set; } - + [JsonProperty("key")] public TKey Key { get; private set; } - + [JsonProperty("value")] public TValue Value { get; private set; } + + [JsonProperty("doc")] + public TDoc Doc { get; private set; } } } -#nullable disable \ No newline at end of file +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewRow`.cs b/src/CouchDB.Driver/Views/CouchViewRow`.cs deleted file mode 100644 index 64a10cc..0000000 --- a/src/CouchDB.Driver/Views/CouchViewRow`.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable disable -using CouchDB.Driver.Types; -using Newtonsoft.Json; - -namespace CouchDB.Driver.Views -{ - public class CouchViewRow : CouchViewRow - where TDoc : CouchDocument - { - [JsonProperty("doc")] - public TDoc Doc { get; private set; } - } -} -#nullable restore \ No newline at end of file diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index dfff06b..5fd9d31 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -6,6 +6,8 @@ using System.Net.Http; using System.Threading.Tasks; using CouchDB.Driver.Extensions; +using CouchDB.Driver.UnitTests._Models; +using CouchDB.Driver.Views; using Xunit; namespace CouchDB.Driver.UnitTests @@ -250,6 +252,127 @@ public async Task AddOrUpdateRange() #endregion + #region View + + [Fact] + public async Task GetViewAsync_WithNoOptions_CallGet() + { + // Arrange + using var httpTest = new HttpTest(); + SetupViewResponse(httpTest); + + // Act + var rebels = await _rebels.GetViewAsync("jedi", "by_name"); + + // Assert + var rebel = Assert.Single(rebels); + Assert.Equal("luke", rebel.Id); + Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); + Assert.Equal(3, rebel.NumberOfBattles); + httpTest + .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") + .WithVerb(HttpMethod.Get); + } + + [Fact] + public async Task GetViewAsync_WithOptions_CallPost() + { + // Arrange + using var httpTest = new HttpTest(); + SetupViewResponse(httpTest); + var options = new CouchViewOptions + { + Key = new[] {"Luke", "Skywalker"}, + Skip = 10 + }; + + // Act + var rebels = await _rebels.GetViewAsync("jedi", "by_name", options); + + // Assert + var rebel = Assert.Single(rebels); + Assert.Equal("luke", rebel.Id); + Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); + Assert.Equal(3, rebel.NumberOfBattles); + httpTest + .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") + .WithVerb(HttpMethod.Post) + .WithRequestBody(@"{""key"":[""Luke"",""Skywalker""],""skip"":10}"); + } + + [Fact] + public async Task GetDetailed_WithNoOptions_CallGet() + { + // Arrange + using var httpTest = new HttpTest(); + SetupViewResponse(httpTest); + + // Act + var list = await _rebels.GetDetailedViewAsync("jedi", "by_name"); + + // Assert + Assert.Equal(10, list.Offset); + Assert.Equal(20, list.TotalRows); + var rebel = Assert.Single(list.Rows); + Assert.Equal("luke", rebel.Id); + Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); + Assert.Equal(3, rebel.NumberOfBattles); + httpTest + .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") + .WithVerb(HttpMethod.Get); + } + + [Fact] + public async Task GetDetailedViewAsync_WithOptions_CallPost() + { + // Arrange + using var httpTest = new HttpTest(); + SetupViewResponse(httpTest); + var options = new CouchViewOptions + { + Key = new[] { "Luke", "Skywalker" }, + Skip = 10 + }; + + // Act + var list = await _rebels.GetDetailedViewAsync("jedi", "by_name", options); + + // Assert + Assert.Equal(10, list.Offset); + Assert.Equal(20, list.TotalRows); + var rebel = Assert.Single(list.Rows); + Assert.Equal("luke", rebel.Id); + Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); + Assert.Equal(3, rebel.NumberOfBattles); + httpTest + .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") + .WithVerb(HttpMethod.Post) + .WithRequestBody(@"{""key"":[""Luke"",""Skywalker""],""skip"":10}"); + } + + private static void SetupViewResponse(HttpTest httpTest) + { + httpTest.RespondWithJson(new + { + Offset = 10, + Total_Rows = 20, + Rows = new[] + { + new + { + Id = "luke", + Key = new [] {"Luke", "Skywalker"}, + Value = new + { + NumberOfBattles = 3 + } + } + } + }); + } + + #endregion + #region Utils [Fact] diff --git a/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs b/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs new file mode 100644 index 0000000..0a9e6a8 --- /dev/null +++ b/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs @@ -0,0 +1,13 @@ +using CouchDB.Driver.UnitTests.Models; +using CouchDB.Driver.Views; + +#nullable disable + +namespace CouchDB.Driver.UnitTests._Models +{ + public class RebelView : CouchView + { + public int NumberOfBattles { get; set; } + } +} +#nullable restore \ No newline at end of file From cb95cc67ab633f403247ee472c3a065bd0582b7f Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 9 Mar 2021 19:11:09 +0100 Subject: [PATCH 19/23] Update Views --- src/CouchDB.Driver/CouchDatabase.cs | 31 +++++-------------- src/CouchDB.Driver/ICouchDatabase.cs | 18 +++++------ src/CouchDB.Driver/Views/CouchView.cs | 16 ++++++++-- src/CouchDB.Driver/Views/CouchViewList.cs | 9 +++--- src/CouchDB.Driver/Views/CouchViewResult.cs | 23 -------------- src/CouchDB.Driver/Views/CouchViewRow.cs | 23 -------------- .../Database_Tests.cs | 12 +++---- .../_Models/RebelView.cs | 2 +- 8 files changed, 41 insertions(+), 93 deletions(-) delete mode 100644 src/CouchDB.Driver/Views/CouchViewResult.cs delete mode 100644 src/CouchDB.Driver/Views/CouchViewRow.cs diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index e76189b..0e97d2b 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -510,20 +510,18 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #region View /// - public async Task> GetViewAsync(string design, string view, + public async Task>> GetViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) - where TView : CouchView { - CouchViewList result = - await GetDetailedViewAsync(design, view, options, cancellationToken) + CouchViewList result = + await GetDetailedViewAsync(design, view, options, cancellationToken) .ConfigureAwait(false); return result.Rows; } /// - public async Task> GetDetailedViewAsync(string design, string view, + public Task> GetDetailedViewAsync(string design, string view, CouchViewOptions? options = null, CancellationToken cancellationToken = default) - where TView : CouchView { Check.NotNull(design, nameof(design)); Check.NotNull(view, nameof(view)); @@ -531,26 +529,13 @@ public async Task> GetDetailedViewAsync>? requestTask = options == null - ? request.GetJsonAsync>(cancellationToken) + Task>? requestTask = options == null + ? request.GetJsonAsync>(cancellationToken) : request .PostJsonAsync(options, cancellationToken) - .ReceiveJson>(); + .ReceiveJson>(); - CouchViewResult result = await requestTask.SendRequestAsync().ConfigureAwait(false); - - return new CouchViewList - { - Offset = result.Offset, - TotalRows = result.TotalRows, - Rows = result.Rows.Select(row => - { - row.Value.Id = row.Id; - row.Value.Key = row.Key; - row.Value.Document = row.Doc; - return row.Value; - }).ToList() - }; + return requestTask.SendRequestAsync(); } #endregion diff --git a/src/CouchDB.Driver/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index a249722..382ec47 100644 --- a/src/CouchDB.Driver/ICouchDatabase.cs +++ b/src/CouchDB.Driver/ICouchDatabase.cs @@ -92,30 +92,28 @@ public interface ICouchDatabase: IOrderedQueryable /// /// Executes the specified view function from the specified design document. /// - /// The type of the value that will be returned. - /// The type of the view. + /// The type of the key. + /// The type of the value. /// The design to use. /// The view to use. /// Options for the request. /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. The task result contains a list of . - Task> GetViewAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default) - where TView : CouchView; + /// A task that represents the asynchronous operation. The task result contains a list of . + Task>> GetViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Executes the specified view function from the specified design document. /// /// The type of the key. - /// The type of the view. + /// The type of the value. /// The design to use. /// The view to use. /// Options for the request. /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. The task result contains the . - Task> GetDetailedViewAsync(string design, string view, - CouchViewOptions? options = null, CancellationToken cancellationToken = default) - where TView : CouchView; + Task> GetDetailedViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default); /// /// Since CouchDB v3, it is deprecated (a no-op). diff --git a/src/CouchDB.Driver/Views/CouchView.cs b/src/CouchDB.Driver/Views/CouchView.cs index c199790..7940f3c 100644 --- a/src/CouchDB.Driver/Views/CouchView.cs +++ b/src/CouchDB.Driver/Views/CouchView.cs @@ -1,26 +1,38 @@ -#nullable disable +using Newtonsoft.Json; + +#nullable disable namespace CouchDB.Driver.Views { /// /// Base class for a view. /// /// The type of the key + /// The type of the value /// The type of the document. - public abstract class CouchView + public sealed class CouchView { /// /// The document ID. /// + [JsonProperty("id")] public string Id { get; set; } /// /// The view key. /// + [JsonProperty("key")] public TKey Key { get; set; } + /// + /// The view key. + /// + [JsonProperty("value")] + public TValue Value { get; set; } + /// /// The document. /// + [JsonProperty("document")] public TDoc Document { get; set; } } } diff --git a/src/CouchDB.Driver/Views/CouchViewList.cs b/src/CouchDB.Driver/Views/CouchViewList.cs index b2a0c83..6954362 100644 --- a/src/CouchDB.Driver/Views/CouchViewList.cs +++ b/src/CouchDB.Driver/Views/CouchViewList.cs @@ -10,10 +10,9 @@ namespace CouchDB.Driver.Views /// Result of a view query. /// /// The type of the key. + /// The type of the value. /// The type of the document. - /// The type of the view. - public class CouchViewList - where TView : CouchView + public class CouchViewList where TDoc : CouchDocument { /// @@ -29,10 +28,10 @@ public class CouchViewList public int Offset { get; set; } /// - /// Array of view row objects. This result contains the document ID, revision and the documents. + /// Array of view row objects. This result contains the document ID, value and the documents. /// [JsonProperty("rows")] - public List Rows { get; set; } + public List> Rows { get; set; } } } #pragma warning restore CA2227 // Collection properties should be read only diff --git a/src/CouchDB.Driver/Views/CouchViewResult.cs b/src/CouchDB.Driver/Views/CouchViewResult.cs deleted file mode 100644 index 7e0b964..0000000 --- a/src/CouchDB.Driver/Views/CouchViewResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -#nullable disable -#pragma warning disable CA2227 // Collection properties should be read only -using System.Collections.Generic; -using CouchDB.Driver.Types; -using Newtonsoft.Json; - -namespace CouchDB.Driver.Views -{ - internal class CouchViewResult - where TDoc : CouchDocument - { - [JsonProperty("total_rows")] - public int TotalRows { get; set; } - - [JsonProperty("offset")] - public int Offset { get; set; } - - [JsonProperty("rows")] - public List> Rows { get; set; } - } -} -#pragma warning restore CA2227 // Collection properties should be read only -#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewRow.cs b/src/CouchDB.Driver/Views/CouchViewRow.cs deleted file mode 100644 index fec4d5a..0000000 --- a/src/CouchDB.Driver/Views/CouchViewRow.cs +++ /dev/null @@ -1,23 +0,0 @@ -#nullable disable -using CouchDB.Driver.Types; -using Newtonsoft.Json; - -namespace CouchDB.Driver.Views -{ - internal class CouchViewRow - where TDoc : CouchDocument - { - [JsonProperty("id")] - public string Id { get; private set; } - - [JsonProperty("key")] - public TKey Key { get; private set; } - - [JsonProperty("value")] - public TValue Value { get; private set; } - - [JsonProperty("doc")] - public TDoc Doc { get; private set; } - } -} -#nullable restore \ No newline at end of file diff --git a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs index 5fd9d31..9ade15a 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -268,7 +268,7 @@ public async Task GetViewAsync_WithNoOptions_CallGet() var rebel = Assert.Single(rebels); Assert.Equal("luke", rebel.Id); Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); - Assert.Equal(3, rebel.NumberOfBattles); + Assert.Equal(3, rebel.Value.NumberOfBattles); httpTest .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") .WithVerb(HttpMethod.Get); @@ -293,7 +293,7 @@ public async Task GetViewAsync_WithOptions_CallPost() var rebel = Assert.Single(rebels); Assert.Equal("luke", rebel.Id); Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); - Assert.Equal(3, rebel.NumberOfBattles); + Assert.Equal(3, rebel.Value.NumberOfBattles); httpTest .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") .WithVerb(HttpMethod.Post) @@ -316,7 +316,7 @@ public async Task GetDetailed_WithNoOptions_CallGet() var rebel = Assert.Single(list.Rows); Assert.Equal("luke", rebel.Id); Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); - Assert.Equal(3, rebel.NumberOfBattles); + Assert.Equal(3, rebel.Value.NumberOfBattles); httpTest .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") .WithVerb(HttpMethod.Get); @@ -331,7 +331,7 @@ public async Task GetDetailedViewAsync_WithOptions_CallPost() var options = new CouchViewOptions { Key = new[] { "Luke", "Skywalker" }, - Skip = 10 + Update = UpdateStyle.Lazy }; // Act @@ -343,11 +343,11 @@ public async Task GetDetailedViewAsync_WithOptions_CallPost() var rebel = Assert.Single(list.Rows); Assert.Equal("luke", rebel.Id); Assert.Equal(new[] { "Luke", "Skywalker" }, rebel.Key); - Assert.Equal(3, rebel.NumberOfBattles); + Assert.Equal(3, rebel.Value.NumberOfBattles); httpTest .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") .WithVerb(HttpMethod.Post) - .WithRequestBody(@"{""key"":[""Luke"",""Skywalker""],""skip"":10}"); + .WithRequestBody(@"{""key"":[""Luke"",""Skywalker""],""update"":""lazy""}"); } private static void SetupViewResponse(HttpTest httpTest) diff --git a/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs b/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs index 0a9e6a8..fcbb320 100644 --- a/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs +++ b/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs @@ -5,7 +5,7 @@ namespace CouchDB.Driver.UnitTests._Models { - public class RebelView : CouchView + public class RebelView { public int NumberOfBattles { get; set; } } From ae39fbf8ef0c4c5d4611e3711b8ba2367e1f8dab Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 9 Mar 2021 19:15:22 +0100 Subject: [PATCH 20/23] Update README --- README.md | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ebdab13..870d300 100644 --- a/README.md +++ b/README.md @@ -529,25 +529,16 @@ var vehicles = client.GetDatabase("troups", nameof(Vehicle)); ## Views -To read a view create a new class that implements `CouchView` where: -* `TKey` is the type of the key; -* `TDoc` is the type of the document. -```csharp -public class RebelView : CouchView -{ - public int NumberOfBattles { get; set; } -} -``` -Then you can query the view: +It's possible to query a view with the following: ```csharp var options = new CouchViewOptions { StartKey = new[] {"Luke", "Skywalker"}, IncludeDocs = true }; -var rebels = await _rebels.GetViewAsync("jedi", "by_name", options); +var viewRows = await _rebels.GetViewAsync("jedi", "by_name", options); // OR -var details = await _rebels.GetDetailedViewAsync("jedi", "by_name", options); +var details = await _rebels.GetDetailedViewAsync("battle", "by_name", options); ``` ## Local (non-replicating) Documents From 065ab794d0d503af1297155c163b05dbe183d962 Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 9 Mar 2021 21:01:30 +0100 Subject: [PATCH 21/23] Fix view --- src/CouchDB.Driver/Views/CouchView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CouchDB.Driver/Views/CouchView.cs b/src/CouchDB.Driver/Views/CouchView.cs index 7940f3c..f5ff6d4 100644 --- a/src/CouchDB.Driver/Views/CouchView.cs +++ b/src/CouchDB.Driver/Views/CouchView.cs @@ -32,7 +32,7 @@ public sealed class CouchView /// /// The document. /// - [JsonProperty("document")] + [JsonProperty("doc")] public TDoc Document { get; set; } } } From e1d34923b8000cd81d412acffddde2d435e2f7df Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 9 Mar 2021 21:28:26 +0100 Subject: [PATCH 22/23] README update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 870d300..6c0a47c 100644 --- a/README.md +++ b/README.md @@ -665,3 +665,5 @@ Thanks to [Ben Origas](https://github.com/borigas) for features, ideas and tests Thanks to [n9](https://github.com/n9) for proxy authentication, some bug fixes, suggestions and the great feedback on the changes feed feature! Thanks to [Marc](https://github.com/bender-ristone) for NullValueHandling, bug fixes and suggestions! + +Thanks to [Panos](https://github.com/panoukos41) for the help with Views and Table splitting. \ No newline at end of file From 4a340d58ae2e24b9307792912b0dc6af9ded885b Mon Sep 17 00:00:00 2001 From: Matteo Bortolazzo Date: Tue, 9 Mar 2021 21:35:21 +0100 Subject: [PATCH 23/23] Update changelog --- CHANGELOG.md | 16 +++++++++++++++- LATEST_CHANGE.md | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77b11fb..9e1ed74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ -# 2.0.1 (2020-09-19) +# 3.0.0 (2020-03-09) + +## Breaking Changes +* Update to [Flurl 3](https://github.com/tmenier/Flurl/releases/tag/Flurl.Http.3.0.0). There should be no differences for the end user, but keep in mind. + +## Features +* **Table Splitting**: Ability to use the same database for different document with automatic filtering. ([#106](https://github.com/matteobortolazzo/couchdb-net/issues/106)) +* **Views**: Ability to get views. Thanks to [panoukos41](https://github.com/panoukos41) + +## Improvements +* **Logical Expressions Prune**: If expressions are constant booleans, they are removed automatically keeping the query valid. ([#113](https://github.com/matteobortolazzo/couchdb-net/issues/113)) +* **IsUpAsync**: Returns false on timeout and on not successful codes. ([#107](https://github.com/matteobortolazzo/couchdb-net/issues/107)) +* **FindAsync**: Faster when document is not found. ([#92](https://github.com/matteobortolazzo/couchdb-net/issues/92)) + +# 2.1.0 (2020-09-19) ## Features * **Indexes"**: Ability to create indexes. ([#102](https://github.com/matteobortolazzo/couchdb-net/issues/102)) diff --git a/LATEST_CHANGE.md b/LATEST_CHANGE.md index fd2e3f7..8da5729 100644 --- a/LATEST_CHANGE.md +++ b/LATEST_CHANGE.md @@ -3,7 +3,7 @@ ## Features * **Table Splitting**: Ability to use the same database for different document with automatic filtering. ([#106](https://github.com/matteobortolazzo/couchdb-net/issues/106)) -* **Views**: Ability to get views. Thanks to [panoukos41](https://github.com/panoukos41) ([#117](https://github.com/matteobortolazzo/couchdb-net/issues/117)) +* **Views**: Ability to get views. Thanks to [panoukos41](https://github.com/panoukos41) ## Improvements * **Logical Expressions Prune**: If expressions are constant booleans, they are removed automatically keeping the query valid. ([#113](https://github.com/matteobortolazzo/couchdb-net/issues/113))