Skip to content

Commit d086284

Browse files
JinhuafeiHongGit
authored andcommitted
Adding Azure Table storage and ComosDB table provider (#5)
* v1 * mend * fix sqlprovider issues * update build script * update build for signing * update build * update CosmosDBTable nupkg version * add iconUrl in nupkg * fix deadlock issue and format code * update module version * update package ref for table provider
1 parent c4c6bbd commit d086284

File tree

41 files changed

+1397
-110
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1397
-110
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
.vs/
44
msbuild.*
55
packages/
6+
*.log

Microsoft.AspNet.OutputCache.sln

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OutputCach
1515
EndProject
1616
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test", "test\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test.csproj", "{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}"
1717
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider", "src\CosmosDBTableAsyncOutputCacheProvider\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.csproj", "{9FDBB781-C469-4D9E-B687-4087CD422B13}"
19+
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test", "test\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test.csproj", "{107EF28E-05AA-485E-A5C1-9A20A6DFE877}"
21+
EndProject
1822
Global
1923
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2024
Debug|Any CPU = Debug|Any CPU
@@ -37,6 +41,14 @@ Global
3741
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
3842
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
3943
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}.Release|Any CPU.Build.0 = Release|Any CPU
44+
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45+
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Debug|Any CPU.Build.0 = Debug|Any CPU
46+
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Release|Any CPU.ActiveCfg = Release|Any CPU
47+
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Release|Any CPU.Build.0 = Release|Any CPU
48+
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49+
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Debug|Any CPU.Build.0 = Debug|Any CPU
50+
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Release|Any CPU.ActiveCfg = Release|Any CPU
51+
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Release|Any CPU.Build.0 = Release|Any CPU
4052
EndGlobalSection
4153
GlobalSection(SolutionProperties) = preSolution
4254
HideSolutionNode = FALSE
@@ -46,6 +58,8 @@ Global
4658
{062FD141-4E51-4943-8C69-385DDE3D2792} = {D256C480-BB19-4239-AD53-DF634A794F41}
4759
{89636B89-D392-47CA-9D81-BEB4C5252D75} = {BCF9496E-D71E-4F4A-814B-B37830A87FF3}
4860
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8} = {BCF9496E-D71E-4F4A-814B-B37830A87FF3}
61+
{9FDBB781-C469-4D9E-B687-4087CD422B13} = {D256C480-BB19-4239-AD53-DF634A794F41}
62+
{107EF28E-05AA-485E-A5C1-9A20A6DFE877} = {BCF9496E-D71E-4F4A-814B-B37830A87FF3}
4963
EndGlobalSection
5064
GlobalSection(ExtensibilityGlobals) = postSolution
5165
SolutionGuid = {51897B01-673A-47FD-87BC-8FD7C01950E6}

MicrosoftAspNetOutputCache.msbuild

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
2-
<Import Project="tools\MicrosoftAspNetOutputCache.settings.targets"/>
3-
42
<ItemGroup>
53
<AssemblyProject Include="src\OutputCacheModuleAsync\Microsoft.AspNet.OutputCache.OutputCacheModuleAsync.csproj" />
64
<AssemblyProject Include="src\SQLAsyncOutputCacheProvider\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.csproj" />
5+
<AssemblyProject Include="src\CosmosDBTableAsyncOutputCacheProvider\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.csproj" />
76
</ItemGroup>
87
<ItemGroup>
98
<TestProject Include="test\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test.csproj" />
109
<TestProject Include="test\Microsoft.AspNet.OutputCache.OutputCacheModuleAsync.Test\Microsoft.AspNet.OutputCache.OutputCacheModuleAsync.Test.csproj" />
10+
<TestProject Include="test\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test.csproj" />
1111
</ItemGroup>
1212
<ItemGroup>
1313
<PackageProject Include="src\Packages\Packages.csproj" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See the License.txt file in the project root for full license information.
3+
4+
namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
5+
using Microsoft.Azure.CosmosDB.Table;
6+
using Microsoft.Azure.Storage;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Runtime.Serialization.Formatters.Binary;
11+
using System.Text;
12+
13+
class CacheEntity : TableEntity {
14+
private static readonly char[] InvalidCharsInResource = { '/', '\\', '?', '#' };
15+
private const char ReplacementOfInvalidChars = '_';
16+
17+
// This is required by TableQuery
18+
public CacheEntity() { }
19+
20+
public CacheEntity(string cacheKey, object cacheItem, DateTime utcExpiry) {
21+
RowKey = SanitizeKey(cacheKey);
22+
PartitionKey = GeneratePartitionKey(cacheKey);
23+
CacheItem = cacheItem;
24+
UtcExpiry = utcExpiry;
25+
}
26+
27+
public object CacheItem { get; set; }
28+
29+
public DateTime UtcExpiry { get; set; }
30+
31+
public override void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext) {
32+
base.ReadEntity(properties, operationContext);
33+
CacheItem = Deserialize(properties[nameof(CacheItem)].BinaryValue);
34+
}
35+
36+
public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext) {
37+
var result = base.WriteEntity(operationContext);
38+
var cacheItemProperty = new EntityProperty(Serialize(CacheItem));
39+
result.Add(nameof(CacheItem), cacheItemProperty);
40+
return result;
41+
}
42+
43+
public static string GeneratePartitionKey(string cacheKey) {
44+
return (cacheKey.Length % 10).ToString();
45+
}
46+
47+
public static string SanitizeKey(string cacheKey) {
48+
// some chars are not allowed in rowkey
49+
// https://docs.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model
50+
var sbKey = new StringBuilder(cacheKey);
51+
52+
foreach (var c in InvalidCharsInResource) {
53+
sbKey.Replace(c, ReplacementOfInvalidChars);
54+
}
55+
return sbKey.ToString();
56+
}
57+
58+
private static byte[] Serialize(object data) {
59+
if (data == null) {
60+
data = new object();
61+
}
62+
63+
using (var memoryStream = new MemoryStream()) {
64+
var binaryFormatter = new BinaryFormatter();
65+
binaryFormatter.Serialize(memoryStream, data);
66+
return memoryStream.ToArray();
67+
}
68+
}
69+
70+
private static object Deserialize(byte[] data) {
71+
if (data == null) {
72+
return null;
73+
}
74+
75+
using (var memoryStream = new MemoryStream(data, 0, data.Length)) {
76+
var binaryFormatter = new BinaryFormatter();
77+
return binaryFormatter.Deserialize(memoryStream);
78+
}
79+
}
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See the License.txt file in the project root for full license information.
3+
4+
namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
5+
using System;
6+
using System.Collections.Specialized;
7+
using System.Threading.Tasks;
8+
using System.Web.Caching;
9+
using System.Web.Configuration;
10+
11+
/// <summary>
12+
/// Async CosmosDB table OutputCache provider
13+
/// </summary>
14+
public class CosmosDBTableAsyncOutputCacheProvider : OutputCacheProviderAsync {
15+
private ITableOutputCacheRepository _tableRepo;
16+
17+
/// <inheritdoc />
18+
public override void Initialize(string name, NameValueCollection config) {
19+
Initialize(name, config, new CosmosDBTableOutputCacheRepository(config, WebConfigurationManager.AppSettings));
20+
}
21+
22+
internal void Initialize(string name, NameValueCollection providerConfig, ITableOutputCacheRepository _repo) {
23+
_tableRepo = _repo;
24+
base.Initialize(name, providerConfig);
25+
}
26+
27+
/// <inheritdoc />
28+
public override object Add(string key, object entry, DateTime utcExpiry) {
29+
return _tableRepo.Add(key, entry, utcExpiry);
30+
}
31+
32+
/// <inheritdoc />
33+
public override Task<object> AddAsync(string key, object entry, DateTime utcExpiry) {
34+
return _tableRepo.AddAsync(key, entry, utcExpiry);
35+
}
36+
37+
/// <inheritdoc />
38+
public override object Get(string key) {
39+
return _tableRepo.Get(key);
40+
}
41+
42+
/// <inheritdoc />
43+
public override Task<object> GetAsync(string key) {
44+
return _tableRepo.GetAsync(key);
45+
}
46+
47+
/// <inheritdoc />
48+
public override void Remove(string key) {
49+
_tableRepo.Remove(key);
50+
}
51+
52+
/// <inheritdoc />
53+
public override Task RemoveAsync(string key) {
54+
return _tableRepo.RemoveAsync(key);
55+
}
56+
57+
/// <inheritdoc />
58+
public override void Set(string key, object entry, DateTime utcExpiry) {
59+
_tableRepo.Set(key, entry, utcExpiry);
60+
}
61+
62+
/// <inheritdoc />
63+
public override Task SetAsync(string key, object entry, DateTime utcExpiry) {
64+
return _tableRepo.SetAsync(key, entry, utcExpiry);
65+
}
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See the License.txt file in the project root for full license information.
3+
4+
namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
5+
using System;
6+
using System.Collections.Specialized;
7+
using System.Configuration;
8+
using System.Threading.Tasks;
9+
using System.Web;
10+
using Microsoft.Azure.CosmosDB.Table;
11+
using Microsoft.Azure.Storage;
12+
using Resource;
13+
14+
class CosmosDBTableOutputCacheRepository : ITableOutputCacheRepository {
15+
private const string TableNameKey = "tableName";
16+
private const string ConnectionStringKey = "connectionStringName";
17+
private const string FixedPartitionKey = "P";
18+
19+
private CloudTable _table;
20+
private string _connectionString;
21+
private string _tableName;
22+
private object _lock = new object();
23+
24+
public CosmosDBTableOutputCacheRepository(NameValueCollection providerConfig, NameValueCollection appSettings) {
25+
var connectionStringName = providerConfig[ConnectionStringKey];
26+
if (string.IsNullOrEmpty(connectionStringName)) {
27+
throw new ConfigurationErrorsException(SR.Cant_find_connectionStringName);
28+
}
29+
30+
_connectionString = appSettings[connectionStringName];
31+
if (string.IsNullOrEmpty(_connectionString)) {
32+
throw new ConfigurationErrorsException(string.Format(SR.Cant_find_connectionString, connectionStringName));
33+
}
34+
35+
_tableName = providerConfig[TableNameKey];
36+
if (string.IsNullOrEmpty(_tableName)) {
37+
throw new ConfigurationErrorsException(SR.TableName_cant_be_empty);
38+
}
39+
}
40+
41+
public object Add(string key, object entry, DateTime utcExpiry) {
42+
var retrieveOp = TableOperationHelper.Retrieve(key);
43+
var retrieveResult = _table.Execute(retrieveOp);
44+
var existingCacheEntry = retrieveResult.Result as CacheEntity;
45+
46+
if (existingCacheEntry != null && existingCacheEntry.UtcExpiry > DateTime.UtcNow) {
47+
return existingCacheEntry.CacheItem;
48+
} else {
49+
Set(key, entry, utcExpiry);
50+
return entry;
51+
}
52+
}
53+
54+
public async Task<object> AddAsync(string key, object entry, DateTime utcExpiry) {
55+
// If there is already a value in the cache for the specified key, the provider must return that value if not expired
56+
// and must not store the data passed by using the Add method parameters.
57+
var retrieveOp = TableOperationHelper.Retrieve(key);
58+
var retrieveResult = await _table.ExecuteAsync(retrieveOp);
59+
var existingCacheEntry = retrieveResult.Result as CacheEntity;
60+
61+
if (existingCacheEntry != null && existingCacheEntry.UtcExpiry > DateTime.UtcNow) {
62+
return existingCacheEntry.CacheItem;
63+
} else {
64+
await SetAsync(key, entry, utcExpiry);
65+
return entry;
66+
}
67+
}
68+
69+
public object Get(string key) {
70+
var retrieveOp = TableOperationHelper.Retrieve(key);
71+
var retrieveResult = _table.Execute(retrieveOp);
72+
var existingCacheEntry = retrieveResult.Result as CacheEntity;
73+
74+
if (existingCacheEntry != null && existingCacheEntry.UtcExpiry < DateTime.UtcNow) {
75+
Remove(key);
76+
return null;
77+
} else {
78+
return existingCacheEntry?.CacheItem;
79+
}
80+
}
81+
82+
public async Task<object> GetAsync(string key) {
83+
// Outputcache module will always first call GetAsync
84+
// so only calling EnsureTableInitializedAsync here is good enough
85+
await EnsureTableInitializedAsync();
86+
87+
var retrieveOp = TableOperationHelper.Retrieve(key);
88+
var retrieveResult = await _table.ExecuteAsync(retrieveOp);
89+
var existingCacheEntry = retrieveResult.Result as CacheEntity;
90+
91+
if (existingCacheEntry != null && existingCacheEntry.UtcExpiry < DateTime.UtcNow) {
92+
await RemoveAsync(key);
93+
return null;
94+
} else {
95+
return existingCacheEntry?.CacheItem;
96+
}
97+
}
98+
99+
public void Remove(string key) {
100+
var removeOp = TableOperationHelper.Delete(key);
101+
_table.Execute(removeOp);
102+
}
103+
104+
public async Task RemoveAsync(string key) {
105+
var removeOp = TableOperationHelper.Delete(key);
106+
await _table.ExecuteAsync(removeOp);
107+
}
108+
109+
public void Set(string key, object entry, DateTime utcExpiry) {
110+
var insertOp = TableOperationHelper.InsertOrReplace(key, entry, utcExpiry);
111+
_table.Execute(insertOp);
112+
}
113+
114+
public async Task SetAsync(string key, object entry, DateTime utcExpiry) {
115+
//Check if the key is already in database
116+
//If there is already a value in the cache for the specified key, the Set method will update it.
117+
//Otherwise it will insert the entry.
118+
var insertOp = TableOperationHelper.InsertOrReplace(key, entry, utcExpiry);
119+
await _table.ExecuteAsync(insertOp);
120+
}
121+
122+
private async Task EnsureTableInitializedAsync() {
123+
if (_table != null) {
124+
return;
125+
}
126+
127+
try {
128+
lock (_lock) {
129+
if (_table != null) {
130+
return;
131+
}
132+
133+
var storageAccount = CreateStorageAccount();
134+
var tableClient = storageAccount.CreateCloudTableClient();
135+
_table = tableClient.GetTableReference(_tableName);
136+
}
137+
138+
// The sync version API causes deadlock when using CosmosDB table.
139+
await _table.CreateIfNotExistsAsync();
140+
} catch (StorageException ex) {
141+
throw new HttpException(SR.Fail_to_create_table, ex);
142+
}
143+
}
144+
145+
private CloudStorageAccount CreateStorageAccount() {
146+
try {
147+
return CloudStorageAccount.Parse(_connectionString);
148+
} catch (FormatException) {
149+
throw new HttpException(SR.Invalid_storage_account_information);
150+
} catch (ArgumentException) {
151+
throw new HttpException(SR.Invalid_storage_account_information);
152+
}
153+
}
154+
}
155+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See the License.txt file in the project root for full license information.
3+
4+
namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
5+
using System;
6+
using System.Threading.Tasks;
7+
8+
interface ITableOutputCacheRepository {
9+
Task<object> AddAsync(string key, object entry, DateTime utcExpiry);
10+
11+
Task<object> GetAsync(string key);
12+
13+
Task RemoveAsync(string key);
14+
15+
Task SetAsync(string key, object entry, DateTime utcExpiry);
16+
17+
object Add(string key, object entry, DateTime utcExpiry);
18+
19+
object Get(string key);
20+
21+
void Remove(string key);
22+
23+
void Set(string key, object entry, DateTime utcExpiry);
24+
}
25+
}

0 commit comments

Comments
 (0)