diff --git a/samples/datasync-server/Sample.Datasync.Server.sln b/samples/datasync-server/Sample.Datasync.Server.sln index 11ceb1d..bf831c0 100644 --- a/samples/datasync-server/Sample.Datasync.Server.sln +++ b/samples/datasync-server/Sample.Datasync.Server.sln @@ -9,11 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Datasync.Server", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "library", "library", "{95358590-6440-469A-8A6A-6ACC47F52966}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server", "..\..\..\..\CommunityToolkit\Datasync\src\CommunityToolkit.Datasync.Server\CommunityToolkit.Datasync.Server.csproj", "{D42DBA41-28AA-4B6D-83A4-A5B839EC8B25}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.Abstractions", "..\..\src\CommunityToolkit.Datasync.Server.Abstractions\CommunityToolkit.Datasync.Server.Abstractions.csproj", "{54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.Abstractions", "..\..\..\..\CommunityToolkit\Datasync\src\CommunityToolkit.Datasync.Server.Abstractions\CommunityToolkit.Datasync.Server.Abstractions.csproj", "{CECB1B21-46C7-4197-B8D2-EC54D0AF8145}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server", "..\..\src\CommunityToolkit.Datasync.Server\CommunityToolkit.Datasync.Server.csproj", "{DEC37ED1-B52A-4287-8F63-8210328AFF70}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.EntityFrameworkCore", "..\..\..\..\CommunityToolkit\Datasync\src\CommunityToolkit.Datasync.Server.EntityFrameworkCore\CommunityToolkit.Datasync.Server.EntityFrameworkCore.csproj", "{8CDD3736-4B8D-43F5-8F9A-222210D73DAC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Server.EntityFrameworkCore", "..\..\src\CommunityToolkit.Datasync.Server.EntityFrameworkCore\CommunityToolkit.Datasync.Server.EntityFrameworkCore.csproj", "{2086DD5C-C7C1-4957-B667-847C5FEE832C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -25,26 +25,26 @@ Global {67A76156-0033-4085-86BE-558DC28688A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {67A76156-0033-4085-86BE-558DC28688A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {67A76156-0033-4085-86BE-558DC28688A6}.Release|Any CPU.Build.0 = Release|Any CPU - {D42DBA41-28AA-4B6D-83A4-A5B839EC8B25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D42DBA41-28AA-4B6D-83A4-A5B839EC8B25}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D42DBA41-28AA-4B6D-83A4-A5B839EC8B25}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D42DBA41-28AA-4B6D-83A4-A5B839EC8B25}.Release|Any CPU.Build.0 = Release|Any CPU - {CECB1B21-46C7-4197-B8D2-EC54D0AF8145}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CECB1B21-46C7-4197-B8D2-EC54D0AF8145}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CECB1B21-46C7-4197-B8D2-EC54D0AF8145}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CECB1B21-46C7-4197-B8D2-EC54D0AF8145}.Release|Any CPU.Build.0 = Release|Any CPU - {8CDD3736-4B8D-43F5-8F9A-222210D73DAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CDD3736-4B8D-43F5-8F9A-222210D73DAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CDD3736-4B8D-43F5-8F9A-222210D73DAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CDD3736-4B8D-43F5-8F9A-222210D73DAC}.Release|Any CPU.Build.0 = Release|Any CPU + {54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6}.Release|Any CPU.Build.0 = Release|Any CPU + {DEC37ED1-B52A-4287-8F63-8210328AFF70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEC37ED1-B52A-4287-8F63-8210328AFF70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEC37ED1-B52A-4287-8F63-8210328AFF70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEC37ED1-B52A-4287-8F63-8210328AFF70}.Release|Any CPU.Build.0 = Release|Any CPU + {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2086DD5C-C7C1-4957-B667-847C5FEE832C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {67A76156-0033-4085-86BE-558DC28688A6} = {4B8E4DD5-C2CA-4729-A10B-8A3C993018EC} - {D42DBA41-28AA-4B6D-83A4-A5B839EC8B25} = {95358590-6440-469A-8A6A-6ACC47F52966} - {CECB1B21-46C7-4197-B8D2-EC54D0AF8145} = {95358590-6440-469A-8A6A-6ACC47F52966} - {8CDD3736-4B8D-43F5-8F9A-222210D73DAC} = {95358590-6440-469A-8A6A-6ACC47F52966} + {54E9A0B2-A0B0-4CB1-8FAD-11DB9E4535A6} = {95358590-6440-469A-8A6A-6ACC47F52966} + {DEC37ED1-B52A-4287-8F63-8210328AFF70} = {95358590-6440-469A-8A6A-6ACC47F52966} + {2086DD5C-C7C1-4957-B667-847C5FEE832C} = {95358590-6440-469A-8A6A-6ACC47F52966} EndGlobalSection EndGlobal diff --git a/samples/datasync-server/src/Sample.Datasync.Server/Db/TodoItem.cs b/samples/datasync-server/src/Sample.Datasync.Server/Db/TodoItem.cs index afd02d6..c320645 100644 --- a/samples/datasync-server/src/Sample.Datasync.Server/Db/TodoItem.cs +++ b/samples/datasync-server/src/Sample.Datasync.Server/Db/TodoItem.cs @@ -10,7 +10,7 @@ namespace Sample.Datasync.Server.Db; public class TodoItem : EntityTableData { [Required, MinLength(1)] - public string Text { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; public bool IsComplete { get; set; } } diff --git a/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj b/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj index 5d9fbd5..8287f2b 100644 --- a/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj +++ b/samples/datasync-server/src/Sample.Datasync.Server/Sample.Datasync.Server.csproj @@ -22,9 +22,9 @@ - - - + + + diff --git a/samples/todoapp/Samples.TodoApp.sln b/samples/todoapp/Samples.TodoApp.sln index 3762275..5692d55 100644 --- a/samples/todoapp/Samples.TodoApp.sln +++ b/samples/todoapp/Samples.TodoApp.sln @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.WinUI3", "TodoApp.W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Datasync.Client", "..\..\src\CommunityToolkit.Datasync.Client\CommunityToolkit.Datasync.Client.csproj", "{2AC73FBE-9E76-4702-B551-B5884383CC68}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApp.WPF", "TodoApp.WPF\TodoApp.WPF.csproj", "{410D4BBD-5ED7-4BC0-A2CF-547A4784732F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Datasync.Server", "..\datasync-server\src\Sample.Datasync.Server\Sample.Datasync.Server.csproj", "{E67734DD-B397-4A65-AA50-D62F37EF05DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +65,38 @@ Global {2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x64.Build.0 = Release|Any CPU {2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x86.ActiveCfg = Release|Any CPU {2AC73FBE-9E76-4702-B551-B5884383CC68}.Release|x86.Build.0 = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|ARM64.Build.0 = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x64.ActiveCfg = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x64.Build.0 = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x86.ActiveCfg = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Debug|x86.Build.0 = Debug|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|Any CPU.Build.0 = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|ARM64.ActiveCfg = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|ARM64.Build.0 = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x64.ActiveCfg = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x64.Build.0 = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x86.ActiveCfg = Release|Any CPU + {410D4BBD-5ED7-4BC0-A2CF-547A4784732F}.Release|x86.Build.0 = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|ARM64.Build.0 = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|x64.Build.0 = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Debug|x86.Build.0 = Debug|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|Any CPU.Build.0 = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|ARM64.ActiveCfg = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|ARM64.Build.0 = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|x64.ActiveCfg = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|x64.Build.0 = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|x86.ActiveCfg = Release|Any CPU + {E67734DD-B397-4A65-AA50-D62F37EF05DD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/todoapp/TodoApp.WPF/App.xaml b/samples/todoapp/TodoApp.WPF/App.xaml new file mode 100644 index 0000000..b4f4840 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/samples/todoapp/TodoApp.WPF/App.xaml.cs b/samples/todoapp/TodoApp.WPF/App.xaml.cs new file mode 100644 index 0000000..3bec578 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/App.xaml.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System.Windows; +using TodoApp.WPF.Database; +using TodoApp.WPF.ViewModels; + +namespace TodoApp.WPF; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application, IDisposable +{ + private readonly SqliteConnection dbConnection; + + /// + /// The IoC service provider + /// + public IServiceProvider Services { get; } + + public App() + { + // Create the connection to the SQLite database + this.dbConnection = new SqliteConnection("Data Source=:memory:"); + this.dbConnection.Open(); + + // Create the IoC Services provider. + Services = new ServiceCollection() + .AddTransient() + .AddScoped() + .AddDbContext(options => options.UseSqlite(this.dbConnection)) + .BuildServiceProvider(); + + // Initialize the database + InitializeDatabase(); + } + + private void InitializeDatabase() + { + // using IServiceScope scope = Ioc.Default.CreateScope(); + using IServiceScope scope = Services.CreateScope(); + IDbInitializer initializer = scope.ServiceProvider.GetRequiredService(); + initializer.Initialize(); + } + + /// + /// A helper method for getting a service from the services collection. + /// + /// + /// You can see this in action in the class. + /// + /// The type of the service. + /// An instance of the service + public static TService GetRequiredService() where TService : notnull + => ((App)App.Current).Services.GetRequiredService(); + + #region IDisposable + private bool hasDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!this.hasDisposed) + { + if (disposing) + { + this.dbConnection.Close(); + } + + this.hasDisposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion +} + diff --git a/samples/todoapp/TodoApp.WPF/AssemblyInfo.cs b/samples/todoapp/TodoApp.WPF/AssemblyInfo.cs new file mode 100644 index 0000000..76e9b08 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/AssemblyInfo.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs b/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs new file mode 100644 index 0000000..dfd5c46 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/Database/AppDbContext.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Client.Http; +using CommunityToolkit.Datasync.Client.Offline; +using Microsoft.EntityFrameworkCore; +using TodoApp.WPF.Services; + +namespace TodoApp.WPF.Database; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet TodoItems => Set(); + + // protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) + // { + // HttpClientOptions clientOptions = new() + // { + // Endpoint = new Uri("https://Y.azurewebsites.net/"), + // HttpPipeline = [new LoggingHandler()] + // }; + // _ = optionsBuilder.UseHttpClientOptions(clientOptions); + // } + + public async Task SynchronizeAsync(CancellationToken cancellationToken = default) + { + // PushResult pushResult = await this.PushAsync(cancellationToken); + // if (!pushResult.IsSuccessful) + // { + // throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + // } + + // PullResult pullResult = await this.PullAsync(cancellationToken); + // if (!pullResult.IsSuccessful) + // { + // throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + // } + } +} + +/// +/// Use this class to initialize the database. In this sample, we just create +/// the database using . However, you +/// may want to use migrations. +/// +/// The context for the database. +public class DbContextInitializer(AppDbContext context) : IDbInitializer +{ + /// + public void Initialize() + => context.Database.EnsureCreated(); + + /// + public Task InitializeAsync(CancellationToken cancellationToken = default) + => context.Database.EnsureCreatedAsync(cancellationToken); +} diff --git a/samples/todoapp/TodoApp.WPF/Database/IDbInitializer.cs b/samples/todoapp/TodoApp.WPF/Database/IDbInitializer.cs new file mode 100644 index 0000000..5be08ff --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/Database/IDbInitializer.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace TodoApp.WPF.Database; + +/// +/// An interface to initialize a database. +/// +public interface IDbInitializer +{ + /// + /// Synchronously initialize the database. + /// + void Initialize(); + + /// + /// Asynchronously initialize the database. + /// + /// A to observe. + /// A task that resolves when complete. + Task InitializeAsync(CancellationToken cancellationToken = default); +} diff --git a/samples/todoapp/TodoApp.WPF/Database/OfflineClientEntity.cs b/samples/todoapp/TodoApp.WPF/Database/OfflineClientEntity.cs new file mode 100644 index 0000000..bcf2223 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/Database/OfflineClientEntity.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel.DataAnnotations; + +namespace TodoApp.WPF.Database; + +/// +/// An abstract class for working with offline entities. +/// +public abstract class OfflineClientEntity +{ + [Key] + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public DateTimeOffset? UpdatedAt { get; set; } + public string? Version { get; set; } + public bool Deleted { get; set; } +} diff --git a/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs b/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs new file mode 100644 index 0000000..1205316 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/Database/TodoItem.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; + +namespace TodoApp.WPF.Database; + +public class TodoItem : OfflineClientEntity +{ + public string Title { get; set; } = string.Empty; + public bool IsComplete { get; set; } = false; + + public override string ToString() + => JsonSerializer.Serialize(this); +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.WPF/Images/AddItem.png b/samples/todoapp/TodoApp.WPF/Images/AddItem.png new file mode 100644 index 0000000..3366479 Binary files /dev/null and b/samples/todoapp/TodoApp.WPF/Images/AddItem.png differ diff --git a/samples/todoapp/TodoApp.WPF/Images/RefreshItems.png b/samples/todoapp/TodoApp.WPF/Images/RefreshItems.png new file mode 100644 index 0000000..51748ed Binary files /dev/null and b/samples/todoapp/TodoApp.WPF/Images/RefreshItems.png differ diff --git a/samples/todoapp/TodoApp.WPF/MainWindow.xaml b/samples/todoapp/TodoApp.WPF/MainWindow.xaml new file mode 100644 index 0000000..cfcdeca --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/MainWindow.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.WPF/MainWindow.xaml.cs b/samples/todoapp/TodoApp.WPF/MainWindow.xaml.cs new file mode 100644 index 0000000..b60bdbf --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/MainWindow.xaml.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Windows; +using System.Windows.Input; +using TodoApp.WPF.ViewModels; + +namespace TodoApp.WPF; +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + DataContext = App.GetRequiredService(); + } + + protected async void TextboxKeyDownHandler(object sender, KeyEventArgs e) + { + if (e.Key is Key.Return or Key.Enter) + { + await ((TodoListViewModel)DataContext).AddItemAsync(); + } + } +} \ No newline at end of file diff --git a/samples/todoapp/TodoApp.WPF/Services/LoggingHandler.cs b/samples/todoapp/TodoApp.WPF/Services/LoggingHandler.cs new file mode 100644 index 0000000..c9c40bb --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/Services/LoggingHandler.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Net.Http; + +namespace TodoApp.WPF.Services; + +/// +/// A delegating handler that logs the request/response to stdout. +/// +public class LoggingHandler : DelegatingHandler +{ + public LoggingHandler() : base() + { + } + + public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Debug.WriteLine($"[HTTP] >>> {request.Method} {request.RequestUri}"); + await WriteContentAsync(request.Content, cancellationToken); + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}"); + await WriteContentAsync(response.Content, cancellationToken); + + return response; + } + + private static async Task WriteContentAsync(HttpContent? content, CancellationToken cancellationToken = default) + { + if (content != null) + { + Debug.WriteLine($"[HTTP] >>> {await content.ReadAsStringAsync(cancellationToken)}"); + } + } +} diff --git a/samples/todoapp/TodoApp.WPF/TodoApp.WPF.csproj b/samples/todoapp/TodoApp.WPF/TodoApp.WPF.csproj new file mode 100644 index 0000000..18a5930 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/TodoApp.WPF.csproj @@ -0,0 +1,31 @@ + + + + WinExe + net8.0-windows + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/todoapp/TodoApp.WPF/ViewModels/NotificationEventArgs.cs b/samples/todoapp/TodoApp.WPF/ViewModels/NotificationEventArgs.cs new file mode 100644 index 0000000..6d1f93d --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/ViewModels/NotificationEventArgs.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace TodoApp.WPF.ViewModels; + +internal record NotificationEventArgs(string Title, string Message, bool IsError) +{ +} diff --git a/samples/todoapp/TodoApp.WPF/ViewModels/TodoListViewModel.cs b/samples/todoapp/TodoApp.WPF/ViewModels/TodoListViewModel.cs new file mode 100644 index 0000000..1acb756 --- /dev/null +++ b/samples/todoapp/TodoApp.WPF/ViewModels/TodoListViewModel.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Client; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.EntityFrameworkCore; +using TodoApp.WPF.Database; + +namespace TodoApp.WPF.ViewModels; + +/// +/// The view model for the TodoListWindow. +/// +public partial class TodoListViewModel(AppDbContext service) : ObservableRecipient +{ + internal event EventHandler? NotificationHandler; + + [ObservableProperty] + private bool isRefreshing = false; + + [ObservableProperty] + private ConcurrentObservableCollection items = new([.. service.TodoItems]); + + [ObservableProperty] + private string title = string.Empty; + + [RelayCommand] + public async Task AddItemAsync(CancellationToken cancellationToken = default) + { + try + { + // Create a new item + TodoItem addition = new() + { + Id = Guid.NewGuid().ToString("N"), + Title = Title + }; + + // Add te item to the database + _ = service.TodoItems.Add(addition); + _ = await service.SaveChangesAsync(cancellationToken); + + // Add the item to the end of the list. + Items.Add(addition); + + // Update the title field ready for ext insertion. + Title = string.Empty; + } + catch (Exception ex) + { + NotificationHandler?.Invoke(this, new NotificationEventArgs(ex.GetType().Name, ex.Message, true)); + } + finally + { + NotificationHandler?.Invoke(this, new NotificationEventArgs("Item Added", "", false)); + } + } + + [RelayCommand] + public async Task EditItemAsync(string itemId, CancellationToken cancellationToken = default) + { + try + { + // Retrieve the item (by ID) from the service. + TodoItem item = await service.TodoItems.FindAsync([itemId], cancellationToken) + ?? throw new ApplicationException($"Item with ID '{itemId}' not found."); + + // Update the item in the database + item.IsComplete = !item.IsComplete; + _ = service.TodoItems.Update(item); + _ = await service.SaveChangesAsync(cancellationToken); + + // Update the item in the list + _ = Items.ReplaceIf(x => x.Id == itemId, item); + } + catch (Exception ex) + { + NotificationHandler?.Invoke(this, new NotificationEventArgs(ex.GetType().Name, ex.Message, true)); + } + finally + { + NotificationHandler?.Invoke(this, new NotificationEventArgs("Item Updated", "", false)); + } + } + + [RelayCommand] + public async Task LoadPageAsync(CancellationToken cancellationToken = default) + { + await RefreshItemsAsync(cancellationToken); + } + + [RelayCommand] + public async Task RefreshItemsAsync(CancellationToken cancellationToken = default) + { + try + { + IsRefreshing = true; + + // Synchronize data with the remote service (if any). + await service.SynchronizeAsync(cancellationToken); + + // Pull all items from the database. + IEnumerable itemsFromDatabase = await service.TodoItems.ToListAsync(cancellationToken); + + // Replace all the items in the collection. + Items.ReplaceAll(itemsFromDatabase); + } + catch (Exception ex) + { + NotificationHandler?.Invoke(this, new NotificationEventArgs(ex.GetType().Name, ex.Message, true)); + } + finally + { + IsRefreshing = false; + NotificationHandler?.Invoke(this, new NotificationEventArgs("Items Refreshed", "", false)); + } + } +} diff --git a/samples/todoapp/TodoApp.WinUI3/App.xaml.cs b/samples/todoapp/TodoApp.WinUI3/App.xaml.cs index 819c2a0..ee40d03 100644 --- a/samples/todoapp/TodoApp.WinUI3/App.xaml.cs +++ b/samples/todoapp/TodoApp.WinUI3/App.xaml.cs @@ -21,6 +21,8 @@ public partial class App : Application, IDisposable private Frame m_frame; private readonly SqliteConnection dbConnection; + public IServiceProvider Services { get; } + public App() { InitializeComponent(); @@ -28,19 +30,23 @@ public App() this.dbConnection = new SqliteConnection("Data Source=:memory:"); this.dbConnection.Open(); - IServiceCollection services = new ServiceCollection() + // Create the IoC Services provider. + Services = new ServiceCollection() .AddTransient() .AddScoped() - .AddDbContext(options => options.UseSqlite(this.dbConnection)); + .AddDbContext(options => options.UseSqlite(this.dbConnection)) + .BuildServiceProvider(); - Ioc.Default.ConfigureServices(services.BuildServiceProvider()); + // Initialize the database using the registered database initializer. + InitializeDatabase(); + } - // Initialize the database - using (IServiceScope scope = Ioc.Default.CreateScope()) - { - IDbInitializer initializer = scope.ServiceProvider.GetRequiredService(); - initializer.Initialize(); - } + private void InitializeDatabase() + { + // using IServiceScope scope = Ioc.Default.CreateScope(); + using IServiceScope scope = Services.CreateScope(); + IDbInitializer initializer = scope.ServiceProvider.GetRequiredService(); + initializer.Initialize(); } protected override void OnLaunched(LaunchActivatedEventArgs args) @@ -52,6 +58,17 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) _ = this.m_frame.Navigate(typeof(TodoListPage)); } + /// + /// A helper method for getting a service from the services collection. + /// + /// + /// You can see this in action in the class. + /// + /// The type of the service. + /// An instance of the service + public static TService GetRequiredService() + => ((App)App.Current).Services.GetRequiredService(); + #region IDisposable private bool hasDisposed; diff --git a/samples/todoapp/TodoApp.WinUI3/Database/AppDbContext.cs b/samples/todoapp/TodoApp.WinUI3/Database/AppDbContext.cs index a1faf2d..98f9faa 100644 --- a/samples/todoapp/TodoApp.WinUI3/Database/AppDbContext.cs +++ b/samples/todoapp/TodoApp.WinUI3/Database/AppDbContext.cs @@ -2,11 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using CommunityToolkit.Datasync.Client.Http; using CommunityToolkit.Datasync.Client.Offline; using Microsoft.EntityFrameworkCore; using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using TodoApp.WinUI3.Services; namespace TodoApp.WinUI3.Database; @@ -16,8 +19,28 @@ public class AppDbContext(DbContextOptions options) : DbContext(op //protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) //{ - // _ = optionsBuilder.UseEndpoint(new Uri("https://myservice.azurewebsites.net")); + // HttpClientOptions clientOptions = new() + // { + // Endpoint = new Uri("https://YOURSITEHERE.azurewebsites.net/"), + // HttpPipeline = [new LoggingHandler()] + // }; + // _ = optionsBuilder.UseHttpClientOptions(clientOptions); //} + + public async Task SynchronizeAsync(CancellationToken cancellationToken = default) + { + //PushResult pushResult = await this.PushAsync(cancellationToken); + //if (!pushResult.IsSuccessful) + //{ + // throw new ApplicationException($"Push failed: {pushResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + //} + + //PullResult pullResult = await this.PullAsync(cancellationToken); + //if (!pullResult.IsSuccessful) + //{ + // throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); + //} + } } /// diff --git a/samples/todoapp/TodoApp.WinUI3/Services/LoggingHandler.cs b/samples/todoapp/TodoApp.WinUI3/Services/LoggingHandler.cs new file mode 100644 index 0000000..3cfab74 --- /dev/null +++ b/samples/todoapp/TodoApp.WinUI3/Services/LoggingHandler.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace TodoApp.WinUI3.Services; + +/// +/// A delegating handler that logs the request/response to stdout. +/// +public class LoggingHandler : DelegatingHandler +{ + public LoggingHandler() : base() + { + } + + public LoggingHandler(HttpMessageHandler innerHandler) : base(innerHandler) + { + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Debug.WriteLine($"[HTTP] >>> {request.Method} {request.RequestUri}"); + await WriteContentAsync(request.Content, cancellationToken); + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + Debug.WriteLine($"[HTTP] <<< {response.StatusCode} {response.ReasonPhrase}"); + await WriteContentAsync(response.Content, cancellationToken); + + return response; + } + + private static async Task WriteContentAsync(HttpContent content, CancellationToken cancellationToken = default) + { + if (content != null) + { + Debug.WriteLine($"[HTTP] >>> {await content.ReadAsStringAsync(cancellationToken)}"); + } + } +} diff --git a/samples/todoapp/TodoApp.WinUI3/ViewModels/TodoListViewModel.cs b/samples/todoapp/TodoApp.WinUI3/ViewModels/TodoListViewModel.cs index f3cded3..af12185 100644 --- a/samples/todoapp/TodoApp.WinUI3/ViewModels/TodoListViewModel.cs +++ b/samples/todoapp/TodoApp.WinUI3/ViewModels/TodoListViewModel.cs @@ -3,13 +3,11 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Datasync.Client; -using CommunityToolkit.Datasync.Client.Offline; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using TodoApp.WinUI3.Database; @@ -104,21 +102,13 @@ public async Task RefreshItemsAsync(CancellationToken cancellationToken = defaul { IsRefreshing = true; - //PushResult pushResult = await service.PushAsync(cancellationToken); - //if (pushResult.IsSuccessful) - //{ - // PullResult pullResult = await service.PullAsync(cancellationToken); - // if (!pullResult.IsSuccessful) - // { - // throw new ApplicationException($"Pull failed: {pullResult.FailedRequests.FirstOrDefault().Value.ReasonPhrase}"); - // } - //} - //else - //{ - // throw new ApplicationException($"Push failed: {pushResult.FailedOperations.FirstOrDefault().Value.ReasonPhrase}"); - //} + // Synchronize data with the remote service (if any). + await service.SynchronizeAsync(cancellationToken); + // Pull all items from the database. IEnumerable itemsFromDatabase = await service.TodoItems.ToListAsync(cancellationToken); + + // Replace all the items in the collection. Items.ReplaceAll(itemsFromDatabase); } catch (Exception ex) diff --git a/samples/todoapp/TodoApp.WinUI3/Views/TodoListPage.xaml.cs b/samples/todoapp/TodoApp.WinUI3/Views/TodoListPage.xaml.cs index 23eecf2..9a64d1e 100644 --- a/samples/todoapp/TodoApp.WinUI3/Views/TodoListPage.xaml.cs +++ b/samples/todoapp/TodoApp.WinUI3/Views/TodoListPage.xaml.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.WinUI.Behaviors; using Microsoft.UI.Xaml.Controls; using System; @@ -15,7 +14,7 @@ public sealed partial class TodoListPage : Page public TodoListPage() { InitializeComponent(); - DataContext = Ioc.Default.GetRequiredService(); + DataContext = App.GetRequiredService(); ViewModel.NotificationHandler += PublishNotification; } diff --git a/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/PushResult.cs b/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/PushResult.cs index bc4f628..f8b9844 100644 --- a/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/PushResult.cs +++ b/src/CommunityToolkit.Datasync.Client/Offline/OperationsQueue/PushResult.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Datasync.Client.Offline; public class PushResult { internal int _completedOperations = 0; - internal readonly ConcurrentDictionary _failedOperations = new(); + internal readonly ConcurrentDictionary _failedRequests = new(); /// /// The number of completed operations. @@ -23,12 +23,12 @@ public class PushResult /// /// The number of failed operations. /// - public IReadOnlyDictionary FailedOperations { get => this._failedOperations.ToImmutableDictionary(); } + public IReadOnlyDictionary FailedRequests { get => this._failedRequests.ToImmutableDictionary(); } /// /// Determines if the operation was successful. /// - public bool IsSuccessful { get => this._failedOperations.IsEmpty; } + public bool IsSuccessful { get => this._failedRequests.IsEmpty; } /// /// Adds an operation result in a thread safe manner. @@ -43,7 +43,7 @@ internal void AddOperationResult(DatasyncOperation operation, ServiceResponse? r } else { - _ = this._failedOperations.TryAdd(operation.Id, response); + _ = this._failedRequests.TryAdd(operation.Id, response); } } } diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/OfflineDbContext_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/OfflineDbContext_Tests.cs index 0a4cc58..9876b51 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Offline/OfflineDbContext_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/OfflineDbContext_Tests.cs @@ -496,7 +496,7 @@ public async Task PushAsync_NoSynchronizableEntities(int nItems) PushResult result = await this.context.PushAsync(entityTypes, options); result.CompletedOperations.Should().Be(0); - result.FailedOperations.Count.Should().Be(0); + result.FailedRequests.Count.Should().Be(0); } [Fact] @@ -507,7 +507,7 @@ public async Task PushAsync_NoOperations() PushResult result = await this.context.PushAsync(entityTypes, options); result.CompletedOperations.Should().Be(0); - result.FailedOperations.Count.Should().Be(0); + result.FailedRequests.Count.Should().Be(0); } [Fact] @@ -517,7 +517,7 @@ public async Task DbSet_PushAsync_NoOperations() PushResult result = await this.context.Movies.PushAsync(options); result.CompletedOperations.Should().Be(0); - result.FailedOperations.Count.Should().Be(0); + result.FailedRequests.Count.Should().Be(0); } [Fact] @@ -535,7 +535,7 @@ public async void PushAsync_Addition_Works() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); @@ -559,7 +559,7 @@ public async void DbSet_PushAsync_Addition_Works() PushResult results = await this.context.Movies.PushAsync(); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); @@ -580,8 +580,8 @@ public async Task PushAsync_Addition_HttpError() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(500); result.HasContent.Should().BeFalse(); @@ -605,8 +605,8 @@ public async Task PushAsync_Addition_Conflict() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(409); result.HasContent.Should().BeTrue(); string content = new StreamReader(result.ContentStream).ReadToEnd(); @@ -632,7 +632,7 @@ public async Task PushAsync_Removal_Works() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); this.context.Movies.Find(clientMovie.Id).Should().BeNull(); @@ -652,7 +652,7 @@ public async Task DbSet_PushAsync_Removal_Works() PushResult results = await this.context.Movies.PushAsync(); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); this.context.Movies.Find(clientMovie.Id).Should().BeNull(); @@ -672,8 +672,8 @@ public async Task PushAsync_Removal_HttpError() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(500); result.HasContent.Should().BeFalse(); @@ -700,8 +700,8 @@ public async Task PushAsync_Removal_Conflict() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(409); result.HasContent.Should().BeTrue(); string content = new StreamReader(result.ContentStream).ReadToEnd(); @@ -731,7 +731,7 @@ public async Task PushAsync_Replacement_Works() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); } @@ -754,7 +754,7 @@ public async Task DbSet_PushAsync_Replacement_Works() PushResult results = await this.context.Movies.PushAsync(); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); } @@ -774,8 +774,8 @@ public async Task PushAsync_Replacement_HttpError() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(500); result.HasContent.Should().BeFalse(); @@ -803,8 +803,8 @@ public async Task PushAsync_Replacement_Conflict() PushResult results = await this.context.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(409); result.HasContent.Should().BeTrue(); string content = new StreamReader(result.ContentStream).ReadToEnd(); diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs index 524b6b8..095f142 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/OperationsQueueManager_Tests.cs @@ -108,7 +108,7 @@ public async Task PushAsync_NoSynchronizableEntities(int nItems) PushResult result = await queueManager.PushAsync(entityTypes, options); result.CompletedOperations.Should().Be(0); - result.FailedOperations.Count.Should().Be(0); + result.FailedRequests.Count.Should().Be(0); } [Fact] @@ -119,7 +119,7 @@ public async Task PushAsync_NoOperations() PushResult result = await queueManager.PushAsync(entityTypes, options); result.CompletedOperations.Should().Be(0); - result.FailedOperations.Count.Should().Be(0); + result.FailedRequests.Count.Should().Be(0); } [Fact] @@ -137,7 +137,7 @@ public async void PushAsync_Addition_Works() PushResult results = await queueManager.PushAsync([ typeof(ClientMovie) ], new PushOptions()); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); @@ -158,8 +158,8 @@ public async Task PushAsync_Addition_HttpError() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(500); result.HasContent.Should().BeFalse(); @@ -183,8 +183,8 @@ public async Task PushAsync_Addition_Conflict() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(409); result.HasContent.Should().BeTrue(); string content = new StreamReader(result.ContentStream).ReadToEnd(); @@ -210,7 +210,7 @@ public async Task PushAsync_Removal_Works() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); this.context.Movies.Find(clientMovie.Id).Should().BeNull(); @@ -230,8 +230,8 @@ public async Task PushAsync_Removal_HttpError() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(500); result.HasContent.Should().BeFalse(); @@ -258,8 +258,8 @@ public async Task PushAsync_Removal_Conflict() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(409); result.HasContent.Should().BeTrue(); string content = new StreamReader(result.ContentStream).ReadToEnd(); @@ -289,7 +289,7 @@ public async Task PushAsync_Replacement_Works() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeTrue(); results.CompletedOperations.Should().Be(1); - results.FailedOperations.Should().BeEmpty(); + results.FailedRequests.Should().BeEmpty(); this.context.DatasyncOperationsQueue.Should().BeEmpty(); } @@ -309,8 +309,8 @@ public async Task PushAsync_Replacement_HttpError() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(500); result.HasContent.Should().BeFalse(); @@ -338,8 +338,8 @@ public async Task PushAsync_Replacement_Conflict() PushResult results = await queueManager.PushAsync([typeof(ClientMovie)], new PushOptions()); results.IsSuccessful.Should().BeFalse(); results.CompletedOperations.Should().Be(0); - results.FailedOperations.Should().HaveCount(1); - ServiceResponse result = results.FailedOperations.First().Value; + results.FailedRequests.Should().HaveCount(1); + ServiceResponse result = results.FailedRequests.First().Value; result.StatusCode.Should().Be(409); result.HasContent.Should().BeTrue(); string content = new StreamReader(result.ContentStream).ReadToEnd();