Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace CommunityToolkit.Datasync.Client.Offline;
public class DatasyncOfflineOptionsBuilder
{
internal IHttpClientFactory? _httpClientFactory;
internal IConflictResolver? _defaultConflictResolver;
internal readonly Dictionary<string, EntityOfflineOptions> _entities;

/// <summary>
Expand Down Expand Up @@ -78,6 +79,19 @@ public DatasyncOfflineOptionsBuilder UseHttpClientOptions(HttpClientOptions clie
return this;
}

/// <summary>
/// Sets the default conflict resolver to use for all entities that do not have a specific
/// conflict resolver set.
/// </summary>
/// <param name="conflictResolver">The default conflict resolver.</param>
/// <returns>The current builder for chaining.</returns>
public DatasyncOfflineOptionsBuilder UseDefaultConflictResolver(IConflictResolver conflictResolver)
{
ArgumentNullException.ThrowIfNull(conflictResolver);
this._defaultConflictResolver = conflictResolver;
return this;
}

/// <summary>
/// Configures the specified entity type for offline operations.
/// </summary>
Expand Down Expand Up @@ -133,7 +147,8 @@ internal OfflineOptions Build()

OfflineOptions result = new()
{
HttpClientFactory = this._httpClientFactory
HttpClientFactory = this._httpClientFactory,
DefaultConflictResolver = this._defaultConflictResolver
};

foreach (EntityOfflineOptions entity in this._entities.Values)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ internal class OfflineOptions()
/// </summary>
public required IHttpClientFactory HttpClientFactory { get; init; }

/// <summary>
/// The default <see cref="IConflictResolver"/> to use for this request.
/// </summary>
public IConflictResolver? DefaultConflictResolver { get; set; }

/// <summary>
/// Adds an entity to the mapping of options.
/// </summary>
Expand Down Expand Up @@ -50,7 +55,7 @@ public EntityDatasyncOptions GetOptions(Type entityType)
{
return new()
{
ConflictResolver = options.ConflictResolver,
ConflictResolver = options.ConflictResolver ?? DefaultConflictResolver,
Endpoint = options.Endpoint,
HttpClient = HttpClientFactory.CreateClient(options.ClientName),
QueryDescription = options.QueryDescription ?? new QueryDescription()
Expand All @@ -60,7 +65,7 @@ public EntityDatasyncOptions GetOptions(Type entityType)
{
return new()
{
ConflictResolver = null,
ConflictResolver = DefaultConflictResolver,
Endpoint = new Uri($"tables/{entityType.Name.ToLowerInvariant()}", UriKind.Relative),
HttpClient = HttpClientFactory.CreateClient(),
QueryDescription = new QueryDescription()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,71 @@ public async Task GenericConflictResolver_BothNull_ShouldReturnDefault()

#region Integration with OperationsQueueManager Tests

[Fact]
public async Task PushAsync_WithDefaultClientWinsResolver_ShouldResolveConflictAndRetry()
{
// Arrange
var context = CreateContext();

// Configure context to use client wins resolver
context.Configurator = builder =>
{
builder.UseDefaultConflictResolver(new ClientWinsConflictResolver());
builder.Entity<ClientMovie>(c =>
{
c.ClientName = "movies";
c.Endpoint = new Uri("/tables/movies", UriKind.Relative);
});
};

// Create a client movie and save it to generate operation
var clientMovie = new ClientMovie(TestData.Movies.BlackPanther)
{
Id = Guid.NewGuid().ToString("N"),
Title = "Client Title"
};
context.Movies.Add(clientMovie);
context.SaveChanges();

// Setup response for conflict followed by success
var serverMovie = new ClientMovie(TestData.Movies.BlackPanther)
{
Id = clientMovie.Id,
Title = "Server Title",
UpdatedAt = DateTimeOffset.UtcNow,
Version = Guid.NewGuid().ToString()
};
string serverJson = DatasyncSerializer.Serialize(serverMovie);

// First response is a conflict
context.Handler.AddResponseContent(serverJson, HttpStatusCode.Conflict);

// Second response (after resolution) is success
var finalMovie = new ClientMovie(TestData.Movies.BlackPanther)
{
Id = clientMovie.Id,
Title = "Client Title", // This should match the client version after resolution
UpdatedAt = DateTimeOffset.UtcNow.AddSeconds(1),
Version = Guid.NewGuid().ToString()
};
string finalJson = DatasyncSerializer.Serialize(finalMovie);
context.Handler.AddResponseContent(finalJson, HttpStatusCode.OK);

// Act
var result = await context.QueueManager.PushAsync([typeof(ClientMovie)], new PushOptions());

// Assert
result.IsSuccessful.Should().BeTrue();
result.CompletedOperations.Should().Be(1);
result.FailedRequests.Should().BeEmpty();

// Verify the database has the right value
var savedMovie = context.Movies.Find(clientMovie.Id);
savedMovie.Should().NotBeNull();
savedMovie!.Title.Should().Be("Client Title");
savedMovie.Version.Should().Be(finalMovie.Version);
}

[Fact]
public async Task PushAsync_WithClientWinsResolver_ShouldResolveConflictAndRetry()
{
Expand Down