Skip to content

Commit e777b52

Browse files
(GH-659) Allow azure storage blob context to respect DatabaseGeneratedOption for primary keys
1 parent 9e9b2a1 commit e777b52

File tree

3 files changed

+178
-6
lines changed

3 files changed

+178
-6
lines changed

src/DotNetToolkit.Repository.AzureStorageBlob/Internal/AzureStorageBlobRepositoryContext.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
using Query.Strategies;
1111
using System;
1212
using System.Collections.Generic;
13+
using System.ComponentModel.DataAnnotations.Schema;
1314
using System.Data;
15+
using System.Globalization;
1416
using System.IO;
1517
using System.Linq;
1618
using System.Linq.Expressions;
19+
using System.Reflection;
1720
using System.Text;
1821
using System.Threading;
1922
using System.Threading.Tasks;
@@ -86,6 +89,62 @@ private TEntity Deserialize<TEntity>(Stream stream)
8689
}
8790
}
8891

92+
protected async Task<Tuple<bool, object>> TryGeneratePrimaryKeyAsync<TEntity>(TEntity entity) where TEntity : class
93+
{
94+
Guard.NotNull(entity, nameof(entity));
95+
96+
var primaryKeyPropertyInfos = Conventions.GetPrimaryKeyPropertyInfos<TEntity>();
97+
98+
if (primaryKeyPropertyInfos.Length > 1)
99+
return Tuple.Create<bool, object>(false, null);
100+
101+
var primaryKeyPropertyInfo = primaryKeyPropertyInfos.First();
102+
var attribute = primaryKeyPropertyInfo.GetCustomAttribute<DatabaseGeneratedAttribute>();
103+
if (attribute?.DatabaseGeneratedOption == DatabaseGeneratedOption.None ||
104+
attribute?.DatabaseGeneratedOption == DatabaseGeneratedOption.Computed)
105+
{
106+
return Tuple.Create<bool, object>(false, null);
107+
}
108+
109+
var propertyType = primaryKeyPropertyInfo.PropertyType;
110+
object newKey = null;
111+
112+
if (propertyType == typeof(Guid))
113+
{
114+
newKey = Guid.NewGuid();
115+
}
116+
else if (propertyType == typeof(string))
117+
{
118+
newKey = Guid.NewGuid().ToString("N");
119+
}
120+
else if (propertyType == typeof(int))
121+
{
122+
var lambda = ExpressionHelper.GetExpression<TEntity>(primaryKeyPropertyInfo.Name);
123+
var func = (Func<TEntity, int>)lambda.Compile();
124+
125+
var key = await AsAsyncEnumerable<TEntity>()
126+
.Select(func)
127+
.OrderByDescending(x => x)
128+
.FirstOrDefaultAsync();
129+
130+
newKey = Convert.ToInt32(key) + 1;
131+
}
132+
133+
if (newKey != null)
134+
{
135+
primaryKeyPropertyInfo.SetValue(entity, newKey);
136+
137+
return Tuple.Create<bool, object>(true, newKey);
138+
}
139+
140+
throw new InvalidOperationException(
141+
string.Format(
142+
CultureInfo.CurrentCulture,
143+
Resources.EntityKeyValueTypeInvalid,
144+
typeof(TEntity).FullName,
145+
propertyType));
146+
}
147+
89148
private BlobContainerClient GetBlobContainer<TEntity>()
90149
{
91150
var container = _containerNameBuilder.Build<TEntity>();
@@ -186,6 +245,9 @@ private async IAsyncEnumerable<TEntity> DownloadEntitiesAsync<TEntity>() where T
186245
private void UploadEntity<TEntity>(TEntity entity) where TEntity : class
187246
{
188247
var blob = GetBlobClient(entity);
248+
249+
TryGeneratePrimaryKey(entity, out var _);
250+
189251
using (var stream = Serialize<TEntity>(entity))
190252
{
191253
var binaryData = BinaryData.FromStream(stream);
@@ -197,6 +259,9 @@ private void UploadEntity<TEntity>(TEntity entity) where TEntity : class
197259
private async Task UploadEntityAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = new CancellationToken()) where TEntity : class
198260
{
199261
var blob = await GetBlobClientAsync(entity);
262+
263+
await TryGeneratePrimaryKeyAsync(entity);
264+
200265
using (var stream = Serialize<TEntity>(entity))
201266
{
202267
var binaryData = await BinaryData.FromStreamAsync(stream, cancellationToken);

src/DotNetToolkit.Repository/Configuration/LinqEnumerableRepositoryContextBase.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
using System;
1111
using System.Collections;
1212
using System.Collections.Generic;
13+
using System.ComponentModel.DataAnnotations.Schema;
1314
using System.Data;
15+
using System.Globalization;
1416
using System.Linq;
1517
using System.Linq.Expressions;
18+
using System.Reflection;
1619
using Transactions;
1720
using Utility;
1821

@@ -87,6 +90,66 @@ protected LinqEnumerableRepositoryContextBase()
8790
/// <returns>The entity's query.</returns>
8891
protected abstract IEnumerable<TEntity> AsEnumerable<TEntity>([CanBeNull] IFetchQueryStrategy<TEntity> fetchStrategy) where TEntity : class;
8992

93+
/// <summary>
94+
/// Returns <c>true</c> if able to generate a primary key for the specified entity; otherwise, <c>false</c>.
95+
/// </summary>
96+
protected bool TryGeneratePrimaryKey<TEntity>(TEntity entity, out object newKey) where TEntity : class
97+
{
98+
Guard.NotNull(entity, nameof(entity));
99+
100+
var primaryKeyPropertyInfos = Conventions.GetPrimaryKeyPropertyInfos<TEntity>();
101+
102+
newKey = null;
103+
104+
if (primaryKeyPropertyInfos.Length > 1)
105+
return false;
106+
107+
var primaryKeyPropertyInfo = primaryKeyPropertyInfos.First();
108+
var attribute = primaryKeyPropertyInfo.GetCustomAttribute<DatabaseGeneratedAttribute>();
109+
if (attribute?.DatabaseGeneratedOption == DatabaseGeneratedOption.None ||
110+
attribute?.DatabaseGeneratedOption == DatabaseGeneratedOption.Computed)
111+
{
112+
return false;
113+
}
114+
115+
var propertyType = primaryKeyPropertyInfo.PropertyType;
116+
117+
if (propertyType == typeof(Guid))
118+
{
119+
newKey = Guid.NewGuid();
120+
}
121+
else if (propertyType == typeof(string))
122+
{
123+
newKey = Guid.NewGuid().ToString("N");
124+
}
125+
else if (propertyType == typeof(int))
126+
{
127+
var lambda = ExpressionHelper.GetExpression<TEntity>(primaryKeyPropertyInfo.Name);
128+
var func = (Func<TEntity, int>)lambda.Compile();
129+
130+
var key = AsEnumerable<TEntity>(fetchStrategy: null)
131+
.Select(func)
132+
.OrderByDescending(x => x)
133+
.FirstOrDefault();
134+
135+
newKey = Convert.ToInt32(key) + 1;
136+
}
137+
138+
if (newKey != null)
139+
{
140+
primaryKeyPropertyInfo.SetValue(entity, newKey);
141+
142+
return true;
143+
}
144+
145+
throw new InvalidOperationException(
146+
string.Format(
147+
CultureInfo.CurrentCulture,
148+
Resources.EntityKeyValueTypeInvalid,
149+
typeof(TEntity).FullName,
150+
propertyType));
151+
}
152+
90153
#endregion
91154

92155
#region Implementation of IRepositoryContext

test/DotNetToolkit.Repository.Integration.Test/Tests/Repository/RepositoryTests.Add.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ public void Add()
1616
[Fact]
1717
public void AddWithSeededIdForIdentity()
1818
{
19-
ForAllRepositoryFactories(TestAddWithSeededIdForIdentity,
20-
ContextProviderType.EntityFrameworkCore,
21-
ContextProviderType.AzureStorageBlob);
19+
ForAllRepositoryFactories(TestAddWithSeededIdForIdentity, ContextProviderType.EntityFrameworkCore);
2220
}
2321

2422
[Fact]
@@ -27,6 +25,12 @@ public void AddWithSeededIdForNoneIdentity()
2725
ForAllRepositoryFactories(TestAddWithSeededIdForNoneIdentity);
2826
}
2927

28+
[Fact]
29+
public void AddWithNotSeededIdForIdentity()
30+
{
31+
ForAllRepositoryFactories(TestAddWithNotSeededIdForIdentity, ContextProviderType.EntityFrameworkCore);
32+
}
33+
3034
[Fact]
3135
public void AddRange()
3236
{
@@ -42,9 +46,7 @@ public void AddAsync()
4246
[Fact]
4347
public void AddWithSeededIdForIdentityAsync()
4448
{
45-
ForAllRepositoryFactoriesAsync(TestAddWithSeededIdForIdentityAsync,
46-
ContextProviderType.EntityFrameworkCore,
47-
ContextProviderType.AzureStorageBlob);
49+
ForAllRepositoryFactoriesAsync(TestAddWithSeededIdForIdentityAsync, ContextProviderType.EntityFrameworkCore);
4850
}
4951

5052
[Fact]
@@ -53,6 +55,12 @@ public void AddWithSeededIdForNoneIdentityAsync()
5355
ForAllRepositoryFactoriesAsync(TestAddWithSeededIdForNoneIdentityAsync);
5456
}
5557

58+
[Fact]
59+
public void AddWithNotSeededIdForIdentityAsync()
60+
{
61+
ForAllRepositoryFactoriesAsync(TestAddWithNotSeededIdForIdentityAsync, ContextProviderType.EntityFrameworkCore);
62+
}
63+
5664
[Fact]
5765
public void AddRangeAsync()
5866
{
@@ -106,6 +114,24 @@ private static void TestAddWithSeededIdForNoneIdentity(IRepositoryFactory repoFa
106114
Assert.True(repo.Exists(key));
107115
}
108116

117+
private static void TestAddWithNotSeededIdForIdentity(IRepositoryFactory repoFactory)
118+
{
119+
var repo = repoFactory.Create<Customer>();
120+
121+
var entity1 = new Customer() { Name = "Random Name 1" };
122+
var entity2 = new Customer() { Name = "Random Name 2" };
123+
124+
Assert.False(repo.Exists(x => x.Name == "Random Name 1"));
125+
Assert.False(repo.Exists(x => x.Name == "Random Name 2"));
126+
127+
repo.Add(entity1);
128+
repo.Add(entity2);
129+
130+
// should be one, and two since it is autogenerating identity models
131+
Assert.Equal(1, entity1.Id);
132+
Assert.Equal(2, entity2.Id);
133+
}
134+
109135
private static void TestAddRange(IRepositoryFactory repoFactory)
110136
{
111137
var repo = repoFactory.Create<Customer>();
@@ -172,6 +198,24 @@ private static async Task TestAddWithSeededIdForNoneIdentityAsync(IRepositoryFac
172198
Assert.True(await repo.ExistsAsync(key));
173199
}
174200

201+
private static async Task TestAddWithNotSeededIdForIdentityAsync(IRepositoryFactory repoFactory)
202+
{
203+
var repo = repoFactory.Create<Customer>();
204+
205+
var entity1 = new Customer() { Name = "Random Name 1" };
206+
var entity2 = new Customer() { Name = "Random Name 2" };
207+
208+
Assert.False(await repo.ExistsAsync(x => x.Name == "Random Name 1"));
209+
Assert.False(await repo.ExistsAsync(x => x.Name == "Random Name 2"));
210+
211+
await repo.AddAsync(entity1);
212+
await repo.AddAsync(entity2);
213+
214+
// should be one, and two since it is autogenerating identity models
215+
Assert.Equal(1, entity1.Id);
216+
Assert.Equal(2, entity2.Id);
217+
}
218+
175219
private static async Task TestAddRangeAsync(IRepositoryFactory repoFactory)
176220
{
177221
var repo = repoFactory.Create<Customer>();

0 commit comments

Comments
 (0)