Skip to content

Commit

Permalink
Adding Azure Table storage and ComosDB table provider (#5)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Jinhuafei authored and HongGit committed Aug 22, 2018
1 parent c4c6bbd commit d086284
Show file tree
Hide file tree
Showing 41 changed files with 1,397 additions and 110 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.vs/
msbuild.*
packages/
*.log
14 changes: 14 additions & 0 deletions Microsoft.AspNet.OutputCache.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OutputCach
EndProject
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider", "src\CosmosDBTableAsyncOutputCacheProvider\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.csproj", "{9FDBB781-C469-4D9E-B687-4087CD422B13}"
EndProject
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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -37,6 +41,14 @@ Global
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8}.Release|Any CPU.Build.0 = Release|Any CPU
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9FDBB781-C469-4D9E-B687-4087CD422B13}.Release|Any CPU.Build.0 = Release|Any CPU
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Debug|Any CPU.Build.0 = Debug|Any CPU
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Release|Any CPU.ActiveCfg = Release|Any CPU
{107EF28E-05AA-485E-A5C1-9A20A6DFE877}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -46,6 +58,8 @@ Global
{062FD141-4E51-4943-8C69-385DDE3D2792} = {D256C480-BB19-4239-AD53-DF634A794F41}
{89636B89-D392-47CA-9D81-BEB4C5252D75} = {BCF9496E-D71E-4F4A-814B-B37830A87FF3}
{B2C127BD-077B-4B9A-8163-F77DF76CE6A8} = {BCF9496E-D71E-4F4A-814B-B37830A87FF3}
{9FDBB781-C469-4D9E-B687-4087CD422B13} = {D256C480-BB19-4239-AD53-DF634A794F41}
{107EF28E-05AA-485E-A5C1-9A20A6DFE877} = {BCF9496E-D71E-4F4A-814B-B37830A87FF3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {51897B01-673A-47FD-87BC-8FD7C01950E6}
Expand Down
4 changes: 2 additions & 2 deletions MicrosoftAspNetOutputCache.msbuild
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="tools\MicrosoftAspNetOutputCache.settings.targets"/>

<ItemGroup>
<AssemblyProject Include="src\OutputCacheModuleAsync\Microsoft.AspNet.OutputCache.OutputCacheModuleAsync.csproj" />
<AssemblyProject Include="src\SQLAsyncOutputCacheProvider\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.csproj" />
<AssemblyProject Include="src\CosmosDBTableAsyncOutputCacheProvider\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.csproj" />
</ItemGroup>
<ItemGroup>
<TestProject Include="test\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test\Microsoft.AspNet.OutputCache.SQLAsyncOutputCacheProvider.Test.csproj" />
<TestProject Include="test\Microsoft.AspNet.OutputCache.OutputCacheModuleAsync.Test\Microsoft.AspNet.OutputCache.OutputCacheModuleAsync.Test.csproj" />
<TestProject Include="test\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test\Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider.Test.csproj" />
</ItemGroup>
<ItemGroup>
<PackageProject Include="src\Packages\Packages.csproj" />
Expand Down
81 changes: 81 additions & 0 deletions src/CosmosDBTableAsyncOutputCacheProvider/CacheEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See the License.txt file in the project root for full license information.

namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
using Microsoft.Azure.CosmosDB.Table;
using Microsoft.Azure.Storage;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;

class CacheEntity : TableEntity {
private static readonly char[] InvalidCharsInResource = { '/', '\\', '?', '#' };
private const char ReplacementOfInvalidChars = '_';

// This is required by TableQuery
public CacheEntity() { }

public CacheEntity(string cacheKey, object cacheItem, DateTime utcExpiry) {
RowKey = SanitizeKey(cacheKey);
PartitionKey = GeneratePartitionKey(cacheKey);
CacheItem = cacheItem;
UtcExpiry = utcExpiry;
}

public object CacheItem { get; set; }

public DateTime UtcExpiry { get; set; }

public override void ReadEntity(IDictionary<string, EntityProperty> properties, OperationContext operationContext) {
base.ReadEntity(properties, operationContext);
CacheItem = Deserialize(properties[nameof(CacheItem)].BinaryValue);
}

public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext) {
var result = base.WriteEntity(operationContext);
var cacheItemProperty = new EntityProperty(Serialize(CacheItem));
result.Add(nameof(CacheItem), cacheItemProperty);
return result;
}

public static string GeneratePartitionKey(string cacheKey) {
return (cacheKey.Length % 10).ToString();
}

public static string SanitizeKey(string cacheKey) {
// some chars are not allowed in rowkey
// https://docs.microsoft.com/en-us/rest/api/storageservices/Understanding-the-Table-Service-Data-Model
var sbKey = new StringBuilder(cacheKey);

foreach (var c in InvalidCharsInResource) {
sbKey.Replace(c, ReplacementOfInvalidChars);
}
return sbKey.ToString();
}

private static byte[] Serialize(object data) {
if (data == null) {
data = new object();
}

using (var memoryStream = new MemoryStream()) {
var binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(memoryStream, data);
return memoryStream.ToArray();
}
}

private static object Deserialize(byte[] data) {
if (data == null) {
return null;
}

using (var memoryStream = new MemoryStream(data, 0, data.Length)) {
var binaryFormatter = new BinaryFormatter();
return binaryFormatter.Deserialize(memoryStream);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See the License.txt file in the project root for full license information.

namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using System.Web.Caching;
using System.Web.Configuration;

/// <summary>
/// Async CosmosDB table OutputCache provider
/// </summary>
public class CosmosDBTableAsyncOutputCacheProvider : OutputCacheProviderAsync {
private ITableOutputCacheRepository _tableRepo;

/// <inheritdoc />
public override void Initialize(string name, NameValueCollection config) {
Initialize(name, config, new CosmosDBTableOutputCacheRepository(config, WebConfigurationManager.AppSettings));
}

internal void Initialize(string name, NameValueCollection providerConfig, ITableOutputCacheRepository _repo) {
_tableRepo = _repo;
base.Initialize(name, providerConfig);
}

/// <inheritdoc />
public override object Add(string key, object entry, DateTime utcExpiry) {
return _tableRepo.Add(key, entry, utcExpiry);
}

/// <inheritdoc />
public override Task<object> AddAsync(string key, object entry, DateTime utcExpiry) {
return _tableRepo.AddAsync(key, entry, utcExpiry);
}

/// <inheritdoc />
public override object Get(string key) {
return _tableRepo.Get(key);
}

/// <inheritdoc />
public override Task<object> GetAsync(string key) {
return _tableRepo.GetAsync(key);
}

/// <inheritdoc />
public override void Remove(string key) {
_tableRepo.Remove(key);
}

/// <inheritdoc />
public override Task RemoveAsync(string key) {
return _tableRepo.RemoveAsync(key);
}

/// <inheritdoc />
public override void Set(string key, object entry, DateTime utcExpiry) {
_tableRepo.Set(key, entry, utcExpiry);
}

/// <inheritdoc />
public override Task SetAsync(string key, object entry, DateTime utcExpiry) {
return _tableRepo.SetAsync(key, entry, utcExpiry);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See the License.txt file in the project root for full license information.

namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Azure.CosmosDB.Table;
using Microsoft.Azure.Storage;
using Resource;

class CosmosDBTableOutputCacheRepository : ITableOutputCacheRepository {
private const string TableNameKey = "tableName";
private const string ConnectionStringKey = "connectionStringName";
private const string FixedPartitionKey = "P";

private CloudTable _table;
private string _connectionString;
private string _tableName;
private object _lock = new object();

public CosmosDBTableOutputCacheRepository(NameValueCollection providerConfig, NameValueCollection appSettings) {
var connectionStringName = providerConfig[ConnectionStringKey];
if (string.IsNullOrEmpty(connectionStringName)) {
throw new ConfigurationErrorsException(SR.Cant_find_connectionStringName);
}

_connectionString = appSettings[connectionStringName];
if (string.IsNullOrEmpty(_connectionString)) {
throw new ConfigurationErrorsException(string.Format(SR.Cant_find_connectionString, connectionStringName));
}

_tableName = providerConfig[TableNameKey];
if (string.IsNullOrEmpty(_tableName)) {
throw new ConfigurationErrorsException(SR.TableName_cant_be_empty);
}
}

public object Add(string key, object entry, DateTime utcExpiry) {
var retrieveOp = TableOperationHelper.Retrieve(key);
var retrieveResult = _table.Execute(retrieveOp);
var existingCacheEntry = retrieveResult.Result as CacheEntity;

if (existingCacheEntry != null && existingCacheEntry.UtcExpiry > DateTime.UtcNow) {
return existingCacheEntry.CacheItem;
} else {
Set(key, entry, utcExpiry);
return entry;
}
}

public async Task<object> AddAsync(string key, object entry, DateTime utcExpiry) {
// If there is already a value in the cache for the specified key, the provider must return that value if not expired
// and must not store the data passed by using the Add method parameters.
var retrieveOp = TableOperationHelper.Retrieve(key);
var retrieveResult = await _table.ExecuteAsync(retrieveOp);
var existingCacheEntry = retrieveResult.Result as CacheEntity;

if (existingCacheEntry != null && existingCacheEntry.UtcExpiry > DateTime.UtcNow) {
return existingCacheEntry.CacheItem;
} else {
await SetAsync(key, entry, utcExpiry);
return entry;
}
}

public object Get(string key) {
var retrieveOp = TableOperationHelper.Retrieve(key);
var retrieveResult = _table.Execute(retrieveOp);
var existingCacheEntry = retrieveResult.Result as CacheEntity;

if (existingCacheEntry != null && existingCacheEntry.UtcExpiry < DateTime.UtcNow) {
Remove(key);
return null;
} else {
return existingCacheEntry?.CacheItem;
}
}

public async Task<object> GetAsync(string key) {
// Outputcache module will always first call GetAsync
// so only calling EnsureTableInitializedAsync here is good enough
await EnsureTableInitializedAsync();

var retrieveOp = TableOperationHelper.Retrieve(key);
var retrieveResult = await _table.ExecuteAsync(retrieveOp);
var existingCacheEntry = retrieveResult.Result as CacheEntity;

if (existingCacheEntry != null && existingCacheEntry.UtcExpiry < DateTime.UtcNow) {
await RemoveAsync(key);
return null;
} else {
return existingCacheEntry?.CacheItem;
}
}

public void Remove(string key) {
var removeOp = TableOperationHelper.Delete(key);
_table.Execute(removeOp);
}

public async Task RemoveAsync(string key) {
var removeOp = TableOperationHelper.Delete(key);
await _table.ExecuteAsync(removeOp);
}

public void Set(string key, object entry, DateTime utcExpiry) {
var insertOp = TableOperationHelper.InsertOrReplace(key, entry, utcExpiry);
_table.Execute(insertOp);
}

public async Task SetAsync(string key, object entry, DateTime utcExpiry) {
//Check if the key is already in database
//If there is already a value in the cache for the specified key, the Set method will update it.
//Otherwise it will insert the entry.
var insertOp = TableOperationHelper.InsertOrReplace(key, entry, utcExpiry);
await _table.ExecuteAsync(insertOp);
}

private async Task EnsureTableInitializedAsync() {
if (_table != null) {
return;
}

try {
lock (_lock) {
if (_table != null) {
return;
}

var storageAccount = CreateStorageAccount();
var tableClient = storageAccount.CreateCloudTableClient();
_table = tableClient.GetTableReference(_tableName);
}

// The sync version API causes deadlock when using CosmosDB table.
await _table.CreateIfNotExistsAsync();
} catch (StorageException ex) {
throw new HttpException(SR.Fail_to_create_table, ex);
}
}

private CloudStorageAccount CreateStorageAccount() {
try {
return CloudStorageAccount.Parse(_connectionString);
} catch (FormatException) {
throw new HttpException(SR.Invalid_storage_account_information);
} catch (ArgumentException) {
throw new HttpException(SR.Invalid_storage_account_information);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See the License.txt file in the project root for full license information.

namespace Microsoft.AspNet.OutputCache.CosmosDBTableAsyncOutputCacheProvider {
using System;
using System.Threading.Tasks;

interface ITableOutputCacheRepository {
Task<object> AddAsync(string key, object entry, DateTime utcExpiry);

Task<object> GetAsync(string key);

Task RemoveAsync(string key);

Task SetAsync(string key, object entry, DateTime utcExpiry);

object Add(string key, object entry, DateTime utcExpiry);

object Get(string key);

void Remove(string key);

void Set(string key, object entry, DateTime utcExpiry);
}
}
Loading

0 comments on commit d086284

Please sign in to comment.