Skip to content

Commit 7a1b3df

Browse files
committed
Add in-memory repository implementations for easier testing
1 parent 5c9ba97 commit 7a1b3df

12 files changed

+700
-58
lines changed

TableStorage.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TableStorage.Newtonsoft.Sou
3838
EndProject
3939
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableStorage.CodeAnalysis", "src\TableStorage.CodeAnalysis\TableStorage.CodeAnalysis.csproj", "{BC1CD1F2-5E6B-43B1-8672-E70F784F90C0}"
4040
EndProject
41+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableStorage.Memory", "src\TableStorage.Memory\TableStorage.Memory.csproj", "{4ABD67F9-D620-4D24-8B62-B2ACD6309B4E}"
42+
EndProject
4143
Global
4244
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4345
Debug|Any CPU = Debug|Any CPU
@@ -92,6 +94,10 @@ Global
9294
{BC1CD1F2-5E6B-43B1-8672-E70F784F90C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
9395
{BC1CD1F2-5E6B-43B1-8672-E70F784F90C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
9496
{BC1CD1F2-5E6B-43B1-8672-E70F784F90C0}.Release|Any CPU.Build.0 = Release|Any CPU
97+
{4ABD67F9-D620-4D24-8B62-B2ACD6309B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
98+
{4ABD67F9-D620-4D24-8B62-B2ACD6309B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
99+
{4ABD67F9-D620-4D24-8B62-B2ACD6309B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
100+
{4ABD67F9-D620-4D24-8B62-B2ACD6309B4E}.Release|Any CPU.Build.0 = Release|Any CPU
95101
EndGlobalSection
96102
GlobalSection(SolutionProperties) = preSolution
97103
HideSolutionNode = FALSE
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Linq.Expressions;
2+
using System.Reflection;
3+
4+
class ConstantReducer : ExpressionVisitor
5+
{
6+
protected override Expression VisitMember(MemberExpression node)
7+
{
8+
// If the expression is a constant or can be reduced to a constant, evaluate it
9+
if (node.Expression is ConstantExpression constantExpr)
10+
{
11+
object? container = constantExpr.Value;
12+
object? value = null;
13+
14+
if (node.Member is FieldInfo field)
15+
value = field.GetValue(container);
16+
else if (node.Member is PropertyInfo prop)
17+
value = prop.GetValue(container);
18+
19+
return Expression.Constant(value, node.Type);
20+
}
21+
22+
// Try to evaluate more complex expressions
23+
try
24+
{
25+
var lambda = Expression.Lambda(node);
26+
var compiled = lambda.Compile();
27+
var value = compiled.DynamicInvoke();
28+
return Expression.Constant(value, node.Type);
29+
}
30+
catch
31+
{
32+
// If evaluation fails, fallback to default behavior
33+
return base.VisitMember(node);
34+
}
35+
}
36+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//<auto-generated/>
2+
#nullable enable
3+
using System;
4+
using System.Collections.Concurrent;
5+
using System.Linq;
6+
using System.Linq.Expressions;
7+
using System.Reflection;
8+
using Azure.Data.Tables;
9+
10+
namespace Devlooped;
11+
12+
/// <summary>
13+
/// Factory methods to create <see cref="ITablePartition{T}"/> instances
14+
/// that store entities using individual columns for entity properties.
15+
/// </summary>
16+
public static partial class MemoryPartition
17+
{
18+
/// <summary>
19+
/// Default table name to use when a value is not not provided
20+
/// (or overriden via <see cref="TableAttribute"/>), which is <c>Entities</c>.
21+
/// </summary>
22+
public const string DefaultTableName = "Entities";
23+
24+
/// <summary>
25+
/// Creates an <see cref="ITablePartition{ITableEntity}"/>.
26+
/// </summary>
27+
/// <param name="tableName">Table name to use.</param>
28+
/// <param name="partitionKey">Fixed partition key to scope entity persistence.</param>
29+
/// <returns>The new <see cref="ITablePartition{TEntity}"/>.</returns>
30+
public static MemoryPartition<TableEntity> Create(string tableName, string partitionKey)
31+
=> new MemoryPartition<TableEntity>(tableName, partitionKey, x => x.RowKey);
32+
33+
/// <summary>
34+
/// Creates an <see cref="ITablePartition{T}"/> for the given entity type
35+
/// <typeparamref name="T"/>, using <see cref="DefaultTableName"/> as the table name and the
36+
/// <typeparamref name="T"/> <c>Name</c> as the partition key.
37+
/// </summary>
38+
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
39+
/// <param name="tableName">Table name to use.</param>
40+
/// <param name="rowKey">Function to retrieve the row key for a given entity.</param>
41+
/// <returns>The new <see cref="ITablePartition{T}"/>.</returns>
42+
public static MemoryPartition<T> Create<T>(
43+
string tableName,
44+
Expression<Func<T, string>> rowKey) where T : class
45+
=> Create<T>(DefaultTableName, default, rowKey);
46+
47+
/// <summary>
48+
/// Creates an <see cref="ITablePartition{T}"/> for the given entity type
49+
/// <typeparamref name="T"/>.
50+
/// </summary>
51+
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
52+
/// <param name="tableName">Optional table name to use. If not provided, <see cref="DefaultTableName"/>
53+
/// will be used, unless a <see cref="TableAttribute"/> on the type overrides it.</param>
54+
/// <param name="partitionKey">Optional fixed partition key to scope entity persistence.
55+
/// If not provided, the <typeparamref name="T"/> <c>Name</c> will be used.</param>
56+
/// <param name="rowKey">Optional function to retrieve the row key for a given entity.
57+
/// If not provided, the class will need a property annotated with <see cref="RowKeyAttribute"/>.</param>
58+
/// <returns>The new <see cref="ITablePartition{T}"/>.</returns>
59+
public static MemoryPartition<T> Create<T>(
60+
string? tableName = default,
61+
string? partitionKey = null,
62+
Expression<Func<T, string>>? rowKey = null) where T : class
63+
{
64+
partitionKey ??= TablePartition.GetDefaultPartitionKey<T>();
65+
rowKey ??= RowKeyAttribute.CreateAccessor<T>();
66+
67+
return new MemoryPartition<T>(tableName ?? TablePartition.GetDefaultTableName<T>(), partitionKey, rowKey);
68+
}
69+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//<auto-generated/>
2+
#nullable enable
3+
using System;
4+
using System.Collections.Generic;
5+
using System.ComponentModel;
6+
using System.Linq;
7+
using System.Linq.Expressions;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Azure.Data.Tables;
11+
using Mono.Linq.Expressions;
12+
13+
namespace Devlooped;
14+
15+
/// <inheritdoc />
16+
public partial class MemoryPartition<T> : ITablePartition<T>, IDocumentPartition<T> where T : class
17+
{
18+
readonly MemoryRepository<T> repository;
19+
20+
/// <summary>
21+
/// Initializes the repository with the given storage account and optional table name.
22+
/// </summary>
23+
/// <param name="storageAccount">The <see cref="CloudStorageAccount"/> to use to connect to the table.</param>
24+
public MemoryPartition()
25+
: this(TablePartition.GetDefaultTableName<T>(),
26+
TablePartition.GetDefaultPartitionKey<T>(),
27+
RowKeyAttribute.CreateAccessor<T>())
28+
{ }
29+
30+
/// <summary>
31+
/// Initializes the repository with the given storage account and optional table name.
32+
/// </summary>
33+
/// <param name="tableName">The table that backs this table partition.</param>
34+
public MemoryPartition(string tableName)
35+
: this(tableName ?? TablePartition.GetDefaultTableName<T>(),
36+
TablePartition.GetDefaultPartitionKey<T>(),
37+
RowKeyAttribute.CreateAccessor<T>())
38+
{ }
39+
40+
/// <summary>
41+
/// Initializes the repository with the given storage account and optional table name.
42+
/// </summary>
43+
/// <param name="tableName">The table that backs this table partition.</param>
44+
/// <param name="partitionKey">The fixed partition key that backs this table partition.</param>
45+
public MemoryPartition(string tableName, string partitionKey)
46+
: this(tableName ?? TablePartition.GetDefaultTableName<T>(),
47+
partitionKey,
48+
RowKeyAttribute.CreateAccessor<T>())
49+
{ }
50+
51+
/// <summary>
52+
/// Initializes the repository with the given storage account and optional table name.
53+
/// </summary>
54+
/// <param name="tableName">The table that backs this table partition.</param>
55+
/// <param name="partitionKey">The fixed partition key that backs this table partition.</param>
56+
/// <param name="rowKey">A function to determine the row key for an entity of type <typeparamref name="T"/> within the partition.</param>
57+
public MemoryPartition(string tableName, string partitionKey, Expression<Func<T, string>> rowKey)
58+
{
59+
partitionKey ??= TablePartition.GetDefaultPartitionKey<T>();
60+
PartitionKey = partitionKey;
61+
62+
repository = new MemoryRepository<T>(tableName, _ => partitionKey,
63+
rowKey ?? RowKeyAttribute.CreateAccessor<T>());
64+
}
65+
66+
/// <inheritdoc />
67+
public string TableName => repository.TableName;
68+
69+
/// <inheritdoc />
70+
public string PartitionKey { get; }
71+
72+
/// <inheritdoc />
73+
public IQueryable<T> CreateQuery() => repository.CreateQuery(PartitionKey);
74+
75+
/// <inheritdoc />
76+
public Task<bool> DeleteAsync(T entity, CancellationToken cancellation = default)
77+
{
78+
if (entity is TableEntity te && !PartitionKey.Equals(te.PartitionKey, StringComparison.Ordinal))
79+
throw new ArgumentException("Entity does not belong to the partition.");
80+
81+
return repository.DeleteAsync(entity, cancellation);
82+
}
83+
84+
/// <inheritdoc />
85+
public Task<bool> DeleteAsync(string rowKey, CancellationToken cancellation = default)
86+
=> repository.DeleteAsync(PartitionKey, rowKey, cancellation);
87+
88+
/// <inheritdoc />
89+
public IAsyncEnumerable<T> EnumerateAsync(CancellationToken cancellation = default)
90+
=> repository.EnumerateAsync(PartitionKey, cancellation);
91+
92+
/// <inheritdoc />
93+
public IAsyncEnumerable<T> EnumerateAsync(Expression<Func<IDocumentEntity, bool>> predicate, CancellationToken cancellation = default)
94+
=> repository.EnumerateAsync(predicate.AndAlso(x => x.PartitionKey == PartitionKey), cancellation);
95+
96+
/// <inheritdoc />
97+
public Task<T?> GetAsync(string rowKey, CancellationToken cancellation = default)
98+
=> repository.GetAsync(PartitionKey, rowKey, cancellation);
99+
100+
/// <inheritdoc />
101+
public Task<T> PutAsync(T entity, CancellationToken cancellation = default)
102+
{
103+
if (entity is TableEntity te && !PartitionKey.Equals(te.PartitionKey, StringComparison.Ordinal))
104+
throw new ArgumentException("Entity does not belong to the partition.");
105+
106+
return repository.PutAsync(entity, cancellation);
107+
}
108+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
using Azure.Data.Tables;
4+
5+
namespace Devlooped;
6+
7+
/// <summary>
8+
/// Factory methods to create in-memory <see cref="ITableRepository{T}"/> and
9+
/// <see cref="IDocumentRepository{T}"/> instances (since <see cref="MemoryRepository{T}"/>
10+
/// implements both.
11+
/// </summary>
12+
public static class MemoryRepository
13+
{
14+
/// <summary>
15+
/// Creates an <see cref="ITableRepository{TableEntity}"/> repository.
16+
/// </summary>
17+
/// <param name="tableName">Table name to use.</param>
18+
/// <returns>The new <see cref="ITableRepository{ITableEntity}"/>.</returns>
19+
public static MemoryRepository<TableEntity> Create(string tableName)
20+
=> new(tableName, x => x.PartitionKey, x => x.RowKey);
21+
22+
/// <summary>
23+
/// Creates an <see cref="ITableRepository{TableEntity}"/> repository.
24+
/// </summary>
25+
/// <returns>The new <see cref="ITableRepository{ITableEntity}"/>.</returns>
26+
public static MemoryRepository<TableEntity> Create()
27+
=> new("Entities", x => x.PartitionKey, x => x.RowKey);
28+
29+
/// <summary>
30+
/// Creates an <see cref="ITableRepository{T}"/> for the given entity type
31+
/// <typeparamref name="T"/>, using the <typeparamref name="T"/> <c>Name</c> as
32+
/// the table name.
33+
/// </summary>
34+
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
35+
/// <param name="partitionKey">Function to retrieve the partition key for a given entity.</param>
36+
/// <param name="rowKey">Function to retrieve the row key for a given entity.</param>
37+
/// <returns>The new <see cref="ITableRepository{T}"/>.</returns>
38+
public static MemoryRepository<T> Create<T>(
39+
Expression<Func<T, string>> partitionKey,
40+
Expression<Func<T, string>> rowKey) where T : class
41+
=> Create<T>(typeof(T).Name, partitionKey, rowKey);
42+
43+
/// <summary>
44+
/// Creates an <see cref="ITableRepository{T}"/> for the given entity type
45+
/// <typeparamref name="T"/>.
46+
/// </summary>
47+
/// <typeparam name="T">The type of entity that the repository will manage.</typeparam>
48+
/// <param name="tableName">Optional table name to use. If not provided, the <typeparamref name="T"/>
49+
/// <param name="partitionKey">Optional function to retrieve the partition key for a given entity.
50+
/// If not provided, the class will need a property annotated with <see cref="PartitionKeyAttribute"/>.</param>
51+
/// <param name="rowKey">Optional function to retrieve the row key for a given entity.
52+
/// If not provided, the class will need a property annotated with <see cref="RowKeyAttribute"/>.</param>
53+
/// <returns>The new <see cref="ITableRepository{T}"/>.</returns>
54+
public static MemoryRepository<T> Create<T>(
55+
string? tableName = default,
56+
Expression<Func<T, string>>? partitionKey = null,
57+
Expression<Func<T, string>>? rowKey = null) where T : class
58+
{
59+
partitionKey ??= PartitionKeyAttribute.CreateAccessor<T>();
60+
rowKey ??= RowKeyAttribute.CreateAccessor<T>();
61+
62+
return new MemoryRepository<T>(tableName ?? TableRepository.GetDefaultTableName<T>(), partitionKey, rowKey);
63+
}
64+
}

0 commit comments

Comments
 (0)