From 1287c9b1e20dd97299f27dee1787951024d141a2 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Wed, 16 Jul 2025 13:33:38 -0700 Subject: [PATCH 1/3] (#330) Blazor WASM Updates --- .github/prompts/wasm-sample.prompt.md | 9 + docs/in-depth/client/advanced/blazor-wasm.md | 35 ++ .../TodoApp.BlazorWasm.Client/App.razor | 12 + .../Pages/TodoList.razor | 259 ++++++++++++ .../TodoApp.BlazorWasm.Client/Program.cs | 33 ++ .../Properties/launchSettings.json | 12 + .../Services/ITodoService.cs | 197 +++++++++ .../Services/TodoService.cs | 207 ++++++++++ .../Shared/MainLayout.razor | 9 + .../TodoApp.BlazorWasm.Client.csproj | 20 + .../TodoApp.BlazorWasm.Client/_Imports.razor | 12 + .../wwwroot/css/app.css | 51 +++ .../wwwroot/css/todomvc.css | 388 ++++++++++++++++++ .../wwwroot/favicon.png | Bin 0 -> 29242 bytes .../wwwroot/index.html | 33 ++ .../Controllers/TodoItemsController.cs | 74 ++++ .../Database/TodoContext.cs | 102 +++++ .../Database/TodoItem.cs | 79 ++++ .../TodoApp.BlazorWasm.Server/Program.cs | 58 +++ .../Properties/launchSettings.json | 40 ++ .../TodoApp.BlazorWasm.Server.csproj | 22 + .../appsettings.Development.json | 9 + .../appsettings.json | 9 + .../Models/DatasyncDto.cs | 79 ++++ .../Models/TodoItemDto.cs | 36 ++ .../TodoApp.BlazorWasm.Shared.csproj | 9 + .../TodoApp.BlazorWasm.sln | 30 ++ 27 files changed, 1824 insertions(+) create mode 100644 .github/prompts/wasm-sample.prompt.md create mode 100644 docs/in-depth/client/advanced/blazor-wasm.md create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/App.razor create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Pages/TodoList.razor create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Program.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Properties/launchSettings.json create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/ITodoService.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/TodoService.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Shared/MainLayout.razor create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/TodoApp.BlazorWasm.Client.csproj create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/_Imports.razor create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/app.css create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/todomvc.css create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/favicon.png create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/index.html create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Controllers/TodoItemsController.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoContext.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoItem.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Program.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Properties/launchSettings.json create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/TodoApp.BlazorWasm.Server.csproj create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.Development.json create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.json create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/DatasyncDto.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/TodoItemDto.cs create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/TodoApp.BlazorWasm.Shared.csproj create mode 100644 samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.sln diff --git a/.github/prompts/wasm-sample.prompt.md b/.github/prompts/wasm-sample.prompt.md new file mode 100644 index 00000000..94ec83f2 --- /dev/null +++ b/.github/prompts/wasm-sample.prompt.md @@ -0,0 +1,9 @@ +This repository contains an open source project for accessing databases through a client-server relationship with LINQ. You can find many samples in the samples directory, but the main one is todoapp, which contains samples for Avalonia, MAUI, Uno, WinUI3, and WPF. There is also a todoapp-mvc that encapsulates a service and client using JavaScript. + +Project Layout: + +- `docs` contains full documentation for the library. `docs/in-depth/client/online.md` is a good resource for online client operations. +- `samples` contains a set of samples. +- `src` and `test` contain the source code and tests for the libraries we distribute. + +Your job is to create a working sample for Blazor WASM. It should implement a TodoList type application (common with the other samples), encapsulated in a server (see todoapp-mvc for an example server implementation). The solution should be placed in `samples/todoapp-blazor-wasm` and should be runnable with F5-Run. Use an in-memory EF Core store for storing the data (so we aren't reliant on an external database service). diff --git a/docs/in-depth/client/advanced/blazor-wasm.md b/docs/in-depth/client/advanced/blazor-wasm.md new file mode 100644 index 00000000..b0111d1f --- /dev/null +++ b/docs/in-depth/client/advanced/blazor-wasm.md @@ -0,0 +1,35 @@ +# Blazor WASM Support + +You can use Blazor WASM with the Datasync Community Toolkit; however, there are some signficant problems: + +1. In the general case, you cannot use SQLite for offline storage. +2. You will have to suppress the `WASM0001` warning for sqlite. + +## Do not use offline mode + +It is possible to use offline mode by storing the SQLite database in local storage within the browser. When you start up your application, you restore the SQLite in-memory database into memory; on a regular basis, you write the SQLite database to local storage. However, this system severely affects performance within the browser and large synchronizations will cause the local storage to overflow. As a result of these problems, it is not recommended, nor is it supported. + +## Suppress the `WASM0001` warning + +When compiling the WASM client, you will see warning `WASM0001`. This is a harmless warning that indicates SQLite is not available. However, you may be running using "Warnings as Errors". The most appropriate solution is to suppress the `WASM0001` warning in your client `.csproj` file as follows: + +```xml + + $(NoWarn);WASM0001 + +``` + +Alternatively, you may also use the following to keep `WASM0001` as a warning even when using "Warnings as Errors": + +```xml + + $(WarningsNotAsErrors);WASM0001 + +``` + +This will suppress the harmless SQLite warning that appears when building Blazor WASM applications that reference libraries containing SQLite dependencies (even when not using SQLite directly). + +## Sample + +We have a Blazor WASM sample in our sample set: [samples/todoapp-blazor-wasm](https://github.com/CommunityToolkit/Datasync/tree/main/samples/todoapp-blazor-wasm). + diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/App.razor b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/App.razor new file mode 100644 index 00000000..36912bb8 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Pages/TodoList.razor b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Pages/TodoList.razor new file mode 100644 index 00000000..b5d46c5d --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Pages/TodoList.razor @@ -0,0 +1,259 @@ +@page "/" +@page "/{filter}" +@inject ITodoService TodoService +@inject IJSRuntime JSRuntime + +TodoMVC - Blazor WASM + +
+
+

todos

+ +
+ + @if (todoItems.Any()) + { +
+ + + +
    + @foreach (var item in FilteredTodos) + { +
  • +
    + + + +
    + @if (editingTodoId == item.Id) + { + + } +
  • + } +
+
+ +
+ + @activeTodoCount @(activeTodoCount == 1 ? "item" : "items") left + + + + + @if (completedTodoCount > 0) + { + + } +
+ } +
+ + + +@code { + [Parameter] public string? Filter { get; set; } + + private List todoItems = new(); + private string newTodoTitle = string.Empty; + private string? editingTodoId; + private string editingTitle = string.Empty; + private bool allCompleted; + private ElementReference editInput; + + private string CurrentFilter => Filter ?? "all"; + + private IEnumerable FilteredTodos => CurrentFilter switch + { + "active" => todoItems.Where(t => !t.Completed), + "completed" => todoItems.Where(t => t.Completed), + _ => todoItems + }; + + private int activeTodoCount => todoItems.Count(t => !t.Completed); + private int completedTodoCount => todoItems.Count(t => t.Completed); + + protected override async Task OnInitializedAsync() + { + await LoadTodos(); + } + + private async Task LoadTodos() + { + try + { + var items = await TodoService.GetTodoItemsAsync(); + todoItems = items.ToList(); + UpdateAllCompletedState(); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading todos: {ex.Message}"); + } + } + + private async Task HandleNewTodoKeyPress(KeyboardEventArgs e) + { + if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(newTodoTitle)) + { + await CreateTodo(); + } + } + + private async Task CreateTodo() + { + try + { + var newItem = await TodoService.CreateTodoItemAsync(newTodoTitle.Trim()); + todoItems.Add(newItem); + newTodoTitle = string.Empty; + UpdateAllCompletedState(); + } + catch (Exception ex) + { + Console.WriteLine($"Error creating todo: {ex.Message}"); + } + } + + private async Task UpdateTodo(TodoItemDto item) + { + try + { + await TodoService.UpdateTodoItemAsync(item); + UpdateAllCompletedState(); + } + catch (Exception ex) + { + Console.WriteLine($"Error updating todo: {ex.Message}"); + } + } + + private async Task ToggleTodo(TodoItemDto item) + { + item.Completed = !item.Completed; + await UpdateTodo(item); + } + + private async Task DeleteTodo(string id) + { + try + { + await TodoService.DeleteTodoItemAsync(id); + todoItems.RemoveAll(t => t.Id == id); + UpdateAllCompletedState(); + } + catch (Exception ex) + { + Console.WriteLine($"Error deleting todo: {ex.Message}"); + } + } + + private async Task ToggleAllTodos() + { + try + { + foreach (var item in todoItems) + { + item.Completed = allCompleted; + await TodoService.UpdateTodoItemAsync(item); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error toggling all todos: {ex.Message}"); + } + } + + private async Task ClearCompleted() + { + try + { + var completedItems = todoItems.Where(t => t.Completed).ToList(); + foreach (var item in completedItems) + { + await TodoService.DeleteTodoItemAsync(item.Id); + } + todoItems.RemoveAll(t => t.Completed); + UpdateAllCompletedState(); + } + catch (Exception ex) + { + Console.WriteLine($"Error clearing completed todos: {ex.Message}"); + } + } + + private async Task StartEditing(string id) + { + editingTodoId = id; + var item = todoItems.First(t => t.Id == id); + editingTitle = item.Title; + + await Task.Delay(1); // Allow DOM to update + await editInput.FocusAsync(); + } + + private async Task HandleEditKeyPress(KeyboardEventArgs e, TodoItemDto item) + { + if (e.Key == "Enter") + { + await SaveEdit(item); + } + else if (e.Key == "Escape") + { + CancelEdit(); + } + } + + private async Task SaveEdit(TodoItemDto item) + { + if (!string.IsNullOrWhiteSpace(editingTitle)) + { + item.Title = editingTitle.Trim(); + await UpdateTodo(item); + } + else + { + await DeleteTodo(item.Id); + } + + editingTodoId = null; + editingTitle = string.Empty; + } + + private void CancelEdit() + { + editingTodoId = null; + editingTitle = string.Empty; + } + + private void UpdateAllCompletedState() + { + allCompleted = todoItems.Any() && todoItems.All(t => t.Completed); + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Program.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Program.cs new file mode 100644 index 00000000..7bf2eb58 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Program.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using TodoApp.BlazorWasm.Client; +using TodoApp.BlazorWasm.Client.Services; +using TodoApp.BlazorWasm.Shared.Models; + +WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +// Configure HTTP client for Datasync +builder.Services.AddHttpClient("DatasyncClient", client => +{ + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); +}); + +// Register Datasync services +builder.Services.AddScoped>(sp => +{ + IHttpClientFactory httpClientFactory = sp.GetRequiredService(); + HttpClient httpClient = httpClientFactory.CreateClient("DatasyncClient"); + Uri tableEndpoint = new("/tables/todoitems", UriKind.Relative); + return new DatasyncServiceClient(tableEndpoint, httpClient); +}); + +builder.Services.AddScoped(); + +await builder.Build().RunAsync(); diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Properties/launchSettings.json b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Properties/launchSettings.json new file mode 100644 index 00000000..3bfc5991 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TodoApp.BlazorWasm.Client": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:53033;http://localhost:53034" + } + } +} \ No newline at end of file diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/ITodoService.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/ITodoService.cs new file mode 100644 index 00000000..c382a397 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/ITodoService.cs @@ -0,0 +1,197 @@ +// 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 TodoApp.BlazorWasm.Shared.Models; + +namespace TodoApp.BlazorWasm.Client.Services; + +/// +/// Defines the contract for todo item management services in the Blazor WebAssembly application. +/// This interface provides an abstraction layer for CRUD operations on todo items, enabling +/// dependency injection and testability while supporting datasync capabilities. +/// +/// +/// +/// The interface establishes a service layer contract that abstracts +/// todo item operations from the underlying datasync implementation. This design enables: +/// +/// Dependency injection and inversion of control in Blazor components +/// Unit testing with mock implementations +/// Flexibility to swap datasync implementations without changing consumer code +/// Clear separation of concerns between UI components and data access logic +/// +/// +/// +/// Implementations of this interface should provide: +/// +/// Asynchronous operations suitable for responsive user interfaces +/// Proper error handling with meaningful exception messages +/// Support for offline-first scenarios when using datasync frameworks +/// Optimistic concurrency control for data consistency +/// Soft delete functionality for proper synchronization +/// +/// +/// +/// This interface is typically consumed by Blazor components and pages that need to perform +/// todo item operations, such as the TodoList component which displays and manages the user's todo items. +/// +/// +/// +/// +/// +public interface ITodoService +{ + /// + /// Retrieves all active (non-deleted) todo items. + /// + /// + /// A that represents the asynchronous operation. + /// The task result contains an of + /// representing all active todo items available to the current user. + /// + /// + /// + /// This method should return only active todo items, automatically filtering out any + /// items that have been soft-deleted. The implementation should leverage caching + /// and synchronization capabilities when available to provide the most up-to-date + /// data while supporting offline scenarios. + /// + /// + /// The returned collection should be suitable for display in user interface components + /// and may be used for filtering, sorting, and other client-side operations. + /// + /// + /// + /// Thrown when the service encounters an error during the retrieval operation, + /// such as network connectivity issues or server errors. + /// + /// + /// Thrown when the current user is not authorized to access todo items. + /// + Task> GetTodoItemsAsync(); + + /// + /// Creates a new todo item with the specified title. + /// + /// The title or description of the new todo item. + /// + /// A that represents the asynchronous operation. + /// The task result contains the newly created with + /// all server-assigned properties populated, including ID, timestamps, and version information. + /// + /// + /// + /// This method creates a new todo item with the provided title. The implementation + /// should automatically generate appropriate default values for other properties + /// such as the completion status (typically false for new items) and unique identifier. + /// + /// + /// The returned todo item should contain all properties populated by the server, + /// including timestamps for creation and modification, version information for + /// concurrency control, and any other metadata required by the datasync framework. + /// + /// + /// + /// The title for the new todo item. Should not be null, empty, or consist only of whitespace. + /// The title length should comply with validation rules defined in . + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when is empty, consists only of whitespace, + /// or violates length restrictions. + /// + /// + /// Thrown when the service fails to create the todo item due to server errors, + /// network issues, or other operational problems. + /// + /// + /// Thrown when the todo item data fails validation on the server side. + /// + Task CreateTodoItemAsync(string title); + + /// + /// Updates an existing todo item with the provided data. + /// + /// The containing the updated data. + /// + /// A that represents the asynchronous operation. + /// The task result contains the updated with + /// refreshed server-managed properties such as timestamps and version information. + /// + /// + /// + /// This method performs a complete update of the specified todo item. The implementation + /// should use optimistic concurrency control based on version information to detect + /// and handle concurrent modifications by other clients. + /// + /// + /// The method should update all user-modifiable properties of the todo item while + /// preserving system-managed properties. The returned item will contain updated + /// timestamps and version information reflecting the successful modification. + /// + /// + /// If the item has been modified by another client since it was last retrieved, + /// the implementation should detect this conflict and handle it appropriately, + /// either by raising an exception or providing conflict resolution mechanisms. + /// + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when the update operation fails due to server errors, network issues, + /// concurrent modifications, or when the item no longer exists on the server. + /// + /// + /// Thrown when the updated todo item data fails server-side validation. + /// + /// + /// Thrown when the item has been modified by another client and the update + /// cannot be completed due to version conflicts. + /// + Task UpdateTodoItemAsync(TodoItemDto item); + + /// + /// Deletes the todo item with the specified identifier. + /// + /// The unique identifier of the todo item to delete. + /// + /// A that represents the asynchronous delete operation. + /// + /// + /// + /// This method performs a deletion of the specified todo item. The implementation + /// should typically use soft delete functionality, marking the item as deleted + /// rather than physically removing it from storage. This approach ensures proper + /// synchronization of delete operations across multiple clients and supports + /// offline scenarios. + /// + /// + /// After successful deletion, the item should no longer appear in results from + /// but may still exist in the underlying storage + /// for synchronization purposes. + /// + /// + /// The method should handle cases where the item has already been deleted or + /// no longer exists, typically treating such scenarios as successful operations + /// since the desired end state (item not available) has been achieved. + /// + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when is empty or consists only of whitespace. + /// + /// + /// Thrown when the delete operation fails due to server errors, network connectivity + /// issues, or when the operation cannot be completed for other reasons. + /// + /// + /// Thrown when the current user is not authorized to delete the specified todo item. + /// + Task DeleteTodoItemAsync(string id); +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/TodoService.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/TodoService.cs new file mode 100644 index 00000000..e8532c8e --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Services/TodoService.cs @@ -0,0 +1,207 @@ +// 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 TodoApp.BlazorWasm.Shared.Models; + +namespace TodoApp.BlazorWasm.Client.Services; + +/// +/// Provides client-side todo item management functionality for the Blazor WebAssembly application. +/// This service implements and acts as a wrapper around the +/// to provide datasync capabilities with automatic +/// synchronization between client and server. +/// +/// +/// +/// The class encapsulates all CRUD operations for todo items while +/// leveraging the CommunityToolkit.Datasync.Client framework for offline-first capabilities, +/// automatic conflict resolution, and real-time synchronization with the server. +/// +/// +/// This service automatically handles: +/// +/// Optimistic concurrency control using ETags and version tracking +/// Soft delete operations (items are marked as deleted rather than physically removed) +/// Automatic ID generation for new todo items +/// Error handling and meaningful exception messages +/// Filtering of deleted items during retrieval operations +/// +/// +/// +/// The service is designed to work seamlessly with Blazor WebAssembly components and provides +/// asynchronous operations suitable for responsive user interfaces. +/// +/// +/// +/// +/// +public class TodoService(DatasyncServiceClient todoClient) : ITodoService +{ + /// + /// Retrieves all active (non-deleted) todo items from the datasync service. + /// + /// + /// A that represents the asynchronous operation. + /// The task result contains an of + /// representing all active todo items. + /// + /// + /// + /// This method automatically filters out soft-deleted items by applying a LINQ Where clause + /// that excludes items where is true. This ensures + /// that only active todo items are returned to the client application. + /// + /// + /// The method leverages the datasync client's query capabilities and will automatically + /// synchronize with the server if connectivity is available, or return cached data + /// when operating in offline mode. + /// + /// + /// + /// Thrown when the datasync service encounters an error during the query operation. + /// + /// + /// Thrown when network communication with the server fails. + /// + public async Task> GetTodoItemsAsync() + { + List items = await todoClient + .Where(item => !item.Deleted) + .ToListAsync(); + return items; + } + + /// + /// Creates a new todo item with the specified title. + /// + /// The title or description of the new todo item. + /// + /// A that represents the asynchronous operation. + /// The task result contains the newly created with + /// server-assigned properties populated. + /// + /// + /// + /// This method creates a new with an automatically generated + /// GUID identifier, sets the title to the provided value, and initializes the + /// property to false. + /// + /// + /// The method sends the new item to the server via the datasync client's + /// method. Upon successful creation, + /// the server returns the persisted item with updated timestamps and version information. + /// + /// + /// The title for the new todo item. Must not be null or empty. + /// + /// Thrown when is null. + /// + /// + /// Thrown when is empty or whitespace only. + /// + /// + /// Thrown when the server operation fails, containing details about the failure reason. + /// + /// + /// Thrown when the todo item fails server-side validation (e.g., title too long). + /// + public async Task CreateTodoItemAsync(string title) + { + TodoItemDto newItem = new() { Title = title }; + ServiceResponse response = await todoClient.AddAsync(newItem); + if (response.IsSuccessful && response.HasValue) + { + return response.Value!; + } + + throw new InvalidOperationException($"Failed to create todo item: {response.ReasonPhrase}"); + } + + /// + /// Updates an existing todo item with the provided data. + /// + /// The containing the updated data. + /// + /// A that represents the asynchronous operation. + /// The task result contains the updated with + /// server-updated properties such as timestamps and version. + /// + /// + /// + /// This method performs a complete replacement of the todo item on the server using + /// the datasync client's method. + /// The operation includes optimistic concurrency control based on the item's version property. + /// + /// + /// If the item has been modified by another client since it was last retrieved, + /// the server will reject the update with a conflict status, which will be reflected + /// in the returned by the datasync client. + /// + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when the server operation fails, including conflicts due to concurrent modifications, + /// validation failures, or network errors. The exception message includes the server's reason phrase. + /// + /// + /// Thrown when the updated todo item fails server-side validation. + /// + public async Task UpdateTodoItemAsync(TodoItemDto item) + { + ServiceResponse response = await todoClient.ReplaceAsync(item); + if (response.IsSuccessful && response.HasValue) + { + return response.Value!; + } + + throw new InvalidOperationException($"Failed to update todo item: {response.ReasonPhrase}"); + } + + /// + /// Performs a soft delete operation on the todo item with the specified identifier. + /// + /// The unique identifier of the todo item to delete. + /// + /// A that represents the asynchronous delete operation. + /// + /// + /// + /// This method performs a soft delete operation, meaning the todo item is marked as deleted + /// on the server but not physically removed from the database. This approach enables + /// proper synchronization of delete operations across multiple clients and supports + /// offline scenarios where delete operations need to be synchronized later. + /// + /// + /// The method uses to configure the delete operation + /// and leverages the datasync client's + /// method to perform the server-side operation. + /// + /// + /// After successful deletion, the item will no longer appear in results from + /// as it filters out items marked as deleted. + /// + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when is empty or whitespace only. + /// + /// + /// Thrown when the server operation fails, such as when the item doesn't exist, + /// has already been deleted, or due to network connectivity issues. + /// The exception message includes the server's reason phrase. + /// + public async Task DeleteTodoItemAsync(string id) + { + ServiceResponse response = await todoClient.RemoveAsync(id, new DatasyncServiceOptions()); + if (!response.IsSuccessful) + { + throw new InvalidOperationException($"Failed to delete todo item: {response.ReasonPhrase}"); + } + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Shared/MainLayout.razor b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Shared/MainLayout.razor new file mode 100644 index 00000000..8ff75aed --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/Shared/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +
+
+
+ @Body +
+
+
diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/TodoApp.BlazorWasm.Client.csproj b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/TodoApp.BlazorWasm.Client.csproj new file mode 100644 index 00000000..cd458f5f --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/TodoApp.BlazorWasm.Client.csproj @@ -0,0 +1,20 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/_Imports.razor b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/_Imports.razor new file mode 100644 index 00000000..4175b208 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using TodoApp.BlazorWasm.Client +@using TodoApp.BlazorWasm.Client.Shared +@using TodoApp.BlazorWasm.Client.Services +@using TodoApp.BlazorWasm.Shared.Models diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/app.css b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/app.css new file mode 100644 index 00000000..d6f11ef3 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/app.css @@ -0,0 +1,51 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + +.loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); +} + +.loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; +} + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + +.loading-progress-text:after { + content: "Loading..."; +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/todomvc.css b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/todomvc.css new file mode 100644 index 00000000..5eb4305f --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/css/todomvc.css @@ -0,0 +1,388 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111111; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + height: 65px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 65px; + font-size: 0; + position: absolute; + top: -65px; + left: -0; +} + +.toggle-all + label:before { + content: '❯'; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:checked + label:before { + color: #484848; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://github.com/twbs/bootstrap/issues/20791 + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22m72%2025-42%2042-14-14-10%2010%2024%2024%2052-52z%22%2F%3E%3C%2Fsvg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + font-weight: 400; + color: #484848; +} + +.todo-list li.completed label { + color: #949494; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover, +.todo-list li .destroy:focus { + color: #C18585; +} + +.todo-list li .destroy:after { + content: '×'; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + padding: 10px 15px; + height: 20px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: #DB7676; +} + +.filters li a.selected { + border-color: #CE4646; +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 19px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/favicon.png b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..f1c369cefa32be563eb105e62b5ffd3f5c55a03d GIT binary patch literal 29242 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 literal 0 HcmV?d00001 diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/index.html b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/index.html new file mode 100644 index 00000000..dab5221e --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Client/wwwroot/index.html @@ -0,0 +1,33 @@ + + + + + + + TodoApp.BlazorWasm.Client + + + + + + + + +
+ + + + +
+
+ +
+ + + + + diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Controllers/TodoItemsController.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..3e96cf73 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Controllers/TodoItemsController.cs @@ -0,0 +1,74 @@ +// 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.Server; +using CommunityToolkit.Datasync.Server.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using TodoApp.BlazorWasm.Server.Database; + +namespace TodoApp.BlazorWasm.Server.Controllers; + +/// +/// Provides RESTful API endpoints and OData query capabilities for managing todo items in the Blazor WebAssembly application. +/// This controller inherits from to provide full CRUDL (Create, Read, Update, Delete, List) +/// operations with datasync capabilities from the CommunityToolkit.Datasync.Server framework. +/// +/// +/// +/// The exposes the following HTTP endpoints for todo item operations: +/// +/// GET /tables/todoitems - Retrieves a list of todo items with optional OData query parameters for filtering, sorting, and paging +/// GET /tables/todoitems/{id} - Retrieves a specific todo item by its unique identifier +/// POST /tables/todoitems - Creates a new todo item +/// PUT /tables/todoitems/{id} - Updates an existing todo item (full replacement) +/// PATCH /tables/todoitems/{id} - Partially updates an existing todo item +/// DELETE /tables/todoitems/{id} - Deletes a todo item (supports both soft and hard delete based on configuration) +/// +/// +/// +/// The controller supports OData query operations including $filter, $orderby, $skip, $top, +/// and $select for advanced querying capabilities. It also provides automatic ETag support for optimistic +/// concurrency control and delta synchronization features for offline-capable applications. +/// +/// +/// All operations are performed through the which provides +/// Entity Framework Core integration for data persistence and change tracking. +/// +/// +/// +/// +/// +/// +[Route("tables/todoitems")] +public class TodoItemsController : TableController +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The that provides access to the todo items database. + /// This context is used to create the underlying + /// for data access operations. + /// + /// + /// + /// The constructor configures the controller with an + /// that uses the provided for Entity Framework Core operations. + /// This setup enables the controller to perform database operations while maintaining + /// compatibility with the datasync framework's requirements. + /// + /// + /// The repository handles all CRUD operations, change tracking, and optimistic concurrency + /// control through Entity Framework Core, while the base + /// provides the REST API surface and OData query capabilities. + /// + /// + /// + /// Thrown when is null. + /// + public TodoItemsController(TodoContext context) : base() + { + Repository = new EntityTableRepository(context); + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoContext.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoContext.cs new file mode 100644 index 00000000..eec94dc0 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoContext.cs @@ -0,0 +1,102 @@ +// 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.EntityFrameworkCore; + +namespace TodoApp.BlazorWasm.Server.Database; + +/// +/// Represents the Entity Framework Core database context for the Todo application. +/// This class manages the database connection and provides access to the TodoItems table +/// for the Blazor WebAssembly server-side data operations. +/// +/// +/// +/// The class is configured to use an in-memory database for development +/// and testing purposes. It inherits from and provides the necessary +/// infrastructure for Entity Framework Core operations including database creation, data seeding, +/// and change tracking. +/// +/// +/// This context is registered as a service in the dependency injection container and is used +/// by the datasync framework to provide real-time synchronization capabilities between +/// server and client applications. +/// +/// +/// The options to be used by the . +/// +/// +/// +public class TodoContext(DbContextOptions options) : DbContext(options) +{ + /// + /// Gets the that can be used to query and save instances of . + /// + /// + /// A that provides access to the TodoItems table in the database. + /// + /// + /// This property represents the TodoItems table in the database and provides methods + /// for querying, adding, updating, and deleting todo items. The DbSet is automatically + /// configured by Entity Framework Core based on the entity definition. + /// + public DbSet TodoItems => Set(); + + /// + /// Initializes the database by ensuring it exists and populating it with sample data if empty. + /// This method is typically called during application startup to prepare the database for use. + /// + /// + /// A representing the asynchronous database initialization operation. + /// + /// + /// + /// This method performs the following operations: + /// + /// Ensures the database is created using . + /// Checks if the TodoItems table is empty using . + /// If empty, clears the change tracker to avoid entity tracking conflicts. + /// Adds three sample todo items with predefined titles. + /// Saves the sample data to the database. + /// + /// + /// + /// The sample data includes three todo items with titles related to learning Blazor WebAssembly, + /// building applications, and deployment. All sample items are created with their default + /// completion status (not completed). + /// + /// + /// The method uses before adding sample data to prevent + /// entity tracking conflicts that could occur if entities with duplicate keys are being tracked. + /// + /// + /// + /// Thrown when database operations fail, such as when the database cannot be created + /// or when saving changes fails due to validation or constraint violations. + /// + /// + /// Thrown when an error occurs while saving changes to the database during the sample data insertion. + /// + public async Task InitializeDatabaseAsync() + { + _ = await Database.EnsureCreatedAsync(); + + // Add some sample data if the database is empty + if (!await TodoItems.AnyAsync()) + { + // Clear any existing tracked entities to avoid conflicts + ChangeTracker.Clear(); + + TodoItem[] sampleItems = + [ + new TodoItem { Title = "Learn Blazor WASM" }, + new TodoItem { Title = "Build awesome apps" }, + new TodoItem { Title = "Deploy to production" } + ]; + + TodoItems.AddRange(sampleItems); + _ = await SaveChangesAsync(); + } + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoItem.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoItem.cs new file mode 100644 index 00000000..090e00ff --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Database/TodoItem.cs @@ -0,0 +1,79 @@ +// 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.Server.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; + +namespace TodoApp.BlazorWasm.Server.Database; + +/// +/// Represents a todo item entity for the server-side database in the Blazor WebAssembly application. +/// This class inherits from to provide datasync capabilities +/// with the CommunityToolkit.Datasync.Server framework. +/// +/// +/// +/// This entity class is used by Entity Framework Core for database operations and by the +/// datasync framework for synchronization with client applications. It includes automatic +/// ID generation and validation attributes to ensure data integrity. +/// +/// +/// The class is mapped to a database table and exposed through the TodoItemsController +/// for RESTful API operations and real-time synchronization. +/// +/// +/// +/// +public class TodoItem : EntityTableData +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The constructor automatically generates a unique identifier using a GUID + /// formatted as a 32-character hexadecimal string without hyphens (format "N"). + /// This ensures each todo item has a unique ID when created. + /// + public TodoItem() : base() + { + Id = Guid.NewGuid().ToString("N"); + } + + /// + /// Gets or sets the title or description of the todo item. + /// + /// + /// The title text of the todo item. Must be between 1 and 255 characters in length. + /// + /// + /// + /// This property is required and cannot be null or empty. The validation attributes + /// ensure that the title has a minimum length of 1 character and does not exceed + /// 255 characters to maintain database compatibility and user experience. + /// + /// + /// The title represents the main content or description of what needs to be accomplished + /// in this todo item. + /// + /// + /// + /// Thrown during model validation if the title is null, empty, or exceeds the maximum length. + /// + [Required, StringLength(255, MinimumLength = 1)] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the todo item has been completed. + /// + /// + /// true if the todo item is completed; otherwise, false. + /// The default value is false for newly created items. + /// + /// + /// This boolean flag tracks the completion status of the todo item. + /// When set to true, it indicates that the task has been finished. + /// This property is commonly used for filtering and displaying items in different states. + /// + public bool Completed { get; set; } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Program.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Program.cs new file mode 100644 index 00000000..03c8a9cc --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Program.cs @@ -0,0 +1,58 @@ +// 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.Server; +using Microsoft.EntityFrameworkCore; +using TodoApp.BlazorWasm.Server.Database; + +#pragma warning disable IDE0058 // Expression value is never used + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddDbContext(options => +{ + options.UseInMemoryDatabase("TodoAppDb"); + if (builder.Environment.IsDevelopment()) + { + options.EnableDetailedErrors(); + options.EnableSensitiveDataLogging(); + } +}); + +builder.Services.AddDatasyncServices(); +builder.Services.AddControllersWithViews(); +builder.Services.AddRazorPages(); + +WebApplication app = builder.Build(); + +// Initialize the database +using (IServiceScope scope = app.Services.CreateScope()) +{ + TodoContext context = scope.ServiceProvider.GetRequiredService(); + await context.InitializeDatabaseAsync(); +} + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseBlazorFrameworkFiles(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapRazorPages(); +app.MapControllers(); +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Properties/launchSettings.json b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Properties/launchSettings.json new file mode 100644 index 00000000..a010904d --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/Properties/launchSettings.json @@ -0,0 +1,40 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:17777", + "sslPort": 44325 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5088", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7088;http://localhost:5088", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/TodoApp.BlazorWasm.Server.csproj b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/TodoApp.BlazorWasm.Server.csproj new file mode 100644 index 00000000..c7c79bbe --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/TodoApp.BlazorWasm.Server.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.Development.json b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.json b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/DatasyncDto.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/DatasyncDto.cs new file mode 100644 index 00000000..a88245b1 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/DatasyncDto.cs @@ -0,0 +1,79 @@ +// 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.BlazorWasm.Shared.Models; + +/// +/// Represents a data transfer object (DTO) for a datasync client item in the application. +/// This class is used for communication between the client and server, containing all necessary +/// properties for datasync operations with the CommunityToolkit.Datasync framework. +/// +/// +/// This DTO includes standard datasync properties such as timestamps, version control, and +/// soft delete functionality to support offline synchronization scenarios. +/// +public abstract class DatasyncDto +{ + /// + /// Gets or sets the unique identifier for the todo item. + /// + /// + /// A string representing the unique ID of the todo item. This is typically a GUID + /// converted to string format for compatibility with datasync operations. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the date and time when the todo item was created. + /// + /// + /// A representing when the item was first created. + /// This timestamp includes timezone information for accurate synchronization across different time zones. + /// + /// + /// This property is automatically managed by the datasync framework and should not + /// be manually modified in most scenarios. + /// + public DateTimeOffset CreatedAt { get; set; } + + /// + /// Gets or sets the date and time when the todo item was last updated. + /// + /// + /// A representing the most recent modification time. + /// This timestamp includes timezone information for accurate synchronization across different time zones. + /// + /// + /// This property is automatically updated by the datasync framework whenever + /// the entity is modified and should not be manually set in most scenarios. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// Gets or sets the version identifier used for optimistic concurrency control. + /// + /// + /// A string representing the current version of the entity, typically used as an ETag + /// for conflict resolution during synchronization operations. + /// + /// + /// This property is managed by the datasync framework to handle concurrent updates + /// and ensure data consistency. It should not be manually modified. + /// + public string Version { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the todo item has been soft-deleted. + /// + /// + /// true if the item has been marked for deletion but not physically removed; + /// otherwise, false. + /// + /// + /// This property supports soft delete functionality in the datasync framework, + /// allowing items to be marked as deleted without physically removing them from storage. + /// This enables proper synchronization of delete operations across clients. + /// + public bool Deleted { get; set; } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/TodoItemDto.cs b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/TodoItemDto.cs new file mode 100644 index 00000000..07859345 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/Models/TodoItemDto.cs @@ -0,0 +1,36 @@ +// 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.BlazorWasm.Shared.Models; + +/// +/// Represents a data transfer object (DTO) for a todo item in the Blazor WebAssembly application. +/// This class is used for communication between the client and server, containing all necessary +/// properties for datasync operations with the CommunityToolkit.Datasync framework. +/// +public class TodoItemDto : DatasyncDto +{ + /// + /// Gets or sets the title or description of the todo item. + /// + /// + /// The title text of the todo item. Must be between 1 and 255 characters in length. + /// + /// + /// This property is required and has validation attributes to ensure the title + /// is not empty and does not exceed the maximum length limit. + /// + [Required, StringLength(255, MinimumLength = 1)] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the todo item has been completed. + /// + /// + /// true if the todo item is completed; otherwise, false. + /// + public bool Completed { get; set; } +} diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/TodoApp.BlazorWasm.Shared.csproj b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/TodoApp.BlazorWasm.Shared.csproj new file mode 100644 index 00000000..9623deb4 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.Shared/TodoApp.BlazorWasm.Shared.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.sln b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.sln new file mode 100644 index 00000000..1773cc83 --- /dev/null +++ b/samples/todoapp-blazor-wasm/TodoApp.BlazorWasm.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.BlazorWasm.Server", "TodoApp.BlazorWasm.Server\TodoApp.BlazorWasm.Server.csproj", "{8B5CA653-AD19-435C-BB5E-85BDFC94F3DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.BlazorWasm.Client", "TodoApp.BlazorWasm.Client\TodoApp.BlazorWasm.Client.csproj", "{92334429-9A41-4C87-8CAF-905119CE4946}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoApp.BlazorWasm.Shared", "TodoApp.BlazorWasm.Shared\TodoApp.BlazorWasm.Shared.csproj", "{C356C681-A691-4E3F-9D66-3716F5916C5E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8B5CA653-AD19-435C-BB5E-85BDFC94F3DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B5CA653-AD19-435C-BB5E-85BDFC94F3DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B5CA653-AD19-435C-BB5E-85BDFC94F3DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B5CA653-AD19-435C-BB5E-85BDFC94F3DC}.Release|Any CPU.Build.0 = Release|Any CPU + {92334429-9A41-4C87-8CAF-905119CE4946}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92334429-9A41-4C87-8CAF-905119CE4946}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92334429-9A41-4C87-8CAF-905119CE4946}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92334429-9A41-4C87-8CAF-905119CE4946}.Release|Any CPU.Build.0 = Release|Any CPU + {C356C681-A691-4E3F-9D66-3716F5916C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C356C681-A691-4E3F-9D66-3716F5916C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C356C681-A691-4E3F-9D66-3716F5916C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C356C681-A691-4E3F-9D66-3716F5916C5E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal From ac4e022462ac3e8aacf58f614c86b31341d7a7f4 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Wed, 16 Jul 2025 13:35:31 -0700 Subject: [PATCH 2/3] (#330) Added Blazor WASM links --- docs/in-depth/client/index.md | 4 ++-- docs/in-depth/client/online.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/in-depth/client/index.md b/docs/in-depth/client/index.md index 8c7d2075..5a8a27f5 100644 --- a/docs/in-depth/client/index.md +++ b/docs/in-depth/client/index.md @@ -1,9 +1,9 @@ # Creating offline clients -This guide shows you how to perform common scenarios using the Datasync Community Toolkit. Use the client library in any .NET 8 application, including AvaloniaUI, MAUI, Uno Platform, WinUI, and WPF applications. +This guide shows you how to perform common scenarios using the Datasync Community Toolkit. Use the client library in any .NET 9 application, including AvaloniaUI, MAUI, Uno Platform, WinUI, and WPF applications. !!! note **Blazor WASM and Blazor Hybrid** - The offline capabilities are known to have issues with Blazor WASM and Blazor Hybrid (since EF Core and SQLite do not work in those environments when running in the browser). Use online-only operations in these environments. + The offline capabilities are known to have issues with Blazor WASM and Blazor Hybrid (since EF Core and SQLite do not work in those environments when running in the browser). Use online-only operations in these environments. For more information, see [our guide on Blazor WASM](./advanced/blazor-wasm.md) This guide primary deals with offline operations. For online operations, see the [Online operations guide](./online.md). diff --git a/docs/in-depth/client/online.md b/docs/in-depth/client/online.md index cd661ca0..0626b88f 100644 --- a/docs/in-depth/client/online.md +++ b/docs/in-depth/client/online.md @@ -2,6 +2,8 @@ Not all data needs to be synchronized. You may want to do an online search of records for a search capability, for example. To support this, The Datasync Community Toolkit supports an online client in addition to offline usage. +If you are using the Datasync Community Toolkit with Blazor WASM, see [our guide on Blazor WASM usage](./advanced/blazor-wasm.md). + ## Creating a Http Client Factory To create an online client, you must create an `IHttpClientFactory` that creates the appropriate `HttpClient` objects that are used to communicate with the remote service. This can handle authentication, logging, and anything else that is required by the remote service. At a minimum, a `BaseAddress` must be established. To facilitate this, the Datasync Community Toolkit provides a default `HttpClientFactory` that can be used: From 9491036376e76ec23197e04af5fb0108336009d3 Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Wed, 16 Jul 2025 13:37:48 -0700 Subject: [PATCH 3/3] (#330) sample updates --- docs/in-depth/client/advanced/blazor-wasm.md | 3 +-- docs/index.md | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/in-depth/client/advanced/blazor-wasm.md b/docs/in-depth/client/advanced/blazor-wasm.md index b0111d1f..77cd3356 100644 --- a/docs/in-depth/client/advanced/blazor-wasm.md +++ b/docs/in-depth/client/advanced/blazor-wasm.md @@ -31,5 +31,4 @@ This will suppress the harmless SQLite warning that appears when building Blazor ## Sample -We have a Blazor WASM sample in our sample set: [samples/todoapp-blazor-wasm](https://github.com/CommunityToolkit/Datasync/tree/main/samples/todoapp-blazor-wasm). - +We have a Blazor WASM sample in our sample set: [samples/todoapp-blazor-wasm](https://github.com/CommunityToolkit/Datasync/tree/main/samples/todoapp-blazor-wasm). This sample comprises a server (which uses an in-memory EF Core database) combined with a Blazor Web Assembly client. diff --git a/docs/index.md b/docs/index.md index 1b2ce749..49995b82 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ Currently, the library supports: Client platforms that have been tested include: * [Avalonia][avalonia] +* [Blazor WASM][blazor-wasm] * [.NET MAUI][maui] * [Uno Platform][uno] * [Windows Presentation Framework (WPF)][wpf] @@ -43,6 +44,7 @@ Find out more about developing datasync applications: [org-github]: https://learn.microsoft.com/en-us/dotnet/communitytoolkit/ [dotnetfdn]: https://dotnetfoundation.org/ [avalonia]: https://www.avaloniaui.net/ +[blazor-wasm]: https://learn.microsoft.com/en-us/aspnet/core/blazor/webassembly-build-tools-and-aot [maui]: https://dotnet.microsoft.com/apps/maui [uno]: https://platform.uno/ [wpf]: https://learn.microsoft.com/dotnet/desktop/wpf/overview/