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

Store models with a common base in the same table #26

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
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 @@
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,45 @@ 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
if (!string.IsNullOrEmpty(entityMapper.TypeField))
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 +67,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 +89,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
Loading