Skip to content

Commit

Permalink
RNET-1109: Add App.UpdateBaseUri (#3587)
Browse files Browse the repository at this point in the history
* Add App.UpdateBaseUri

* Add more tests

* Use CreateApp

* Add changelog
  • Loading branch information
nirinchev authored Apr 30, 2024
1 parent 66f54a1 commit 221d4c3
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 2 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
## vNext (TBD)

### Enhancements
* None
* Added an experimental API to update the base url for an application at runtime - `App.UpdateBaseUriAsync()`. This intended to be used for roaming between edge server and cloud. (Issue [#3521](https://github.com/realm/realm-dotnet/issues/3521))

### Fixed
* The returned value from `MongoClient.Collection.FindOneAsync` is now a nullable document to more explicitly convey that `null` may be returned in case no object matched the filter. ([PR #3586](https://github.com/realm/realm-dotnet/pull/3586))
* The returned value from `MongoClient.Collection.FindOneAsync` is now a nullable document to more explicitly convey that `null` may be returned in case no object matched the filter. (PR [#3586](https://github.com/realm/realm-dotnet/pull/3586))

### Compatibility
* Realm Studio: 15.0.0 or later.
Expand Down
23 changes: 23 additions & 0 deletions Realm/Realm/Handles/AppHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ public static extern IntPtr get_user_for_testing(

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_get_default_url", CallingConvention = CallingConvention.Cdecl)]
public static extern StringValue get_default_url(out NativeException ex);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_update_base_url", CallingConvention = CallingConvention.Cdecl)]
public static extern void update_base_uri(AppHandle appHandle,
[MarshalAs(UnmanagedType.LPWStr)] string url_buf, IntPtr url_len,
IntPtr tcs_ptr,
out NativeException ex);
}

static AppHandle()
Expand Down Expand Up @@ -343,6 +349,23 @@ public Uri GetBaseUri()
return new Uri(uriString);
}

public async Task UpdateBaseUriAsync(Uri? newUri)
{
var tcs = new TaskCompletionSource();
var tcsHandle = GCHandle.Alloc(tcs);
try
{
var url = newUri?.ToString().TrimEnd('/') ?? string.Empty;
NativeMethods.update_base_uri(this, url, (IntPtr)url.Length, GCHandle.ToIntPtr(tcsHandle), out var ex);
ex.ThrowIfNecessary();
await tcs.Task;
}
finally
{
tcsHandle.Free();
}
}

public string GetId()
{
var value = NativeMethods.get_id(this, out var ex);
Expand Down
64 changes: 64 additions & 0 deletions Realm/Realm/Helpers/ExperimentalAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#if !NET8_0_OR_GREATER

using System.ComponentModel;

namespace System.Diagnostics.CodeAnalysis;

/// <summary>
/// Indicates that an API is experimental and it may change in the future.
/// </summary>
/// <remarks>
/// This attribute allows call sites to be flagged with a diagnostic that indicates that an experimental
/// feature is used. Authors can use this attribute to ship preview features in their assemblies.
/// <br/>
/// This is a polyfill of the ExperimentalAttribute added in .NET 8.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
[AttributeUsage(
AttributeTargets.Assembly
| AttributeTargets.Module
| AttributeTargets.Class
| AttributeTargets.Struct
| AttributeTargets.Enum
| AttributeTargets.Constructor
| AttributeTargets.Method
| AttributeTargets.Property
| AttributeTargets.Field
| AttributeTargets.Event
| AttributeTargets.Interface
| AttributeTargets.Delegate,
Inherited = false)]
public sealed class ExperimentalAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ExperimentalAttribute"/> class, specifying the ID that the compiler will use
/// when reporting a use of the API the attribute applies to.
/// </summary>
/// <param name="diagnosticId">The ID that the compiler will use when reporting a use of the API the attribute applies to.</param>
public ExperimentalAttribute(string diagnosticId)
{
DiagnosticId = diagnosticId;
}

/// <summary>
/// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to.
/// </summary>
/// <value>The unique diagnostic ID.</value>
/// <remarks>
/// <para>The diagnostic ID is shown in build output for warnings and errors.</para>
/// <para>This property represents the unique ID that can be used to suppress the warnings or errors, if needed.</para>
/// </remarks>
public string DiagnosticId { get; }

/// <summary>
/// <para>Gets or sets the URL for corresponding documentation.</para>
/// <para>The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID.</para>
/// </summary>
/// <value>The format string that represents a URL to corresponding documentation.</value>
/// <remarks>
/// <para>An example format string is <c>https://contoso.com/obsoletion-warnings/{0}</c>.</para>
/// </remarks>
public string? UrlFormat { get; set; }
}

#endif
22 changes: 22 additions & 0 deletions Realm/Realm/Sync/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
////////////////////////////////////////////////////////////////////////////

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -270,6 +271,27 @@ public Task DeleteUserFromServerAsync(User user)
return Handle.DeleteUserAsync(user.Handle);
}

/// <summary>
/// Temporarily overrides the <see cref="AppConfiguration.BaseUri"/> value from <see cref="AppConfiguration"/>
/// with a new <paramref name="newUri"/> value used for communicating with the server.
/// </summary>
/// <param name="newUri">
/// The new uri that will be used for communicating with the server. If set to <c>null</c>, the base uri will
/// be reset to its default value.
/// </param>
/// <returns>An awaitable <see cref="Task"/> that represents the asynchronous operation.</returns>
/// <remarks>
/// The App will revert to using the value in [AppConfiguration] when it is restarted.
/// <br/>
/// This API must be called after sync sessions have been manually stopped and at a point
/// where the server at <paramref name="newUri"/> is reachable. Once the base uri has been
/// updated, sync sessions should be resumed and the user needs to reauthenticate.
/// <br/>
/// This API is experimental and subject to change without a major version increase.
/// </remarks>
[Experimental("Rlm001", UrlFormat = "www.mongodb.com/docs/atlas/app-services/edge-server/connect/#roaming-between-edge-servers")]
public Task UpdateBaseUriAsync(Uri? newUri) => Handle.UpdateBaseUriAsync(newUri);

/// <inheritdoc />
public override bool Equals(object? obj)
{
Expand Down
40 changes: 40 additions & 0 deletions Tests/Realm.Tests/Sync/AppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Baas;
using NUnit.Framework;
using Realms.Logging;
using Realms.PlatformHelpers;
Expand Down Expand Up @@ -398,5 +399,44 @@ public void RealmConfigurationBaseUrl_ReturnsExpectedValue()
var config = new AppConfiguration("abc");
Assert.That(config.BaseUri, Is.EqualTo(new Uri("https://services.cloud.mongodb.com")));
}

[Test]
public void App_UpdateBaseUri_UpdatesBaseUri()
{
SyncTestHelpers.RunBaasTestAsync(async () =>
{
var appConfig = SyncTestHelpers.GetAppConfig(AppConfigType.FlexibleSync);
appConfig.BaseUri = new Uri("https://services.mongodb.com");
var app = CreateApp(appConfig);

Assert.That(app.BaseUri, Is.EqualTo(new Uri("https://services.mongodb.com")));

#pragma warning disable Rlm001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
await app.UpdateBaseUriAsync(SyncTestHelpers.BaasUri!);
#pragma warning restore Rlm001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

Assert.That(app.BaseUri, Is.EqualTo(SyncTestHelpers.BaasUri));
});
}

[Test]
public void App_UpdateBaseUri_WhenUnreachable_Throws()
{
SyncTestHelpers.RunBaasTestAsync(async () =>
{
var appConfig = SyncTestHelpers.GetAppConfig(AppConfigType.FlexibleSync);
appConfig.BaseUri = new Uri("https://services.mongodb.com");
var app = CreateApp(appConfig);

Assert.That(app.BaseUri, Is.EqualTo(new Uri("https://services.mongodb.com")));

#pragma warning disable Rlm001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
var ex = await TestHelpers.AssertThrows<AppException>(() => app.UpdateBaseUriAsync(new Uri("https://google.com")));
#pragma warning restore Rlm001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

Assert.That(ex.Message, Does.Contain("404"));
Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
});
}
}
}
19 changes: 19 additions & 0 deletions wrappers/src/app_cs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,25 @@ extern "C" {
});
}

REALM_EXPORT void shared_app_update_base_url(SharedApp& app, uint16_t* url_buf, size_t url_len, void* tcs_ptr, NativeException::Marshallable& ex)
{
return handle_errors(ex, [&]() {
std::string url(Utf16StringAccessor(url_buf, url_len));

app->update_base_url(url, [tcs_ptr](util::Optional<AppError> err) {
if (err) {
auto& err_copy = *err;
MarshaledAppError app_error(err_copy);

s_void_callback(tcs_ptr, app_error);
}
else {
s_void_callback(tcs_ptr, MarshaledAppError());
}
});
});
}

#pragma region EmailPassword

REALM_EXPORT void shared_app_email_register_user(SharedApp& app, uint16_t* username_buf, size_t username_len, uint16_t* password_buf, size_t password_len, void* tcs_ptr, NativeException::Marshallable& ex)
Expand Down

0 comments on commit 221d4c3

Please sign in to comment.