Skip to content
Closed
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
33 changes: 33 additions & 0 deletions LibsAndSamples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9B0B5396-4D95-4C15-82ED-DC22B5A3123F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client", "src\client\Microsoft.Identity.Client\Microsoft.Identity.Client.csproj", "{60117A9B-4BB8-472E-BFFF-52CBF67CA95A}"
ProjectSection(ProjectDependencies) = postProject
{57AB6744-A9D0-47BD-AEDF-D1B17639996D} = {57AB6744-A9D0-47BD-AEDF-D1B17639996D}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "devapps", "devapps", "{34BE693E-3496-45A4-B1D2-D3A0E068EEDB}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -185,6 +188,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Intune-xamarin-Android", "t
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Client.Broker", "src\client\Microsoft.Identity.Client.Broker\Microsoft.Identity.Client.Broker.csproj", "{6839F934-45F0-4026-8AF3-C3AEFB7D48A9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.ServiceEssentials.Abstractions", "src\client\Abstractions\Microsoft.Identity.ServiceEssentials.Abstractions.csproj", "{57AB6744-A9D0-47BD-AEDF-D1B17639996D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1584,6 +1589,34 @@ Global
{6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x64.Build.0 = Release|Any CPU
{6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x86.ActiveCfg = Release|Any CPU
{6839F934-45F0-4026-8AF3-C3AEFB7D48A9}.Release|x86.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|ARM64.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhone.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x64.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x64.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x86.ActiveCfg = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Debug|x86.Build.0 = Debug|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|Any CPU.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM64.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|ARM64.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhone.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhone.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x64.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x64.Build.0 = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x86.ActiveCfg = Release|Any CPU
{57AB6744-A9D0-47BD-AEDF-D1B17639996D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
131 changes: 131 additions & 0 deletions src/client/Abstractions/CacheEntryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.ComponentModel;

namespace Microsoft.Identity.ServiceEssentials
{
/// <summary>
/// Represents the cache options applied to an <see cref="ICacheEntry{T}"/>.
/// </summary>
public class CacheEntryOptions
{
private TimeSpan _timeToExpire = TimeSpan.FromHours(1);
private TimeSpan _timeToRefresh = TimeSpan.MaxValue;
private TimeSpan _timeToRemove = TimeSpan.FromHours(1);

/// <summary>
/// Logical time-to-live for the <see cref="ICacheEntry{T}"/>, relative to time now.
/// </summary>
public TimeSpan TimeToExpire
{
get => _timeToExpire;
set
{
if (value <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_timeToExpire = value;
}
}

/// <summary>
/// Emergency time-to-live for the cache item, relative to time now.
/// Represents the actual expiration time for the <see cref="ICacheEntry{T}"/> in a cache storage and can be used as a 'last-known-good'
/// value in scenarios when primary data source is not available.
/// </summary>
/// <remarks>
/// Defaults to <see cref="TimeToExpire"/>.
/// </remarks>
public TimeSpan TimeToRemove
{
get => _timeToRemove;
set
{
if (value <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_timeToRemove = value;
}
}

/// <summary>
/// Refresh time-to-live for the <see cref="ICacheEntry{T}"/>, relative to time now.
/// This value can be used in proactive <see cref="ICacheEntry{T}"/> refresh scenarios.
/// </summary>
/// <remarks>
/// <see cref="TimeSpan.MaxValue"/> for no refresh (default).
/// </remarks>
public TimeSpan TimeToRefresh
{
get => _timeToRefresh;
set
{
if (value <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(value));
_timeToRefresh = value;
}
}

/// <summary>
/// Flag that indicates whether the <see cref="ICacheEntry{T}"/> should be stored only in a local cache.
/// </summary>
public bool StoreToLocalCacheOnly { get; set; }

/// <summary>
/// Value that can be used to randomize spans in <see cref="CacheEntryOptions"/>.
/// </summary>
/// <remarks>
/// Negative value will be used to randomize spans to a maximum of -<see cref="JitterInSeconds"/>,
/// while positive value will be used to randomize spans to a maximum of +-<see cref="JitterInSeconds"/>.
/// </remarks>
public int JitterInSeconds { get; set; }

/// <summary>
/// Should be used for deserialization only.
/// </summary>
/// <remarks>
/// <see cref=" MissingMethodException"/> will be thrown if System.Text.Json is used to
/// serialize this class on targets below .NET Core 3.0, if public parameterless constructor is not defined.
/// https://docs.microsoft.com/en-us/dotnet/core/compatibility/serialization/5.0/non-public-parameterless-constructors-not-used-for-deserialization
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This constructor is for serialization")]
public CacheEntryOptions()
{
}

/// <summary>
/// Creates a new instance of the <see cref="CacheEntryOptions"/>.
/// </summary>
/// <param name="timeToExpire">Logical time-to-live of the cache item, relative to now.</param>
/// <remarks>
/// <see cref="TimeToRemove"/> is set to <paramref name="timeToExpire"/> by default.
/// <see cref="TimeToRefresh"/> is set to <paramref name="timeToExpire"/> by default.
/// <see cref="JitterInSeconds"/> is set to 0 by default.
/// </remarks>
public CacheEntryOptions(TimeSpan timeToExpire)
: this (timeToExpire, 0)
{ }

/// <summary>
/// Creates a new instance of the <see cref="CacheEntryOptions"/>.
/// </summary>
/// <param name="timeToExpire">Logical time-to-live of the cache item, relative to now.</param>
/// <param name="jitterInSeconds">
/// Negative value will be used to randomize spans to a maximum of -<paramref name="jitterInSeconds"/>,
/// while positive value will be used to randomize spans to a maximum of +-<paramref name="jitterInSeconds"/>.
/// </param>
/// <remarks>
/// <see cref="TimeToRemove"/> is set to <paramref name="timeToExpire"/> by default.
/// <see cref="TimeToRefresh"/> is set to <paramref name="timeToExpire"/> by default.
/// </remarks>
public CacheEntryOptions(TimeSpan timeToExpire, int jitterInSeconds)
{
TimeToExpire = timeToExpire;
TimeToRemove = timeToExpire;
TimeToRefresh = TimeSpan.MaxValue;
StoreToLocalCacheOnly = false;
JitterInSeconds = jitterInSeconds;
}
}
}
120 changes: 120 additions & 0 deletions src/client/Abstractions/CacheEntryOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.Identity.ServiceEssentials
{
// CAN be made internal and moved to implementation

/// <summary>
/// <see cref="CacheEntryOptions"/> extension methods.
/// </summary>
public static class CacheEntryOptionsExtensions
{
private static readonly Random _random = new Random();

/// <summary>
/// Returns <see cref="CacheEntryOptions.TimeToRefresh"/> of <paramref name="cacheEntryOptions"/>
/// randomized by <see cref="CacheEntryOptions.JitterInSeconds"/>
/// </summary>
/// <param name="cacheEntryOptions">
/// <see cref="CacheEntryOptions"/> instance that contains <see cref="CacheEntryOptions.TimeToRefresh"/>
/// and <see cref="CacheEntryOptions.JitterInSeconds"/>.
/// </param>
/// <returns>
/// <see cref="CacheEntryOptions.TimeToRefresh"/> of <paramref name="cacheEntryOptions"/>
/// randomized by <see cref="CacheEntryOptions.JitterInSeconds"/>.
/// </returns>
/// <remarks>
/// Jitter should be applied where possible, to avoid the thundering herd problem.
/// </remarks>
public static TimeSpan GetTimeToRefreshWithJitter(this CacheEntryOptions cacheEntryOptions)
{
_ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions));

if (cacheEntryOptions.TimeToRefresh == TimeSpan.MaxValue)
return cacheEntryOptions.TimeToRefresh;

var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds);
return cacheEntryOptions.TimeToRefresh.AddOrCap(randomSpan);
}

/// <summary>
/// Returns <see cref="CacheEntryOptions.TimeToRemove"/> of <paramref name="cacheEntryOptions"/>
/// randomized by <see cref="CacheEntryOptions.JitterInSeconds"/>
/// </summary>
/// <param name="cacheEntryOptions">
/// <see cref="CacheEntryOptions"/> instance that contains <see cref="CacheEntryOptions.TimeToRemove"/>
/// and <see cref="CacheEntryOptions.JitterInSeconds"/>.
/// </param>
/// <returns>
/// <see cref="CacheEntryOptions.TimeToRemove"/> of <paramref name="cacheEntryOptions"/>
/// randomized by <see cref="CacheEntryOptions.JitterInSeconds"/>.
/// </returns>
/// <remarks>
/// Jitter should be applied where possible, to avoid the thundering herd problem.
/// </remarks>
public static TimeSpan GetTimeToRemoveWithJitter(this CacheEntryOptions cacheEntryOptions)
{
_ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions));

if (cacheEntryOptions.TimeToRemove == TimeSpan.MaxValue)
return cacheEntryOptions.TimeToRefresh;

var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds);
return cacheEntryOptions.TimeToRemove.AddOrCap(randomSpan);
}

/// <summary>
/// Returns <see cref="CacheEntryOptions.TimeToExpire"/> of <paramref name="cacheEntryOptions"/>
/// randomized by <see cref="CacheEntryOptions.JitterInSeconds"/>
/// </summary>
/// <param name="cacheEntryOptions">
/// <see cref="CacheEntryOptions"/> instance that contains <see cref="CacheEntryOptions.TimeToExpire"/>
/// and <see cref="CacheEntryOptions.JitterInSeconds"/>.
/// </param>
/// <returns>
/// <see cref="CacheEntryOptions.TimeToExpire"/> of <paramref name="cacheEntryOptions"/>
/// randomized by <see cref="CacheEntryOptions.JitterInSeconds"/>.
/// </returns>
/// <remarks>
/// Jitter should be applied where possible, to avoid the thundering herd problem.
/// </remarks>
public static TimeSpan GetTimeToExpireWithJitter(this CacheEntryOptions cacheEntryOptions)
{
_ = cacheEntryOptions ?? throw new ArgumentNullException(nameof(cacheEntryOptions));

if (cacheEntryOptions.TimeToExpire == TimeSpan.MaxValue)
return cacheEntryOptions.TimeToExpire;

var randomSpan = GetRandomSpan(cacheEntryOptions.JitterInSeconds);
return cacheEntryOptions.TimeToExpire.AddOrCap(randomSpan);
}

private static TimeSpan GetRandomSpan(int jitterSpanInSeconds)
{
if (jitterSpanInSeconds == 0)
return TimeSpan.Zero;
else if (jitterSpanInSeconds < 0)
return TimeSpan.FromSeconds((long)((_random.NextDouble()) * jitterSpanInSeconds));
else
return TimeSpan.FromSeconds((long)((_random.NextDouble() * 2.0 - 1.0) * jitterSpanInSeconds));
}

private static TimeSpan AddOrCap(this TimeSpan timeSpan1, TimeSpan timeSpan2)
{
if (timeSpan1 == TimeSpan.MaxValue || timeSpan2 == TimeSpan.MaxValue)
return TimeSpan.MaxValue;

// checking only if timeSpan == TimeSpan.MaxValue is unsufficient
// as jitter might be applied to the timeSpan.
// based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92
long result = timeSpan1.Ticks + timeSpan2.Ticks;
if ((timeSpan1.Ticks >> 63 == timeSpan2.Ticks >> 63) && (timeSpan1.Ticks >> 63 != result >> 63))
return TimeSpan.MaxValue;
else
return timeSpan1.Add(timeSpan2);
}
}
}
36 changes: 36 additions & 0 deletions src/client/Abstractions/DateTimeOffsetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.Identity.ServiceEssentials
{
// CAN be made internal and moved to implementation

/// <summary>
/// <see cref="DateTimeOffset"/> extension methods.
/// </summary>
public static class DateTimeOffsetExtensions
{
/// <summary>
/// Adds <paramref name="timeSpan"/> to <paramref name="dateTime"/>.
/// If sum of <paramref name="dateTime"/> and <paramref name="timeSpan"/>
/// exeeds <see cref="DateTimeOffset.MaxValue"/>, the resulting <see cref="DateTimeOffset"/>,
/// result will be set to to <see cref="DateTimeOffset.MaxValue"/>.
/// </summary>
public static DateTimeOffset AddOrCap(this DateTimeOffset dateTime, TimeSpan timeSpan)
{
if (dateTime == DateTimeOffset.MaxValue || timeSpan == TimeSpan.MaxValue)
return DateTimeOffset.MaxValue;

// checking only if timeSpan == TimeSpan.MaxValue is unsufficient
// as jitter might be applied to the timeSpan.
// based on: https://referencesource.microsoft.com/#mscorlib/system/timespan.cs,92
long result = dateTime.UtcTicks + timeSpan.Ticks;
if ((dateTime.UtcTicks >> 63 == timeSpan.Ticks >> 63) && (dateTime.UtcTicks >> 63 != result >> 63))
return DateTimeOffset.MaxValue;
else
return dateTime.Add(timeSpan);
}
}
}
36 changes: 36 additions & 0 deletions src/client/Abstractions/ICacheEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Identity.ServiceEssentials
{
/// <summary>
/// Represents a cache item.
/// </summary>
public interface ICacheEntry<T>
{
/// <summary>
/// Gets the value in cache.
/// </summary>
public T Value { get; }

/// <summary>
/// Checks if <see cref="CacheEntry{T}"/> is found in a cache
/// and if it is still within its time-to-live.
/// </summary>
/// <returns>
/// <see langword="true"/> if the <see cref="CacheEntry{T}.Value"/> is found and
/// is still within its time-to-live, <see langword="false"/> otherwise.
/// </returns>
bool IsValid();

/// <summary>
/// Checks if <see cref="CacheEntry{T}"/> is found in a cache
/// and can be used as a last-known-good value.
/// </summary>
/// <returns>
/// <see langword="true"/> if the <see cref="CacheEntry{T}.Value"/> is found and
/// can be used as a last-known-good value, <see langword="false"/> otherwise.
/// </returns>
bool IsValidAsLastKnownGood();
}
}
Loading