Datasync Community Toolkit

repository·main·Indexed 41 days ago

https://github.com/communitytoolkit/datasync

A collection of .NET libraries for implementing client-server systems to synchronize data from database tables. It provides tools for scaffolding servers via dotnet templates, offline-capable DbContexts, and support for MAUI Native AOT, Blazor WASM, and the Uno Platform.

Tokens
50.5K
Snippets
124
Records
155
Agent score
51%

What's inside Datasync

  1. Overview of standard Datasync repositories

    main

    The Datasync Community Toolkit provides five standard, production-ready repositories to cover most data storage scenarios:

    • AutoMapper: For mapping between different data models.
    • Entity Framework Core: For standard SQL database integration.
    • In-Memory: For testing or transient storage.
    • LiteDb: For embedded, serverless .NET storage.
    • MongoDB: For NoSQL document storage (requires flat JSON structures).

    If your use case requires something not covered (such as an existing database using auto-incrementing integer keys), the toolkit's pluggable architecture allows you to implement your own custom repository.

  2. What is the Datasync Community Toolkit?

    main

    The Datasync Community Toolkit is an open-source set of libraries designed for building client-server applications with offline data availability.

    Unlike proprietary solutions like Google Firebase or AWS AppSync, this toolkit provides:

    • Database Agnostic: Connect to any database.
    • Flexible Authentication: Use any authentication provider.
    • Robust Authorization: Implement custom authorization rules.
    • Deployment Flexibility: Run anywhere (local machine, containers, or any cloud provider).

    Platform Support:

    • Server-side: Implemented using ASP.NET Core Web APIs.
    • Client-side: Compatible with any .NET client technology, including Avalonia, MAUI, Uno Platform, WinUI3, and WPF.
  3. Compatibility and supported platforms

    main

    The Datasync Community Toolkit is designed for client-server data synchronization. When building your application, ensure your environment meets the following requirements:

    Runtime Requirements

    • Server: ASP.NET Core 10.x or later.
    • Client: .NET 10.x or later.

    Note: Always match the version of the Datasync Community Toolkit to the version of .NET you are using.

    Supported Client Platforms

    The client library contains no platform-specific code and is compatible with any .NET-based technology. Tested platforms include:

    • Avalonia
    • Blazor WASM
    • .NET MAUI
    • Uno Platform
    • Windows Presentation Foundation (WPF)
    • Windows UI Library (WinUI) 3

    Supported Databases

    The toolkit supports several database providers. You can also add support for other databases via Entity Framework Core or custom repository implementations:

    • Azure SQL
    • Cosmos
    • LiteDb
    • MongoDb
    • MySQL
    • PostgreSQL
    • SQLite
    • In-memory store
  4. Trigger events from the Datasync service

    main

    There are two primary ways to trigger an event within the Datasync service to facilitate real-time updates:

    1. Access Control Provider: Use the PostCommitHookAsync method on the access control provider. This is ideal if every table controller uses its own dedicated access control provider.
    2. Table Controller: Hook into the RepositoryUpdated event on the table controller. This is preferred if you want better separation of concerns between authorization and eventing.
  5. Core Concepts of Data Synchronization

    main

    When building with Datasync, understand these fundamental synchronization behaviors:

    • Resilience: Enables data availability during connectivity drops or network changes.
    • Flexibility: Local storage allows for faster updates and a wider variety of search capabilities compared to direct server communication.
    • User Experience: Reduces latency by fulfilling user interactions locally before syncing with the server.
    • Optimistic Concurrency: Allows clients to accept changes while disconnected. The server performs a concurrency check when the change is sent; if the Version token does not match, the change is rejected.
    • Incremental Synchronization: Reduces bandwidth by only transmitting new, removed, or changed entities. This requires that each data set is isolated (no foreign keys allowed between synchronized data sets).
    • Soft Delete: Uses a Deleted flag to propagate deletions to all clients, ensuring consistency before a server-side cleanup process purges the records.
  6. Create a model entity class

    main

    All model classes must implement ITableData. For most database providers, you should inherit from the specific abstract class provided by your repository type to ensure correct implementation of required properties.

    Common Repository Base Classes

    • EntityTableData: For most EF-Core based providers.
    • InMemoryTableData: For in-memory stores.
    • LiteDbTableData: For LiteDb.
    • CosmosEntityTableData: For Cosmos DB via EF Core.
    • CosmosTableData: For Cosmos DB via the SDK.
    • MongoTableData: For MongoDB via the SDK.
    • SqliteEntityTableData: For SQLite.

    Required ITableData Properties

    Do not modify these properties in your code; the Datasync libraries or the database maintain them:

    • Id (string): A globally unique ID.
    • UpdatedAt (DateTimeOffset?): Last update timestamp with millisecond accuracy.
    • Version (byte[]): An opaque value that changes on every write.
    • Deleted (bool): True if the item is marked for deletion but not yet purged.
    // Example: TodoItem for PostgreSQL using EF Core
    public class TodoItem : EntityTableData
    {
      public required string Title { get; set; }
      public bool IsComplete { get; set; }
    }
  7. How authentication and authorization work in Datasync

    main

    Authentication in the Datasync Community Toolkit is handled by passing a JSON Web Token (JWT) from the client to the server. The JWT is generated by an OAuth2 or OIDC service (such as Microsoft Entra ID, Google, Facebook, Auth0, or custom identity services) that both the client and server agree upon.

    Because the client and server must agree on the identity provider, you must configure authentication on both sides of the application. The server uses the token to verify identity, and the client uses a library (like MSAL) to acquire the token and include it in requests.

  8. CosmosTableOptions implementations

    main

    When using the Cosmos DB SDK with Datasync, you must provide an ICosmosTableOptions<TEntity> implementation to define how data is stored.

    • CosmosSharedTableOptions<TEntity>: Stores all entities of a specific type in a single container. It uses the entity name as the partition key.
    • CosmosSingleTableOptions<TEntity>: Stores entities by compressing the ID and the partition key into the entity's unique ID.

    You can also implement ICosmosTableOptions<TEntity> yourself to provide custom logic for determining partition keys and IDs.

  9. Avoid client-side evaluations in Cosmos EF Core queries

    main

    The LINQ provider for Cosmos EF Core does not support certain operations. Using these in a query will trigger a "client-side evaluation".

    Impact of client-side evaluation:

    • v8.x and earlier: The entire dataset is read into memory to apply the filter, causing severe performance degradation.
    • v9.x and later: Client-side evaluations are unsupported and will result in a 400 Bad Request or 500 Internal Server Error.

    Operations to avoid in queries:

    • Math operations (e.g., division, multiplication, floor, ceiling, round).
    • Accessing date/time components (e.g., year, day, month).
    • Using DateOnly and TimeOnly types.
  10. How the HTTP pipeline and delegating handlers work

    main

    The Datasync Community Toolkit uses an HTTP pipeline to process requests and responses. You can customize this pipeline by adding DelegatingHandler instances to the HttpPipeline property of HttpClientOptions.

    Pipeline Execution Flow

    When a DatasyncServiceClient method (like GetAsync) is called:

    1. The client constructs an HttpRequestMessage.
    2. The request passes through the HttpPipeline in the order they are defined (from the first element to the last).
    3. The final handler in the pipeline passes the request to the HttpClientHandler, which transmits it to the service.
    4. The HttpResponseMessage travels back up the chain in reverse order (from the last handler to the first).
    5. The client decodes the response and returns it to your code.

    Importance of Order

    The order of handlers in the HttpPipeline array is critical. For example, if you place a LoggingHandler before an AuthenticationHandler, the logger will not see the authentication headers added by the subsequent handler.

    HttpClientOptions options = new()
    {
      Endpoint = new Uri("https://myserver/"),
      HttpPipeline = [
        new LoggingHandler(),
        new AuthenticationHandler()
      ],
      Timeout = TimeSpan.FromSeconds(120),
      UserAgent = "Enterprise/Datasync-myserver-service"
    };
    DatasyncServiceClient<TodoItem> serviceClient = new(options);
  11. Filter data on the service vs. the client

    main

    The location where filtering occurs depends on where the .Where() method is placed in the method chain relative to the terminating method:

    1. Service-side filtering: When .Where() is called before the terminating method, the filter is translated into an OData query and executed on the server. This is more efficient as it reduces data transfer.
    2. Client-side filtering: When .Where() is called after the terminating method, the entire dataset (or the subset returned by the service) is downloaded to the client first, and then the filter is applied locally.

    Supported Service-side Operations (OData subset):

    • Relational operators: ==, !=, <, <=, >, >=
    • Arithmetic operators: +, -, /, *, %
    • Number precision: Math.Floor, Math.Ceiling
    • String functions: Length, Substring, Replace, IndexOf, Equals, StartsWith, EndsWith (ordinal and invariant cultures only)
    • Date properties: Year, Month, Day, Hour, Minute, Second
    • Object access: Accessing properties of an object.
    WARNING

    NoSQL data stores (e.g., MongoDB, Cosmos DB) typically limit available filtering operations. Most arithmetic, math, string, and property access filters may not be supported on the server side for these stores.

    // Service-side filtering (efficient)
    var items = (await client.Where(x => !x.IsComplete)).ToListAsync();
    
    // Client-side filtering (downloads all incomplete items first)
    var items = (await client.Where(x => !x.IsComplete).ToListAsync()).Where(x => x.Title.StartsWith("The"));
  12. Implement access control with IAccessControlProvider<TEntity>

    main

    To control data access on a per-user basis, implement the IAccessControlProvider<TEntity> interface (available in the CommunityToolkit.Datasync.Abstractions package). This provider allows you to define what data a user can see, whether they can perform specific operations, and how to modify entities before they are committed to the database.

    An access control provider consists of four main parts:

    1. GetDataView(): Returns an Expression<Func<TEntity, bool>> used to filter the data visible to the user (similar to a LINQ .Where() clause).
    2. IsAuthorizedAsync(TableOperation op, TEntity? entity, ...): Determines if the user is allowed to perform a specific TableOperation (e.g., Query, Read, Create, Update, Delete).
    3. PreCommitHookAsync(TableOperation op, TEntity entity, ...): A hook called immediately before writing to the database, useful for modifying the entity (e.g., setting a UserId).
    4. PostCommitHookAsync(TableOperation op, TEntity entity, ...): A hook called after a successful write, typically used for event management or real-time notifications rather than access control.

    To use it, attach the provider to your TableController by setting its AccessControlProvider property.

    public class ArticleAccessControlProvider(IHttpContextAccessor contextAccessor) : IAccessControlProvider<Article>
    {
      public Expression<Func<Article, bool>> GetDataView()
        => article => article.CreatedAt > DateTimeOffset.AddDays(-30);
    
      public ValueTask<bool> IsAuthorizedAsync(TableOperation op, Article? entity, CancellationToken cancellationToken = default)
        => ValueTask.FromResult(op == TableOperation.Query || op == TableOperation.Read || IsAuthenticated);
    
      public ValueTask PreCommitHookAsync(TableOperation op, Article entity, CancellationToken cancellationToken = default)
        => ValueTask.CompletedTask;
    
      public ValueTask PostCommitHookAsync(TableOperation op, Article entity, CancellationToken cancellationToken = default)
        => ValueTask.CompletedTask;
    }