From 20b77fa50decdd96b58b245af70b9a3ffcf6465a Mon Sep 17 00:00:00 2001 From: gtmf74 Date: Wed, 1 Feb 2023 11:32:03 +0100 Subject: [PATCH 1/3] feature: clear metadata per url --- src/Simple.OData.Client.Core/ISession.cs | 5 +++++ src/Simple.OData.Client.Core/ODataClient.cs | 8 ++++++++ src/Simple.OData.Client.Core/Session.cs | 1 + .../MetadataODataTests.cs | 19 +++++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/Simple.OData.Client.Core/ISession.cs b/src/Simple.OData.Client.Core/ISession.cs index 632e5ae3..0b626007 100644 --- a/src/Simple.OData.Client.Core/ISession.cs +++ b/src/Simple.OData.Client.Core/ISession.cs @@ -40,4 +40,9 @@ public interface ISession : IDisposable /// /// An instance. HttpConnection GetHttpConnection(); + + /// + /// Clears base url metadata cache + /// + void ClearMetadataCache(); } diff --git a/src/Simple.OData.Client.Core/ODataClient.cs b/src/Simple.OData.Client.Core/ODataClient.cs index a268b6fb..7219513e 100644 --- a/src/Simple.OData.Client.Core/ODataClient.cs +++ b/src/Simple.OData.Client.Core/ODataClient.cs @@ -107,6 +107,14 @@ public static void ClearMetadataCache() EdmMetadataCache.Clear(); } + /// + /// Clears base url metadata cache + /// + public void ClearBaseUrlMetadataCache() + { + Session.ClearMetadataCache(); + } + /// /// Returns an instance of a fluent OData client for the specified collection. /// diff --git a/src/Simple.OData.Client.Core/Session.cs b/src/Simple.OData.Client.Core/Session.cs index 189fde75..9f97049f 100644 --- a/src/Simple.OData.Client.Core/Session.cs +++ b/src/Simple.OData.Client.Core/Session.cs @@ -110,6 +110,7 @@ public void ClearMetadataCache() { EdmMetadataCache.Clear(metadataCache.Key); MetadataCache = null; + _adapter = null; } } diff --git a/src/Simple.OData.Client.IntegrationTests/MetadataODataTests.cs b/src/Simple.OData.Client.IntegrationTests/MetadataODataTests.cs index 5657adbd..f77c0f71 100644 --- a/src/Simple.OData.Client.IntegrationTests/MetadataODataTests.cs +++ b/src/Simple.OData.Client.IntegrationTests/MetadataODataTests.cs @@ -83,4 +83,23 @@ await Task.WhenAll( Assert.Equal(1, metadataCallsCount); } + + [Fact] + public async Task MetadataClear() + { + ODataClient.ClearMetadataCache(); + var metadataCallsCount = 0; + var settings = new ODataClientSettings + { + BaseUri = _serviceUri, + BeforeRequest = _ => metadataCallsCount++ + }; + var client = new ODataClient(settings); + + await client.GetMetadataAsStringAsync(); + client.ClearBaseUrlMetadataCache(); + await client.GetMetadataAsStringAsync(); + + Assert.Equal(2, metadataCallsCount); + } } From b55674a7bd52a37728cfb1c9221a0d4e1121a717 Mon Sep 17 00:00:00 2001 From: gtmf74 Date: Thu, 2 Feb 2023 17:45:05 +0100 Subject: [PATCH 2/3] feature: factory for metadata cache --- .../EdmMetadataCache.cs | 91 ------------------- .../Metadata/EdmMetadataCache.cs | 33 +++++++ .../Metadata/EdmMetadataCacheFactory.cs | 61 +++++++++++++ .../Metadata/IEdmMetadataCache.cs | 8 ++ .../Metadata/IEdmMetadataCacheFactory.cs | 11 +++ src/Simple.OData.Client.Core/ODataClient.cs | 3 +- .../ODataClientSettings.cs | 7 ++ src/Simple.OData.Client.Core/Session.cs | 18 ++-- .../SpecialTests.cs | 11 ++- 9 files changed, 140 insertions(+), 103 deletions(-) delete mode 100644 src/Simple.OData.Client.Core/EdmMetadataCache.cs create mode 100644 src/Simple.OData.Client.Core/Metadata/EdmMetadataCache.cs create mode 100644 src/Simple.OData.Client.Core/Metadata/EdmMetadataCacheFactory.cs create mode 100644 src/Simple.OData.Client.Core/Metadata/IEdmMetadataCache.cs create mode 100644 src/Simple.OData.Client.Core/Metadata/IEdmMetadataCacheFactory.cs diff --git a/src/Simple.OData.Client.Core/EdmMetadataCache.cs b/src/Simple.OData.Client.Core/EdmMetadataCache.cs deleted file mode 100644 index 9f3bbd64..00000000 --- a/src/Simple.OData.Client.Core/EdmMetadataCache.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; - -using Simple.OData.Client.Extensions; - -namespace Simple.OData.Client; - -internal class EdmMetadataCache -{ - private static readonly ConcurrentDictionary _instances = new(); - private static readonly SemaphoreSlim semaphore = new(1); - - public static void Clear() - { - _instances.Clear(); - // NOTE: Is this necessary, if so should we wipe the ITypeCache constructors? - DictionaryExtensions.ClearCache(); - } - - public static void Clear(string key) - { - _instances.TryRemove(key, out _); - } - - public static EdmMetadataCache GetOrAdd(string key, Func valueFactory) - { - return _instances.GetOrAdd(key, valueFactory); - } - - public async static Task GetOrAddAsync(string key, Func> valueFactory) - { - // Cheaper to check first before we do the remote call - if (_instances.TryGetValue(key, out var found)) - { - return found; - } - - // Just allow one schema request at a time, unlikely to be much contention but avoids multiple requests for same endpoint. - await semaphore - .WaitAsync() - .ConfigureAwait(false); - - try - { - if (_instances.TryGetValue(key, out found)) - { - return found; - } - - found = await valueFactory(key) - .ConfigureAwait(false); - - return _instances.GetOrAdd(key, found); - } - finally - { - semaphore.Release(); - } - } - - private readonly ITypeCache typeCache; - - public EdmMetadataCache(string key, string metadataDocument, ITypeCache typeCache) - { - if (string.IsNullOrWhiteSpace(key)) - { - throw new ArgumentNullException(nameof(key)); - } - - if (string.IsNullOrWhiteSpace(metadataDocument)) - { - throw new ArgumentNullException(nameof(metadataDocument)); - } - - this.typeCache = typeCache; - - Key = key; - MetadataDocument = metadataDocument; - } - - public string Key { get; } - - public string MetadataDocument { get; } - - public IODataAdapter GetODataAdapter(ISession session) - { - return session.Settings.AdapterFactory.CreateAdapterLoader(MetadataDocument, typeCache)(session); - } -} diff --git a/src/Simple.OData.Client.Core/Metadata/EdmMetadataCache.cs b/src/Simple.OData.Client.Core/Metadata/EdmMetadataCache.cs new file mode 100644 index 00000000..f6520525 --- /dev/null +++ b/src/Simple.OData.Client.Core/Metadata/EdmMetadataCache.cs @@ -0,0 +1,33 @@ +using System; + +namespace Simple.OData.Client.Metadata; + +internal class EdmMetadataCache: IEdmMetadataCache +{ + private readonly ITypeCache typeCache; + + public EdmMetadataCache(string key, string metadataDocument, ITypeCache typeCache) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + if (string.IsNullOrWhiteSpace(metadataDocument)) + { + throw new ArgumentNullException(nameof(metadataDocument)); + } + this.typeCache = typeCache; + + Key = key; + MetadataDocument = metadataDocument; + } + public string Key { get; } + + public string MetadataDocument { get; } + + public IODataAdapter GetODataAdapter(ISession session) + { + return session.Settings.AdapterFactory.CreateAdapterLoader(MetadataDocument, typeCache)(session); + } +} \ No newline at end of file diff --git a/src/Simple.OData.Client.Core/Metadata/EdmMetadataCacheFactory.cs b/src/Simple.OData.Client.Core/Metadata/EdmMetadataCacheFactory.cs new file mode 100644 index 00000000..9b010e90 --- /dev/null +++ b/src/Simple.OData.Client.Core/Metadata/EdmMetadataCacheFactory.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Simple.OData.Client.Extensions; + +namespace Simple.OData.Client.Metadata; + +internal class EdmMetadataCacheFactory : IEdmMetadataCacheFactory +{ + private static readonly ConcurrentDictionary _instances = new(); + private static readonly SemaphoreSlim semaphore = new(1); + + public static void Clear() + { + _instances.Clear(); + // NOTE: Is this necessary, if so should we wipe the ITypeCache constructors? + DictionaryExtensions.ClearCache(); + } + + public void Clear(string key) + { + _instances.TryRemove(key, out _); + } + + public IEdmMetadataCache GetOrAdd(string key, Func valueFactory) + { + return _instances.GetOrAdd(key, valueFactory); + } + + public async Task GetOrAddAsync(string key, Func> valueFactory) + { + // Cheaper to check first before we do the remote call + if (_instances.TryGetValue(key, out var found)) + { + return found; + } + + // Just allow one schema request at a time, unlikely to be much contention but avoids multiple requests for same endpoint. + await semaphore + .WaitAsync() + .ConfigureAwait(false); + + try + { + if (_instances.TryGetValue(key, out found)) + { + return found; + } + + found = await valueFactory(key) + .ConfigureAwait(false); + + return _instances.GetOrAdd(key, found); + } + finally + { + semaphore.Release(); + } + } +} \ No newline at end of file diff --git a/src/Simple.OData.Client.Core/Metadata/IEdmMetadataCache.cs b/src/Simple.OData.Client.Core/Metadata/IEdmMetadataCache.cs new file mode 100644 index 00000000..f82837cd --- /dev/null +++ b/src/Simple.OData.Client.Core/Metadata/IEdmMetadataCache.cs @@ -0,0 +1,8 @@ +namespace Simple.OData.Client.Metadata; + +public interface IEdmMetadataCache +{ + string Key { get; } + string MetadataDocument { get; } + IODataAdapter GetODataAdapter(ISession session); +} \ No newline at end of file diff --git a/src/Simple.OData.Client.Core/Metadata/IEdmMetadataCacheFactory.cs b/src/Simple.OData.Client.Core/Metadata/IEdmMetadataCacheFactory.cs new file mode 100644 index 00000000..e4f0e0a3 --- /dev/null +++ b/src/Simple.OData.Client.Core/Metadata/IEdmMetadataCacheFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +namespace Simple.OData.Client.Metadata; + +public interface IEdmMetadataCacheFactory +{ + void Clear(string key); + IEdmMetadataCache GetOrAdd(string key, Func valueFactory); + Task GetOrAddAsync(string key, Func> valueFactory); +} \ No newline at end of file diff --git a/src/Simple.OData.Client.Core/ODataClient.cs b/src/Simple.OData.Client.Core/ODataClient.cs index 7219513e..5832545f 100644 --- a/src/Simple.OData.Client.Core/ODataClient.cs +++ b/src/Simple.OData.Client.Core/ODataClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using Simple.OData.Client.Metadata; namespace Simple.OData.Client; @@ -104,7 +105,7 @@ public static T ParseMetadataString(string metadataString) /// public static void ClearMetadataCache() { - EdmMetadataCache.Clear(); + EdmMetadataCacheFactory.Clear(); } /// diff --git a/src/Simple.OData.Client.Core/ODataClientSettings.cs b/src/Simple.OData.Client.Core/ODataClientSettings.cs index bc91cc1f..4785a132 100644 --- a/src/Simple.OData.Client.Core/ODataClientSettings.cs +++ b/src/Simple.OData.Client.Core/ODataClientSettings.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Simple.OData.Client.Metadata; namespace Simple.OData.Client; @@ -126,6 +127,12 @@ public Uri? BaseUri /// public string? MetadataDocument { get; set; } + + /// + /// Gets or sets factory for custom metadata management that is used by the session. + /// + public IEdmMetadataCacheFactory? MetadataCacheFactory { get; set; } + /// /// Gets the associated with the uri, used to register converters and dynamic types. /// diff --git a/src/Simple.OData.Client.Core/Session.cs b/src/Simple.OData.Client.Core/Session.cs index 9f97049f..1e0b99d9 100644 --- a/src/Simple.OData.Client.Core/Session.cs +++ b/src/Simple.OData.Client.Core/Session.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Simple.OData.Client.Metadata; namespace Simple.OData.Client; @@ -32,6 +33,8 @@ private Session(ODataClientSettings settings) // Create as early as possible as most unit tests require this and also makes it simpler when assigning a static document MetadataCache = InitializeStaticMetadata(Settings.MetadataDocument); } + + _metadataCacheFactory = settings.MetadataCacheFactory ?? new EdmMetadataCacheFactory(); } public IODataAdapter Adapter @@ -55,7 +58,7 @@ public IODataAdapter Adapter public IMetadata Metadata => Adapter.GetMetadata(); - public EdmMetadataCache? MetadataCache { get; private set; } + public IEdmMetadataCache? MetadataCache { get; private set; } public ODataClientSettings Settings { get; } @@ -74,6 +77,7 @@ public void Dispose() } private readonly SemaphoreSlim _initializeSemaphore = new(1); + private readonly IEdmMetadataCacheFactory _metadataCacheFactory; public async Task Initialize(CancellationToken cancellationToken) { // Just allow one schema request at a time, unlikely to be much contention but avoids multiple requests for same endpoint. @@ -108,7 +112,7 @@ public void ClearMetadataCache() var metadataCache = MetadataCache; if (metadataCache != null) { - EdmMetadataCache.Clear(metadataCache.Key); + _metadataCacheFactory.Clear(metadataCache.Key); MetadataCache = null; _adapter = null; } @@ -158,16 +162,16 @@ private async Task SendMetadataRequestAsync(CancellationTok return await new RequestRunner(this).ExecuteRequestAsync(request, cancellationToken).ConfigureAwait(false); } - private EdmMetadataCache InitializeStaticMetadata(string metadata) + private IEdmMetadataCache InitializeStaticMetadata(string metadata) { - return EdmMetadataCache.GetOrAdd( + return _metadataCacheFactory.GetOrAdd( Settings.BaseUri.AbsoluteUri, uri => CreateMdc(uri, metadata)); } - private async Task InitializeMetadataCache(CancellationToken cancellationToken) + private async Task InitializeMetadataCache(CancellationToken cancellationToken) { - return await EdmMetadataCache.GetOrAddAsync( + return await _metadataCacheFactory.GetOrAddAsync( Settings.BaseUri.AbsoluteUri, async uri => { @@ -189,7 +193,7 @@ private async Task ResolveMetadataAsync(CancellationToken cancellationTo return metadataDocument; } - private EdmMetadataCache CreateMdc(string key, string metadata) + private IEdmMetadataCache CreateMdc(string key, string metadata) { return new EdmMetadataCache(key, metadata, TypeCaches.TypeCache(key, Settings.NameMatchResolver)); } diff --git a/src/Simple.OData.Client.IntegrationTests/SpecialTests.cs b/src/Simple.OData.Client.IntegrationTests/SpecialTests.cs index 58eaca3f..2eff7306 100644 --- a/src/Simple.OData.Client.IntegrationTests/SpecialTests.cs +++ b/src/Simple.OData.Client.IntegrationTests/SpecialTests.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.Data.Edm; +using Simple.OData.Client.Metadata; using Xunit; namespace Simple.OData.Client.Tests; @@ -189,9 +190,10 @@ public async Task MetadataErrorIsNotCached() { const string uriString = "ftp://localhost/"; var baseUri = new Uri(uriString); + var factory = new EdmMetadataCacheFactory(); try { - var settings = new ODataClientSettings { BaseUri = baseUri }; + var settings = new ODataClientSettings { BaseUri = baseUri, MetadataCacheFactory = factory }; var client = new ODataClient(settings); await client.GetMetadataAsync().ConfigureAwait(false); } @@ -201,7 +203,7 @@ public async Task MetadataErrorIsNotCached() } var wasCached = true; - var cached = EdmMetadataCache.GetOrAdd(uriString, x => + var cached = factory.GetOrAdd(uriString, x => { wasCached = false; return null; @@ -213,11 +215,12 @@ public async Task MetadataErrorIsNotCached() [Fact] public async Task MetadataIsCached() { - var settings = new ODataClientSettings { BaseUri = _serviceUri }; + var factory = new EdmMetadataCacheFactory(); + var settings = new ODataClientSettings { BaseUri = _serviceUri, MetadataCacheFactory = factory }; var client = new ODataClient(settings); await client.GetMetadataAsync().ConfigureAwait(false); - EdmMetadataCache.GetOrAdd(_serviceUri.ToString(), x => throw new Exception("metadata was not cached.")); + factory.GetOrAdd(_serviceUri.ToString(), x => throw new Exception("metadata was not cached.")); settings.BeforeRequest = x => throw new Exception("metadata cache was not used."); await client.GetMetadataAsync().ConfigureAwait(false); From 1cfc247f2df27b0ec20fb6781b29959c9b9f2063 Mon Sep 17 00:00:00 2001 From: gtmf74 Date: Mon, 6 Feb 2023 12:09:23 +0100 Subject: [PATCH 3/3] fix --- src/Simple.OData.Client.Core/Session.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Simple.OData.Client.Core/Session.cs b/src/Simple.OData.Client.Core/Session.cs index 1e0b99d9..54257136 100644 --- a/src/Simple.OData.Client.Core/Session.cs +++ b/src/Simple.OData.Client.Core/Session.cs @@ -27,14 +27,13 @@ private Session(ODataClientSettings settings) } Settings = settings; + _metadataCacheFactory = settings.MetadataCacheFactory ?? new EdmMetadataCacheFactory(); if (!string.IsNullOrEmpty(Settings.MetadataDocument)) { // Create as early as possible as most unit tests require this and also makes it simpler when assigning a static document MetadataCache = InitializeStaticMetadata(Settings.MetadataDocument); } - - _metadataCacheFactory = settings.MetadataCacheFactory ?? new EdmMetadataCacheFactory(); } public IODataAdapter Adapter