Refactoring
This commit is contained in:
486
AMWD.Common/Utilities/CryptographyHelper.cs
Normal file
486
AMWD.Common/Utilities/CryptographyHelper.cs
Normal file
@@ -0,0 +1,486 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace System.Security.Cryptography
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides cryptographic functions ready-to-use.
|
||||
/// </summary>
|
||||
public class CryptographyHelper
|
||||
{
|
||||
private static readonly int saltLength = 8;
|
||||
|
||||
private readonly string masterKeyFile;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CryptographyHelper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="keyFile">The (absolute) path to the crypto key file. On <c>null</c> the file 'crypto.key' at the executing assembly location will be used.</param>
|
||||
public CryptographyHelper(string keyFile = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyFile))
|
||||
keyFile = "crypto.key";
|
||||
|
||||
if (!Path.IsPathRooted(keyFile))
|
||||
{
|
||||
string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||
keyFile = Path.Combine(dir, keyFile);
|
||||
}
|
||||
masterKeyFile = keyFile;
|
||||
|
||||
string pw = File.Exists(masterKeyFile) ? File.ReadAllText(masterKeyFile) : null;
|
||||
if (string.IsNullOrWhiteSpace(pw))
|
||||
File.WriteAllText(masterKeyFile, GetRandomString(64));
|
||||
}
|
||||
|
||||
#region Instance methods
|
||||
|
||||
#region AES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption (optional).</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public byte[] DecryptAes(byte[] cipher, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return AesDecrypt(cipher, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption (optional).</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public byte[] EncryptAes(byte[] plain, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return AesEncrypt(plain, password);
|
||||
}
|
||||
|
||||
#endregion AES
|
||||
|
||||
#region Triple DES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption (optional).</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public byte[] DecryptTripleDes(byte[] cipher, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return TripleDesDecrypt(cipher, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the <paramref name="password"/> parameter is <c>null</c>, the key from the file (set on initialize) is used instead.
|
||||
/// </remarks>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption (optional).</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public byte[] EncryptTripleDes(byte[] plain, string password = null)
|
||||
{
|
||||
if (password == null)
|
||||
password = File.ReadAllText(masterKeyFile);
|
||||
|
||||
return TripleDesEncrypt(plain, password);
|
||||
}
|
||||
|
||||
#endregion Triple DES
|
||||
|
||||
#endregion Instance methods
|
||||
|
||||
#region Static methods
|
||||
|
||||
#region Encryption
|
||||
|
||||
#region AES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption.</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public static byte[] AesDecrypt(byte[] cipher, string password)
|
||||
{
|
||||
byte[] salt = new byte[saltLength];
|
||||
Array.Copy(cipher, salt, saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var aes = Aes.Create();
|
||||
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = gen.GetBytes(aes.KeySize / 8);
|
||||
aes.IV = gen.GetBytes(aes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write);
|
||||
|
||||
cs.Write(cipher, saltLength, cipher.Length - saltLength);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the AES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption.</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public static byte[] AesEncrypt(byte[] plain, string password)
|
||||
{
|
||||
byte[] salt = GetRandomBytes(saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var aes = Aes.Create();
|
||||
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
aes.Key = gen.GetBytes(aes.KeySize / 8);
|
||||
aes.IV = gen.GetBytes(aes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
|
||||
ms.Write(salt, 0, salt.Length);
|
||||
cs.Write(plain, 0, plain.Length);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
#endregion AES
|
||||
|
||||
#region Triple DES
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="cipher">The encrypted data (cipher).</param>
|
||||
/// <param name="password">The password to use for decryption.</param>
|
||||
/// <returns>The decrypted data.</returns>
|
||||
public static byte[] TripleDesDecrypt(byte[] cipher, string password)
|
||||
{
|
||||
byte[] salt = new byte[saltLength];
|
||||
Array.Copy(cipher, salt, saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var tdes = TripleDES.Create();
|
||||
|
||||
tdes.Mode = CipherMode.CBC;
|
||||
tdes.Padding = PaddingMode.PKCS7;
|
||||
tdes.Key = gen.GetBytes(tdes.KeySize / 8);
|
||||
tdes.IV = gen.GetBytes(tdes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, tdes.CreateDecryptor(), CryptoStreamMode.Write);
|
||||
|
||||
cs.Write(cipher, saltLength, cipher.Length - saltLength);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts data using the triple DES algorithm and a password.
|
||||
/// </summary>
|
||||
/// <param name="plain">The data to encrypt.</param>
|
||||
/// <param name="password">The password to use for encryption.</param>
|
||||
/// <returns>The encrypted data (cipher).</returns>
|
||||
public static byte[] TripleDesEncrypt(byte[] plain, string password)
|
||||
{
|
||||
byte[] salt = GetRandomBytes(saltLength);
|
||||
|
||||
using var gen = new Rfc2898DeriveBytes(password, salt);
|
||||
using var tdes = TripleDES.Create();
|
||||
|
||||
tdes.Mode = CipherMode.CBC;
|
||||
tdes.Padding = PaddingMode.PKCS7;
|
||||
tdes.Key = gen.GetBytes(tdes.KeySize / 8);
|
||||
tdes.IV = gen.GetBytes(tdes.BlockSize / 8);
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
using var cs = new CryptoStream(ms, tdes.CreateEncryptor(), CryptoStreamMode.Write);
|
||||
|
||||
ms.Write(salt, 0, salt.Length);
|
||||
cs.Write(plain, 0, plain.Length);
|
||||
cs.FlushFinalBlock();
|
||||
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
#endregion Triple DES
|
||||
|
||||
#endregion Encryption
|
||||
|
||||
#region Hashing
|
||||
|
||||
#region MD5
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5(string str)
|
||||
{
|
||||
return Md5(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5File(string fileName)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return md5.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the MD5 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The MD5 hash value, in hexadecimal notation.</returns>
|
||||
public static string Md5(byte[] bytes)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
return md5.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion MD5
|
||||
|
||||
#region SHA-1
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1(string str)
|
||||
{
|
||||
return Sha1(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1File(string fileName)
|
||||
{
|
||||
using var sha1 = SHA1.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return sha1.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-1 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-1 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha1(byte[] bytes)
|
||||
{
|
||||
using var sha1 = SHA1.Create();
|
||||
return sha1.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion SHA-1
|
||||
|
||||
#region SHA-256
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256(string str)
|
||||
{
|
||||
return Sha256(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256File(string fileName)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return sha256.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-256 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-256 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha256(byte[] bytes)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
return sha256.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion SHA-256
|
||||
|
||||
#region SHA-512
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a string using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to hash, using UTF-8 encoding.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512(string str)
|
||||
{
|
||||
return Sha512(Encoding.UTF8.GetBytes(str));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash value from a file using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The name of the file to read.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512File(string fileName)
|
||||
{
|
||||
using var sha512 = SHA512.Create();
|
||||
using var fs = new FileStream(fileName, FileMode.Open);
|
||||
return sha512.ComputeHash(fs).BytesToHex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a hash from a byte array value using the SHA-512 algorithm.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array.</param>
|
||||
/// <returns>The SHA-512 hash value, in hexadecimal notation.</returns>
|
||||
public static string Sha512(byte[] bytes)
|
||||
{
|
||||
using var sha512 = SHA512.Create();
|
||||
return sha512.ComputeHash(bytes).BytesToHex();
|
||||
}
|
||||
|
||||
#endregion SHA-512
|
||||
|
||||
#endregion Hashing
|
||||
|
||||
#region Random
|
||||
|
||||
/// <summary>
|
||||
/// Generates an array with random (non-zero) bytes.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of bytes to generate.</param>
|
||||
/// <returns></returns>
|
||||
public static byte[] GetRandomBytes(int count)
|
||||
{
|
||||
using var gen = RandomNumberGenerator.Create();
|
||||
byte[] bytes = new byte[count];
|
||||
gen.GetNonZeroBytes(bytes);
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string with random characters.
|
||||
/// </summary>
|
||||
/// <param name="length">The length of the string to generate.</param>
|
||||
/// <param name="pool">The characters to use (Default: [a-zA-Z0-9]).</param>
|
||||
/// <returns></returns>
|
||||
public static string GetRandomString(int length, string pool = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pool))
|
||||
pool = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
|
||||
var sb = new StringBuilder(length);
|
||||
int multiply = sizeof(int) / sizeof(byte);
|
||||
int len = length * multiply;
|
||||
byte[] bytes = GetRandomBytes(len);
|
||||
for (int i = 0; i < bytes.Length; i += multiply)
|
||||
{
|
||||
uint number = BitConverter.ToUInt32(bytes, i);
|
||||
sb.Append(pool[(int)(number % pool.Length)]);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion Random
|
||||
|
||||
#region Probing security
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two strings are equal in constant time. This method does not stop
|
||||
/// early if a difference was detected, unless the length differs.
|
||||
/// </summary>
|
||||
/// <param name="a">The first string.</param>
|
||||
/// <param name="b">The second string.</param>
|
||||
/// <returns>true, if both strings are equal; otherwise, false.</returns>
|
||||
public static bool SecureEquals(string a, string b)
|
||||
{
|
||||
if ((a == null) != (b == null))
|
||||
return false;
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
int differentBits = 0;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
differentBits |= a[i] ^ b[i];
|
||||
}
|
||||
return differentBits == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two byte arrays are equal in constant time. This method does not stop
|
||||
/// early if a difference was detected, unless the length differs.
|
||||
/// </summary>
|
||||
/// <param name="a">The first array.</param>
|
||||
/// <param name="b">The second array.</param>
|
||||
/// <returns>true, if both arrays are equal; otherwise, false.</returns>
|
||||
public static bool SecureEquals(byte[] a, byte[] b)
|
||||
{
|
||||
if ((a == null) != (b == null))
|
||||
return false;
|
||||
if (a.Length != b.Length)
|
||||
return false;
|
||||
|
||||
int differentBits = 0;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
differentBits |= a[i] ^ b[i];
|
||||
}
|
||||
return differentBits == 0;
|
||||
}
|
||||
|
||||
#endregion Probing security
|
||||
|
||||
#endregion Static methods
|
||||
}
|
||||
}
|
||||
590
AMWD.Common/Utilities/DelayedTask.cs
Normal file
590
AMWD.Common/Utilities/DelayedTask.cs
Normal file
@@ -0,0 +1,590 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AMWD.Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements an awaitable task that runs after a specified delay. The delay can be reset
|
||||
/// before and after the task has run. By resetting the delay, the task can be executed multiple
|
||||
/// times. The scheduled or executing or last executed task can be awaited, until the delay is
|
||||
/// reset. After that, the next execution can be awaited.
|
||||
/// </summary>
|
||||
public class DelayedTask
|
||||
{
|
||||
#region Data
|
||||
|
||||
/// <summary>
|
||||
/// The synchronisation object.
|
||||
/// </summary>
|
||||
protected readonly object syncObj = new();
|
||||
|
||||
/// <summary>
|
||||
/// The exception handler.
|
||||
/// </summary>
|
||||
protected Action<Exception> exceptionHandler;
|
||||
|
||||
private Timer timer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the timer is running and an execution is scheduled. This
|
||||
/// is mutually exclusive to <see cref="IsRunning"/>.
|
||||
/// </summary>
|
||||
public bool IsWaitingToRun { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the action is currently running. This is mutually
|
||||
/// exclusive to <see cref="IsWaitingToRun"/>.
|
||||
/// </summary>
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the action shall be executed again after the currently ongoing
|
||||
/// execution has completed.
|
||||
/// </summary>
|
||||
private bool nextRunPending;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the <see cref="Task"/> for the <see cref="GetAwaiter"/> method.
|
||||
/// </summary>
|
||||
protected TaskCompletionSourceWrapper tcs;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to execute.
|
||||
/// </summary>
|
||||
protected Action Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay to wait before executing the action.
|
||||
/// </summary>
|
||||
public TimeSpan Delay { get; protected set; }
|
||||
|
||||
#endregion Data
|
||||
|
||||
#region Static methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task instance that executes the specified action after the delay, but does
|
||||
/// not start it yet. Multiple executions are allowed when calling <see cref="Reset"/> after
|
||||
/// the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTask Create(Action action, TimeSpan delay)
|
||||
{
|
||||
return new DelayedTask { Action = action, Delay = delay };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task instance that executes the specified action after the delay, but does
|
||||
/// not start it yet. Multiple executions are allowed when calling <see cref="Reset"/> after
|
||||
/// the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTaskWithResult<TResult> Create<TResult>(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return DelayedTaskWithResult<TResult>.Create(action, delay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified action after the delay. Multiple executions are allowed when
|
||||
/// calling <see cref="Reset"/> after the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTask Run(Action action, TimeSpan delay)
|
||||
{
|
||||
return new DelayedTask { Action = action, Delay = delay }.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the specified action after the delay. Multiple executions are allowed when
|
||||
/// calling <see cref="Reset"/> after the executed was started.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <param name="delay">The delay.</param>
|
||||
/// <returns></returns>
|
||||
public static DelayedTaskWithResult<TResult> Run<TResult>(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return DelayedTaskWithResult<TResult>.Run(action, delay);
|
||||
}
|
||||
|
||||
#endregion Static methods
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelayedTask"/> class.
|
||||
/// </summary>
|
||||
protected DelayedTask()
|
||||
{
|
||||
tcs = CreateTcs();
|
||||
SetLastResult(tcs);
|
||||
}
|
||||
|
||||
#endregion Constructors
|
||||
|
||||
#region Public instance methods
|
||||
|
||||
/// <summary>
|
||||
/// Resets the delay and restarts the timer. If an execution is currently pending, it is
|
||||
/// postponed until the full delay has elapsed again. If no execution is pending, the action
|
||||
/// will be executed again after the delay.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
if (!IsWaitingToRun && !IsRunning)
|
||||
{
|
||||
// Let callers wait for the next execution
|
||||
tcs = CreateTcs();
|
||||
}
|
||||
IsWaitingToRun = true;
|
||||
if (timer != null)
|
||||
{
|
||||
timer.Change(Delay, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the delay. Any pending executions are cleared. If the action was pending but not
|
||||
/// yet executing, this task is cancelled. If the action was not pending or is already
|
||||
/// executing, this task will be completed successfully after the action has completed.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
TaskCompletionSourceWrapper localTcs = null;
|
||||
lock (syncObj)
|
||||
{
|
||||
IsWaitingToRun = false;
|
||||
nextRunPending = false;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
if (!IsRunning)
|
||||
{
|
||||
localTcs = tcs;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the task (as cancelled) so that nobody needs to wait for an execution that
|
||||
// isn't currently scheduled
|
||||
localTcs?.TrySetCanceled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a pending execution immediately, not waiting for the timer to elapse.
|
||||
/// </summary>
|
||||
/// <returns>true, if an execution was started; otherwise, false.</returns>
|
||||
/// <remarks>
|
||||
/// A new execution is only started if one is currently waiting to run, or already running.
|
||||
/// In the former case, the execution is scheduled immediately with the timer; in the latter
|
||||
/// case, it is scheduled for when the currently running execution has completed. If an
|
||||
/// execution has been started (the method returned true), it can be awaited normally.
|
||||
/// </remarks>
|
||||
public bool ExecutePending()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
if (!IsWaitingToRun && !IsRunning)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IsWaitingToRun = true;
|
||||
if (timer != null)
|
||||
{
|
||||
timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
else
|
||||
{
|
||||
timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an awaiter used to await this <see cref="DelayedTask"/>.
|
||||
/// </summary>
|
||||
/// <returns>An awaiter instance.</returns>
|
||||
public TaskAwaiter GetAwaiter()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
return tcs.Task.GetAwaiter();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="System.Threading.Tasks.Task"/> that represents the current awaitable
|
||||
/// operation.
|
||||
/// </summary>
|
||||
public Task Task
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="DelayedTask"/> to
|
||||
/// <see cref="System.Threading.Tasks.Task"/>.
|
||||
/// </summary>
|
||||
/// <param name="delayedTask">The <see cref="DelayedTask"/> instance to cast.</param>
|
||||
/// <returns>A <see cref="System.Threading.Tasks.Task"/> that represents the current
|
||||
/// awaitable operation.</returns>
|
||||
public static implicit operator Task(DelayedTask delayedTask) => delayedTask.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exception of the last execution. If the action has not yet thrown any
|
||||
/// exceptions, this will return null.
|
||||
/// </summary>
|
||||
public Exception Exception => Task.Exception;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an unhandled exception handler to this <see cref="DelayedTask"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="exceptionHandler">The action that handles an exception.</param>
|
||||
/// <returns>The current instance.</returns>
|
||||
public DelayedTask WithExceptionHandler(Action<Exception> exceptionHandler)
|
||||
{
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion Public instance methods
|
||||
|
||||
#region Non-public methods
|
||||
|
||||
/// <summary>
|
||||
/// Starts the current instance after creating it.
|
||||
/// </summary>
|
||||
/// <returns>The current instance.</returns>
|
||||
protected DelayedTask Start()
|
||||
{
|
||||
tcs = CreateTcs();
|
||||
IsWaitingToRun = true;
|
||||
timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="TaskCompletionSourceWrapper"/> instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual TaskCompletionSourceWrapper CreateTcs()
|
||||
{
|
||||
return new TaskCompletionSourceWrapper<object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the timer has elapsed.
|
||||
/// </summary>
|
||||
/// <param name="state">Unused.</param>
|
||||
protected void OnTimerCallback(object state)
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
if (!IsWaitingToRun)
|
||||
{
|
||||
// Already cancelled, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
IsWaitingToRun = false;
|
||||
if (IsRunning)
|
||||
{
|
||||
// Currently running, remember and do nothing for now
|
||||
nextRunPending = true;
|
||||
return;
|
||||
}
|
||||
IsRunning = true;
|
||||
}
|
||||
|
||||
// Run as long as there are pending executions and the instance has not been disposed of
|
||||
bool runAgain;
|
||||
TaskCompletionSourceWrapper localTcs = null;
|
||||
Exception exception = null;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = ex;
|
||||
lock (syncObj)
|
||||
{
|
||||
runAgain = false;
|
||||
IsRunning = false;
|
||||
nextRunPending = false;
|
||||
localTcs = tcs;
|
||||
if (!IsWaitingToRun)
|
||||
{
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
exceptionHandler?.Invoke(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
runAgain = nextRunPending;
|
||||
IsRunning = runAgain;
|
||||
nextRunPending = false;
|
||||
if (!runAgain)
|
||||
{
|
||||
if (!IsWaitingToRun)
|
||||
{
|
||||
localTcs = tcs;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (runAgain);
|
||||
|
||||
// Unblock waiters if not already waiting for the next execution.
|
||||
// This task can be awaited again after the Reset method has been called.
|
||||
if (exception != null)
|
||||
localTcs?.TrySetException(exception);
|
||||
else
|
||||
SetLastResult(localTcs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the action of the task.
|
||||
/// </summary>
|
||||
protected virtual void Run()
|
||||
{
|
||||
Action();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="TaskCompletionSourceWrapper"/> result from the last action.
|
||||
/// </summary>
|
||||
/// <param name="tcs">The <see cref="TaskCompletionSourceWrapper"/> to set the result of.</param>
|
||||
protected virtual void SetLastResult(TaskCompletionSourceWrapper tcs)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<object>)tcs;
|
||||
myTcs?.TrySetResult(default);
|
||||
}
|
||||
|
||||
#endregion Non-public methods
|
||||
|
||||
#region Internal TaskCompletionSourceWrapper classes
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a <see cref="TaskCompletionSource{TResult}"/> instance in a non-generic way to
|
||||
/// allow sharing it in the non-generic base class.
|
||||
/// </summary>
|
||||
protected abstract class TaskCompletionSourceWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Task{TResult}"/> of the <see cref="TaskCompletionSource{TResult}"/>.
|
||||
/// </summary>
|
||||
public abstract Task Task { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Faulted"/> state and binds it to a specified exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to bind to this <see cref="Task{TResult}"/>.</param>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetException(Exception)"/>
|
||||
public abstract void TrySetException(Exception exception);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Canceled"/> state.
|
||||
/// </summary>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetCanceled()"/>
|
||||
public abstract void TrySetCanceled();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TaskCompletionSourceWrapper"/> that provides a result value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the result value.</typeparam>
|
||||
protected class TaskCompletionSourceWrapper<TResult> : TaskCompletionSourceWrapper
|
||||
{
|
||||
private readonly TaskCompletionSource<TResult> tcs;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Task{TResult}"/> of the <see cref="TaskCompletionSource{TResult}"/>.
|
||||
/// </summary>
|
||||
public override Task Task => tcs.Task;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TaskCompletionSourceWrapper{TResult}"/> class.
|
||||
/// </summary>
|
||||
public TaskCompletionSourceWrapper()
|
||||
{
|
||||
tcs = new TaskCompletionSource<TResult>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.RanToCompletion"/> state.
|
||||
/// </summary>
|
||||
/// <param name="result">The result value to bind to this <see cref="Task{TResult}"/>.</param>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetResult(TResult)"/>
|
||||
public void TrySetResult(TResult result)
|
||||
{
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Faulted"/> state and binds it to a specified exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception to bind to this <see cref="Task{TResult}"/>.</param>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetException(Exception)"/>
|
||||
public override void TrySetException(Exception exception)
|
||||
{
|
||||
tcs.TrySetException(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
||||
/// <see cref="TaskStatus.Canceled"/> state.
|
||||
/// </summary>
|
||||
/// <seealso cref="TaskCompletionSource{TResult}.TrySetCanceled()"/>
|
||||
public override void TrySetCanceled()
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Internal TaskCompletionSourceWrapper classes
|
||||
}
|
||||
|
||||
#region Generic derived classes
|
||||
|
||||
/// <summary>
|
||||
/// Implements an awaitable task that runs after a specified delay. The delay can be reset
|
||||
/// before and after the task has run.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The type of the return value of the action.</typeparam>
|
||||
public class DelayedTaskWithResult<TResult> : DelayedTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The result of the last execution of the action.
|
||||
/// </summary>
|
||||
protected TResult lastResult;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action to execute.
|
||||
/// </summary>
|
||||
protected new Func<TResult> Action { get; set; }
|
||||
|
||||
internal static DelayedTaskWithResult<TResult> Create(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return new DelayedTaskWithResult<TResult> { Action = action, Delay = delay };
|
||||
}
|
||||
|
||||
internal static DelayedTaskWithResult<TResult> Run(Func<TResult> action, TimeSpan delay)
|
||||
{
|
||||
return (DelayedTaskWithResult<TResult>)new DelayedTaskWithResult<TResult> { Action = action, Delay = delay }.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="DelayedTask.TaskCompletionSourceWrapper"/> instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override TaskCompletionSourceWrapper CreateTcs()
|
||||
{
|
||||
return new TaskCompletionSourceWrapper<TResult>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the action of the task.
|
||||
/// </summary>
|
||||
protected override void Run()
|
||||
{
|
||||
lastResult = Action();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="DelayedTask.TaskCompletionSourceWrapper"/> result from the last action.
|
||||
/// </summary>
|
||||
/// <param name="tcs">The <see cref="DelayedTask.TaskCompletionSourceWrapper"/> to set the result of.</param>
|
||||
protected override void SetLastResult(TaskCompletionSourceWrapper tcs)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
||||
myTcs?.TrySetResult(lastResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an awaiter used to await this <see cref="DelayedTask"/>.
|
||||
/// </summary>
|
||||
/// <returns>An awaiter instance.</returns>
|
||||
public new TaskAwaiter<TResult> GetAwaiter()
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
||||
var myTask = (Task<TResult>)myTcs.Task;
|
||||
return myTask.GetAwaiter();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Task{TResult}"/> that represents the current awaitable operation.
|
||||
/// </summary>
|
||||
public new Task<TResult> Task
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncObj)
|
||||
{
|
||||
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
||||
var myTask = (Task<TResult>)myTcs.Task;
|
||||
return myTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs an implicit conversion from <see cref="DelayedTaskWithResult{TResult}"/> to
|
||||
/// <see cref="Task{TResult}"/>.
|
||||
/// </summary>
|
||||
/// <param name="delayedTask">The <see cref="DelayedTaskWithResult{TResult}"/> instance to cast.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> that represents the current awaitable operation.</returns>
|
||||
public static implicit operator Task<TResult>(DelayedTaskWithResult<TResult> delayedTask)
|
||||
{
|
||||
return delayedTask.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an unhandled exception handler to this <see cref="DelayedTask"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="exceptionHandler">The action that handles an exception.</param>
|
||||
/// <returns>The current instance.</returns>
|
||||
public new DelayedTaskWithResult<TResult> WithExceptionHandler(Action<Exception> exceptionHandler)
|
||||
{
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Generic derived classes
|
||||
}
|
||||
81
AMWD.Common/Utilities/NetworkHelper.cs
Normal file
81
AMWD.Common/Utilities/NetworkHelper.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace AMWD.Common.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides some network utils.
|
||||
/// </summary>
|
||||
public static class NetworkHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to resolve a <paramref name="hostname"/> into an <see cref="IPAddress"/> to connect to.
|
||||
/// </summary>
|
||||
/// <param name="hostname">The hostname to resolve.</param>
|
||||
/// <param name="addressFamily">An address family to use (available: <see cref="AddressFamily.InterNetwork"/> and <see cref="AddressFamily.InterNetworkV6"/>).</param>
|
||||
/// <param name="fallback">The fallback ip address when resolving failed.</param>
|
||||
/// <returns>The resolved <see cref="IPAddress"/> to connect to or <paramref name="fallback"/> value.</returns>
|
||||
public static IPAddress ResolveHost(string hostname, AddressFamily addressFamily = AddressFamily.Unspecified, IPAddress fallback = null)
|
||||
{
|
||||
if (IPAddress.TryParse(hostname, out var ipAddress))
|
||||
{
|
||||
if (ipAddress.AddressFamily != AddressFamily.InterNetwork && ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
return fallback;
|
||||
|
||||
if (addressFamily != AddressFamily.Unspecified && ipAddress.AddressFamily != addressFamily)
|
||||
return fallback;
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
return Dns.GetHostAddresses(hostname)
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.Where(a => addressFamily == AddressFamily.Unspecified || a.AddressFamily == addressFamily)
|
||||
.OrderBy(a => a.AddressFamily)
|
||||
.FirstOrDefault() ?? fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve a <paramref name="iface"/> into an <see cref="IPAddress"/> to bind (listen) on.
|
||||
/// </summary>
|
||||
/// <param name="iface">The interface name to resolve.</param>
|
||||
/// <param name="addressFamily">An address family to use (available: <see cref="AddressFamily.InterNetwork"/> and <see cref="AddressFamily.InterNetworkV6"/>).</param>
|
||||
/// <param name="fallback">The fallback ip address when resolving failed.</param>
|
||||
/// <returns>The resolved <see cref="IPAddress"/> to bind on or <paramref name="fallback"/> value.</returns>
|
||||
public static IPAddress ResolveInterface(string iface, AddressFamily addressFamily = AddressFamily.Unspecified, IPAddress fallback = null)
|
||||
{
|
||||
if (IPAddress.TryParse(iface, out var ipAddress))
|
||||
{
|
||||
if (ipAddress.AddressFamily != AddressFamily.InterNetwork && ipAddress.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
return fallback;
|
||||
|
||||
if (addressFamily != AddressFamily.Unspecified && ipAddress.AddressFamily != addressFamily)
|
||||
return fallback;
|
||||
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Dns.GetHostAddresses(iface)
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.Where(a => addressFamily == AddressFamily.Unspecified || a.AddressFamily == addressFamily)
|
||||
.OrderBy(a => a.AddressFamily)
|
||||
.FirstOrDefault() ?? fallback;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
return NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.Name.Equals(iface, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses.Select(ai => ai.Address))
|
||||
.Where(a => a.AddressFamily == AddressFamily.InterNetwork || a.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
.Where(a => addressFamily == AddressFamily.Unspecified || a.AddressFamily == addressFamily)
|
||||
.OrderBy(a => a.AddressFamily)
|
||||
.FirstOrDefault() ?? fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user