Skip to content

Commit 9d6e6fb

Browse files
authored
(#34) CRUD for remote dataset (#50)
* (#34) Remote dataset, exceptions, and tests
1 parent 2c1c392 commit 9d6e6fb

File tree

57 files changed

+2636
-88
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2636
-88
lines changed

Datasync.Toolkit.sln

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ VisualStudioVersion = 17.0.31903.59
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{84AD662A-4B9E-4E64-834D-72529FB7FCE5}"
77
ProjectSection(SolutionItems) = preProject
8-
src\Shared.Build.props = src\Shared.Build.props
8+
src\Directory.Build.props = src\Directory.Build.props
99
EndProjectSection
1010
EndProject
1111
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D59F1489-5D74-4F52-B78B-88037EAB2838}"
1212
ProjectSection(SolutionItems) = preProject
13-
tests\Shared.Build.props = tests\Shared.Build.props
13+
tests\Directory.Build.props = tests\Directory.Build.props
14+
tests\EFCore.Packages.props = tests\EFCore.Packages.props
1415
EndProjectSection
1516
EndProject
1617
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.Abstractions", "src\CommunityToolkit.Datasync.Server.Abstractions\CommunityToolkit.Datasync.Server.Abstractions.csproj", "{852F8266-603E-4FC6-A5CB-E492E747924F}"
@@ -59,9 +60,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.S
5960
EndProject
6061
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.Swashbuckle", "src\CommunityToolkit.Datasync.Server.Swashbuckle\CommunityToolkit.Datasync.Server.Swashbuckle.csproj", "{45D47A4E-AD58-40C8-B4CC-95BC888C47A7}"
6162
EndProject
62-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Client", "src\CommunityToolkit.Datasync.Client\CommunityToolkit.Datasync.Client.csproj", "{D3B72031-D4BD-44D3-973C-2752AB1570F6}"
63+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client", "src\CommunityToolkit.Datasync.Client\CommunityToolkit.Datasync.Client.csproj", "{D3B72031-D4BD-44D3-973C-2752AB1570F6}"
6364
EndProject
64-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Datasync.Client.Test", "tests\CommunityToolkit.Datasync.Client.Test\CommunityToolkit.Datasync.Client.Test.csproj", "{2889E6B2-9CD1-437C-A43C-98CFAFF68B99}"
65+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client.Test", "tests\CommunityToolkit.Datasync.Client.Test\CommunityToolkit.Datasync.Client.Test.csproj", "{2889E6B2-9CD1-437C-A43C-98CFAFF68B99}"
6566
EndProject
6667
Global
6768
GlobalSection(SolutionConfigurationPlatforms) = preSolution

src/CommunityToolkit.Datasync.Client/CommunityToolkit.Datasync.Client.csproj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
<Description>The client capabilities for developing applications using the Datasync Toolkit.</Description>
44
</PropertyGroup>
55

6-
<Import Project="..\Shared.Build.props" />
7-
86
<ItemGroup>
9-
<InternalsVisibleTo Include="CommunityToolkit.Datasync.Client.Test"/>
7+
<InternalsVisibleTo Include="CommunityToolkit.Datasync.Client.Test" />
108
</ItemGroup>
119

1210
<ItemGroup>

src/CommunityToolkit.Datasync.Client/DatasyncClient.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5-
#pragma warning disable IDE0058 // Expression value is never used
6-
75
using CommunityToolkit.Datasync.Client.Http;
86
using CommunityToolkit.Datasync.Common;
97
using System.Diagnostics.CodeAnalysis;
@@ -22,6 +20,7 @@ public class DatasyncClient : IDisposable
2220
protected DatasyncClient()
2321
{
2422
ClientOptions = new DatasyncClientOptions();
23+
ServiceOptions = new DatasyncServiceOptions();
2524
Endpoint = new Uri("http://localhost/");
2625
HttpClientFactory = new DefaultHttpClientFactory(Endpoint, new HttpClientOptions());
2726
}
@@ -62,13 +61,10 @@ public DatasyncClient(string endpoint, DatasyncClientOptions options) : this(new
6261
/// <exception cref="UriFormatException">if the endpoint is not a valid datasync Uri.</exception>
6362
public DatasyncClient(Uri endpoint, DatasyncClientOptions options)
6463
{
65-
Ensure.That(endpoint, nameof(endpoint)).IsNotNull().And.IsDatasyncEndpoint();
66-
Ensure.That(options, nameof(options)).IsNotNull();
67-
68-
ClientOptions = options;
64+
Endpoint = NormalizeEndpoint(Ensure.That(endpoint, nameof(endpoint)).IsNotNull().And.IsDatasyncEndpoint().Value);
65+
ClientOptions = Ensure.That(options, nameof(options)).IsNotNull().Value;
6966
ServiceOptions = options.DatasyncServiceOptions;
70-
Endpoint = NormalizeEndpoint(endpoint);
71-
HttpClientFactory = options.HttpClientFactory ?? new DefaultHttpClientFactory(endpoint, new HttpClientOptions());
67+
HttpClientFactory = options.HttpClientFactory ?? new DefaultHttpClientFactory(Endpoint, new HttpClientOptions());
7268
}
7369

7470
/// <summary>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Text.Json;
7+
8+
namespace CommunityToolkit.Datasync.Client;
9+
10+
/// <summary>
11+
/// An exception used by the Datasync client library to report server conflict errors.
12+
/// </summary>
13+
/// <typeparam name="T">The type of entity in conflict</typeparam>
14+
public class ConflictException<T> : DatasyncHttpException where T : notnull
15+
{
16+
/// <inheritdoc />
17+
public ConflictException(string? message) : base(message)
18+
{
19+
}
20+
21+
/// <inheritdoc />
22+
[ExcludeFromCodeCoverage(Justification = "Standard exception constructor")]
23+
public ConflictException(string? message, Exception? innerException) : base(message, innerException)
24+
{
25+
}
26+
27+
/// <summary>
28+
/// The parsed server-side entity (or null).
29+
/// </summary>
30+
public T? ServerEntity { get; set; } = default;
31+
32+
/// <summary>
33+
/// Creates a new <see cref="ConflictException{T}"/> for the response.
34+
/// </summary>
35+
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> that is creating the exception.</param>
36+
/// <param name="serializerOptions">The JSON serializer options to use for deserializing content.</param>
37+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
38+
/// <returns></returns>
39+
public static async Task<ConflictException<T>> CreateAsync(HttpResponseMessage responseMessage, JsonSerializerOptions serializerOptions, CancellationToken cancellationToken = default)
40+
{
41+
try
42+
{
43+
string mediaType = responseMessage.Content.Headers.ContentType?.MediaType ?? string.Empty;
44+
string content = await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
45+
return new ConflictException<T>(responseMessage.ReasonPhrase)
46+
{
47+
StatusCode = responseMessage.StatusCode,
48+
ContentType = mediaType,
49+
Payload = content,
50+
ServerEntity = JsonSerializer.Deserialize<T>(content, serializerOptions)
51+
};
52+
}
53+
catch (JsonException ex)
54+
{
55+
throw new DatasyncException("Invalid JSON content received from server", ex);
56+
}
57+
}
58+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Text.Json;
7+
8+
namespace CommunityToolkit.Datasync.Client;
9+
10+
/// <summary>
11+
/// A base exception for the Datasync client library.
12+
/// </summary>
13+
[ExcludeFromCodeCoverage(Justification = "Standard exception")]
14+
public class DatasyncException : Exception
15+
{
16+
/// <inheritdoc />
17+
public DatasyncException()
18+
{
19+
}
20+
21+
/// <inheritdoc />
22+
public DatasyncException(string? message) : base(message)
23+
{
24+
}
25+
26+
/// <inheritdoc />
27+
public DatasyncException(string? message, Exception? innerException) : base(message, innerException)
28+
{
29+
}
30+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Net;
7+
8+
namespace CommunityToolkit.Datasync.Client;
9+
10+
/// <summary>
11+
/// An exception used by the Datasync client library to report HTTP errors.
12+
/// </summary>
13+
public class DatasyncHttpException : DatasyncException
14+
{
15+
/// <inheritdoc />
16+
public DatasyncHttpException(string? message) : base(message)
17+
{
18+
}
19+
20+
/// <inheritdoc />
21+
[ExcludeFromCodeCoverage(Justification = "Standard exception constructor")]
22+
public DatasyncHttpException(string? message, Exception? innerException) : base(message, innerException)
23+
{
24+
}
25+
26+
/// <summary>
27+
/// The HTTP Status Code that was returned.
28+
/// </summary>
29+
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Ambiguous;
30+
31+
/// <summary>
32+
/// The MIME content type for the payload, or the empty string if none was found.
33+
/// </summary>
34+
public string ContentType { get; set; } = string.Empty;
35+
36+
/// <summary>
37+
/// The payload within the response, or the empty string if none was found.
38+
/// </summary>
39+
public string Payload { get; set; } = string.Empty;
40+
41+
/// <summary>
42+
/// Creates a new <see cref="DatasyncHttpException"/> for the response.
43+
/// </summary>
44+
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> that is creating the exception.</param>
45+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe.</param>
46+
/// <returns></returns>
47+
public static async Task<DatasyncHttpException> CreateAsync(HttpResponseMessage responseMessage, CancellationToken cancellationToken = default)
48+
{
49+
if (responseMessage.StatusCode == HttpStatusCode.NotFound)
50+
{
51+
throw new EntityNotFoundException();
52+
}
53+
54+
string mediaType = responseMessage.Content.Headers.ContentType?.MediaType ?? string.Empty;
55+
string content = await responseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
56+
return new DatasyncHttpException(responseMessage.ReasonPhrase)
57+
{
58+
StatusCode = responseMessage.StatusCode,
59+
ContentType = mediaType,
60+
Payload = content
61+
};
62+
}
63+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
7+
namespace CommunityToolkit.Datasync.Client;
8+
9+
/// <summary>
10+
/// An exception that indicates the requested entity does not exist.
11+
/// </summary>
12+
[ExcludeFromCodeCoverage(Justification = "Standard exception")]
13+
public class EntityNotFoundException : DatasyncException
14+
{
15+
/// <inheritdoc />
16+
public EntityNotFoundException()
17+
{
18+
}
19+
20+
/// <inheritdoc />
21+
public EntityNotFoundException(string? message) : base(message)
22+
{
23+
}
24+
25+
/// <inheritdoc />
26+
public EntityNotFoundException(string? message, Exception? innerException) : base(message, innerException)
27+
{
28+
}
29+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace CommunityToolkit.Datasync.Client.Models;
6+
7+
/// <summary>
8+
/// A collection of values that may take multiple service requests to iterate over.
9+
/// </summary>
10+
/// <typeparam name="T">The type of the entity being iterated over.</typeparam>
11+
/// <example>
12+
/// Example of enumerating an <see cref="AsyncPageable{T}"/> using the <c>async foreach</c> loop:
13+
/// <code snippet="Snippet:AsyncPageable">
14+
/// AsyncPageable&lt;Movie&gt; allMovies = dataset.QueryAsync();
15+
/// await foreach (Movie movie in allMovies)
16+
/// {
17+
/// ProcessMovie(movie);
18+
/// }
19+
/// </code>
20+
/// </example>
21+
public abstract class AsyncPageable<T> : IAsyncEnumerable<T> where T : notnull
22+
{
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="AsyncPageable{T}"/> class.
25+
/// </summary>
26+
/// <remarks>
27+
/// Used for unit test mocking.
28+
/// </remarks>
29+
protected AsyncPageable()
30+
{
31+
}
32+
33+
/// <summary>
34+
/// Enumerate the values a page of items at a time. This may take multiple service requests.
35+
/// </summary>
36+
/// <param name="continuationToken">A continuation token indicating where to resume paging, or <c>null</c> to begin paging from the beginning.</param>
37+
/// <returns>An async sequence of <see cref="Page{T}"/> entities</returns>
38+
public abstract IAsyncEnumerable<Page<T>> AsPages(string? continuationToken = default);
39+
40+
/// <summary>
41+
/// Enumerate the values in the collection asynchronously. This may make multiple service requests.
42+
/// </summary>
43+
/// <param name="token">A <see cref="CancellationToken"/> used for requests made while enumerating asynchronously.</param>
44+
/// <returns>An async sequence of values.</returns>
45+
public virtual async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token = default)
46+
{
47+
await foreach (Page<T> page in AsPages().ConfigureAwait(false).WithCancellation(token))
48+
{
49+
Count = page.Count;
50+
if (page.Items != null)
51+
{
52+
foreach (T value in page.Items)
53+
{
54+
yield return value;
55+
}
56+
}
57+
}
58+
}
59+
60+
/// <summary>
61+
/// The total number of items that would be returned by the query, if not for paging.
62+
/// This is populated only if the total count ($count=true) is requested on the query.
63+
/// </summary>
64+
public long? Count { get; private set; }
65+
}
66+
67+
/// <summary>
68+
/// Creates a new <see cref="AsyncPageable{T}"/> with a function iterator.
69+
/// </summary>
70+
/// <param name="pageFunc">The function that gets the next page</param>
71+
internal class FuncAsyncPageable<T>(Func<string?, Task<Page<T>>> pageFunc) : AsyncPageable<T> where T : notnull
72+
{
73+
/// <inheritdoc />
74+
public override async IAsyncEnumerable<Page<T>> AsPages(string? requestUri = default)
75+
{
76+
do
77+
{
78+
Page<T> pageResponse = await pageFunc(requestUri).ConfigureAwait(false);
79+
requestUri = pageResponse.NextLink?.ToString();
80+
yield return pageResponse ?? new Page<T>();
81+
} while (requestUri != null);
82+
}
83+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace CommunityToolkit.Datasync.Client.Models;
6+
7+
/// <summary>
8+
/// The model for the response from a query operation.
9+
/// </summary>
10+
public class Page<T> where T : notnull
11+
{
12+
/// <summary>
13+
/// The items in a page.
14+
/// </summary>
15+
public IEnumerable<T> Items { get; set; } = [];
16+
17+
/// <summary>
18+
/// The number of items that would be returned by the query,
19+
/// if not for paging.
20+
/// </summary>
21+
public long? Count { get; set; }
22+
23+
/// <summary>
24+
/// The Uri to the nexty page in the result set.
25+
/// </summary>
26+
public string NextLink { get; set; } = string.Empty;
27+
}

0 commit comments

Comments
 (0)