Skip to content

Commit 9d04d79

Browse files
#106 - Implement optional discriminator in query builder
1 parent 40d93ff commit 9d04d79

18 files changed

+140
-44
lines changed

src/CouchDB.Driver/CouchClient.cs

+12-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using CouchDB.Driver.DTOs;
1212
using CouchDB.Driver.Exceptions;
1313
using Newtonsoft.Json;
14-
using System.Net.Http;
1514
using System.Net;
1615
using System.Threading;
1716
using CouchDB.Driver.Options;
@@ -109,16 +108,16 @@ private IFlurlClient GetConfiguredClient() =>
109108
#region CRUD
110109

111110
/// <inheritdoc />
112-
public ICouchDatabase<TSource> GetDatabase<TSource>(string database) where TSource : CouchDocument
111+
public ICouchDatabase<TSource> GetDatabase<TSource>(string database, string? discriminator = null) where TSource : CouchDocument
113112
{
114113
CheckDatabaseName(database);
115114
var queryContext = new QueryContext(Endpoint, database);
116-
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext);
115+
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext, discriminator);
117116
}
118117

119118
/// <inheritdoc />
120119
public async Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string database,
121-
int? shards = null, int? replicas = null, CancellationToken cancellationToken = default)
120+
int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default)
122121
where TSource : CouchDocument
123122
{
124123
QueryContext queryContext = NewQueryContext(database);
@@ -127,7 +126,7 @@ public async Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string d
127126

128127
if (response.IsSuccessful())
129128
{
130-
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext);
129+
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext, discriminator);
131130
}
132131

133132
if (response.StatusCode == (int)HttpStatusCode.PreconditionFailed)
@@ -140,7 +139,7 @@ public async Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string d
140139

141140
/// <inheritdoc />
142141
public async Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(string database,
143-
int? shards = null, int? replicas = null, CancellationToken cancellationToken = default)
142+
int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default)
144143
where TSource : CouchDocument
145144
{
146145
QueryContext queryContext = NewQueryContext(database);
@@ -149,7 +148,7 @@ public async Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(str
149148

150149
if (response.IsSuccessful() || response.StatusCode == (int)HttpStatusCode.PreconditionFailed)
151150
{
152-
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext);
151+
return new CouchDatabase<TSource>(_flurlClient, _options, queryContext, discriminator);
153152
}
154153

155154
throw new CouchException($"Something wrong happened while creating database {database}.");
@@ -206,17 +205,17 @@ public ICouchDatabase<TSource> GetDatabase<TSource>() where TSource : CouchDocum
206205
}
207206

208207
/// <inheritdoc />
209-
public Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null,
208+
public Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null, string? discriminator = null,
210209
CancellationToken cancellationToken = default) where TSource : CouchDocument
211210
{
212-
return CreateDatabaseAsync<TSource>(GetClassName<TSource>(), shards, replicas, cancellationToken);
211+
return CreateDatabaseAsync<TSource>(GetClassName<TSource>(), shards, replicas, discriminator, cancellationToken);
213212
}
214213

215214
/// <inheritdoc />
216-
public Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null,
215+
public Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null, string? discriminator = null,
217216
CancellationToken cancellationToken = default) where TSource : CouchDocument
218217
{
219-
return GetOrCreateDatabaseAsync<TSource>(GetClassName<TSource>(), shards, replicas, cancellationToken);
218+
return GetOrCreateDatabaseAsync<TSource>(GetClassName<TSource>(), shards, replicas, discriminator, cancellationToken);
220219
}
221220

222221
/// <inheritdoc />
@@ -244,13 +243,13 @@ public ICouchDatabase<TUser> GetUsersDatabase<TUser>() where TUser : CouchUser
244243
/// <inheritdoc />
245244
public Task<ICouchDatabase<CouchUser>> GetOrCreateUsersDatabaseAsync(CancellationToken cancellationToken = default)
246245
{
247-
return GetOrCreateDatabaseAsync<CouchUser>(null, null, cancellationToken);
246+
return GetOrCreateDatabaseAsync<CouchUser>(null, null, null, cancellationToken);
248247
}
249248

250249
/// <inheritdoc />
251250
public Task<ICouchDatabase<TUser>> GetOrCreateUsersDatabaseAsync<TUser>(CancellationToken cancellationToken = default) where TUser : CouchUser
252251
{
253-
return GetOrCreateDatabaseAsync<TUser>(null, null, cancellationToken);
252+
return GetOrCreateDatabaseAsync<TUser>(null, null, null, cancellationToken);
254253
}
255254

256255
#endregion

src/CouchDB.Driver/CouchDB.Driver.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
<ItemGroup>
4141
<Folder Include="License\" />
42+
<Folder Include="Attributes\" />
4243
</ItemGroup>
4344

4445
</Project>

src/CouchDB.Driver/CouchDatabase.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class CouchDatabase<TSource> : ICouchDatabase<TSource>
3838
private readonly IFlurlClient _flurlClient;
3939
private readonly CouchOptions _options;
4040
private readonly QueryContext _queryContext;
41+
private readonly string? _discriminator;
4142

4243
/// <inheritdoc />
4344
public string Database => _queryContext.DatabaseName;
@@ -48,16 +49,17 @@ public class CouchDatabase<TSource> : ICouchDatabase<TSource>
4849
/// <inheritdoc />
4950
public ILocalDocuments LocalDocuments { get; }
5051

51-
internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryContext queryContext)
52+
internal CouchDatabase(IFlurlClient flurlClient, CouchOptions options, QueryContext queryContext, string? discriminator)
5253
{
5354
_flurlClient = flurlClient;
5455
_options = options;
5556
_queryContext = queryContext;
57+
_discriminator = discriminator;
5658

5759
var queryOptimizer = new QueryOptimizer();
5860
var queryTranslator = new QueryTranslator(options);
5961
var querySender = new QuerySender(flurlClient, queryContext);
60-
var queryCompiler = new QueryCompiler(queryOptimizer, queryTranslator, querySender);
62+
var queryCompiler = new QueryCompiler(queryOptimizer, queryTranslator, querySender, _discriminator);
6163
_queryProvider = new CouchQueryProvider(queryCompiler);
6264

6365
Security = new CouchSecurity(NewRequest);
@@ -187,6 +189,7 @@ public async Task<TSource> AddAsync(TSource document, bool batch = false, Cancel
187189
request = request.SetQueryParam("batch", "ok");
188190
}
189191

192+
document.Discriminator = _discriminator;
190193
DocumentSaveResponse response = await request
191194
.PostJsonAsync(document, cancellationToken)
192195
.ReceiveJson<DocumentSaveResponse>()
@@ -218,6 +221,7 @@ public async Task<TSource> AddOrUpdateAsync(TSource document, bool batch = false
218221
request = request.SetQueryParam("batch", "ok");
219222
}
220223

224+
document.Discriminator = _discriminator;
221225
DocumentSaveResponse response = await request
222226
.PutJsonAsync(document, cancellationToken)
223227
.ReceiveJson<DocumentSaveResponse>()
@@ -262,6 +266,11 @@ public async Task<IEnumerable<TSource>> AddOrUpdateRangeAsync(IList<TSource> doc
262266
{
263267
Check.NotNull(documents, nameof(documents));
264268

269+
foreach(TSource? document in documents)
270+
{
271+
document.Discriminator = _discriminator;
272+
}
273+
265274
DocumentSaveResponse[] response = await NewRequest()
266275
.AppendPathSegment("_bulk_docs")
267276
.PostJsonAsync(new { docs = documents }, cancellationToken)

src/CouchDB.Driver/Helpers/MethodCallExpressionBuilder.cs

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq.Expressions;
44
using System.Reflection;
55
using CouchDB.Driver.Extensions;
6+
using CouchDB.Driver.Types;
67

78
namespace CouchDB.Driver.Helpers
89
{
@@ -108,6 +109,17 @@ public static MethodCallExpression WrapInMethodWithoutSelector(this MethodCallEx
108109
return Expression.Call(genericMethodInfo, node);
109110
}
110111

112+
public static MethodCallExpression WrapInDiscriminatorFilter<TSource>(this Expression node, string discriminator)
113+
where TSource : CouchDocument
114+
{
115+
Check.NotNull(node, nameof(node));
116+
117+
Expression<Func<TSource, bool>> filter = (d) => d.Discriminator == discriminator;
118+
119+
return Expression.Call(typeof(Queryable), nameof(Queryable.Where),
120+
new[] { typeof(TSource) }, node, filter);
121+
}
122+
111123
#endregion
112124
}
113125
}

src/CouchDB.Driver/ICouchClient.cs

+10-5
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ public interface ICouchClient: IAsyncDisposable
1313
/// </summary>
1414
/// <typeparam name="TSource">The type of database documents.</typeparam>
1515
/// <param name="database">The database name.</param>
16+
/// <param name="discriminator">Filters documents by the given discriminator.</param>
1617
/// <returns>An instance of the CouchDB database with given name.</returns>
17-
ICouchDatabase<TSource> GetDatabase<TSource>(string database) where TSource : CouchDocument;
18+
ICouchDatabase<TSource> GetDatabase<TSource>(string database, string? discriminator = null) where TSource : CouchDocument;
1819

1920
/// <summary>
2021
/// Returns an instance of the CouchDB database with the given name.
@@ -25,10 +26,11 @@ public interface ICouchClient: IAsyncDisposable
2526
/// <param name="database">The database name.</param>
2627
/// <param name="shards">The number of range partitions. Default is 8, unless overridden in the cluster config.</param>
2728
/// <param name="replicas">The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config.</param>
29+
/// <param name="discriminator">Filters documents by the given discriminator.</param>
2830
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
2931
/// <returns>A task that represents the asynchronous operation. The task result contains the newly created CouchDB database.</returns>
3032
Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string database,
31-
int? shards = null, int? replicas = null, CancellationToken cancellationToken = default)
33+
int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default)
3234
where TSource : CouchDocument;
3335

3436
/// <summary>
@@ -40,10 +42,11 @@ Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(string database,
4042
/// <param name="database">The database name.</param>
4143
/// <param name="shards">Used when creating. The number of range partitions. Default is 8, unless overridden in the cluster config.</param>
4244
/// <param name="replicas">Used when creating. The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config.</param>
45+
/// <param name="discriminator">Filters documents by the given discriminator.</param>
4346
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
4447
/// <returns>A task that represents the asynchronous operation. The task result contains the newly created CouchDB database.</returns>
4548
Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(string database,
46-
int? shards = null, int? replicas = null, CancellationToken cancellationToken = default)
49+
int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default)
4750
where TSource : CouchDocument;
4851

4952
/// <summary>
@@ -69,9 +72,10 @@ Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(string database,
6972
/// <typeparam name="TSource">The type of database documents.</typeparam>
7073
/// <param name="shards">The number of range partitions. Default is 8, unless overridden in the cluster config.</param>
7174
/// <param name="replicas">The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config.</param>
75+
/// <param name="discriminator">Filters documents by the given discriminator.</param>
7276
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
7377
/// <returns>A task that represents the asynchronous operation. The task result contains the newly created CouchDB database.</returns>
74-
Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null, CancellationToken cancellationToken = default) where TSource : CouchDocument;
78+
Task<ICouchDatabase<TSource>> CreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default) where TSource : CouchDocument;
7579

7680
/// <summary>
7781
/// Returns an instance of the CouchDB database with the name type <see cref="TSource"/>.
@@ -80,9 +84,10 @@ Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(string database,
8084
/// <typeparam name="TSource">The type of database documents.</typeparam>
8185
/// <param name="shards">Used when creating. The number of range partitions. Default is 8, unless overridden in the cluster config.</param>
8286
/// <param name="replicas">Used when creating. The number of copies of the database in the cluster. The default is 3, unless overridden in the cluster config.</param>
87+
/// <param name="discriminator">Filters documents by the given discriminator.</param>
8388
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
8489
/// <returns>A task that represents the asynchronous operation. The task result contains the newly created CouchDB database.</returns>
85-
Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null, CancellationToken cancellationToken = default)
90+
Task<ICouchDatabase<TSource>> GetOrCreateDatabaseAsync<TSource>(int? shards = null, int? replicas = null, string? discriminator = null, CancellationToken cancellationToken = default)
8691
where TSource : CouchDocument;
8792

8893
/// <summary>

src/CouchDB.Driver/Options/CouchOptions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ internal CouchOptions()
3939
PluralizeEntities = true;
4040
DocumentsCaseType = DocumentCaseType.UnderscoreCase;
4141
PropertiesCase = PropertyCaseType.CamelCase;
42+
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
4243
LogOutOnDispose = true;
4344
}
4445
}

src/CouchDB.Driver/Query/IQueryOptimizer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace CouchDB.Driver.Query
44
{
55
internal interface IQueryOptimizer
66
{
7-
Expression Optimize(Expression e);
7+
Expression Optimize(Expression e, string? discriminator);
88
}
99
}

src/CouchDB.Driver/Query/QueryCompiler.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal class QueryCompiler : IQueryCompiler
1616
private readonly IQueryOptimizer _queryOptimizer;
1717
private readonly IQueryTranslator _queryTranslator;
1818
private readonly IQuerySender _requestSender;
19-
19+
private readonly string? _discriminator;
2020
private static readonly MethodInfo RequestSendMethod
2121
= typeof(IQuerySender).GetRuntimeMethods()
2222
.Single(m => (m.Name == nameof(IQuerySender.Send)) && m.IsGenericMethod);
@@ -29,16 +29,17 @@ private static readonly MethodInfo PostProcessResultAsyncMethod
2929
= typeof(QueryCompiler).GetMethod(nameof(PostProcessResultAsync),
3030
BindingFlags.NonPublic | BindingFlags.Static);
3131

32-
public QueryCompiler(IQueryOptimizer queryOptimizer, IQueryTranslator queryTranslator, IQuerySender requestSender)
32+
public QueryCompiler(IQueryOptimizer queryOptimizer, IQueryTranslator queryTranslator, IQuerySender requestSender, string? discriminator)
3333
{
3434
_queryOptimizer = queryOptimizer;
3535
_queryTranslator = queryTranslator;
3636
_requestSender = requestSender;
37+
_discriminator = discriminator;
3738
}
3839

3940
public string ToString(Expression query)
4041
{
41-
Expression optimizedQuery = _queryOptimizer.Optimize(query);
42+
Expression optimizedQuery = _queryOptimizer.Optimize(query, _discriminator);
4243
return _queryTranslator.Translate(optimizedQuery);
4344
}
4445

@@ -70,15 +71,15 @@ private TResult SendRequest<TResult>(Expression query, bool async, CancellationT
7071

7172
private TResult SendRequestWithoutFilter<TResult>(Expression query, bool async, CancellationToken cancellationToken)
7273
{
73-
Expression optimizedQuery = _queryOptimizer.Optimize(query);
74+
Expression optimizedQuery = _queryOptimizer.Optimize(query, _discriminator);
7475
var body = _queryTranslator.Translate(optimizedQuery);
7576
return _requestSender.Send<TResult>(body, async, cancellationToken);
7677
}
7778

7879
private TResult SendRequestWithFilter<TResult>(MethodCallExpression methodCallExpression, Expression query,
7980
bool async, CancellationToken cancellationToken)
8081
{
81-
Expression optimizedQuery = _queryOptimizer.Optimize(query);
82+
Expression optimizedQuery = _queryOptimizer.Optimize(query, _discriminator);
8283

8384
if (!(optimizedQuery is MethodCallExpression optimizedMethodCall))
8485
{

src/CouchDB.Driver/Query/QueryOptimizer.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,18 @@ public QueryOptimizer()
2323
_nextWhereCalls = new Queue<MethodCallExpression>();
2424
}
2525

26-
public Expression Optimize(Expression e)
26+
public Expression Optimize(Expression e, string? discriminator)
2727
{
28+
if (discriminator is not null)
29+
{
30+
Type? sourceType = e.Type.GetGenericArguments()[0];
31+
MethodInfo? wrapInWhere = typeof(MethodCallExpressionBuilder)
32+
.GetMethod(nameof(MethodCallExpressionBuilder.WrapInDiscriminatorFilter))
33+
.MakeGenericMethod(new[] { sourceType });
34+
35+
e = (Expression)wrapInWhere.Invoke(null, new object[] { e, discriminator });
36+
}
37+
2838
e = LocalExpressions.PartialEval(e);
2939
return Visit(e);
3040
}

src/CouchDB.Driver/Query/Translators/MethodCallExpressionTranslator.cs

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Reflection;
77
using System.Security.Authentication;
88
using CouchDB.Driver.Extensions;
9-
using CouchDB.Driver.Helpers;
109
using CouchDB.Driver.Shared;
1110

1211
#pragma warning disable IDE0058 // Expression value is never used

src/CouchDB.Driver/Types/CouchDocument.cs

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ private Dictionary<string, CouchAttachment> AttachmentsSetter
5757
[JsonIgnore]
5858
public CouchAttachmentsCollection Attachments { get; internal set; }
5959

60+
[DataMember]
61+
[JsonProperty("_discriminator", NullValueHandling = NullValueHandling.Ignore)]
62+
internal string Discriminator { get; set; }
63+
6064
[OnDeserialized]
6165
internal void OnDeserializedMethod(StreamingContext context)
6266
{

0 commit comments

Comments
 (0)