diff --git a/CHANGELOG.md b/CHANGELOG.md index b4502bbfe1..7ddd72495e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Realm/Realm/Handles/AppHandle.cs b/Realm/Realm/Handles/AppHandle.cs index 59fc97f668..2a4746cc3e 100644 --- a/Realm/Realm/Handles/AppHandle.cs +++ b/Realm/Realm/Handles/AppHandle.cs @@ -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() @@ -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); diff --git a/Realm/Realm/Helpers/ExperimentalAttribute.cs b/Realm/Realm/Helpers/ExperimentalAttribute.cs new file mode 100644 index 0000000000..380af2e3f2 --- /dev/null +++ b/Realm/Realm/Helpers/ExperimentalAttribute.cs @@ -0,0 +1,64 @@ +#if !NET8_0_OR_GREATER + +using System.ComponentModel; + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Indicates that an API is experimental and it may change in the future. +/// +/// +/// 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. +///
+/// This is a polyfill of the ExperimentalAttribute added in .NET 8. +///
+[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 +{ + /// + /// Initializes a new instance of the class, specifying the ID that the compiler will use + /// when reporting a use of the API the attribute applies to. + /// + /// The ID that the compiler will use when reporting a use of the API the attribute applies to. + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + /// + /// Gets the ID that the compiler will use when reporting a use of the API the attribute applies to. + /// + /// The unique diagnostic ID. + /// + /// The diagnostic ID is shown in build output for warnings and errors. + /// This property represents the unique ID that can be used to suppress the warnings or errors, if needed. + /// + public string DiagnosticId { get; } + + /// + /// Gets or sets the URL for corresponding documentation. + /// The API accepts a format string instead of an actual URL, creating a generic URL that includes the diagnostic ID. + /// + /// The format string that represents a URL to corresponding documentation. + /// + /// An example format string is https://contoso.com/obsoletion-warnings/{0}. + /// + public string? UrlFormat { get; set; } +} + +#endif diff --git a/Realm/Realm/Sync/App.cs b/Realm/Realm/Sync/App.cs index febfbb525b..88036677bc 100644 --- a/Realm/Realm/Sync/App.cs +++ b/Realm/Realm/Sync/App.cs @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; @@ -270,6 +271,27 @@ public Task DeleteUserFromServerAsync(User user) return Handle.DeleteUserAsync(user.Handle); } + /// + /// Temporarily overrides the value from + /// with a new value used for communicating with the server. + /// + /// + /// The new uri that will be used for communicating with the server. If set to null, the base uri will + /// be reset to its default value. + /// + /// An awaitable that represents the asynchronous operation. + /// + /// The App will revert to using the value in [AppConfiguration] when it is restarted. + ///
+ /// This API must be called after sync sessions have been manually stopped and at a point + /// where the server at is reachable. Once the base uri has been + /// updated, sync sessions should be resumed and the user needs to reauthenticate. + ///
+ /// This API is experimental and subject to change without a major version increase. + ///
+ [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); + /// public override bool Equals(object? obj) { diff --git a/Tests/Realm.Tests/Sync/AppTests.cs b/Tests/Realm.Tests/Sync/AppTests.cs index f8a9c4c7bc..7f109511fc 100644 --- a/Tests/Realm.Tests/Sync/AppTests.cs +++ b/Tests/Realm.Tests/Sync/AppTests.cs @@ -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; @@ -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(() => 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)); + }); + } } } diff --git a/wrappers/src/app_cs.cpp b/wrappers/src/app_cs.cpp index 90fe1e144c..ccf3f694d1 100644 --- a/wrappers/src/app_cs.cpp +++ b/wrappers/src/app_cs.cpp @@ -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 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)