Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 5 additions & 5 deletions src/CoreEx.RefData/Abstractions/ReferenceDataCollectionCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public abstract class ReferenceDataCollectionCore<TId, TRef> : IReferenceDataCol
#endif
private readonly ConcurrentDictionary<object, TRef> _rdcId = new();
private readonly ConcurrentDictionary<string, TRef> _rdcCode;
private Dictionary<(string, object?), TRef>? _mappingsDict;
private ConcurrentDictionary<(string, object?), TRef>? _mappingsDict;

/// <summary>
/// Initializes a new instance of the <see cref="ReferenceDataCollection{TItem, TId}"/> class.
Expand Down Expand Up @@ -75,7 +75,7 @@ public void Add(TRef item)

if (item.HasMappings)
{
_mappingsDict ??= [];
_mappingsDict ??= new();

// Make sure there are no duplicates.
foreach (var map in item.Mappings!)
Expand All @@ -87,7 +87,7 @@ public void Add(TRef item)
// Now add 'em in.
foreach (var map in item.Mappings)
{
_mappingsDict.Add((map.Key, map.Value), item);
_mappingsDict.TryAdd((map.Key, map.Value), item);
}
}

Expand Down Expand Up @@ -156,7 +156,7 @@ public bool TryGetById(TId id, [NotNullWhen(true)] out TRef? item)
}

/// <inheritdoc/>
public TRef? GetById(TId id) => id is null ? default : _rdcId[id];
public TRef? GetById(TId id) => id is null ? default : _rdcId.TryGetValue(id, out var item) ? item : default;

/// <inheritdoc/>
public bool ContainsCode(string code) => _rdcCode.ContainsKey(code);
Expand All @@ -172,7 +172,7 @@ public bool TryGetByCode(string code, [NotNullWhen(true)] out TRef? item)
}

/// <inheritdoc/>
public TRef? GetByCode(string code) => code is null ? default : _rdcCode[code];
public TRef? GetByCode(string code) => code is null ? default : _rdcCode.TryGetValue(code, out var item) ? item : default;

/// <inheritdoc/>
public bool ContainsMapping<T>(string name, T value) where T : IComparable<T>, IEquatable<T> => _mappingsDict is not null && _mappingsDict.ContainsKey((name, value));
Expand Down
15 changes: 13 additions & 2 deletions src/CoreEx.RefData/ReferenceDataCodeCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,21 @@ namespace CoreEx.RefData;
public void Clear() => _codes.Clear();

/// <inheritdoc/>
public bool Contains(TRef item) => ((IList)_codes).Contains(item);
public bool Contains(TRef item) => _codes.Contains(item?.Code);

/// <inheritdoc/>
public void CopyTo(TRef[] array, int arrayIndex) => ((IList)_codes).CopyTo(array, arrayIndex);
public void CopyTo(TRef[] array, int arrayIndex)
{
array.ThrowIfNull();
if (arrayIndex < 0 || arrayIndex + Count > array.Length)
throw new ArgumentOutOfRangeException(nameof(arrayIndex));

var i = arrayIndex;
foreach (var item in this)
{
array[i++] = item;
}
}

/// <inheritdoc/>
public IEnumerator<TRef> GetEnumerator()
Expand Down
12 changes: 6 additions & 6 deletions src/CoreEx.RefData/ReferenceDataHybridCache.TypedInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public partial class ReferenceDataHybridCache
* This functionality is required as the underlying cache *may* leverage serialization, and as such, we have to get it in a typed manner as IReferenceDataCollection (interface) is not valid.
*/

private static readonly MethodInfo TryGetByKeyAsync_OpenGeneric = typeof(IHybridCache).GetMethod(nameof(IHybridCache.TryGetByKeyAsync)) ?? throw new InvalidOperationException($"{nameof(IHybridCache)}.{nameof(IHybridCache.TryGetByKeyAsync)} public instance method not found.");
private static readonly MethodInfo _tryGetByKeyAsync_OpenGeneric = typeof(IHybridCache).GetMethod(nameof(IHybridCache.TryGetByKeyAsync)) ?? throw new InvalidOperationException($"{nameof(IHybridCache)}.{nameof(IHybridCache.TryGetByKeyAsync)} public instance method not found.");
private static readonly ConcurrentDictionary<Type, TryGetByKeyInvoker> _invokers = new();

private delegate Task<(bool Exists, object? Value)> TryGetByKeyInvoker(IHybridCache cache, string key, HybridCacheEntryOptions options, CancellationToken cancellationToken);
Expand All @@ -20,7 +20,7 @@ public partial class ReferenceDataHybridCache
private static TryGetByKeyInvoker GetInvokerForType(Type type) => _invokers.GetOrAdd(type, type =>
{
// Close the generic: TryGetByKeyAsync<T>
var closed = TryGetByKeyAsync_OpenGeneric.MakeGenericMethod(type);
var closed = _tryGetByKeyAsync_OpenGeneric.MakeGenericMethod(type);

// Parameters: (cache, key, options, cancellationToken) =>
var cacheParam = Expression.Parameter(typeof(IHybridCache), "cache");
Expand All @@ -31,8 +31,8 @@ private static TryGetByKeyInvoker GetInvokerForType(Type type) => _invokers.GetO
// Expression: cache.TryGetByKeyAsync<TVal>(key, options, ct)
var call = Expression.Call(cacheParam, closed, keyParam, optParam, ctParam);

// Build method body: ToTupleTask<T>(call).
var method = typeof(ReferenceDataHybridCache).GetMethod(nameof(ToTupleTask), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(type);
// Build method body: ToTupleAsync<T>(call).
var method = typeof(ReferenceDataHybridCache).GetMethod(nameof(ToTupleAsync), BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(type);
var body = Expression.Call(method, call);
var lambda = Expression.Lambda<TryGetByKeyInvoker>(body, cacheParam, keyParam, optParam, ctParam);
return lambda.Compile();
Expand All @@ -41,5 +41,5 @@ private static TryGetByKeyInvoker GetInvokerForType(Type type) => _invokers.GetO
/// <summary>
/// Underlying method to invoke the typed <see cref="IHybridCache.TryGetByKeyAsync{T}"/>.
/// </summary>
private static async Task<(bool Exists, object? Value)> ToTupleTask<T>(Task<(bool Exists, T? Value)> task) => await task.ConfigureAwait(false);
}
private static async Task<(bool Exists, object? Value)> ToTupleAsync<T>(Task<(bool Exists, T? Value)> task) => await task.ConfigureAwait(false);
}
2 changes: 1 addition & 1 deletion src/CoreEx/Caching/HybridCacheEntryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public static HybridCacheEntryOptions CreateForName(string name, TimeSpan? local
/// <returns>A <see cref="HybridCacheEntryOptions"/> instance associated with the specified type.</returns>
/// <remarks>The <typeparamref name="T"/> <see cref="MemberInfo.Name"/> is used as the name; see <see cref="CreateForName(string, TimeSpan?, TimeSpan?, CacheStrategy?)"/>.</remarks>
public static HybridCacheEntryOptions CreateFor<T>(TimeSpan? localExpiration = null, TimeSpan? distributedExpiration = null, CacheStrategy? strategy = null)
=> CreateForName(nameof(T), localExpiration, distributedExpiration, strategy);
=> CreateForName(typeof(T).Name, localExpiration, distributedExpiration, strategy);

/// <summary>
/// Gets or sets the <see cref="CacheStrategy"/>.
Expand Down
17 changes: 16 additions & 1 deletion src/CoreEx/CoreExExtensions.DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public static IServiceCollection AddDynamicServicesUsing(this IServiceCollection
{
foreach (var assembly in assemblies.Distinct())
{
foreach (var match in from type in assembly.GetTypes()
foreach (var match in from type in GetLoadableTypes(assembly)
where !type.IsAbstract && !type.IsGenericTypeDefinition
let sla = ServiceLifetimeAttribute.GetCustomAttribute(type)
where sla is not null
Expand All @@ -123,6 +123,21 @@ where sla is not null
return services;
}

/// <summary>
/// Gets the types from the specified <paramref name="assembly"/>, tolerating types that fail to load (e.g. due to missing dependencies).
/// </summary>
private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
{
try
{
return assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.OfType<Type>();
}
Comment thread
Copilot marked this conversation as resolved.
}

/// <summary>
/// Adds a <b>singleton</b> service for the internal <see cref="IMemoryCache"/>.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Entities/ETag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public static string ParseETag(ReadOnlySpan<char> etag)
return etag[1..^1].ToString();

if (etag.StartsWith("W/\"") && etag[^1] == '\"')
return etag[2..^1].ToString();
return etag[3..^1].ToString();

return etag.ToString();
}
Expand Down
4 changes: 2 additions & 2 deletions src/CoreEx/ExecutionContext.Infra.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ public static T GetRequiredKeyedService<T>(object? serviceKey) where T : notnull
public static object? GetKeyedService(Type type, object? serviceKey)
{
type.ThrowIfNull();
if (TryGetCurrent(out var executionContext) && executionContext.ServiceProvider is not null)
return executionContext.ServiceProvider.GetKeyedServices(type, serviceKey).FirstOrDefault(s => s?.GetType() == type);
if (TryGetCurrent(out var executionContext) && executionContext.ServiceProvider is IKeyedServiceProvider ksp)
return ksp.GetKeyedService(type, serviceKey);

return null;
}
Expand Down
2 changes: 2 additions & 0 deletions src/CoreEx/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ public virtual ExecutionContext CreateCopy()
ec.User = User;
ec.TenantId = TenantId;
ec.UICulture = UICulture;
ec.OperationType = OperationType;
ec.IncludeRelatedText = IncludeRelatedText;
ec._isCopied = true;

if (_attributes.IsValueCreated)
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Extensions.HttpRequestMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ private static StringBuilder AddQuery(this StringBuilder sb, string name, string
if (sb.Length > 0)
sb.Append('&');

sb.Append($"{name}={value}");
sb.Append(name).Append('=').Append(Uri.EscapeDataString(value));
return sb;
}
}
1 change: 1 addition & 0 deletions src/CoreEx/Extensions.HttpResponseMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static partial class Extensions
if (pd is not null)
return new ProblemDetailsException(pd, new HttpRequestException($"{CreateMessage(response)} Problem details:{content}"));
}
catch (OperationCanceledException) { throw; } // Let cancellation propagate; do not treat as "not a problem details".
catch { } // Swallow and assume not a problem details.

return null;
Expand Down
4 changes: 1 addition & 3 deletions src/CoreEx/Hosting/HostedServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,14 +323,12 @@ public async Task StopAsync(CancellationToken cancellationToken)
{
lock (SyncLock)
{
if (Status.IsStop)

Status = ServiceStatus.Stopping;
if (Logger.IsEnabled(LogLevel.Information))
Logger.LogInformation("{ServiceName} stop requested.", ServiceName);
}

await OnStopAsync(cancellationToken);
await OnStopAsync(cancellationToken).ConfigureAwait(false);

lock (SyncLock)
{
Expand Down
4 changes: 2 additions & 2 deletions src/CoreEx/Hosting/Work/WorkOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ public class WorkOrchestrator(IWorkProvider provider, JsonSerializerOptions? jso
/// <summary>
/// Gets the <see cref="IWorkProvider"/>.
/// </summary>
public IWorkProvider Provider = provider.ThrowIfNull(nameof(provider));
public IWorkProvider Provider { get; } = provider.ThrowIfNull(nameof(provider));

/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/>.
/// </summary>
/// <remarks>Defaults to <see cref="JsonDefaults.SerializerOptions"/>.</remarks>
public JsonSerializerOptions JsonSerializerOptions = jsonSerializerOptions ?? JsonDefaults.SerializerOptions;
public JsonSerializerOptions JsonSerializerOptions { get; } = jsonSerializerOptions ?? JsonDefaults.SerializerOptions;

/// <summary>
/// Gets or sets the work expiry <see cref="TimeSpan"/>.
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Json/JsonExceptionConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public override void Write(Utf8JsonWriter writer, TException value, JsonSerializ
Ignore = uu.GetCustomAttribute<JsonIgnoreAttribute>(),
JsonName = uu.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name
})
.Where(uu => uu.Ignore is not null && uu.Name != nameof(Exception.TargetSite));
.Where(uu => uu.Ignore is null && uu.Name != nameof(Exception.TargetSite));

if (options?.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull)
serializableProperties = serializableProperties.Where(uu => uu.Value is not null);
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Json/JsonSubstituteNamingPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ public JsonSubstituteNamingPolicy()

/// <inheritdoc/>
/// <remarks>Converts using the <see cref="Substitutions"/> then the <see cref="FallbackPolicy"/>.</remarks>
public override string ConvertName(string name) => Substitutions.TryGetValue(name, out var substitution) ? substitution : CamelCase.ConvertName(name);
public override string ConvertName(string name) => Substitutions.TryGetValue(name, out var substitution) ? substitution : FallbackPolicy.ConvertName(name);
}
11 changes: 10 additions & 1 deletion src/CoreEx/Mapping/Converters/EncodedStringToUInt32Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ namespace CoreEx.Mapping.Converters;
/// </summary>
public readonly struct EncodedStringToUInt32Converter : IConverter<string?, uint>
{
private static readonly ValueConverter<string?, uint> _convertToDestination = new(s => s == null ? 0 : BitConverter.ToUInt32(Convert.FromBase64String(s)));
private static readonly ValueConverter<string?, uint> _convertToDestination = new(s =>
{
if (s == null)
return 0;

var bytes = Convert.FromBase64String(s);
return bytes.Length == 4
? BitConverter.ToUInt32(bytes)
: throw new FormatException($"The decoded value must be exactly 4 bytes to convert to a {nameof(UInt32)}; the specified value decoded to {bytes.Length} byte(s).");
});
private static readonly ValueConverter<uint, string?> _convertToSource = new(d => d == 0 ? null : Convert.ToBase64String(BitConverter.GetBytes(d)));

/// <summary>
Expand Down
1 change: 0 additions & 1 deletion src/CoreEx/Mapping/IntoMapperT3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ namespace CoreEx.Mapping;
/// </summary>
/// <param name="source">The source value.</param>
/// <param name="destination">The destination value.</param>
[return: NotNullIfNotNull(nameof(source))]
public static new void MapInto(TSource source, TDestination destination)
{
Default.OnMapInto(source.ThrowIfNull(), destination.ThrowIfNull());
Expand Down
5 changes: 3 additions & 2 deletions src/CoreEx/Metadata/RuntimeMetadata.AreEqual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static bool AreEqual<T>(T? left, T? right)
// Short circuit arrays, collections, lists, and dictionaries based on count difference.
if (left is ICollection lc)
{
if (lc.Count != ((ICollection)right).Count)
if (right is not ICollection rc || lc.Count != rc.Count)
return false;
}

Expand Down Expand Up @@ -146,7 +146,8 @@ static bool EnumerateObjectAreEqual(IEnumerable l, IEnumerable r)
return false;
}

return true;
// Ensure the right-hand sequence does not have additional trailing elements.
return !er.MoveNext();
}

return (left, right) switch
Expand Down
3 changes: 2 additions & 1 deletion src/CoreEx/Metadata/RuntimeMetadata.Internal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ private static bool TypedEnumerateAreEqual<T>(IEnumerable<T> l, IEnumerable<T> r
return false;
}

return true;
// Ensure the right-hand sequence does not have additional trailing elements.
return !er.MoveNext();
}

}
2 changes: 1 addition & 1 deletion src/CoreEx/RefData/IReferenceDataCollectionT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ bool IReferenceDataCollection.TryGetByCode(string code, [NotNullWhen(true)] out
}

/// <inheritdoc/>
IReferenceData? IReferenceDataCollection.GetById(object? id) => GetById(id);
IReferenceData? IReferenceDataCollection.GetById(object? id) => id is TId typedId ? GetById(typedId) : null;

Comment thread
chullybun marked this conversation as resolved.
/// <inheritdoc/>
IReferenceData? IReferenceDataCollection.GetByCode(string code) => GetByCode(code);
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Results/ResultsExtensions.When.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public static Result<T> When<T>(this Result<T> result, Predicate<T> condition, F
if (condition(result.Value))
return func(result.Value).Combine(result);
else
return otherwise is null ? result : func(result.Value).Combine(result);
return otherwise is null ? result : otherwise(result.Value).Combine(result);
}

/// <summary>
Expand Down
14 changes: 12 additions & 2 deletions src/CoreEx/Validation/DecimalRuleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,18 @@ public static int CalcIntegralPartLength(decimal value)
if (absValue == 0m)
return 0;

// Use Log10 for O(1) performance; cast to double is safe here as we only need the magnitude for digit counting.
return (int)Math.Floor(Math.Log10((double)absValue)) + 1;
// Use Log10 for O(1) performance as an estimate; the cast to double can lose precision for values with 16+
// significant digits (decimal supports up to 29), which can round the magnitude up or down across a
// power-of-ten boundary (e.g. 99999999999999999m rounds to 1e17 as a double). Correct any such drift below.
var length = (int)Math.Floor(Math.Log10((double)absValue)) + 1;

while (length > 0 && GetPowerOf10(length - 1) > absValue)
length--;

while (length < 29 && GetPowerOf10(length) <= absValue)
length++;

return length;
}

/// <summary>
Expand Down
Loading
Loading