Skip to content

Commit

Permalink
Add shared table feature
Browse files Browse the repository at this point in the history
  • Loading branch information
petero-dk committed Nov 27, 2023
1 parent 3bee61a commit 2261859
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 11 deletions.
57 changes: 57 additions & 0 deletions CoreHelpers.WindowsAzure.Storage.Table.Tests/ITS030SharedTable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using CoreHelpers.WindowsAzure.Storage.Table.Tests.Extensions;
using CoreHelpers.WindowsAzure.Storage.Table.Tests.Models;
using Xunit.DependencyInjection;

namespace CoreHelpers.WindowsAzure.Storage.Table.Tests
{
[Startup(typeof(Startup))]
[Collection("Sequential")]
public class ITS030SharedTable
{
private readonly IStorageContext _rootContext;

public ITS030SharedTable(IStorageContext context)
{
_rootContext = context;

}


[Fact]
public async Task VerifyGetItem()
{
using (var scp = _rootContext.CreateChildContext())
{
// set the tablename context
scp.SetTableContext();

// configure the entity mapper
scp.AddAttributeMapper(typeof(MultipleModelsBase));


var model1 = new MultipleModels1() { P = "P1", Contact = "C1", Model1Field = "Model1Field" };
var model2 = new MultipleModels2() { P = "P1", Contact = "C2", Model2Field = "Model2Field" };


scp.EnableAutoCreateTable();

await scp.MergeOrInsertAsync<MultipleModelsBase>(new[] {model1});
await scp.MergeOrInsertAsync<MultipleModelsBase>(new[] {model2});


var result1 = await scp.QueryAsync<MultipleModelsBase>("P1", "C1");
Assert.Equivalent(model1, result1, true);
Assert.IsType<MultipleModels1>(result1);

var result2 = await scp.QueryAsync<MultipleModelsBase>("P1", "C2");
Assert.Equivalent(model2, result2, true);
Assert.IsType<MultipleModels2>(result2);

// cleanup
await scp.DropTableAsync<MultipleModelsBase>();
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using CoreHelpers.WindowsAzure.Storage.Table.Attributes;

namespace CoreHelpers.WindowsAzure.Storage.Table.Tests.Models
{

[Storable(TypeField = nameof(Type))]
public class MultipleModelsBase
{
[PartitionKey]
public string P { get; set; } = "Partition01";

[RowKey]
public string Contact { get; set; } = String.Empty;

}

public class MultipleModels1 : MultipleModelsBase
{
public string Model1Field { get; set; } = String.Empty;

}

public class MultipleModels2 : MultipleModelsBase
{
public string Model2Field { get; set; } = String.Empty;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ namespace CoreHelpers.WindowsAzure.Storage.Table.Attributes
public class StorableAttribute : Attribute
{
public string Tablename { get; set; }

public string TypeField { get; set; } = null;

public StorableAttribute() {}

public StorableAttribute(string Tablename, string TypeField)
{
this.Tablename = Tablename;
this.TypeField = TypeField;
}

public StorableAttribute(string Tablename)
{
this.Tablename = Tablename;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -94,8 +95,20 @@ private bool MoveNextInternal(bool initialPage)
return MoveNextInternal(false);
}

var entityMapper = _context.context.GetEntityMapper<T>();

// set the item
Current = TableEntityDynamic.fromEntity<T>(_inPageEnumerator.Current, _context.context.GetEntityMapper<T>());
if (entityMapper.TypeField == null)
Current = TableEntityDynamic.fromEntity<T>(_inPageEnumerator.Current, entityMapper);
else
{
var entity = _inPageEnumerator.Current;
var typeName = entity.GetString(entityMapper.TypeField);
Type type = Type.GetType(typeName);
MethodInfo method = typeof(TableEntityDynamic).GetMethod(nameof(TableEntityDynamic.fromEntity));
MethodInfo genericMethod = method.MakeGenericMethod(type);
Current = genericMethod.Invoke(null, [_inPageEnumerator.Current, entityMapper] ) as T;

Check failure on line 110 in CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs

View workflow job for this annotation

GitHub Actions / build-core

Invalid expression term '['

Check failure on line 110 in CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs

View workflow job for this annotation

GitHub Actions / build-core

Invalid expression term '['

Check failure on line 110 in CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs

View workflow job for this annotation

GitHub Actions / build-core

Invalid expression term '['

Check failure on line 110 in CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs

View workflow job for this annotation

GitHub Actions / build-core

Invalid expression term '['

Check failure on line 110 in CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs

View workflow job for this annotation

GitHub Actions / build-core

Invalid expression term '['

Check failure on line 110 in CoreHelpers.WindowsAzure.Storage.Table/Internal/StorageContextQueryNow.cs

View workflow job for this annotation

GitHub Actions / build-core

Invalid expression term '['
}

// done
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,44 @@ internal static class TableEntityDynamic
{
if (context as StorageContext == null)
throw new Exception("Invalid interface implemnetation");
else
else
return TableEntityDynamic.ToEntity<T>(model, (context as StorageContext).GetEntityMapper<T>());
}

public static TableEntity ToEntity<T>(T model, StorageEntityMapper entityMapper) where T: new()
public static TableEntity ToEntity<T>(T model, StorageEntityMapper entityMapper) where T : new()
{
var builder = new TableEntityBuilder();

// set the keys
builder.AddPartitionKey(GetTableStorageDefaultProperty<string, T>(entityMapper.PartitionKeyFormat, model));
builder.AddRowKey(GetTableStorageDefaultProperty<string, T>(entityMapper.RowKeyFormat, model), entityMapper.RowKeyEncoding);

var modelType = model.GetType();

// get all properties from model
IEnumerable<PropertyInfo> objectProperties = model.GetType().GetTypeInfo().GetProperties();
IEnumerable<PropertyInfo> objectProperties = modelType.GetTypeInfo().GetProperties();

// it is not required and preferred NOT to have the type field in the model as we can ensure equality
builder.AddProperty(entityMapper.TypeField, modelType.AssemblyQualifiedName);

// visit all properties
foreach (PropertyInfo property in objectProperties)
{
if (property.Name == entityMapper.TypeField)
continue;

if (ShouldSkipProperty(property))
continue;

// check if we have a special convert attached via attribute if so generate the required target
// properties with the correct converter
var virtualTypeAttribute = property.GetCustomAttributes().Where(a => a is IVirtualTypeAttribute).Select(a => a as IVirtualTypeAttribute).FirstOrDefault<IVirtualTypeAttribute>();
if (virtualTypeAttribute != null)
virtualTypeAttribute.WriteProperty<T>(property, model, builder);
virtualTypeAttribute.WriteProperty<T>(property, model, builder);
else
builder.AddProperty(property.Name, property.GetValue(model, null));
builder.AddProperty(property.Name, property.GetValue(model, null));
}

// build the result
return builder.Build();
}
Expand All @@ -58,13 +66,13 @@ internal static class TableEntityDynamic

// get all properties from model
IEnumerable<PropertyInfo> objectProperties = model.GetType().GetTypeInfo().GetProperties();

// visit all properties
foreach (PropertyInfo property in objectProperties)
{
if (ShouldSkipProperty(property))
continue;

// check if we have a special convert attached via attribute if so generate the required target
// properties with the correct converter
var virtualTypeAttribute = property.GetCustomAttributes().Where(a => a is IVirtualTypeAttribute).Select(a => a as IVirtualTypeAttribute).FirstOrDefault<IVirtualTypeAttribute>();
Expand All @@ -80,7 +88,7 @@ internal static class TableEntityDynamic
if (!entity.TryGetValue(property.Name, out objectValue))
continue;

if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTimeOffset) || property.PropertyType == typeof(DateTimeOffset?) )
if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTimeOffset) || property.PropertyType == typeof(DateTimeOffset?))
property.SetDateTimeOffsetValue(model, objectValue);
else
property.SetValue(model, objectValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class StorageEntityMapper
public String RowKeyFormat { get; set; }
public nVirtualValueEncoding RowKeyEncoding { get; set; }
public String TableName { get; set; }
public string TypeField { get; internal set; }

public StorageEntityMapper()
{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,18 @@ public void AddAttributeMapper(Type type)

public void AddAttributeMapper(Type type, String optionalTablenameOverride)
{
string typeField = null;

// get the concrete attribute
var storableAttribute = type.GetTypeInfo().GetCustomAttribute<StorableAttribute>();
if (String.IsNullOrEmpty(storableAttribute.Tablename))
{
storableAttribute.Tablename = type.Name;
}
if (!String.IsNullOrEmpty(storableAttribute.TypeField))
{
typeField = storableAttribute.TypeField;
}

// store the neded properties
string partitionKeyFormat = null;
Expand Down Expand Up @@ -111,7 +117,8 @@ public void AddAttributeMapper(Type type, String optionalTablenameOverride)
TableName = String.IsNullOrEmpty(optionalTablenameOverride) ? storableAttribute.Tablename : optionalTablenameOverride,
PartitionKeyFormat = partitionKeyFormat,
RowKeyFormat = rowKeyFormat,
RowKeyEncoding = rowKeyEncoding
RowKeyEncoding = rowKeyEncoding,
TypeField = typeField,
});
}

Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,56 @@ public class JObjectModel
}
```

## Store Multiple Objects Types in the same Table
If multiple objects share a common base class, it can be used to store them in the same table. The base class must be decorated with the Storable attribute with the `TypeField` parameter set. It is best practice to NOT include the type field in the model.

```csharp
[Storable(TypeField = "Type")]
public class BaseModel
{
[PartitionKey]
public string P { get; set; } = "Partition01";

[RowKey]
public string R { get; set; } = String.Empty;

}

public class MultipleModels1 : MultipleModelsBase
{
public string Model1Field { get; set; } = String.Empty;

}

public class MultipleModels2 : MultipleModelsBase
{
public string Model2Field { get; set; } = String.Empty;

}

```

When saving and querying it is important to use the base class as the generic type.

```csharp
using (var storageContext = new StorageContext(storageKey, storageSecret))
{
storageContext.AddAttributeMapper();

storageContext.CreateTable<BaseModel>();

storageContext.MergeOrInsert<BaseModel>(new MultipleModels1() { R = "Row01", Model1Field = "Model1Field" });
storageContext.MergeOrInsert<BaseModel>(new MultipleModels2() { R = "Row02", Model2Field = "Model2Field" });

var result = storageContext.Query<BaseModel>();

foreach (var r in result)
{
Console.WriteLine(r.GetType().Name);
}
}
```

# Contributing to Azure Storage Table
Fork as usual and go crazy!

Expand Down

0 comments on commit 2261859

Please sign in to comment.