Skip to content

Commit

Permalink
v2.5.0 - New features and fixes (#53)
Browse files Browse the repository at this point in the history
* StringCase + CodeAnalysis

* Fix doco type.

* Further Code Analysis updates.

* Fix/enhance dictionary capabilities.
Correct SQL identifier quoting.

* Update change log.
  • Loading branch information
chullybun authored Jan 19, 2023
1 parent 2ac40fd commit 8a792c6
Show file tree
Hide file tree
Showing 37 changed files with 289 additions and 103 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

Represents the **NuGet** versions.

## v2.5.0
- *Enhancement:* Added string casing support to `Cleaner` and `EntityCore` using new `StringCase`; being `None` (as-is default), `Upper`, `Lower` and `Title`. Leverages standard .NET `TextInfo` to implement underlying case conversion.
- *Fixed:* Applied all changes identified by Code Analysis.
- *Fixed:* `NullReferenceException` in `EntityBaseDictionary` where item value is `null` corrected.
- *Enhancement:* Added `KeyModifier` function to `ObservableDictionary` to allow key manipulation to ensure consistency, i.e. all uppercase.
- *Fixed:* Potential SQL Injection opportunity within `DatabaseExtendedExtensions.ReferenceData` corrected when schema and table names were being specified explicitly; now quoted using `DbCommandBuilder.QuoteIdentifier`.

## v2.4.0
- *Enhancement:* Added `CompareValuesRule`, `EnumRule` and `EnumValueRule` as additional validation rules.

Expand Down
2 changes: 1 addition & 1 deletion Common.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>2.4.0</Version>
<Version>2.5.0</Version>
<LangVersion>preview</LangVersion>
<Authors>Avanade</Authors>
<Company>Avanade</Company>
Expand Down
4 changes: 2 additions & 2 deletions src/CoreEx.AutoMapper/AutoMapperConverterWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public abstract class AutoMapperConverterWrapper<TSource, TDestination, TConvert
/// <summary>
/// Represents the source to destination <see cref="AutoMapper.IValueConverter{TSource, TDestination}"/> struct.
/// </summary>
public struct ToDestinationMapper : AutoMapper.IValueConverter<TSource, TDestination>
public readonly struct ToDestinationMapper : AutoMapper.IValueConverter<TSource, TDestination>
{
private readonly IValueConverter<TSource, TDestination> _valueConverter;

Expand All @@ -54,7 +54,7 @@ public struct ToDestinationMapper : AutoMapper.IValueConverter<TSource, TDestina
/// <summary>
/// Represents the destination to source <see cref="AutoMapper.IValueConverter{TDestination, TSource}"/> struct.
/// </summary>
public struct ToSourceMapper : AutoMapper.IValueConverter<TDestination, TSource>
public readonly struct ToSourceMapper : AutoMapper.IValueConverter<TDestination, TSource>
{
private readonly IValueConverter<TDestination, TSource> _valueConverter;

Expand Down
5 changes: 4 additions & 1 deletion src/CoreEx.Cosmos/CosmosDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ public class CosmosDb : ICosmosDb
private readonly ConcurrentDictionary<Key, Func<IQueryable, IQueryable>> _filters = new();
private PartitionKey? _partitionKey;

private struct Key
/// <summary>
/// Provides key as combination of model type and container identifier.
/// </summary>
private readonly struct Key
{
public Key(Type modelType, string containerId)
{
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.Database/Extended/DatabaseArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CoreEx.Database.Extended
/// </summary>
public struct DatabaseArgs
{
private IDatabaseMapper? _mapper = null;
private readonly IDatabaseMapper? _mapper = null;

/// <summary>
/// Initializes a new instance of the <see cref="DatabaseArgs"/> struct.
Expand Down
18 changes: 15 additions & 3 deletions src/CoreEx.Database/Extended/DatabaseExtendedExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,26 @@ public static RefDataLoader<TColl, TItem, TId> ReferenceData<TColl, TItem, TId>(
/// <typeparam name="TItem">The <see cref="IReferenceData"/> item <see cref="Type"/>.</typeparam>
/// <typeparam name="TId">The <see cref="IReferenceData"/> <see cref="IIdentifier.Id"/> <see cref="Type"/>.</typeparam>
/// <param name="database">The <see cref="IDatabase"/>.</param>
/// <param name="schemaName">The database schema name.</param>
/// <param name="schemaName">The database schema name (optional).</param>
/// <param name="tableName">The database table name.</param>
/// <returns>The <see cref="RefDataLoader{TColl, TItem, TId}"/>.</returns>
public static RefDataLoader<TColl, TItem, TId> ReferenceData<TColl, TItem, TId>(this IDatabase database, string schemaName, string tableName)
/// <remarks>The <paramref name="schemaName"/> and <paramref name="tableName"/> should not be escaped/quoted as this is performed internally to minimize SQL injection opportunity.</remarks>
public static RefDataLoader<TColl, TItem, TId> ReferenceData<TColl, TItem, TId>(this IDatabase database, string? schemaName, string tableName)
where TColl : class, IReferenceDataCollection<TId, TItem>, new()
where TItem : class, IReferenceData<TId>, new()
where TId : IComparable<TId>, IEquatable<TId>
=> ReferenceData<TColl, TItem, TId>((database ?? throw new ArgumentNullException(nameof(database))).SqlStatement($"SELECT * FROM [{schemaName ?? throw new ArgumentNullException(nameof(schemaName))}].[{tableName ?? throw new ArgumentNullException(nameof(tableName))}]"));
{
if (!database.Provider.CanCreateCommandBuilder)
throw new NotSupportedException("Database Provider can not CreateCommandBuilder which is required to quote the identifiers to minimize SQL inject possibility.");

var cb = database.Provider.CreateCommandBuilder();
if (string.IsNullOrEmpty(schemaName))
return ReferenceData<TColl, TItem, TId>((database ?? throw new ArgumentNullException(nameof(database)))
.SqlStatement($"SELECT * FROM {cb.QuoteIdentifier(tableName ?? throw new ArgumentNullException(nameof(tableName)))}"));
else
return ReferenceData<TColl, TItem, TId>((database ?? throw new ArgumentNullException(nameof(database)))
.SqlStatement($"SELECT * FROM {cb.QuoteIdentifier(schemaName ?? throw new ArgumentNullException(nameof(schemaName)))}.{cb.QuoteIdentifier(tableName ?? throw new ArgumentNullException(nameof(tableName)))}"));
}

/// <summary>
/// Creates a <see cref="DatabaseQuery{T}"/> to enable select-like capabilities.
Expand Down
2 changes: 0 additions & 2 deletions src/CoreEx.Database/Extended/RefDataMultiSetCollArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ internal class RefDataMultiSetCollArgs<TColl, TItem, TId> : MultiSetCollArgs
{
private readonly Action<TItem> _item;
private readonly RefDataMapper<TItem, TId> _refDataMapper;
private readonly Action<DatabaseRecord, TItem>? _additionalProperties;
private readonly Func<DatabaseRecord, TItem, bool>? _confirmItemIsToBeAdded;

/// <summary>
Expand All @@ -35,7 +34,6 @@ public RefDataMultiSetCollArgs(IDatabase database, Action<TItem> item, string? i
{
_item = item;
_refDataMapper = new RefDataMapper<TItem, TId>(database, idColumnName, additionalProperties);
_additionalProperties = additionalProperties;
_confirmItemIsToBeAdded = confirmItemIsToBeAdded;
}

Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.Database/Mapping/ChangeLogDatabaseMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CoreEx.Database.Mapping
/// <summary>
/// Represents a <see cref="ChangeLog"/> <see cref="IDatabaseMapper"/>.
/// </summary>
public struct ChangeLogDatabaseMapper : IDatabaseMapper<ChangeLog>
public readonly struct ChangeLogDatabaseMapper : IDatabaseMapper<ChangeLog>
{
private static readonly Lazy<ChangeLogDatabaseMapper> _default = new(() => new(), true);

Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.Database/Mapping/ChangeLogExDatabaseMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CoreEx.Database.Mapping
/// <summary>
/// Represents a <see cref="ChangeLogEx"/> <see cref="IDatabaseMapper"/>.
/// </summary>
public struct ChangeLogExDatabaseMapper : IDatabaseMapper<ChangeLogEx>
public readonly struct ChangeLogExDatabaseMapper : IDatabaseMapper<ChangeLogEx>
{
private static readonly Lazy<ChangeLogExDatabaseMapper> _default = new(() => new(), true);

Expand Down
3 changes: 1 addition & 2 deletions src/CoreEx.Database/MultiSetCollArgsT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ public override void DatasetRecord(DatabaseRecord dr)
if (dr == null)
throw new ArgumentNullException(nameof(dr));

if (_coll == null)
_coll = new TColl();
_coll ??= new TColl();

var item = Mapper.MapFromDb(dr);
if (item != null)
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.EntityFrameworkCore/EfDbEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CoreEx.EntityFrameworkCore
/// </summary>
/// <typeparam name="T">The resultant <see cref="Type"/>.</typeparam>
/// <typeparam name="TModel">The entity framework model <see cref="Type"/>.</typeparam>
public struct EfDbEntity<T, TModel> : IEfDbEntity where T : class, IEntityKey, new() where TModel : class, new()
public readonly struct EfDbEntity<T, TModel> : IEfDbEntity where T : class, IEntityKey, new() where TModel : class, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="EfDbEntity{T, TModel}"/> class.
Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx.Newtonsoft/Json/JsonPreFilterInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace CoreEx.Newtonsoft.Json
/// <summary>
/// Provides pre (prior) to filtering JSON inspection.
/// </summary>
public struct JsonPreFilterInspector : IJsonPreFilterInspector
public readonly struct JsonPreFilterInspector : IJsonPreFilterInspector
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonPreFilterInspector"/> struct.
Expand Down
4 changes: 1 addition & 3 deletions src/CoreEx.Validation/PropertyContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,7 @@ public ValidationArgs CreateValidationArgs()
// Copy the configuration values; do not allow the higher-level dictionaries (stack) to be extended by lower-level validators.
if (Parent?.Config != null)
{
if (args.Config == null)
args.Config = new Dictionary<string, object?>();

args.Config ??= new Dictionary<string, object?>();
foreach (var cfg in Parent.Config)
{
args.Config.Add(cfg.Key, cfg.Value);
Expand Down
3 changes: 0 additions & 3 deletions src/CoreEx.Validation/PropertyRuleBase.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.Abstractions.Reflection;
using CoreEx.Localization;
using CoreEx.Validation.Clauses;
using CoreEx.Validation.Rules;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace CoreEx.Validation
{

/// <summary>
/// Represents a base validation rule for an entity property.
/// </summary>
Expand Down
3 changes: 0 additions & 3 deletions src/CoreEx.Validation/ValidationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,6 @@ public static IPropertyRule<TEntity, TProperty> CompareProperty<TEntity, TProper

#region String

#pragma warning disable CA1720 // Identifier contains type name; by-design, best name.

/// <summary>
/// Adds a <see cref="string"/> validation with a maximum length (see <see cref="StringRule{TEntity}"/>).
/// </summary>
Expand Down Expand Up @@ -577,7 +575,6 @@ public static IPropertyRule<TEntity, string> String<TEntity>(this IPropertyRule<
/// <returns>A <see cref="IPropertyRule{TEntity, String}"/>.</returns>
public static IPropertyRule<TEntity, string> String<TEntity>(this IPropertyRule<TEntity, string> rule, Regex? regex = null, LText? errorText = null) where TEntity : class
=> (rule ?? throw new ArgumentNullException(nameof(rule))).AddRule(new StringRule<TEntity> { Regex = regex, ErrorText = errorText });
#pragma warning restore CA1720

#endregion

Expand Down
2 changes: 2 additions & 0 deletions src/CoreEx/Abstractions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using CoreEx.Abstractions.Reflection;
using System;
using System.Diagnostics.CodeAnalysis;

namespace CoreEx
{
Expand Down Expand Up @@ -33,6 +34,7 @@ public static T Adjust<T>(this T value, Action<T> adjuster) where T : class
/// <returns>The <see cref="string"/> as sentence case.</returns>
/// <remarks>For example a value of '<c>VarNameDB</c>' would return '<c>Var Name DB</c>'.
/// <para>Uses the <see cref="PropertyExpression.SentenceCaseConverter"/> function to perform the conversion.</para></remarks>
[return: NotNullIfNotNull("text")]
public static string? ToSentenceCase(this string? text) => PropertyExpression.ToSentenceCase(text);
}
}
2 changes: 1 addition & 1 deletion src/CoreEx/Abstractions/Reflection/PropertyExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class PropertyExpression
private static IMemoryCache? _fallbackCache;

/// <summary>
/// The <see cref="Regex"/> pattern for splitting sentence strings into words.
/// The <see cref="Regex"/> pattern for splitting strings into a sentence of words.
/// </summary>
public const string SentenceCaseWordSplitPattern = "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))";

Expand Down
39 changes: 33 additions & 6 deletions src/CoreEx/Entities/Cleaner.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx

using CoreEx.Globalization;
using System;
using System.Collections.Generic;
using System.Globalization;
Expand All @@ -15,6 +16,7 @@ public static class Cleaner
private static DateTimeTransform _dateTimeTransform = DateTimeTransform.DateTimeUtc;
private static StringTransform _stringTransform = StringTransform.EmptyToNull;
private static StringTrim _stringTrim = StringTrim.End;
private static StringCase _stringCase = StringCase.None;

/// <summary>
/// Gets or sets the default <see cref="Entities.DateTimeTransform"/> for all entities unless explicitly overridden. Defaults to <see cref="DateTimeTransform.DateTimeUtc"/>.
Expand All @@ -26,7 +28,7 @@ public static DateTimeTransform DefaultDateTimeTransform
}

/// <summary>
/// Gets or sets the default <see cref="Entities.DateTimeTransform"/> for all entities unless explicitly overridden. Defaults to <see cref="StringTransform.EmptyToNull"/>.
/// Gets or sets the default <see cref="Entities.StringTransform"/> for all entities unless explicitly overridden. Defaults to <see cref="StringTransform.EmptyToNull"/>.
/// </summary>
public static StringTransform DefaultStringTransform
{
Expand All @@ -35,37 +37,50 @@ public static StringTransform DefaultStringTransform
}

/// <summary>
/// Gets or sets the default <see cref="Entities.DateTimeTransform"/> for all entities unless explicitly overridden. Defaults to <see cref="StringTransform.EmptyToNull"/>.
/// Gets or sets the default <see cref="Entities.StringTrim"/> for all entities unless explicitly overridden. Defaults to <see cref="StringTrim.End"/>.
/// </summary>
public static StringTrim DefaultStringTrim
{
get => _stringTrim;
set => _stringTrim = value == StringTrim.UseDefault ? throw new ArgumentException("The default cannot be set to UseDefault.", nameof(DefaultStringTrim)) : value;
}

/// <summary>
/// Gets or sets the default <see cref="Entities.StringCase"/> for all entities unless explicitly overridden. Defaults to <see cref="StringCase.None"/>.
/// </summary>
public static StringCase DefaultStringCase
{
get => _stringCase;
set => _stringCase = value == StringCase.UseDefault ? throw new ArgumentException("The default cannot be set to UseDefault.", nameof(DefaultStringCase)) : value;
}

/// <summary>
/// Cleans a <see cref="string"/>.
/// </summary>
/// <param name="value">The value to clean.</param>
/// <returns>The cleaned value.</returns>
/// <remarks>The <paramref name="value"/> will be trimmed and transformed using the respective <see cref="DefaultStringTrim"/> and <see cref="DefaultStringTransform"/> values.</remarks>
public static string? Clean(string? value) => Clean(value, StringTrim.UseDefault, StringTransform.UseDefault);
public static string? Clean(string? value) => Clean(value, StringTrim.UseDefault, StringTransform.UseDefault, StringCase.UseDefault);

/// <summary>
/// Cleans a <see cref="string"/> using the specified <paramref name="trim"/> and <paramref name="transform"/>.
/// </summary>
/// <param name="value">The value to clean.</param>
/// <param name="trim">The <see cref="StringTrim"/> (defaults to <see cref="DefaultStringTrim"/>).</param>
/// <param name="transform">The <see cref="StringTransform"/> (defaults to <see cref="DefaultStringTransform"/>).</param>
/// <param name="casing">The <see cref="StringCase"/> (defaults to <see cref="DefaultStringCase"/>).</param>
/// <returns>The cleaned value.</returns>
public static string? Clean(string? value, StringTrim trim = StringTrim.UseDefault, StringTransform transform = StringTransform.UseDefault)
public static string? Clean(string? value, StringTrim trim = StringTrim.UseDefault, StringTransform transform = StringTransform.UseDefault, StringCase casing = StringCase.UseDefault)
{
if (trim == StringTrim.UseDefault)
trim = DefaultStringTrim;

if (transform == StringTransform.UseDefault)
transform = DefaultStringTransform;

if (casing == StringCase.UseDefault)
casing = DefaultStringCase;

// Handle a null string.
if (value == null)
{
Expand All @@ -85,12 +100,24 @@ public static StringTrim DefaultStringTrim
};

// Transform the string.
return transform switch
tmp = transform switch
{
StringTransform.EmptyToNull => (tmp.Length == 0) ? null : tmp,
StringTransform.NullToEmpty => tmp ?? string.Empty,
_ => tmp,
};

if (string.IsNullOrEmpty(tmp))
return tmp;

// Apply casing to the string.
return casing switch
{
StringCase.Lower => CultureInfo.CurrentCulture.TextInfo.ToCasing(tmp, TextInfoCasing.Lower),
StringCase.Upper => CultureInfo.CurrentCulture.TextInfo.ToCasing(tmp, TextInfoCasing.Upper),
StringCase.Title => CultureInfo.CurrentCulture.TextInfo.ToCasing(tmp, TextInfoCasing.Title),
_ => tmp,
};
}

/// <summary>
Expand Down Expand Up @@ -193,7 +220,7 @@ public static DateTime Clean(DateTime value, DateTimeTransform transform)
public static T Clean<T>(T value, bool overrideWithDefaultWhenIsInitial)
{
if (value is string str)
return (T)Convert.ChangeType(Clean(str, StringTrim.UseDefault, StringTransform.UseDefault), typeof(string), CultureInfo.CurrentCulture)!;
return (T)Convert.ChangeType(Clean(str, StringTrim.UseDefault, StringTransform.UseDefault, StringCase.UseDefault), typeof(string), CultureInfo.CurrentCulture)!;
else if (value is DateTime dte)
return (T)Convert.ChangeType(Clean(dte, DateTimeTransform.UseDefault), typeof(DateTime), CultureInfo.CurrentCulture);

Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Entities/CompositeKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace CoreEx.Entities
/// are supported: <see cref="string"/>, <see cref="char"/>, <see cref="short"/>, <see cref="int"/>, <see cref="long"/>, <see cref="ushort"/>, <see cref="uint"/>, <see cref="ulong"/>, <see cref="Guid"/>, <see cref="DateTimeOffset"/> (converted to a <see cref="DateTime"/>) and <see cref="DateTime"/>.</remarks>
[System.Diagnostics.DebuggerStepThrough]
[System.Diagnostics.DebuggerDisplay("Key = {ToString()}")]
public struct CompositeKey : IEquatable<CompositeKey>
public readonly struct CompositeKey : IEquatable<CompositeKey>
{
private readonly ImmutableArray<object?> _args;

Expand Down
2 changes: 1 addition & 1 deletion src/CoreEx/Entities/EntityKeyCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ public class EntityKeyCollection<T> : List<T>, ICompositeKeyCollection<T> where
/// Removes all items with the specified primary <paramref name="keys"/>.
/// </summary>
/// <param name="keys">The key values.</param>
void RemoveByKey(params object?[] keys) => RemoveByKey(new CompositeKey(keys));
public void RemoveByKey(params object?[] keys) => RemoveByKey(new CompositeKey(keys));
}
}
Loading

0 comments on commit 8a792c6

Please sign in to comment.