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 136c07d..8da5729 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) + +## 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/README.md b/README.md index c73b623..6c0a47c 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ The produced Mango JSON: * [Indexing](#indexing) * [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) @@ -487,16 +489,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 +499,48 @@ 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)); +``` + +## Views + +It's possible to query a view with the following: +```csharp +var options = new CouchViewOptions +{ + StartKey = new[] {"Luke", "Skywalker"}, + IncludeDocs = true +}; +var viewRows = await _rebels.GetViewAsync("jedi", "by_name", options); +// OR +var details = await _rebels.GetDetailedViewAsync("battle", "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. @@ -630,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 diff --git a/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj b/src/CouchDB.Driver.DependencyInjection/CouchDB.Driver.DependencyInjection.csproj index 64b6eec..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 @@ -32,8 +32,8 @@ - - + + 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/CouchClient.cs b/src/CouchDB.Driver/CouchClient.cs index e5357cd..6b77538 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; @@ -26,8 +25,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; } @@ -107,28 +108,28 @@ 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); - 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); + return new CouchDatabase(_flurlClient, _options, queryContext, discriminator); } - if (response.StatusCode == HttpStatusCode.PreconditionFailed) + if (response.StatusCode == (int)HttpStatusCode.PreconditionFailed) { throw new CouchException($"Database with name {database} already exists."); } @@ -138,16 +139,16 @@ 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); - 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); + return new CouchDatabase(_flurlClient, _options, queryContext, discriminator); } throw new CouchException($"Something wrong happened while creating database {database}."); @@ -171,7 +172,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() @@ -204,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); } /// @@ -242,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 @@ -259,12 +260,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 +274,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 +339,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 @@ -349,6 +361,12 @@ public async ValueTask DisposeAsync() 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/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..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,14 +39,47 @@ 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) + SetupDiscriminators(databaseBuilder); + InitializeDatabases(options, databaseBuilder); + } + + public async ValueTask 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 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}); + .Invoke(this, new object[] { dbProperty, options, databaseBuilder }); initDatabasesTask.ConfigureAwait(false).GetAwaiter().GetResult(); var applyDatabaseChangesTask = (Task)ApplyDatabaseChangesGenericMethod.MakeGenericMethod(documentType) @@ -55,31 +88,46 @@ protected CouchContext(CouchOptions options) } } - public ValueTask DisposeAsync() - { - return Client.DisposeAsync(); - } - - private async Task InitDatabasesAsync(PropertyInfo propertyInfo, CouchOptions options) + 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); @@ -116,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 1ab8f96..ceef1ae 100644 --- a/src/CouchDB.Driver/CouchDB.Driver.csproj +++ b/src/CouchDB.Driver/CouchDB.Driver.csproj @@ -14,9 +14,9 @@ Library en - 8.0 + latest enable - 2.0.0 + 3.0.0 icon.png LICENSE.txt @@ -32,10 +32,9 @@ - + - - + diff --git a/src/CouchDB.Driver/CouchDatabase.cs b/src/CouchDB.Driver/CouchDatabase.cs index 3ecb4ac..0e97d2b 100644 --- a/src/CouchDB.Driver/CouchDatabase.cs +++ b/src/CouchDB.Driver/CouchDatabase.cs @@ -23,6 +23,10 @@ using CouchDB.Driver.Options; using CouchDB.Driver.Query; using Newtonsoft.Json; +using System.Net; +using CouchDB.Driver.Views; +using Flurl.Http.Configuration; +using Newtonsoft.Json.Serialization; namespace CouchDB.Driver { @@ -30,13 +34,14 @@ 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; private readonly IFlurlClient _flurlClient; private readonly CouchOptions _options; private readonly QueryContext _queryContext; + private readonly string? _discriminator; /// public string Database => _queryContext.DatabaseName; @@ -47,16 +52,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); @@ -69,28 +75,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 +120,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 +137,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>() @@ -192,6 +192,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() @@ -223,6 +224,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() @@ -267,6 +269,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) @@ -346,7 +353,7 @@ private async Task UpdateAttachments(TSource document, CancellationToken cancell { document.Rev = response.Rev; document.Attachments.RemoveAttachment(attachment); - } + } } InitAttachments(document); @@ -380,7 +387,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); @@ -399,7 +406,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 +422,7 @@ public async IAsyncEnumerable> GetContinuousC #endregion #region Index - + /// public async Task> GetIndexesAsync(CancellationToken cancellationToken = default) { @@ -448,7 +455,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 +469,7 @@ internal async Task CreateIndexAsync(string name, sb.Append($",\"partitioned\":{options.Partitioned.ToString().ToLowerInvariant()}"); } - sb.Append("}"); + sb.Append('}'); var request = sb.ToString(); @@ -500,6 +507,39 @@ public Task DeleteIndexAsync(IndexInfo indexInfo, CancellationToken cancellation #endregion + #region View + + /// + public async Task>> GetViewAsync(string design, string view, + CouchViewOptions? options = null, CancellationToken cancellationToken = default) + { + CouchViewList result = + await GetDetailedViewAsync(design, view, options, cancellationToken) + .ConfigureAwait(false); + return result.Rows; + } + + /// + 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); + + Task>? requestTask = options == null + ? request.GetJsonAsync>(cancellationToken) + : request + .PostJsonAsync(options, cancellationToken) + .ReceiveJson>(); + + return requestTask.SendRequestAsync(); + } + + #endregion + #region Utils /// @@ -568,7 +608,7 @@ public override string ToString() #endregion #region Helper - + /// public IFlurlRequest NewRequest() { 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 f83a5a4..f09cbbf 100644 --- a/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs +++ b/src/CouchDB.Driver/Extensions/FlurlRequestExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -18,46 +19,52 @@ 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(); } 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; } + + 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/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/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/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/ICouchClient.cs b/src/CouchDB.Driver/ICouchClient.cs index ff9c44c..2a61a9c 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; /// @@ -155,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/ICouchDatabase.cs b/src/CouchDB.Driver/ICouchDatabase.cs index 73379bd..382ec47 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 @@ -88,6 +89,32 @@ 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); + /// + /// Executes the specified view function from the specified design document. + /// + /// 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); + + /// + /// Executes the specified view function from the specified design document. + /// + /// 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 the . + 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/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/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/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/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/Extensions/ExpressionExtensions.cs b/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs new file mode 100644 index 0000000..2b6df88 --- /dev/null +++ b/src/CouchDB.Driver/Query/Extensions/ExpressionExtensions.cs @@ -0,0 +1,22 @@ +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; + } + + public static bool IsBoolean(this Expression expression) + { + return expression is ConstantExpression c && c.Value is bool; + } + } +} 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..d0033fb 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 @@ -15,6 +16,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; @@ -23,8 +26,15 @@ 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 = WrapInWhereGenericMethod.MakeGenericMethod(new[] { sourceType }); + e = (Expression)wrapInWhere.Invoke(null, new object[] { e, discriminator }); + } + e = LocalExpressions.PartialEval(e); return Visit(e); } @@ -52,7 +62,10 @@ protected override Expression VisitMethodCall(MethodCallExpression node) _isVisitingWhereMethodOrChild = true; Expression whereNode = VisitMethodCall(node); _isVisitingWhereMethodOrChild = false; - return whereNode; + + return whereNode.IsBoolean() + ? node.Arguments[0] + : whereNode; } #endregion @@ -71,8 +84,14 @@ protected override Expression VisitMethodCall(MethodCallExpression node) while (_nextWhereCalls.Count > 0) { - Expression nextWhereBody = Visit(_nextWhereCalls.Dequeue().GetLambdaBody()); - conditionExpression = Expression.And(nextWhereBody, conditionExpression); + Expression nextWhereBody = _nextWhereCalls.Dequeue().GetLambdaBody(); + conditionExpression = Expression.AndAlso(nextWhereBody, conditionExpression); + conditionExpression = Visit(conditionExpression); + } + + if (conditionExpression.IsBoolean()) + { + return conditionExpression; } Expression conditionLambda = conditionExpression.WrapInLambda(currentLambda.Parameters); @@ -271,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/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..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) { @@ -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..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 @@ -180,7 +179,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 +207,7 @@ void InspectOrdering(Expression e) return; } - _sb.Append(","); + _sb.Append(','); } InspectOrdering(m); @@ -235,7 +234,7 @@ void InspectOrdering(Expression e) break; case "ThenByDescending": InspectOrdering(o.Arguments[0]); - _sb.Append("{"); + _sb.Append('{'); Visit(lambdaBody); _sb.Append(":\"desc\"}"); break; @@ -243,7 +242,7 @@ void InspectOrdering(Expression e) return; } - _sb.Append(","); + _sb.Append(','); } InspectOrdering(m); @@ -274,7 +273,7 @@ private Expression VisitSelectMethod(MethodCallExpression m) foreach (Expression a in n.Arguments) { Visit(a); - _sb.Append(","); + _sb.Append(','); } _sb.Length--; } @@ -298,7 +297,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 +307,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 +325,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 +333,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 +377,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 +392,7 @@ public Expression VisitIncludeConflictsMethod(MethodCallExpression m) { Visit(m.Arguments[0]); _sb.Append("\"conflicts\":true"); - _sb.Append(","); + _sb.Append(','); return m; } @@ -410,7 +409,7 @@ public Expression VisitSelectFieldMethod(MethodCallExpression m) foreach (Expression a in fieldExpressions) { Visit(a); - _sb.Append(","); + _sb.Append(','); } _sb.Length--; @@ -446,7 +445,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 +454,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 +469,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 +477,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 +493,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 +507,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/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/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/src/CouchDB.Driver/Views/CouchView.cs b/src/CouchDB.Driver/Views/CouchView.cs new file mode 100644 index 0000000..f5ff6d4 --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchView.cs @@ -0,0 +1,39 @@ +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 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("doc")] + public TDoc Document { get; set; } + } +} +#nullable restore \ No newline at end of file diff --git a/src/CouchDB.Driver/Views/CouchViewList.cs b/src/CouchDB.Driver/Views/CouchViewList.cs new file mode 100644 index 0000000..6954362 --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewList.cs @@ -0,0 +1,38 @@ +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 key. + /// The type of the value. + /// The type of the document. + public class CouchViewList + 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, value and the documents. + /// + [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/CouchViewOptions.cs b/src/CouchDB.Driver/Views/CouchViewOptions.cs new file mode 100644 index 0000000..444a5fc --- /dev/null +++ b/src/CouchDB.Driver/Views/CouchViewOptions.cs @@ -0,0 +1,177 @@ +#nullable disable +#pragma warning disable CA2227 // Collection properties should be read only +using System.Collections.Generic; +using System.ComponentModel; +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")] + [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; } + + /// + /// Stop returning records when the specified document ID is reached. + /// Ignored if is not set. + /// + [JsonProperty("endkey_docid")] + [DefaultValue(null)] + 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")] + [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")] + [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; } + + /// + /// 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")] + [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")] + [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. + /// 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; } + + /// + /// 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")] + [DefaultValue("true")] + 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")] + [DefaultValue(false)] + 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/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 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/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/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 4a83aba..9ade15a 100644 --- a/tests/CouchDB.Driver.UnitTests/Database_Tests.cs +++ b/tests/CouchDB.Driver.UnitTests/Database_Tests.cs @@ -6,11 +6,13 @@ 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 { - public class Database_Tests: IAsyncDisposable + public class Database_Tests : IAsyncDisposable { private readonly ICouchClient _client; private readonly ICouchDatabase _rebels; @@ -57,8 +59,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); } @@ -112,6 +114,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: "myRebels"); + 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() { @@ -220,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.Value.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.Value.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.Value.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" }, + Update = UpdateStyle.Lazy + }; + + // 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.Value.NumberOfBattles); + httpTest + .ShouldHaveCalled("http://localhost/rebels/_design/jedi/_view/by_name") + .WithVerb(HttpMethod.Post) + .WithRequestBody(@"{""key"":[""Luke"",""Skywalker""],""update"":""lazy""}"); + } + + 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/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; } } 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_Miscellaneous.cs b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs index 46ea435..0b488d7 100644 --- a/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs +++ b/tests/CouchDB.Driver.UnitTests/Find/Find_Miscellaneous.cs @@ -137,6 +137,63 @@ 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() + { + 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 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() { 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/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] 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/RebelView.cs b/tests/CouchDB.Driver.UnitTests/_Models/RebelView.cs new file mode 100644 index 0000000..fcbb320 --- /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 + { + public int NumberOfBattles { get; set; } + } +} +#nullable restore \ No newline at end of file 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 {