Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace BinaryFormatter with BinaryWriter #51

Merged
merged 39 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d6f4232
feat: use BinaryWriter instead of BinaryFormatter
phnx47 Dec 23, 2022
2eed946
feat: new simple constructor
phnx47 Dec 23, 2022
058c927
test: cleanup
phnx47 Dec 23, 2022
29fdf49
docs: update readme
phnx47 Dec 23, 2022
91696ed
test: nowarn - CS0618
phnx47 Dec 31, 2022
c22ee9b
test: update benchmarks runner
phnx47 Dec 31, 2022
b5011b2
refactor: cleanup create
phnx47 Jan 20, 2023
776723b
feat: support (s)byte type
phnx47 Jan 20, 2023
958697d
test: cleanup array tests
phnx47 Jan 20, 2023
ba52ac2
test: cleanup bool tests
phnx47 Jan 20, 2023
06bd337
test: cleanup extensions tests
phnx47 Jan 20, 2023
3c91202
test: rename vars
phnx47 Jan 20, 2023
209c8ee
test: cleanup string tests
phnx47 Jan 20, 2023
4b71a1a
refactor: to file-scoped namespace
phnx47 Jan 20, 2023
07a5c32
feat: support byte[] type
phnx47 Jan 22, 2023
9e1c958
refactor: remove useless For<TProperty>
phnx47 Jan 22, 2023
4038b03
Merge branch 'main' into feat/binformat
phnx47 Mar 26, 2023
3f6ad14
feat: support char type
phnx47 Mar 26, 2023
9e6d386
style: update .editorconfig
phnx47 Mar 26, 2023
f74b918
test: add double tests
phnx47 Mar 28, 2023
d2b32d5
feat: support decimal type
phnx47 Mar 28, 2023
0e4b56a
feat: support (u)short type
phnx47 Mar 29, 2023
730ee05
test: clean props naming
phnx47 Mar 29, 2023
0274ed0
feat: support (u)int type
phnx47 Mar 30, 2023
f924110
feat: support (u)long type
phnx47 Mar 31, 2023
7c19bd1
feat: support float type
phnx47 Apr 1, 2023
6f10a96
test: rename model
phnx47 Apr 2, 2023
f55f19a
test: create with null compute hash
phnx47 Apr 2, 2023
770faaa
test: create with null hash algorithm
phnx47 Apr 2, 2023
4ed8290
test: create TypeTests folder
phnx47 Apr 2, 2023
5608415
test: add Half type check
phnx47 Apr 2, 2023
5a775ee
refactor: remove unreachable code
phnx47 Apr 2, 2023
7b3b100
test: check for duplicate prop
phnx47 Apr 3, 2023
a6794de
test: rename folder
phnx47 Apr 3, 2023
f40eff8
test: check that prop is MemberExpression
phnx47 Apr 3, 2023
5246320
chore: change ex msg
phnx47 Apr 3, 2023
4e91e2a
refactor: move trim, toLower from extensions
phnx47 Apr 3, 2023
1235560
test: check extensions
phnx47 Apr 3, 2023
33d2d01
docs: update benchmarks
phnx47 Apr 4, 2023
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
5 changes: 2 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,14 @@ csharp_style_conditional_delegate_call = true:suggestion
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true

## Naming

### private fields should be _camelCase

dotnet_naming_style.underscore_prefix.capitalization = camel_case
dotnet_naming_style.underscore_prefix.required_prefix = _

# private fields should be _camelCase
dotnet_naming_rule.private_fields_with_underscore.symbols = private_fields
dotnet_naming_rule.private_fields_with_underscore.style = underscore_prefix
dotnet_naming_rule.private_fields_with_underscore.severity = suggestion
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
*.user
*.opencover.xml
*.orig

.idea

bin
obj
BenchmarkDotNet.Artifacts
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
Declare class:

```c#
class UserInfo
class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
Expand All @@ -27,8 +27,8 @@ class UserInfo
Configure Func:

```c#
var fingerprint = FingerprintBuilder<UserInfo>
.Create(SHA256.Create().ComputeHash)
var sha256 = FingerprintBuilder<User>
.Create(SHA256.Create())
.For(p => p.FirstName)
.For(p => p.LastName)
.Build();
Expand All @@ -37,29 +37,29 @@ var fingerprint = FingerprintBuilder<UserInfo>
Get hash:

```c#
var user = new UserInfo { FirstName = "John", LastName = "Smith" };
var hash = fingerprint(user).ToLowerHexString(); // 9996c4bbc1da4938144886b27b7c680e75932b5a56d911754d75ae4e0a9b4f1a
var user = new User { FirstName = "John", LastName = "Smith" };
var hash = sha256(user).ToLowerHexString(); // 62565a67bf16004038c502eb68907411fcf7871c66ee01a1aa274cc18d9fb541
```

## Benchmarks

```ini
BenchmarkDotNet=v0.12.1, OS=arch

BenchmarkDotNet=v0.13.5, OS=arch
Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.300
[Host] : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
Job-PCQRMO : .NET Core 3.1.4 (CoreCLR 4.700.20.20201, CoreFX 4.700.20.22101), X64 RyuJIT
.NET SDK=7.0.103
[Host] : .NET 7.0.3 (7.0.323.12801), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.3 (7.0.323.12801), X64 RyuJIT AVX2

Runtime=.NET Core 3.1 IterationCount=50 LaunchCount=2
RunStrategy=Throughput WarmupCount=10
```

| Method | Mean | Error | StdDev | Min | Max | Median |
|-------------------- |---------:|----------:|----------:|---------:|---------:|---------:|
| MD5_Model_To_Hex | 4.277 μs | 0.0763 μs | 0.2128 μs | 4.007 μs | 4.612 μs | 4.275 μs |
| SHA1_Model_To_Hex | 4.303 μs | 0.0232 μs | 0.0639 μs | 4.178 μs | 4.475 μs | 4.300 μs |
| SHA256_Model_To_Hex | 5.183 μs | 0.0526 μs | 0.1500 μs | 4.987 μs | 5.627 μs | 5.151 μs |
| SHA512_Model_To_Hex | 6.842 μs | 0.0688 μs | 0.1908 μs | 6.626 μs | 7.470 μs | 6.795 μs |
| MD5_Model_To_Hex | 2.142 μs | 0.0142 μs | 0.0118 μs | 2.125 μs | 2.163 μs | 2.146 μs |
| SHA1_Model_To_Hex | 2.379 μs | 0.0155 μs | 0.0121 μs | 2.355 μs | 2.400 μs | 2.384 μs |
| SHA256_Model_To_Hex | 3.059 μs | 0.0245 μs | 0.0217 μs | 3.031 μs | 3.107 μs | 3.054 μs |
| SHA512_Model_To_Hex | 4.564 μs | 0.0182 μs | 0.0161 μs | 4.540 μs | 4.598 μs | 4.563 μs |



## License

Expand Down
182 changes: 132 additions & 50 deletions src/FingerprintBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,76 +1,158 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Cryptography;

namespace FingerprintBuilder
namespace FingerprintBuilder;

public class FingerprintBuilder<T> : IFingerprintBuilder<T>
{
public class FingerprintBuilder<T> : IFingerprintBuilder<T>
private readonly Func<byte[], byte[]> _computeHash;
private readonly IDictionary<string, Func<T, object>> _fingerprints = new SortedDictionary<string, Func<T, object>>(StringComparer.OrdinalIgnoreCase);

private readonly Type[] _supportedTypes =
{
private readonly Func<byte[], byte[]> _computeHash;
private readonly IDictionary<string, Func<T, object>> _fingerprints;
typeof(bool),
typeof(byte),
typeof(sbyte),
typeof(byte[]),
typeof(char),
typeof(char[]),
typeof(double),
typeof(decimal),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
typeof(float),
typeof(string)
};

private FingerprintBuilder(Func<byte[], byte[]> computeHash)
{
_computeHash = computeHash ?? throw new ArgumentNullException(nameof(computeHash));
_fingerprints = new SortedDictionary<string, Func<T, object>>(StringComparer.OrdinalIgnoreCase);
}
private FingerprintBuilder(Func<byte[], byte[]> computeHash)
{
_computeHash = computeHash ?? throw new ArgumentNullException(nameof(computeHash));
}

public static IFingerprintBuilder<T> Create(Func<byte[], byte[]> computeHash) =>
new FingerprintBuilder<T>(computeHash);
public static IFingerprintBuilder<T> Create(HashAlgorithm hashAlgorithm) => Create(hashAlgorithm.ComputeHash);

public IFingerprintBuilder<T> For<TProperty>(Expression<Func<T, TProperty>> expression) =>
For<TProperty>(expression, _ => _);
public static IFingerprintBuilder<T> Create(Func<byte[], byte[]> computeHash) => new FingerprintBuilder<T>(computeHash);

public IFingerprintBuilder<T> For<TProperty>(Expression<Func<T, TProperty>> expression, Expression<Func<TProperty, string>> fingerprint) =>
For<TProperty, string>(expression, fingerprint);
public IFingerprintBuilder<T> For<TProperty>(Expression<Func<T, TProperty>> expression) => For(expression, f => f);

public IFingerprintBuilder<T> For<TProperty>(Expression<Func<T, TProperty>> expression, Expression<Func<TProperty, TProperty>> fingerprint)
public IFingerprintBuilder<T> For(Expression<Func<T, string>> expression, bool toLower, bool trim)
{
var format = (Func<string, string>)(input =>
{
return For<TProperty, TProperty>(expression, fingerprint);
}
if (toLower)
input = input.ToLowerInvariant();

private IFingerprintBuilder<T> For<TProperty, TPropertyType>(Expression<Func<T, TProperty>> expression, Expression<Func<TProperty, TPropertyType>> fingerprint)
{
if (!(expression.Body is MemberExpression memberExpression))
throw new ArgumentException("Expression must be a member expression");
if (trim)
input = input.Trim();

if (_fingerprints.ContainsKey(memberExpression.Member.Name))
throw new ArgumentException($"Member {memberExpression.Member.Name} has already been added.");
return input;
});

var getValue = expression.Compile();
var getFingerprint = fingerprint.Compile();
return For(expression, input => format(input));
}

_fingerprints[memberExpression.Member.Name] = obj =>
{
var value = getValue(obj);
return value == null ? default : getFingerprint(value);
};
public IFingerprintBuilder<T> For<TProperty>(Expression<Func<T, TProperty>> expression, Expression<Func<TProperty, string>> fingerprint) =>
For<TProperty, string>(expression, fingerprint);

return this;
}
private IFingerprintBuilder<T> For<TProperty, TReturnType>(Expression<Func<T, TProperty>> expression, Expression<Func<TProperty, TReturnType>> fingerprint)
{
if (expression.Body is not MemberExpression memberExpression)
throw new ArgumentException("Expression must be a member expression");

public Func<T, byte[]> Build()
var memberName = memberExpression.Member.Name;

if (_fingerprints.ContainsKey(memberExpression.Member.Name))
throw new ArgumentException("Member has already been added", memberName);

var returnType = typeof(TReturnType);
if (!_supportedTypes.Contains(typeof(TReturnType)))
throw new ArgumentException($"Unsupported Type: {returnType.Name}", memberName);

var getValue = expression.Compile();
var getFingerprint = fingerprint.Compile();

_fingerprints[memberExpression.Member.Name] = entity =>
{
var binaryFormatter = new BinaryFormatter();
var value = getValue(entity);
return value == null ? default : getFingerprint(value);
};

return obj =>
return this;
}

public Func<T, byte[]> Build()
{
return entity =>
{
using var memory = new MemoryStream();
using var binaryWriter = new BinaryWriter(memory);
foreach (var item in _fingerprints)
{
using (var memory = new MemoryStream())
var value = item.Value(entity);
switch (value)
{
foreach (var item in _fingerprints)
{
var graph = item.Value(obj);
if (graph != null)
binaryFormatter.Serialize(memory, graph);
}

var arr = memory.ToArray();
lock (_computeHash)
return _computeHash(arr);
case bool typedValue:
binaryWriter.Write(typedValue);
break;
case byte typedValue:
binaryWriter.Write(typedValue);
break;
case sbyte typedValue:
binaryWriter.Write(typedValue);
break;
case byte[] typedValue:
binaryWriter.Write(typedValue);
break;
case char typedValue:
binaryWriter.Write(typedValue);
break;
case char[] typedValue:
binaryWriter.Write(typedValue);
break;
case double typedValue:
binaryWriter.Write(typedValue);
break;
case decimal typedValue:
binaryWriter.Write(typedValue);
break;
case short typedValue:
binaryWriter.Write(typedValue);
break;
case ushort typedValue:
binaryWriter.Write(typedValue);
break;
case int typedValue:
binaryWriter.Write(typedValue);
break;
case uint typedValue:
binaryWriter.Write(typedValue);
break;
case long typedValue:
binaryWriter.Write(typedValue);
break;
case ulong typedValue:
binaryWriter.Write(typedValue);
break;
case float typedValue:
binaryWriter.Write(typedValue);
break;
case string typedValue:
binaryWriter.Write(typedValue);
break;
}
};
}
}

var bytes = memory.ToArray();
lock (_computeHash)
return _computeHash(bytes);
};
}
}
66 changes: 24 additions & 42 deletions src/FingerprintBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,53 +1,35 @@
using System;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;

namespace FingerprintBuilder
namespace FingerprintBuilder;

public static class FingerprintBuilderExtensions
{
public static class FingerprintBuilderExtensions
/// <summary>
/// Convert to LowerCase Hexadecimal string
/// </summary>
public static string ToLowerHexString(this byte[] source)
{
public static IFingerprintBuilder<T> For<T>(this IFingerprintBuilder<T> builder, Expression<Func<T, string>> expression, bool toLowerCase, bool ignoreWhiteSpace)
{
var format = (Func<string, string>)(input =>
{
if (toLowerCase)
input = input.ToLowerInvariant();

if (ignoreWhiteSpace)
input = input.Trim();

return input;
});

return builder.For(expression, input => format(input));
}

/// <summary>
/// Convert to LowerCase Hexadecimal string
/// </summary>
public static string ToLowerHexString(this byte[] source)
{
return source.ToString("x2");
}
return source.ToString("x2");
}

/// <summary>
/// Convert to UpperCase Hexadecimal string
/// </summary>
public static string ToUpperHexString(this byte[] source)
{
return source.ToString("X2");
}
/// <summary>
/// Convert to UpperCase Hexadecimal string
/// </summary>
public static string ToUpperHexString(this byte[] source)
{
return source.ToString("X2");
}

/// <summary>
/// Convert to string
/// </summary>
private static string ToString(this byte[] source, string format)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
/// <summary>
/// Convert to string
/// </summary>
private static string ToString(this byte[] source, string format)
{
if (source == null)
throw new ArgumentNullException(nameof(source));

return string.Join("", source.Select(ch => ch.ToString(format, CultureInfo.InvariantCulture)));
}
return string.Join("", source.Select(ch => ch.ToString(format, CultureInfo.InvariantCulture)));
}
}
Loading