diff --git a/.github/actions/benchmark-uploader/package-lock.json b/.github/actions/benchmark-uploader/package-lock.json index ba6e45d5ef..168ba5c810 100644 --- a/.github/actions/benchmark-uploader/package-lock.json +++ b/.github/actions/benchmark-uploader/package-lock.json @@ -4118,9 +4118,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -7198,9 +7198,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "workerpool": { diff --git a/.github/templates/test-code-coverage.yml b/.github/templates/test-code-coverage.yml index fb9ce22bda..68d8276755 100644 --- a/.github/templates/test-code-coverage.yml +++ b/.github/templates/test-code-coverage.yml @@ -19,7 +19,7 @@ jobs: echo "${{ github.workspace }}/tools" >> $GITHUB_PATH - #@ template.replace(dotnetPublish("Tests/Realm.Tests", "net7.0", "linux-x64", { "RealmTestsStandaloneExe": "true" })) - name: Run the tests - run: #@ "./tools/coverlet ${{ steps.dotnet-publish.outputs.executable-path }} -t ${{ steps.dotnet-publish.outputs.executable-path }}/Realm.Tests -a '--result=TestResults.Linux.xml --labels=After" + baasTestArgs("code-coverage") + "' -f lcov -o ./report.lcov --exclude '[Realm.Tests]*' --exclude '[Realm.Fody]*'" + run: #@ "./tools/coverlet ${{ steps.dotnet-publish.outputs.executable-path }} -t ${{ steps.dotnet-publish.outputs.executable-path }}/Realm.Tests -a '--result=TestResults.Linux.xml --labels=After" + baasTestArgs("code-coverage") + "' -f lcov -o ./report.lcov --exclude '[Realm.Tests]*' --exclude '[Realm.Fody]*' --exclude '[Realm.PlatformHelpers]*'" - name: Publish Coverage id: publish-coveralls uses: #@ actionCoveralls diff --git a/.github/workflows/test-code-coverage.yml b/.github/workflows/test-code-coverage.yml index a70146b920..12f25b33ae 100755 --- a/.github/workflows/test-code-coverage.yml +++ b/.github/workflows/test-code-coverage.yml @@ -160,7 +160,7 @@ jobs: run: echo 'executable-path=./Tests/Realm.Tests/bin/Release/net7.0/linux-x64' >> $GITHUB_OUTPUT shell: bash - name: Run the tests - run: ./tools/coverlet ${{ steps.dotnet-publish.outputs.executable-path }} -t ${{ steps.dotnet-publish.outputs.executable-path }}/Realm.Tests -a '--result=TestResults.Linux.xml --labels=After --baasurl=${{ inputs.realmUrl }} --baascluster=${{ inputs.clusterName }} --baasapikey=${{ secrets.AtlasPublicKey}} --baasprivateapikey=${{ secrets.AtlasPrivateKey}} --baasprojectid=${{ secrets.AtlasProjectId }} --baasdifferentiator=code-coverage' -f lcov -o ./report.lcov --exclude '[Realm.Tests]*' --exclude '[Realm.Fody]*' + run: ./tools/coverlet ${{ steps.dotnet-publish.outputs.executable-path }} -t ${{ steps.dotnet-publish.outputs.executable-path }}/Realm.Tests -a '--result=TestResults.Linux.xml --labels=After --baasurl=${{ inputs.realmUrl }} --baascluster=${{ inputs.clusterName }} --baasapikey=${{ secrets.AtlasPublicKey}} --baasprivateapikey=${{ secrets.AtlasPrivateKey}} --baasprojectid=${{ secrets.AtlasProjectId }} --baasdifferentiator=code-coverage' -f lcov -o ./report.lcov --exclude '[Realm.Tests]*' --exclude '[Realm.Fody]*' --exclude '[Realm.PlatformHelpers]*' - name: Publish Coverage id: publish-coveralls uses: coverallsapp/github-action@ca2a9dd5a20b12fa9aa428e0a6418a60a38bab22 diff --git a/CHANGELOG.md b/CHANGELOG.md index ea32c28fd8..b0407b3472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ ## vNext (TBD) +### Breaking Changes +* `AppConfiguration.LocalAppName` and `AppConfiguration.LocalAppVersion` have been deprecated and will be removed in a future version. They have never had an effect as the values supplied by the SDK was never sent to the server. (PR [#3387](https://github.com/realm/realm-dotnet/pull/3387)) + ### Enhancements -* None +* Added `App.BaseFilePath`, `App.BaseUri`, and `App.Id` properties that return the values supplied in `AppConfiguration`. (PR [#3385](https://github.com/realm/realm-dotnet/pull/3385)) +* Added `AppConfiguration.UseAppCache` property that controls whether the `App` instance returned from `App.Create` should be cached or not. The general recommendation is to not set it (i.e. leave the default value of `true`), but it can be useful when writing unit tests. (Issue [#3382](https://github.com/realm/realm-dotnet/issues/3382)). ### Fixed +* Fixed a Unity Editor crash when the domain is reloaded while a `Realm.GetInstanceAsync` operation is in progress. (Issue [#3344](https://github.com/realm/realm-dotnet/issues/3344)) +* Fixed the implementation `App.Equals` and `App.GetHashCode` to return correct results, particularly when the `App` instance is cached. (PR [#3385](https://github.com/realm/realm-dotnet/pull/3385)) * Fixed an issue where building for Android on Unity would fail with "Could not analyze the user's assembly. Object reference not set to an instance of an object". (Issue [#3380](https://github.com/realm/realm-dotnet/issues/3380)) ### Compatibility diff --git a/Realm/Realm/Configurations/PartitionSyncConfiguration.cs b/Realm/Realm/Configurations/PartitionSyncConfiguration.cs index fd05d217a6..5c5e5de479 100644 --- a/Realm/Realm/Configurations/PartitionSyncConfiguration.cs +++ b/Realm/Realm/Configurations/PartitionSyncConfiguration.cs @@ -120,7 +120,7 @@ private PartitionSyncConfiguration(User user, RealmValue partition, string? path internal override IDisposable? OnBeforeRealmOpen(AsyncOpenTaskHandle handle) { var onProgress = OnProgress; - if (onProgress == null) + if (onProgress is null) { return null; } diff --git a/Realm/Realm/Configurations/SyncConfigurationBase.cs b/Realm/Realm/Configurations/SyncConfigurationBase.cs index f65fb9fefc..2bbda590e6 100644 --- a/Realm/Realm/Configurations/SyncConfigurationBase.cs +++ b/Realm/Realm/Configurations/SyncConfigurationBase.cs @@ -157,7 +157,7 @@ internal virtual Native.SyncConfiguration CreateNativeSyncConfiguration() { SyncUserHandle = User.Handle, session_stop_policy = SessionStopPolicy, - schema_mode = _schema == null ? SchemaMode.AdditiveDiscovered : SchemaMode.AdditiveExplicit, + schema_mode = _schema is null ? SchemaMode.AdditiveDiscovered : SchemaMode.AdditiveExplicit, client_resync_mode = ClientResetHandler.ClientResetMode, cancel_waits_on_nonfatal_error = CancelAsyncOperationsOnNonFatalErrors, }; diff --git a/Realm/Realm/DatabaseTypes/RealmValue.cs b/Realm/Realm/DatabaseTypes/RealmValue.cs index 77eacb5932..c82f8d61dc 100644 --- a/Realm/Realm/DatabaseTypes/RealmValue.cs +++ b/Realm/Realm/DatabaseTypes/RealmValue.cs @@ -394,7 +394,7 @@ public Guid AsGuid() private RealmInteger AsRealmInteger(T value) where T : struct, IComparable, IFormattable, IConvertible, IEquatable { - if (_objectHandle == null) + if (_objectHandle is null) { return new(value); } diff --git a/Realm/Realm/Handles/AppHandle.cs b/Realm/Realm/Handles/AppHandle.cs index 7221a42521..2d08b1185a 100644 --- a/Realm/Realm/Handles/AppHandle.cs +++ b/Realm/Realm/Handles/AppHandle.cs @@ -103,6 +103,19 @@ public static extern IntPtr get_user_for_testing( [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_clear_cached_apps", CallingConvention = CallingConvention.Cdecl)] public static extern void clear_cached_apps(out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_get_base_file_path", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_base_file_path(AppHandle app, IntPtr buffer, IntPtr buffer_length, out NativeException ex); + + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_get_base_uri", CallingConvention = CallingConvention.Cdecl)] + public static extern StringValue get_base_uri(AppHandle app, out NativeException ex); + + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_get_id", CallingConvention = CallingConvention.Cdecl)] + public static extern StringValue get_id(AppHandle app, out NativeException ex); + + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_is_same_instance", CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool is_same_instance(AppHandle lhs, AppHandle rhs, out NativeException ex); + public static class EmailPassword { [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_app_email_register_user", CallingConvention = CallingConvention.Cdecl)] @@ -349,6 +362,36 @@ public SyncUserHandle GetUserForTesting(string id, string refreshToken, string a return new SyncUserHandle(result); } + public string GetBaseFilePath() + { + return MarshalHelpers.GetString((IntPtr buffer, IntPtr length, out bool isNull, out NativeException ex) => + { + isNull = false; + return NativeMethods.get_base_file_path(this, buffer, length, out ex); + })!; + } + + public Uri GetBaseUri() + { + var value = NativeMethods.get_base_uri(this, out var ex); + ex.ThrowIfNecessary(); + return new Uri(value!); + } + + public string GetId() + { + var value = NativeMethods.get_id(this, out var ex); + ex.ThrowIfNecessary(); + return value!; + } + + public bool IsSameInstance(AppHandle other) + { + var result = NativeMethods.is_same_instance(this, other, out var ex); + ex.ThrowIfNecessary(); + return result; + } + protected override void Unbind() => NativeMethods.destroy(handle); [MonoPInvokeCallback(typeof(NativeMethods.UserCallback))] diff --git a/Realm/Realm/Handles/AsyncOpenTaskHandle.cs b/Realm/Realm/Handles/AsyncOpenTaskHandle.cs index 488739bdc9..876c9b6dcb 100644 --- a/Realm/Realm/Handles/AsyncOpenTaskHandle.cs +++ b/Realm/Realm/Handles/AsyncOpenTaskHandle.cs @@ -17,12 +17,15 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Collections.Concurrent; using System.Runtime.InteropServices; namespace Realms { internal class AsyncOpenTaskHandle : StandaloneHandle { + private static ConcurrentDictionary _handles = new(); + private static class NativeMethods { [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_asyncopentask_destroy", CallingConvention = CallingConvention.Cdecl)] @@ -40,6 +43,7 @@ private static class NativeMethods public AsyncOpenTaskHandle(IntPtr handle) : base(handle) { + _handles.TryAdd(this, true); } public void Cancel() @@ -48,7 +52,30 @@ public void Cancel() ex.ThrowIfNecessary(); } - protected override void Unbind() => NativeMethods.destroy(handle); + protected override void Unbind() + { + _handles.TryRemove(this, out _); + + NativeMethods.destroy(handle); + } + + /// + /// Cancels all in-flight async open tasks. This should only be used when the domain is being torn down. + /// The case this handles is: + /// 1. GetInstanceAsync. + /// 2. Domain Reload wipes all coordinator caches. + /// 3. AsyncOpen completes, calls back into managed (because s_can_call_managed is true again). + /// 4. Undefined behavior as the state from before the domain reload is no longer valid. + /// + /// This fixes the issue reported in https://github.com/realm/realm-dotnet/issues/3344. + public static void CancelInFlightTasks() + { + var keys = _handles.Keys; + foreach (var value in keys) + { + value.Cancel(); + } + } public ulong RegisterProgressNotifier(GCHandle managedHandle) { diff --git a/Realm/Realm/Handles/SessionHandle.cs b/Realm/Realm/Handles/SessionHandle.cs index a22d536940..f6bd2d47cf 100644 --- a/Realm/Realm/Handles/SessionHandle.cs +++ b/Realm/Realm/Handles/SessionHandle.cs @@ -342,7 +342,7 @@ private static IntPtr NotifyBeforeClientReset(IntPtr beforeFrozen, IntPtr manage } catch (Exception ex) { - var handlerType = syncConfig == null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name; + var handlerType = syncConfig is null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name; Logger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnBeforeReset during a client reset: {ex}"); var exHandle = GCHandle.Alloc(ex); @@ -380,7 +380,7 @@ private static IntPtr NotifyAfterClientReset(IntPtr beforeFrozen, IntPtr after, } catch (Exception ex) { - var handlerType = syncConfig == null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name; + var handlerType = syncConfig is null ? "ClientResetHandler" : syncConfig.ClientResetHandler.GetType().Name; Logger.Default.Log(LogLevel.Error, $"An error has occurred while executing {handlerType}.OnAfterReset during a client reset: {ex}"); var exHandle = GCHandle.Alloc(ex); @@ -429,7 +429,7 @@ private static void HandleSessionPropertyChangedCallback(IntPtr managedSessionHa _ => throw new NotSupportedException($"Unexpected notifiable property value: {property}") }; var session = (Session)GCHandle.FromIntPtr(managedSessionHandle).Target!; - if (session == null) + if (session is null) { // We're taking a weak handle to the session, so it's possible that it's been collected return; diff --git a/Realm/Realm/Handles/SyncUserHandle.cs b/Realm/Realm/Handles/SyncUserHandle.cs index e75bc65e3e..21f132115f 100644 --- a/Realm/Realm/Handles/SyncUserHandle.cs +++ b/Realm/Realm/Handles/SyncUserHandle.cs @@ -301,9 +301,9 @@ public async Task CreateApiKeyAsync(AppHandle app, string name) var result = await tcs.Task; - Debug.Assert(result == null || result.Length <= 1, "The result of the fetch operation should be either null, or an array of 0 or 1 elements."); + Debug.Assert(result is null || result.Length <= 1, "The result of the fetch operation should be either null, or an array of 0 or 1 elements."); - return result == null || result.Length == 0 ? null : result.Single(); + return result is null || result.Length == 0 ? null : result.Single(); } finally { diff --git a/Realm/Realm/Helpers/LinqHelper.cs b/Realm/Realm/Helpers/LinqHelper.cs deleted file mode 100644 index 82c22425f1..0000000000 --- a/Realm/Realm/Helpers/LinqHelper.cs +++ /dev/null @@ -1,58 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2018 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -namespace Realms.Helpers -{ - internal static class LinqHelper - { - public static string[] ToStringPaths(this Expression>[] expressions) - { - return expressions.Select(ToStringPath).ToArray(); - } - - public static string ToStringPath(this Expression> expression) - { - var visitor = new PropertyVisitor(); - visitor.Visit(expression.Body); - visitor.Path.Reverse(); - return string.Join(".", visitor.Path); - } - - private class PropertyVisitor : ExpressionVisitor - { - public List Path { get; } = new List(); - - protected override Expression VisitMember(MemberExpression node) - { - if (!(node.Member is PropertyInfo)) - { - throw new ArgumentException("The path can only contain properties", nameof(node)); - } - - Path.Add(node.Member.Name); - return base.VisitMember(node); - } - } - } -} diff --git a/Realm/Realm/Helpers/Observer.cs b/Realm/Realm/Helpers/Observer.cs deleted file mode 100644 index c91d360fcf..0000000000 --- a/Realm/Realm/Helpers/Observer.cs +++ /dev/null @@ -1,51 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2017 Realm Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -//////////////////////////////////////////////////////////////////////////// - -using System; - -namespace Realms.Sync -{ - internal class Observer : IObserver - { - private readonly Action? _onNext; - private readonly Action? _onCompleted; - private readonly Action? _onError; - - public Observer(Action? onNext = null, Action? onCompleted = null, Action? onError = null) - { - _onNext = onNext; - _onCompleted = onCompleted; - _onError = onError; - } - - public void OnCompleted() - { - _onCompleted?.Invoke(); - } - - public void OnError(Exception error) - { - _onError?.Invoke(error); - } - - public void OnNext(T value) - { - _onNext?.Invoke(value); - } - } -} diff --git a/Realm/Realm/Native/AppConfiguration.cs b/Realm/Realm/Native/AppConfiguration.cs index 18d91c8e42..a1effe052d 100644 --- a/Realm/Realm/Native/AppConfiguration.cs +++ b/Realm/Realm/Native/AppConfiguration.cs @@ -63,32 +63,6 @@ internal string BaseUrl } } - [MarshalAs(UnmanagedType.LPWStr)] - private string? local_app_name; - private IntPtr local_app_name_len; - - internal string? LocalAppName - { - set - { - local_app_name = value; - local_app_name_len = value.IntPtrLength(); - } - } - - [MarshalAs(UnmanagedType.LPWStr)] - private string? local_app_version; - private IntPtr local_app_version_len; - - internal string? LocalAppVersion - { - set - { - local_app_version = value; - local_app_version_len = value.IntPtrLength(); - } - } - internal UInt64 default_request_timeout_ms; private MetadataPersistenceMode metadata_persistence; @@ -116,5 +90,8 @@ internal MetadataPersistenceMode? MetadataPersistence internal UInt64 sync_pong_keep_alive_timeout_ms; internal UInt64 sync_fast_reconnect_limit; + + [MarshalAs(UnmanagedType.U1)] + internal bool use_cache; } } diff --git a/Realm/Realm/Native/NativeCommon.cs b/Realm/Realm/Native/NativeCommon.cs index 4ca7471650..c72d34ce07 100644 --- a/Realm/Realm/Native/NativeCommon.cs +++ b/Realm/Realm/Native/NativeCommon.cs @@ -99,6 +99,7 @@ public static void CleanupNativeResources(string reason) sw.Start(); AppHandle.ForceCloseHandles(); + AsyncOpenTaskHandle.CancelInFlightTasks(); SharedRealmHandle.ForceCloseNativeRealms(); sw.Stop(); diff --git a/Realm/Realm/Native/PrimitiveValue.cs b/Realm/Realm/Native/PrimitiveValue.cs index 2275faae80..3471ced265 100644 --- a/Realm/Realm/Native/PrimitiveValue.cs +++ b/Realm/Realm/Native/PrimitiveValue.cs @@ -197,7 +197,7 @@ public static PrimitiveValue Object(ObjectHandle handle) { return new PrimitiveValue { - Type = handle == null ? RealmValueType.Null : RealmValueType.Object, + Type = handle is null ? RealmValueType.Null : RealmValueType.Object, link_value = new LinkValue { object_ptr = handle?.DangerousGetHandle() ?? IntPtr.Zero diff --git a/Realm/Realm/Native/SynchronizationContextScheduler.cs b/Realm/Realm/Native/SynchronizationContextScheduler.cs index 375efd4052..c31d4a2664 100644 --- a/Realm/Realm/Native/SynchronizationContextScheduler.cs +++ b/Realm/Realm/Native/SynchronizationContextScheduler.cs @@ -132,7 +132,7 @@ internal void Invalidate() private static IntPtr GetCurrentSynchronizationContext() { var context = SynchronizationContext.Current; - if (context == null) + if (context is null) { return IntPtr.Zero; } diff --git a/Realm/Realm/Realm.cs b/Realm/Realm/Realm.cs index 4ced533e29..1840eb514b 100644 --- a/Realm/Realm/Realm.cs +++ b/Realm/Realm/Realm.cs @@ -246,12 +246,12 @@ public Session SyncSession throw new NotSupportedException("Realm.SyncSession is only valid for synchronized Realms (i.e. ones that are opened with FlexibleSyncConfiguration or PartitionSyncConfiguration)."); } - if (_sessionRef == null || !_sessionRef.TryGetTarget(out var session) || session.IsClosed) + if (_sessionRef is null || !_sessionRef.TryGetTarget(out var session) || session.IsClosed) { var sessionHandle = SharedRealmHandle.GetSession(); session = new Session(sessionHandle); - if (_sessionRef == null) + if (_sessionRef is null) { _sessionRef = new WeakReference(session); } @@ -318,7 +318,7 @@ internal Realm(SharedRealmHandle sharedRealmHandle, RealmConfigurationBase confi } } - if (state == null) + if (state is null) { state = new State(); sharedRealmHandle.SetManagedStateHandle(state); @@ -341,7 +341,7 @@ private Metadata CreateRealmObjectMetadata(ObjectSchema schema) if (schema.Type != null && !Config.IsDynamic) { var wovenAtt = schema.Type.GetCustomAttribute(); - if (wovenAtt == null) + if (wovenAtt is null) { throw new RealmException($"Fody not properly installed. {schema.Type.FullName} is a RealmObjectBase but has not been woven."); } @@ -408,7 +408,7 @@ private void NotifyChanged(EventArgs e) internal void NotifyError(Exception ex) { - if (Error == null) + if (Error is null) { Logger.LogDefault(LogLevel.Error, "A realm-level exception has occurred. To handle and react to those, subscribe to the Realm.Error event."); } @@ -486,7 +486,7 @@ private void ThrowIfFrozen(string message) private bool Equals(Realm? other) { - if (other == null) + if (other is null) { return false; } @@ -758,7 +758,7 @@ public Transaction BeginWrite() SharedRealmHandle.BeginTransaction(); - Debug.Assert(_activeTransaction == null, "There should be no active transaction"); + Debug.Assert(_activeTransaction is null, "There should be no active transaction"); return _activeTransaction = new Transaction(this); } @@ -878,7 +878,7 @@ public async Task BeginWriteAsync(CancellationToken cancellationTok await SharedRealmHandle.BeginTransactionAsync(synchronizationContext, cancellationToken); - Debug.Assert(_activeTransaction == null, "There should be no active transaction"); + Debug.Assert(_activeTransaction is null, "There should be no active transaction"); return _activeTransaction = new Transaction(this); } @@ -1344,11 +1344,6 @@ internal void DrainTransactionQueue() internal void ExecuteOutsideTransaction(Action action) { - if (action == null) - { - return; - } - if (IsInTransaction) { _state.AfterTransactionQueue.Enqueue(action); diff --git a/Realm/Realm/Schema/RealmSchema.cs b/Realm/Realm/Schema/RealmSchema.cs index fd679e15c9..95d89e28b8 100644 --- a/Realm/Realm/Schema/RealmSchema.cs +++ b/Realm/Realm/Schema/RealmSchema.cs @@ -193,7 +193,7 @@ internal static RealmSchema CreateFromObjectStoreSchema(Native.Schema nativeSche /// null if is null; a containing the supplied s otherwise. /// [return: NotNullIfNotNull("objects")] - public static implicit operator RealmSchema?(ObjectSchema[]? objects) => objects == null ? null : new Builder(objects).Build(); + public static implicit operator RealmSchema?(ObjectSchema[]? objects) => objects is null ? null : new Builder(objects).Build(); /// /// Constructs a from a list of instances. @@ -206,7 +206,7 @@ internal static RealmSchema CreateFromObjectStoreSchema(Native.Schema nativeSche /// null if is null; a containing the supplied s otherwise. /// [return: NotNullIfNotNull("objects")] - public static implicit operator RealmSchema?(List? objects) => objects == null ? null : new Builder(objects).Build(); + public static implicit operator RealmSchema?(List? objects) => objects is null ? null : new Builder(objects).Build(); /// /// Constructs a from an array of instances. @@ -217,7 +217,7 @@ internal static RealmSchema CreateFromObjectStoreSchema(Native.Schema nativeSche /// /// [return: NotNullIfNotNull("objects")] - public static implicit operator RealmSchema?(Type[]? objects) => objects == null ? null : new Builder(objects).Build(); + public static implicit operator RealmSchema?(Type[]? objects) => objects is null ? null : new Builder(objects).Build(); /// /// Constructs a from a List of instances. @@ -228,7 +228,7 @@ internal static RealmSchema CreateFromObjectStoreSchema(Native.Schema nativeSche /// /// [return: NotNullIfNotNull("objects")] - public static implicit operator RealmSchema?(List? objects) => objects == null ? null : new Builder(objects).Build(); + public static implicit operator RealmSchema?(List? objects) => objects is null ? null : new Builder(objects).Build(); /// /// Constructs a from a HashSet of instances. @@ -239,7 +239,7 @@ internal static RealmSchema CreateFromObjectStoreSchema(Native.Schema nativeSche /// /// [return: NotNullIfNotNull("objects")] - public static implicit operator RealmSchema?(HashSet? objects) => objects == null ? null : new Builder(objects).Build(); + public static implicit operator RealmSchema?(HashSet? objects) => objects is null ? null : new Builder(objects).Build(); /// /// A convenience operator to construct a from a by calling the diff --git a/Realm/Realm/Sync/App.cs b/Realm/Realm/Sync/App.cs index d7458ddf2c..6fea27efbd 100644 --- a/Realm/Realm/Sync/App.cs +++ b/Realm/Realm/Sync/App.cs @@ -48,12 +48,7 @@ namespace Realms.Sync ///
/// To create an app that is linked with a remote Realm App initialize Realm and configure the App as shown below: /// - /// var appConfig = new AppConfiguration("my-realm-app-id") - /// { - /// LocalAppName = "My amazing iOS app", - /// LocalAppVersion = "1.2.3" - /// }; - /// + /// var appConfig = new AppConfiguration("my-realm-app-id"); /// var app = new App(appConfig); /// /// After configuring the App you can start managing users, configure Synchronized Realms, call remote Realm Functions, and access remote data through Mongo Collections. @@ -119,6 +114,27 @@ public class App .Select(handle => new User(handle, this)) .ToArray(); + /// + /// Gets the root folder relative to which all local data for this application will be stored. This data includes + /// metadata for users and synchronized Realms. + /// + /// The app's base path. + /// + public string BaseFilePath => Handle.GetBaseFilePath(); + + /// + /// Gets the base url for this Realm application. + /// + /// The app's base url. + /// + public Uri BaseUri => Handle.GetBaseUri(); + + /// + /// Gets the unique app id that identifies the Realm application. + /// + /// The Atlas App Services App's id. + public string Id => Handle.GetId(); + internal App(AppHandle handle) { Handle = handle; @@ -129,7 +145,6 @@ internal App(AppHandle handle) ///
/// The , specifying key parameters for the app behavior. /// An instance can now be used to login users, call functions, or open synchronized Realms. - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The http client is owned by the app and must be kept alive.")] public static App Create(AppConfiguration config) { Argument.NotNull(config, nameof(config)); @@ -148,15 +163,13 @@ public static App Create(AppConfiguration config) } } - var httpClient = config.HttpClientHandler == null ? new HttpClient() : new HttpClient(config.HttpClientHandler); + var httpClient = config.HttpClientHandler is null ? new HttpClient() : new HttpClient(config.HttpClientHandler); var clientHandle = GCHandle.Alloc(httpClient); var nativeConfig = new Native.AppConfiguration { AppId = config.AppId, BaseFilePath = config.BaseFilePath, BaseUrl = config.BaseUri.ToString().TrimEnd('/'), - LocalAppName = config.LocalAppName, - LocalAppVersion = config.LocalAppVersion, MetadataPersistence = config.MetadataPersistenceMode, default_request_timeout_ms = (ulong)config.DefaultRequestTimeout.TotalMilliseconds, managed_http_client = GCHandle.ToIntPtr(clientHandle), @@ -165,6 +178,7 @@ public static App Create(AppConfiguration config) sync_fast_reconnect_limit = (ulong)syncTimeouts.FastReconnectLimit.TotalMilliseconds, sync_ping_keep_alive_period_ms = (ulong)syncTimeouts.PingKeepAlivePeriod.TotalMilliseconds, sync_pong_keep_alive_timeout_ms = (ulong)syncTimeouts.PongKeepAliveTimeout.TotalMilliseconds, + use_cache = config.UseAppCache, }; var handle = AppHandle.CreateApp(nativeConfig, config.MetadataEncryptionKey); @@ -246,6 +260,44 @@ public Task DeleteUserFromServerAsync(User user) return Handle.DeleteUserAsync(user.Handle); } + /// + public override bool Equals(object? obj) + { + if (obj is not App app) + { + return false; + } + + return Handle.IsSameInstance(app.Handle); + } + + /// + public override int GetHashCode() => Id.GetHashCode(); + + /// + /// Determines whether two instances are equal. + /// + /// The first app to compare. + /// The second app to compare. + /// true if the two instances are equal; false otherwise. + public static bool operator ==(App? app1, App? app2) + { + if (app1 is null || app2 is null) + { + return app1 is null && app2 is null; + } + + return app1.Equals(app2); + } + + /// + /// Determines whether two instances are different. + /// + /// The first app to compare. + /// The second app to compare. + /// true if the two instances are different; false otherwise. + public static bool operator !=(App? app1, App? app2) => !(app1 == app2); + /// /// A sync manager, handling synchronization of local Realm with MongoDB Atlas. It is always scoped to a /// particular app and can only be accessed via . diff --git a/Realm/Realm/Sync/AppConfiguration.cs b/Realm/Realm/Sync/AppConfiguration.cs index b44d428814..928e8bad84 100644 --- a/Realm/Realm/Sync/AppConfiguration.cs +++ b/Realm/Realm/Sync/AppConfiguration.cs @@ -65,24 +65,16 @@ public string BaseFilePath /// /// Gets or sets the local app's name. /// - /// - /// The local app name is typically used to differentiate between client applications that use the same - /// Atlas App Services app. These can be the same conceptual app developed for different platforms, or - /// significantly different client side applications that operate on the same data - e.g. an event managing - /// service that has different clients apps for organizers and attendees. - /// /// The friendly name identifying the current client application. - /// + [Obsolete("This property has no effect and will be removed in a future version.")] public string? LocalAppName { get; set; } /// /// Gets or sets the local app's version. /// - /// - /// The local app version is typically used to differentiate between versions of the same client application. - /// /// The client application's version. /// + [Obsolete("This property has no effect and will be removed in a future version.")] public string? LocalAppVersion { get; set; } /// @@ -145,6 +137,17 @@ public byte[]? MetadataEncryptionKey /// The sync timeout options applied to synchronized Realms. public SyncTimeoutOptions SyncTimeoutOptions { get; set; } = new(); + /// + /// Gets or sets a value indicating whether to cache app instances created with this configuration. + /// + /// + /// When an app is created using , the default behavior is + /// to get or add the instance from a cache keyed on the app id. This has certain + /// performance benefits when calling multiple times. + /// + /// true if the app should be cached; false otherwise. Default value is true. + public bool UseAppCache { get; set; } = true; + /// /// Initializes a new instance of the class with the specified . /// diff --git a/Realm/Realm/Sync/FlexibleSync/SubscriptionSet.cs b/Realm/Realm/Sync/FlexibleSync/SubscriptionSet.cs index fa121e5c12..240e198a21 100644 --- a/Realm/Realm/Sync/FlexibleSync/SubscriptionSet.cs +++ b/Realm/Realm/Sync/FlexibleSync/SubscriptionSet.cs @@ -385,7 +385,7 @@ public void Dispose() [MemberNotNull(nameof(_enumerating))] private void ThrowIfDisposed() { - if (_enumerating == null) + if (_enumerating is null) { throw new ObjectDisposedException(nameof(Enumerator)); } diff --git a/Realm/Realm/Sync/User.cs b/Realm/Realm/Sync/User.cs index 726b729917..f56086de3b 100644 --- a/Realm/Realm/Sync/User.cs +++ b/Realm/Realm/Sync/User.cs @@ -124,7 +124,7 @@ public string DeviceId where T : class { var customData = GetCustomData(); - if (customData == null) + if (customData is null) { return null; } @@ -161,10 +161,9 @@ public UserIdentity[] Identities internal readonly SyncUserHandle Handle; - [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The App instance will own its handle.")] internal User(SyncUserHandle handle, App? app = null) { - if (app == null && handle.TryGetApp(out var appHandle)) + if (app is null && handle.TryGetApp(out var appHandle)) { app = new App(appHandle); } @@ -208,7 +207,7 @@ internal User(SyncUserHandle handle, App? app = null) where T : class { var result = await RefreshCustomDataAsync(); - if (result == null) + if (result is null) { return null; } diff --git a/Realm/Realm/Thread Handover/ThreadSafeReference.cs b/Realm/Realm/Thread Handover/ThreadSafeReference.cs index 6514ea5e92..d28d803b25 100644 --- a/Realm/Realm/Thread Handover/ThreadSafeReference.cs +++ b/Realm/Realm/Thread Handover/ThreadSafeReference.cs @@ -51,7 +51,7 @@ public abstract class ThreadSafeReference internal ThreadSafeReference(IThreadConfined value, Type type) { - if (value == null) + if (value is null) { throw new ArgumentNullException(nameof(value)); } diff --git a/Realm/Realm/Transaction.cs b/Realm/Realm/Transaction.cs index 41ff4d0a78..8025035cd7 100644 --- a/Realm/Realm/Transaction.cs +++ b/Realm/Realm/Transaction.cs @@ -72,7 +72,7 @@ internal Transaction(Realm realm) /// public void Dispose() { - if (_realm == null) + if (_realm is null) { return; } @@ -138,7 +138,7 @@ public async Task CommitAsync(CancellationToken cancellationToken = default) [MemberNotNull(nameof(_realm))] private void EnsureActionFeasibility(string executingAction) { - if (_realm == null) + if (_realm is null) { throw new Exception($"Transaction was already closed. Cannot {executingAction}"); } diff --git a/Tests/Realm.Tests/Database/InMemoryTests.cs b/Tests/Realm.Tests/Database/InMemoryTests.cs index 1ee38f9bfa..931f6cc5f4 100644 --- a/Tests/Realm.Tests/Database/InMemoryTests.cs +++ b/Tests/Realm.Tests/Database/InMemoryTests.cs @@ -166,5 +166,31 @@ public void InMemoryRealmWithFrozenObjects_WhenDeleted_DoesNotThrow() Assert.That(realm.IsClosed, Is.True); Assert.DoesNotThrow(() => Realm.DeleteRealm(_config)); } + + [Test] + public void InMemoryConfiguration_Identifier_ReturnsSuppliedValue() + { + var config = new InMemoryConfiguration("foo-bar"); + Assert.That(config.Identifier, Is.EqualTo("foo-bar")); + } + + [Test] + public void InMemoryConfiguration_GetInstanceAsync() + { + TestHelpers.RunAsyncTest(async () => + { + var asyncRealm = await GetRealmAsync(_config); + + asyncRealm.Write(() => asyncRealm.Add(new IntPropertyObject + { + Int = 42 + })); + + Assert.That(File.Exists(_config.DatabasePath)); + + var syncRealm = GetRealm(_config); + Assert.That(syncRealm.All().Single().Int, Is.EqualTo(42)); + }); + } } } diff --git a/Tests/Realm.Tests/Sync/AppTests.cs b/Tests/Realm.Tests/Sync/AppTests.cs index 1fe31b1ce6..98a43a62a3 100644 --- a/Tests/Realm.Tests/Sync/AppTests.cs +++ b/Tests/Realm.Tests/Sync/AppTests.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -39,7 +40,7 @@ public class AppTests : SyncTestBase [Test] public void DeviceInfo_OutputsMeaningfulInfo() { - void AssertBundleId(params string[] expectedValues) + static void AssertBundleId(params string[] expectedValues) { var values = expectedValues.Concat(new[] { "ReSharperTestRunner" }).Select(Platform.Sha256).ToArray(); Assert.That(values, Does.Contain(Platform.BundleId)); @@ -127,20 +128,24 @@ void AssertBundleId(params string[] expectedValues) [Test] public void AppCreate_CreatesApp() { + var basePath = Path.Combine(InteropConfig.GetDefaultStorageFolder("No error expected here"), "foo-bar"); + Directory.CreateDirectory(basePath); + // This is mostly a smoke test to ensure that nothing blows up when setting all properties. var config = new AppConfiguration("abc-123") { BaseUri = new Uri("http://foo.bar"), - LocalAppName = "My app", - LocalAppVersion = "1.2.3", MetadataEncryptionKey = new byte[64], MetadataPersistenceMode = MetadataPersistenceMode.Encrypted, - BaseFilePath = InteropConfig.GetDefaultStorageFolder("No error expected here"), + BaseFilePath = basePath, DefaultRequestTimeout = TimeSpan.FromSeconds(123) }; var app = CreateApp(config); Assert.That(app.Sync, Is.Not.Null); + Assert.That(app.BaseUri, Is.EqualTo(config.BaseUri)); + Assert.That(app.BaseFilePath, Is.EqualTo(config.BaseFilePath)); + Assert.That(app.Id, Is.EqualTo(config.AppId)); } [Test] @@ -190,7 +195,7 @@ public void RealmConfiguration_WithCustomHttpClientHandler_CleanedUpAfterAppDest { var handler = new HttpClientHandler(); - app = App.Create(new AppConfiguration("abc") + app = CreateApp(new AppConfiguration("abc") { HttpClientHandler = handler }); @@ -207,6 +212,125 @@ public void RealmConfiguration_WithCustomHttpClientHandler_CleanedUpAfterAppDest }); } + [Test] + public void AppCreate_CacheTests([Values(true, false, null)] bool? useCache) + { + var config1 = GetConfig(); + var app1 = CreateApp(config1); + + var config2 = GetConfig(); + var app2 = CreateApp(config2); + + Assert.That(config1.BaseFilePath, Is.Not.EqualTo(config2.BaseFilePath)); + Assert.That(app1.Id, Is.EqualTo(app2.Id)); + Assert.That(app1.GetHashCode(), Is.EqualTo(app2.GetHashCode())); + + // null or true mean cache should be used + if (useCache != false) + { + // If we cached the app, the second base file path should have been ignored + Assert.That(app1.BaseFilePath, Is.EqualTo(app2.BaseFilePath)); + Assert.That(app1.Equals(app2), Is.True); + } + else + { + Assert.That(app1.BaseFilePath, Is.Not.EqualTo(app2.BaseFilePath)); + Assert.That(app1.Equals(app2), Is.False); + } + + AppConfiguration GetConfig() + { + var baseFilePath = Path.Combine(InteropConfig.GetDefaultStorageFolder("no error expected"), Guid.NewGuid().ToString()); + var config = new AppConfiguration("abc") + { + BaseFilePath = baseFilePath, + }; + + if (useCache.HasValue) + { + config.UseAppCache = useCache.Value; + } + + Directory.CreateDirectory(config.BaseFilePath); + return config; + } + } + + [Test] + public void App_Create_SameId_DifferentBaseUri_ReturnsDifferentApps() + { + var config1 = GetConfig("https://localhost:443"); + var config2 = GetConfig("http://localhost:80"); + + var app1 = CreateApp(config1); + var app2 = CreateApp(config2); + + Assert.That(app1.Id, Is.EqualTo(app2.Id)); + Assert.That(app1.GetHashCode(), Is.EqualTo(app2.GetHashCode())); + + Assert.That(app1.Equals(app2), Is.False); + Assert.That(app1 == app2, Is.False); + Assert.That(app1 != app2, Is.True); + Assert.That(app1.BaseUri, Is.Not.EqualTo(app2.BaseUri)); + Assert.That(app1.BaseFilePath, Is.Not.EqualTo(app2.BaseFilePath)); + + static AppConfiguration GetConfig(string uri) + { + var baseFilePath = Path.Combine(InteropConfig.GetDefaultStorageFolder("no error expected"), Guid.NewGuid().ToString()); + var config = new AppConfiguration("abc") + { + BaseFilePath = baseFilePath, + BaseUri = new Uri(uri) + }; + + Directory.CreateDirectory(config.BaseFilePath); + return config; + } + } + + [Test] + public void App_EqualsGetHashCodeTests() + { + var config1 = new AppConfiguration("abc"); + var config2 = new AppConfiguration("cde"); + var config3 = new AppConfiguration("abc"); + var config4 = new AppConfiguration("abc") + { + UseAppCache = false + }; + + var app1 = CreateApp(config1); + var app2 = CreateApp(config2); + var app3 = CreateApp(config3); + var app4 = CreateApp(config4); + + Assert.That(app1.GetHashCode(), Is.Not.EqualTo(app2.GetHashCode())); + Assert.That(app1.GetHashCode(), Is.EqualTo(app3.GetHashCode())); + Assert.That(app1.GetHashCode(), Is.EqualTo(app4.GetHashCode())); + + Assert.That(app1.Equals(app2), Is.False); + Assert.That(app1.Equals(app3), Is.True); + Assert.That(app1.Equals(app4), Is.False); // app4 is uncached, so a different instance is returned + + Assert.That(app1 == app2, Is.False); + Assert.That(app1 == app3, Is.True); + Assert.That(app1 == app4, Is.False); // app4 is uncached, so a different instance is returned + + Assert.That(app1 != app2, Is.True); + Assert.That(app1 != app3, Is.False); + Assert.That(app1 != app4, Is.True); // app4 is uncached, so a different instance is returned + + Assert.That(app1.Equals(app1.Id), Is.False); + Assert.That(app1.Equals(null), Is.False); + Assert.That(app1 == null, Is.False); + Assert.That(app1 != null, Is.True); + + App? app5 = null; + + Assert.That(app5 == null, Is.True); + Assert.That(app5 != null, Is.False); + } + private class TestHttpClientHandler : DelegatingHandler { public readonly List<(HttpMethod Method, string Url)> Requests = new(); diff --git a/Tests/Realm.Tests/Sync/SessionTests.cs b/Tests/Realm.Tests/Sync/SessionTests.cs index 462583d43b..a3f4332492 100644 --- a/Tests/Realm.Tests/Sync/SessionTests.cs +++ b/Tests/Realm.Tests/Sync/SessionTests.cs @@ -1222,6 +1222,28 @@ public void Session_PermissionDenied_DoesntCrash() }, timeout: 1000000); } + [Test] + public void Session_GetUser_GetApp_ReturnsMeaningfulValue() + { + // Session.User.App doesn't have a reference to the managed app, which means we need to + // recreate it from the native one. This test verifies that the properties on the native + // app match the ones on the managed one. + var fakeConfig = new Realms.Sync.AppConfiguration("foo-bar") + { + BaseUri = new Uri("http://localhost:12345"), + BaseFilePath = Path.Combine(InteropConfig.GetDefaultStorageFolder("no error expected"), Guid.NewGuid().ToString()) + }; + Directory.CreateDirectory(fakeConfig.BaseFilePath); + + var fakeApp = CreateApp(fakeConfig); + + var realm = GetRealm(GetFakeConfig(fakeApp)); + var session = GetSession(realm); + + Assert.That(session.User.App.BaseFilePath, Is.EqualTo(fakeConfig.BaseFilePath)); + Assert.That(session.User.App.BaseUri, Is.EqualTo(fakeConfig.BaseUri)); + } + private static ClientResetHandlerBase GetClientResetHandler( Type type, BeforeResetCallback? beforeCb = null, diff --git a/Tests/Realm.Tests/Sync/SyncTestBase.cs b/Tests/Realm.Tests/Sync/SyncTestBase.cs index 18691b7989..39e5b94b2c 100644 --- a/Tests/Realm.Tests/Sync/SyncTestBase.cs +++ b/Tests/Realm.Tests/Sync/SyncTestBase.cs @@ -53,7 +53,13 @@ protected override void CustomTearDown() base.CustomTearDown(); - _apps.DrainQueue(app => app.Handle.ResetForTesting()); + _apps.DrainQueue(app => + { + if (!app.Handle.IsClosed) + { + app.Handle.ResetForTesting(); + } + }); _clientResetAppsToRestore.DrainQueueAsync(appConfigType => SyncTestHelpers.SetRecoveryModeOnServer(appConfigType, enabled: true)); } @@ -96,7 +102,7 @@ protected static async Task WaitForObjectAsync(T obj, Realm realm2, string { var id = obj.DynamicApi.Get("_id"); - return (await TestHelpers.WaitForConditionAsync(() => realm2.FindCore(id), o => o != null, errorMessage: message))!; + return (await WaitForConditionAsync(() => realm2.FindCore(id), o => o != null, errorMessage: message))!; } protected async Task GetUserAsync(App? app = null, string? username = null, string? password = null) diff --git a/wrappers/src/app_cs.cpp b/wrappers/src/app_cs.cpp index fbc150ce7b..840709c718 100644 --- a/wrappers/src/app_cs.cpp +++ b/wrappers/src/app_cs.cpp @@ -70,12 +70,6 @@ namespace realm { uint16_t* base_url; size_t base_url_len; - uint16_t* local_app_name; - size_t local_app_name_len; - - uint16_t* local_app_version; - size_t local_app_version_len; - uint64_t request_timeout_ms; realm::SyncClientConfig::MetadataMode metadata_mode; @@ -93,6 +87,8 @@ namespace realm { uint64_t sync_pong_keep_alive_timeout_ms; uint64_t sync_fast_reconnect_limit; + + bool use_cache; }; } } @@ -144,14 +140,6 @@ extern "C" { config.transport = std::make_shared(app_config.managed_http_client); config.base_url = Utf16StringAccessor(app_config.base_url, app_config.base_url_len).to_string(); - if (app_config.local_app_name != nullptr) { - config.local_app_name = Utf16StringAccessor(app_config.local_app_name, app_config.local_app_name_len).to_string(); - } - - if (app_config.local_app_version != nullptr) { - config.local_app_version = Utf16StringAccessor(app_config.local_app_version, app_config.local_app_version_len).to_string(); - } - if (app_config.request_timeout_ms > 0) { config.default_request_timeout_ms = app_config.request_timeout_ms; } @@ -180,7 +168,11 @@ extern "C" { sync_client_config.custom_encryption_key = std::vector(key.begin(), key.end()); } - return new SharedApp(App::get_shared_app(std::move(config), std::move(sync_client_config))); + SharedApp app = app_config.use_cache + ? App::get_shared_app(std::move(config), std::move(sync_client_config)) + : App::get_uncached_app(std::move(config), std::move(sync_client_config)); + + return new SharedApp(app); }); } @@ -312,6 +304,35 @@ extern "C" { App::clear_cached_apps(); } + REALM_EXPORT size_t shared_app_get_base_file_path(SharedApp& app, uint16_t* buffer, size_t buffer_length, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() { + std::string base_file_path(app->sync_manager()->config().base_file_path); + return stringdata_to_csharpstringbuffer(base_file_path, buffer, buffer_length); + }); + } + + REALM_EXPORT realm_string_t shared_app_get_base_uri(SharedApp& app, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() { + return to_capi(app->base_url()); + }); + } + + REALM_EXPORT realm_string_t shared_app_get_id(SharedApp& app, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() { + return to_capi(app->config().app_id); + }); + } + + REALM_EXPORT bool shared_app_is_same_instance(SharedApp& lhs, SharedApp& rhs, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]() { + return lhs == rhs; // just compare raw pointers inside the smart pointers + }); + } + #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)