Added module Orchard.Azure. Added Azure Cache providers to it.

Deleted old module Orchard.AzureBlobStorage.
Moved all Azure code into Orchard.Azure module, deleting the old Orchard.Azure project.
Cleaned up Web.config and Config\* files in Orchard.Web and Orchard.Azure.Web.
This commit is contained in:
Daniel Stolt
2013-08-24 20:16:32 +02:00
committed by Sebastien Ros
parent d4154fa1fe
commit 92f8d0df80
35 changed files with 1576 additions and 1307 deletions

View File

@@ -0,0 +1,21 @@
Name: Orchard.Azure
AntiForgery: enabled
Author: The Orchard Team
Website: http://orchardproject.net
Version: 1.0
OrchardVersion: 1.7
Description: Provides a set of Orchard service implementations targeting Windows Azure services.
Features:
Orchard.Azure.OutputCache:
Name: Windows Azure Output Cache
Description: Activates an Orchard output cache provider that targets Windows Azure Cache.
Dependencies: Orchard.OutputCache
Category: Hosting
Orchard.Azure.DatabaseCache:
Name: Windows Azure Database Cache
Description: Activates an NHibernate second-level cache provider that targets Windows Azure Cache.
Category: Hosting
Orchard.Azure.MediaStorage:
Name: Windows Azure Media Storage
Description: Activates an Orchard media storage provider that targets Windows Azure Blob Storage.
Category: Hosting

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{CBC7993C-57D8-4A6C-992C-19E849DFE71D}</ProjectGuid>
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Orchard.Azure</RootNamespace>
<AssemblyName>Orchard.Azure</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<MvcBuildViews>false</MvcBuildViews>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>4.0</OldToolsVersion>
<UpgradeBackupLocation />
<TargetFrameworkProfile />
<UseIISExpress>false</UseIISExpress>
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="log4net, Version=1.2.11.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\log4net\log4net.dll</HintPath>
</Reference>
<Reference Include="Microsoft.ApplicationServer.Caching.Client, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="Microsoft.ApplicationServer.Caching.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.WindowsAzure.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="Microsoft.WindowsAzure.Diagnostics, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="Microsoft.WindowsAzure.ServiceRuntime, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="Microsoft.WindowsAzure.Storage, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="NHibernate, Version=3.3.1.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Web.Abstractions" />
<Reference Include="System.Web.Routing" />
<Reference Include="System.Xml" />
<Reference Include="System.Configuration" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Content Include="Web.config" />
<Content Include="Properties\AssemblyInfo.cs" />
<Content Include="Module.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
<Name>Orchard.Framework</Name>
</ProjectReference>
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.OutputCache\Orchard.OutputCache.csproj">
<Project>{6e444ff1-a47c-4cf6-bb3f-507c8ebd776d}</Project>
<Name>Orchard.OutputCache</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Services\Caching\Database\AzureCacheClient.cs" />
<Compile Include="Services\Caching\Database\AzureCacheConfiguration.cs" />
<Compile Include="Services\Caching\Database\AzureCacheProvider.cs" />
<Compile Include="Services\Caching\Output\AzureOutputCacheStorageProvider.cs" />
<Compile Include="Services\Environment\Configuration\AzureConfigShellSettingsManager.cs" />
<Compile Include="Services\Environment\Configuration\AzureBlobShellSettingsManager.cs" />
<Compile Include="Services\FileSystems\AzureFileSystem.cs" />
<Compile Include="Services\FileSystems\CloudBlobContainerExtensions.cs" />
<Compile Include="Services\FileSystems\Media\AzureBlobStorageProvider.cs" />
<Compile Include="Services\Logging\AzureDiagnosticsAppender.cs" />
</ItemGroup>
<ItemGroup />
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target> -->
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
<PropertyGroup>
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
</PropertyGroup>
<!-- If this is an area child project, uncomment the following line:
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
-->
<!-- If this is an area parent project, uncomment the following lines:
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
-->
</Target>
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>45979</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Orchard.AzureBlobStorage")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Orchard")]
[assembly: AssemblyCopyright("Copyright <20> Outercurve Foundation 2013")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("bcd6a602-4041-4a40-af4f-82f3dc0ab136")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("1.0")]
[assembly: AssemblyFileVersion("1.0")]

View File

@@ -0,0 +1,180 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.ApplicationServer.Caching;
using NHibernate;
using NHibernate.Cache;
namespace Orchard.Azure.Services.Caching.Database {
public class AzureCacheClient : ICache {
public AzureCacheClient(string cacheHostIdentifier, string cacheName, string region) {
var dataCacheFactoryConfiguration = new DataCacheFactoryConfiguration() {
AutoDiscoverProperty = new DataCacheAutoDiscoverProperty(true, cacheHostIdentifier),
MaxConnectionsToServer = 32,
UseLegacyProtocol = false
};
var dataCacheFactory = new DataCacheFactory(dataCacheFactoryConfiguration);
_logger = LoggerProvider.LoggerFor(typeof(AzureCacheClient));
_region = region ?? _defaultRegion;
// Azure Cache supports only alphanumeric strings for regions and
// Orchard can get a lot more creative than that. Remove all non
// alphanumering characters from the region, and append the hash code
// of the original string to mitigate the risk of two distinct original
// region strings yielding the same transformed region string.
_regionAlphaNumeric = new String(Array.FindAll(_region.ToCharArray(), c => Char.IsLetterOrDigit(c))) + _region.GetHashCode().ToString();
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Creating instance with CacheName='{0}' and Region='{1}' (original Region='{2}').", cacheName, _regionAlphaNumeric, _region);
if (!String.IsNullOrEmpty(cacheName))
_cache = dataCacheFactory.GetCache(cacheName);
else
_cache = dataCacheFactory.GetDefaultCache();
_cache.CreateRegion(_regionAlphaNumeric);
_lockHandleDictionary = new ConcurrentDictionary<object, DataCacheLockHandle>();
_lockTimeout = TimeSpan.FromSeconds(30);
}
private const string _defaultRegion = "NHibernate";
private readonly IInternalLogger _logger;
private readonly string _region;
private readonly string _regionAlphaNumeric;
private readonly DataCache _cache;
private readonly ConcurrentDictionary<object, DataCacheLockHandle> _lockHandleDictionary;
private readonly TimeSpan _lockTimeout;
#region ICache Members
public object Get(object key) {
if (key == null)
throw new ArgumentNullException("key", "The parameter 'key' must not be null.");
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Get() invoked with key='{0}' in region '{1}'.", key, _regionAlphaNumeric);
return _cache.Get(key.ToString(), _regionAlphaNumeric);
}
public void Put(object key, object value) {
if (key == null)
throw new ArgumentNullException("key", "The parameter 'key' must not be null.");
if (value == null)
throw new ArgumentNullException("value", "The parameter 'value' must not be null.");
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Put() invoked with key='{0}' and value='{1}' in region '{2}'.", key, value, _regionAlphaNumeric);
_cache.Put(key.ToString(), value, _regionAlphaNumeric);
}
public void Remove(object key) {
if (key == null)
throw new ArgumentNullException("key", "The parameter 'key' must not be null.");
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Remove() invoked with key='{0}' in region '{1}'.", key, _regionAlphaNumeric);
_cache.Remove(key.ToString(), _regionAlphaNumeric);
}
public void Clear() {
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Clear() invoked in region '{0}'.", _regionAlphaNumeric);
_cache.ClearRegion(_regionAlphaNumeric);
}
public void Destroy() {
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Destroy() invoked in region '{0}'.", _regionAlphaNumeric);
Clear();
}
// The NHibernate locking mechanism and the Azure Cache pessimistic concurrency
// model are not a perfect fit. For example, Azure Cache has atomic "get-and-lock"
// and "put-and-unlock" semantics but there is no corresponding atomic operations
// defined on the ICache interface of NHibernate. Also, Azure Cache does not
// strictly enforce the pessimistic concurrency model - clients are responsible
// for following the locking protocol and therefore the implementation assumes that
// NHibernate will always call ICache.Lock() before calling ICache.Put() for data
// with concurrency management requirements. The implementations of ICache.Lock()
// and ICache.Unlock() below are therefore not as elegant as they would otherwise
// be (if not downright hackish).
// TODO: Try to understand how it's used, and make locking more robust.
public void Lock(object key) {
if (key == null)
throw new ArgumentNullException("key", "The parameter 'key' must not be null.");
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Lock() invoked with key='{0}' in region '{1}'.", key, _regionAlphaNumeric);
try {
DataCacheLockHandle lockHandle = null;
_cache.GetAndLock(key.ToString(), _lockTimeout, out lockHandle, _regionAlphaNumeric);
_lockHandleDictionary.TryAdd(key, lockHandle);
}
catch (Exception ex) {
if (_logger.IsErrorEnabled)
_logger.Error("Exception thrown while trying to lock object in cache.", ex);
throw;
}
}
// TODO: Try to understand how it's used, and make locking more robust.
public void Unlock(object key) {
if (key == null)
throw new ArgumentNullException("key", "The parameter 'key' must not be null.");
if (_logger.IsDebugEnabled)
_logger.DebugFormat("Unlock() invoked with key='{0}' in region '{1}'.", key, _regionAlphaNumeric);
try {
DataCacheLockHandle lockHandle = null;
if (_lockHandleDictionary.TryRemove(key, out lockHandle))
_cache.Unlock(key.ToString(), lockHandle, _regionAlphaNumeric);
}
catch (Exception ex) {
if (_logger.IsErrorEnabled)
_logger.Error("Exception thrown while trying to unlock object in cache.", ex);
throw;
}
}
// TODO: Try to understand what this is for and how it's used.
public long NextTimestamp() {
if (_logger.IsDebugEnabled)
_logger.DebugFormat("NextTimestamp() invoked in region '{0}'.", _regionAlphaNumeric);
return Timestamper.Next();
}
// TODO: Try to understand what this is for and how it's used.
public int Timeout {
get {
return Timestamper.OneMs * (int)_lockTimeout.TotalMilliseconds;
}
}
public string RegionName {
get {
// Return original region here (which may be non-alphanumeric) so NHibernate
// will recognize it as the same region supplied to the constructor.
return _region;
}
}
#endregion
}
}

View File

@@ -0,0 +1,36 @@
using NHibernate.Cfg.Loquacious;
using Orchard;
using Orchard.Data;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
using Orchard.Logging;
namespace Orchard.Azure.Services.Caching.Database {
[OrchardFeature("Orchard.Azure.DatabaseCache")]
[OrchardSuppressDependency("Orchard.Data.DefaultDatabaseCacheConfiguration")]
public class AzureCacheConfiguration : Component, IDatabaseCacheConfiguration {
public static string CacheHostIdentifier;
public static string CacheName;
public AzureCacheConfiguration(ShellSettings shellSettings)
: base() {
_shellSettings = shellSettings;
CacheHostIdentifier = shellSettings["Azure.DatabaseCache.HostIdentifier"];
CacheName = shellSettings["Azure.DatabaseCache.CacheName"];
}
private readonly ShellSettings _shellSettings;
public void Configure(ICacheConfigurationProperties cache) {
cache.Provider<AzureCacheProvider>();
cache.UseQueryCache = true;
cache.RegionsPrefix = _shellSettings.Name;
//cache.RegionsPrefix = "Orchard";
Logger.Information("Configured NHibernate cache provider '{0}' with regions prefix '{1}'.", typeof(AzureCacheProvider).Name, _shellSettings.Name);
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using Microsoft.WindowsAzure.ServiceRuntime;
using NHibernate.Cache;
namespace Orchard.Azure.Services.Caching.Database {
public class AzureCacheProvider : ICacheProvider {
#region ICacheProvider Members
public ICache BuildCache(string regionName, IDictionary<string, string> properties) {
return new AzureCacheClient(AzureCacheConfiguration.CacheHostIdentifier, AzureCacheConfiguration.CacheName, regionName);
}
public long NextTimestamp() {
return Timestamper.Next();
}
public void Start(IDictionary<string, string> properties) {
}
public void Stop() {
}
#endregion
}
}

View File

@@ -0,0 +1,102 @@
using Microsoft.ApplicationServer.Caching;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Orchard.Azure.Services.Caching.Output {
[OrchardFeature("Orchard.Azure.OutputCache")]
[OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")]
public class AzureOutputCacheStorageProvider : Component, IOutputCacheStorageProvider {
public AzureOutputCacheStorageProvider(ShellSettings shellSettings)
: base() {
var cacheHostIdentifier = shellSettings["Azure.OutputCache.HostIdentifier"];
var cacheName = shellSettings["Azure.OutputCache.CacheName"];
var dataCacheFactoryConfiguration = new DataCacheFactoryConfiguration() {
AutoDiscoverProperty = new DataCacheAutoDiscoverProperty(true, cacheHostIdentifier),
MaxConnectionsToServer = 32,
UseLegacyProtocol = false
};
var dataCacheFactory = new DataCacheFactory(dataCacheFactoryConfiguration);
if (!String.IsNullOrEmpty(cacheName))
_cache = dataCacheFactory.GetCache(cacheName);
else
_cache = dataCacheFactory.GetDefaultCache();
_usingSharedCaching = Boolean.Parse(shellSettings["Azure.OutputCache.IsSharedCaching"]);
if (!_usingSharedCaching)
{
// If not using Windows Azure Shared Caching we can enable additional features by
// storing all cache items in a region. This enables enumerating and counting all
// items currently in the cache.
_region = shellSettings.Name;
_cache.CreateRegion(_region);
}
}
private readonly DataCache _cache;
private readonly bool _usingSharedCaching; // True if using Windows Azure Shared Caching rather than role-based caching.
private readonly string _region;
public void Set(string key, CacheItem cacheItem) {
if (_usingSharedCaching)
_cache.Put(key, cacheItem);
else
_cache.Put(key, cacheItem, _region);
}
public void Remove(string key) {
if (_usingSharedCaching)
_cache.Remove(key);
else
_cache.Remove(key, _region);
}
public void RemoveAll() {
if (_usingSharedCaching)
_cache.Clear();
else
_cache.ClearRegion(_region);
}
public CacheItem GetCacheItem(string key) {
if (_usingSharedCaching)
return _cache.Get(key) as CacheItem;
else
return _cache.Get(key, _region) as CacheItem;
}
public IEnumerable<CacheItem> GetCacheItems(int skip, int count) {
if (_usingSharedCaching)
throw new NotSupportedException("The GetCacheItems() method is not supported with Windows Azure Shared Caching.");
return _cache.GetObjectsInRegion(_region).AsParallel()
.Select(x => x.Value)
.OfType<CacheItem>()
.Skip(skip)
.Take(count)
.ToArray();
}
public int GetCacheItemsCount() {
if (_usingSharedCaching)
throw new NotSupportedException("The GetCacheItemsCount() method is not supported with Windows Azure Shared Caching.");
return _cache.GetObjectsInRegion(_region).AsParallel()
.Select(x => x.Value)
.OfType<CacheItem>()
.Count();
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Orchard.Azure.Services.FileSystems;
using Orchard.Environment.Configuration;
using Orchard.FileSystems.Media;
namespace Orchard.Azure.Services.Environment.Configuration {
public class AzureBlobShellSettingsManager : Component, IShellSettingsManager {
public const string ConnectionStringSettingName = "Orchard.StorageConnectionString";
public const string ContainerName = "sites"; // Container names must be lower cased.
public const string SettingsFilename = "Settings.txt";
public const char Separator = ':';
public const string EmptyValue = "null";
protected readonly IShellSettingsManagerEventHandler Events;
protected readonly AzureFileSystem FileSystem;
public AzureBlobShellSettingsManager(IShellSettingsManagerEventHandler events, IMimeTypeProvider mimeTypeProvider)
: this(CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting(ConnectionStringSettingName)), events, mimeTypeProvider) {
}
public AzureBlobShellSettingsManager(CloudStorageAccount storageAccount, IShellSettingsManagerEventHandler events, IMimeTypeProvider mimeTypeProvider) {
Events = events;
FileSystem = new AzureFileSystem(ContainerName, String.Empty, true, mimeTypeProvider);
}
public virtual IEnumerable<ShellSettings> LoadSettings() {
var settings = LoadSettingsInternal().ToArray();
return settings;
}
public virtual void SaveSettings(ShellSettings settings) {
var content = ShellSettingsSerializer.ComposeSettings(settings);
var filePath = FileSystem.Combine(settings.Name, SettingsFilename);
var file = FileSystem.FileExists(filePath)
? FileSystem.GetFile(filePath)
: FileSystem.CreateFile(filePath);
using (var stream = file.OpenWrite()) {
using (var writer = new StreamWriter(stream)) {
writer.Write(content);
}
}
Events.Saved(settings);
}
private IEnumerable<ShellSettings> LoadSettingsInternal() {
foreach (var folder in FileSystem.ListFolders(null)) {
foreach (var file in FileSystem.ListFiles(folder.GetPath())) {
if (!String.Equals(file.GetName(), SettingsFilename))
continue;
using (var stream = file.OpenRead()) {
using (var reader = new StreamReader(stream))
yield return ShellSettingsSerializer.ParseSettings(reader.ReadToEnd());
}
}
}
}
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.Storage;
using Orchard.Environment.Configuration;
using Orchard.FileSystems.Media;
using Orchard.Logging;
namespace Orchard.Azure.Services.Environment.Configuration {
/// <summary>
/// Provides a ShellSettingsManager implementation that reads each value first
/// from platform configuration and second from the Settings.txt file stored in
/// Azure Blob Storage (using AzureBlobShellSettingsManager).
/// </summary>
/// <remarks>
/// This provider can be activated by configuration in Orchard.Web or Orchard.Azure.Web.
///
/// For each setting {SettingName} found in the Settings.txt file, this provider also looks
/// for a corresponding platform setting Orchard.{TenantName}.{SettingName}. The
/// <see cref="Microsoft.WindowsAzure.CloudConfigurationManager"/> class is used to search
/// for platform configuration settings in Azure Cloud Service configuration settings, Azure
/// Web Sites configuration settings and finally the Web.config appSettings element.
///
/// If a settings value is found, that value takes precedence over the Settings.txt
/// file when settings are read. When settings are written, they are always only written
/// to the backing Settings.txt file since cloud configuration settings are read-only.
///
/// This can be very useful in scenarios when you deploy your site to a number of different
/// environments and want to use cloud configuration profiles to maintain separate
/// settings for each of them that are automatically used when publishing. The fallback
/// mechanism enables you to keep environment-specific read-only settings (such as
/// SqlServerConnectionString) in cloud configuration profiles while leaving the
/// remaining and writeable settings (such as State) in the Settings.txt file.
///
/// When distributing your settings across cloud configuration and Settings.txt, you
/// must ensure that any settings that Orchard considers writeable (such as State) are
/// not placed in cloud configuration.
///
/// This provider also handles role configuration change events when running in an
/// Azure Cloud Service to ensure all Orchard tenents are notified whenever a role configuration
/// settings is changed though the management portal or API.
/// </remarks>
public class AzureConfigShellSettingsManager : AzureBlobShellSettingsManager {
private const string _prefix = "Orchard";
public AzureConfigShellSettingsManager(IShellSettingsManagerEventHandler events, IMimeTypeProvider mimeTypeProvider)
: this(CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting(ConnectionStringSettingName)), events, mimeTypeProvider) {
}
public AzureConfigShellSettingsManager(CloudStorageAccount storageAccount, IShellSettingsManagerEventHandler events, IMimeTypeProvider mimeTypeProvider)
: base(storageAccount, events, mimeTypeProvider) {
if (RoleEnvironment.IsAvailable) {
RoleEnvironment.Changing += RoleEnvironment_Changing;
RoleEnvironment.Changed += RoleEnvironment_Changed;
}
}
public override IEnumerable<ShellSettings> LoadSettings() {
var settingsList = base.LoadSettings();
foreach (var settings in settingsList) {
foreach (var key in settings.Keys) {
var cloudConfigurationKey = String.Format("{0}.{1}.{2}", _prefix, settings.Name, key);
var cloudConfigurationValue = ParseValue(CloudConfigurationManager.GetSetting(cloudConfigurationKey));
if (cloudConfigurationValue != null)
settings[key] = cloudConfigurationValue;
}
}
return settingsList;
}
void RoleEnvironment_Changing(object sender, RoleEnvironmentChangingEventArgs e) {
// Indicate to the fabric controller that we can handle any changes.
e.Cancel = false;
}
private void RoleEnvironment_Changed(object sender, RoleEnvironmentChangedEventArgs e) {
Logger.Debug("Handling RoleEnvironmentChanged event.");
var configurationChangeQuery =
from c in e.Changes
where c is RoleEnvironmentConfigurationSettingChange
select c;
// If there's a configuration settings change, inform all Orchard tenants.
if (configurationChangeQuery.Any()) {
Logger.Information("Role configuration settings changed; refreshing Orchard shell settings.");
var settingsList = LoadSettings();
foreach (var settings in settingsList)
Events.Saved(settings);
}
}
private string ParseValue(string value) {
if (value == EmptyValue || String.IsNullOrWhiteSpace(value))
return null;
return value;
}
}
}

View File

@@ -0,0 +1,397 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Orchard.FileSystems.Media;
namespace Orchard.Azure.Services.FileSystems {
public class AzureFileSystem {
public const string FolderEntry = "$$$ORCHARD$$$.$$$";
private readonly bool _isPrivate;
private readonly IMimeTypeProvider _mimeTypeProvider;
protected string _root;
protected string _absoluteRoot;
private CloudStorageAccount _storageAccount;
private CloudBlobClient _blobClient;
private CloudBlobContainer _container;
public string ContainerName {
get;
protected set;
}
public CloudBlobClient BlobClient {
get {
EnsureInitialized();
return _blobClient;
}
}
public CloudBlobContainer Container {
get {
EnsureInitialized();
return _container;
}
}
public AzureFileSystem(string containerName, string root, bool isPrivate, IMimeTypeProvider mimeTypeProvider) {
_isPrivate = isPrivate;
_mimeTypeProvider = mimeTypeProvider;
ContainerName = containerName;
_root = String.IsNullOrEmpty(root) ? "" : root + "/";
}
private void EnsureInitialized() {
if (_storageAccount != null) {
return;
}
_storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("Orchard.StorageConnectionString"));
_absoluteRoot = Combine(Combine(_storageAccount.BlobEndpoint.AbsoluteUri, ContainerName), _root);
_blobClient = _storageAccount.CreateCloudBlobClient();
// Get and create the container if it does not exist
// The container is named with DNS naming restrictions (i.e. all lower case)
_container = _blobClient.GetContainerReference(ContainerName);
_container.CreateIfNotExists();
_container.SetPermissions(_isPrivate
? new BlobContainerPermissions {
PublicAccess = BlobContainerPublicAccessType.Off
}
: new BlobContainerPermissions {
PublicAccess = BlobContainerPublicAccessType.Container
});
}
private static string ConvertToRelativeUriPath(string path) {
var newPath = path.Replace(@"\", "/");
if (newPath.StartsWith("/") || newPath.StartsWith("http://") || newPath.StartsWith("https://"))
throw new ArgumentException("Path must be relative");
return newPath;
}
public string Combine(string path1, string path2) {
if (path1 == null) {
throw new ArgumentNullException("path1");
}
if (path2 == null) {
throw new ArgumentNullException("path2");
}
if (String.IsNullOrEmpty(path2)) {
return path1;
}
if (String.IsNullOrEmpty(path1)) {
return path2;
}
if (path2.StartsWith("http://") || path2.StartsWith("https://")) {
return path2;
}
var ch = path1[path1.Length - 1];
if (ch != '/') {
return (path1.TrimEnd('/') + '/' + path2.TrimStart('/'));
}
return (path1 + path2);
}
public IStorageFile GetFile(string path) {
path = ConvertToRelativeUriPath(path);
Container.EnsureBlobExists(String.Concat(_root, path));
return new AzureBlobFileStorage(Container.GetBlockBlobReference(String.Concat(_root, path)), _absoluteRoot);
}
public bool FileExists(string path) {
return Container.BlobExists(String.Concat(_root, path));
}
public bool FolderExists(string path) {
return Container.DirectoryExists(String.Concat(_root, path));
}
public IEnumerable<IStorageFile> ListFiles(string path) {
path = path ?? String.Empty;
path = ConvertToRelativeUriPath(path);
string prefix = Combine(Combine(Container.Name, _root), path);
if (!prefix.EndsWith("/"))
prefix += "/";
return BlobClient.ListBlobs(prefix, true)
.OfType<CloudBlockBlob>()
.Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry))
.Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot))
.ToArray();
}
public IEnumerable<IStorageFolder> ListFolders(string path) {
path = path ?? String.Empty;
path = ConvertToRelativeUriPath(path);
// return root folders
if (String.Concat(_root, path) == String.Empty) {
return Container.ListBlobs()
.OfType<CloudBlobDirectory>()
.Select<CloudBlobDirectory, IStorageFolder>(d => new AzureBlobFolderStorage(d, _absoluteRoot))
.ToList();
}
if (!Container.DirectoryExists(String.Concat(_root, path))) {
try {
CreateFolder(path);
}
catch (Exception ex) {
throw new ArgumentException(string.Format("The folder could not be created at path: {0}. {1}",
path, ex));
}
}
return Container.GetDirectoryReference(String.Concat(_root, path))
.ListBlobs()
.OfType<CloudBlobDirectory>()
.Select<CloudBlobDirectory, IStorageFolder>(d => new AzureBlobFolderStorage(d, _absoluteRoot))
.ToList();
}
public bool TryCreateFolder(string path) {
try {
if (!Container.DirectoryExists(String.Concat(_root, path))) {
CreateFolder(path);
return true;
}
// return false to be consistent with FileSystemProvider's implementation
return false;
}
catch {
return false;
}
}
public void CreateFolder(string path) {
path = ConvertToRelativeUriPath(path);
Container.EnsureDirectoryDoesNotExist(String.Concat(_root, path));
// Creating a virtually hidden file to make the directory an existing concept
CreateFile(Combine(path, FolderEntry));
int lastIndex;
while ((lastIndex = path.LastIndexOf('/')) > 0) {
path = path.Substring(0, lastIndex);
if (!Container.DirectoryExists(String.Concat(_root, path))) {
CreateFile(Combine(path, FolderEntry));
}
}
}
public void DeleteFolder(string path) {
path = ConvertToRelativeUriPath(path);
Container.EnsureDirectoryExists(String.Concat(_root, path));
foreach (var blob in Container.GetDirectoryReference(String.Concat(_root, path)).ListBlobs()) {
if (blob is CloudBlockBlob)
((CloudBlockBlob)blob).Delete();
if (blob is CloudBlobDirectory)
DeleteFolder(blob.Uri.ToString().Substring(Container.Uri.ToString().Length + 1 + _root.Length));
}
}
public void RenameFolder(string path, string newPath) {
path = ConvertToRelativeUriPath(path);
newPath = ConvertToRelativeUriPath(newPath);
if (!path.EndsWith("/"))
path += "/";
if (!newPath.EndsWith("/"))
newPath += "/";
foreach (var blob in Container.GetDirectoryReference(_root + path).ListBlobs()) {
if (blob is CloudBlockBlob) {
string filename = Path.GetFileName(blob.Uri.ToString());
string source = String.Concat(path, filename);
string destination = String.Concat(newPath, filename);
RenameFile(source, destination);
}
if (blob is CloudBlobDirectory) {
string foldername = blob.Uri.Segments.Last();
string source = String.Concat(path, foldername);
string destination = String.Concat(newPath, foldername);
RenameFolder(source, destination);
}
}
}
public void DeleteFile(string path) {
path = ConvertToRelativeUriPath(path);
Container.EnsureBlobExists(Combine(_root, path));
var blob = Container.GetBlockBlobReference(Combine(_root, path));
blob.DeleteIfExists();
}
public void RenameFile(string path, string newPath) {
path = ConvertToRelativeUriPath(path);
newPath = ConvertToRelativeUriPath(newPath);
Container.EnsureBlobExists(String.Concat(_root, path));
Container.EnsureBlobDoesNotExist(String.Concat(_root, newPath));
var blob = Container.GetBlockBlobReference(String.Concat(_root, path));
var newBlob = Container.GetBlockBlobReference(String.Concat(_root, newPath));
newBlob.StartCopyFromBlob(blob);
}
public IStorageFile CreateFile(string path) {
path = ConvertToRelativeUriPath(path);
if (Container.BlobExists(String.Concat(_root, path))) {
throw new ArgumentException("File " + path + " already exists");
}
// create all folder entries in the hierarchy
int lastIndex;
var localPath = path;
while ((lastIndex = localPath.LastIndexOf('/')) > 0) {
localPath = localPath.Substring(0, lastIndex);
var folder = Container.GetBlockBlobReference(String.Concat(_root, Combine(localPath, FolderEntry)));
folder.OpenWrite().Dispose();
}
var blob = Container.GetBlockBlobReference(String.Concat(_root, path));
var contentType = _mimeTypeProvider.GetMimeType(path);
if (!String.IsNullOrWhiteSpace(contentType)) {
blob.Properties.ContentType = contentType;
}
blob.UploadFromStream(new MemoryStream(new byte[0]));
return new AzureBlobFileStorage(blob, _absoluteRoot);
}
public string GetPublicUrl(string path) {
path = ConvertToRelativeUriPath(path);
Container.EnsureBlobExists(String.Concat(_root, path));
return Container.GetBlockBlobReference(String.Concat(_root, path)).Uri.ToString();
}
private class AzureBlobFileStorage : IStorageFile {
private CloudBlockBlob _blob;
private readonly string _rootPath;
public AzureBlobFileStorage(CloudBlockBlob blob, string rootPath) {
_blob = blob;
_rootPath = rootPath;
}
public string GetPath() {
return _blob.Uri.ToString().Substring(_rootPath.Length).Trim('/');
}
public string GetName() {
return Path.GetFileName(GetPath());
}
public long GetSize() {
return _blob.Properties.Length;
}
public DateTime GetLastUpdated() {
return _blob.Properties.LastModified.GetValueOrDefault().DateTime;
}
public string GetFileType() {
return Path.GetExtension(GetPath());
}
public Stream OpenRead() {
return _blob.OpenRead();
}
public Stream OpenWrite() {
return _blob.OpenWrite();
}
public Stream CreateFile() {
// as opposed to the File System implementation, if nothing is done on the stream
// the file will be emptied, because Azure doesn't implement FileMode.Truncate
_blob.DeleteIfExists();
_blob = _blob.Container.GetBlockBlobReference(_blob.Uri.ToString());
_blob.UploadFromStream(new MemoryStream(new byte[0]));
return OpenWrite();
}
}
private class AzureBlobFolderStorage : IStorageFolder {
private readonly CloudBlobDirectory _blob;
private readonly string _rootPath;
public AzureBlobFolderStorage(CloudBlobDirectory blob, string rootPath) {
_blob = blob;
_rootPath = rootPath;
}
public string GetName() {
var path = GetPath();
return path.Substring(path.LastIndexOf('/') + 1);
}
public string GetPath() {
return _blob.Uri.ToString().Substring(_rootPath.Length).Trim('/');
}
public long GetSize() {
return GetDirectorySize(_blob);
}
public DateTime GetLastUpdated() {
return DateTime.MinValue;
}
public IStorageFolder GetParent() {
if (_blob.Parent != null) {
return new AzureBlobFolderStorage(_blob.Parent, _rootPath);
}
throw new ArgumentException("Directory " + _blob.Uri + " does not have a parent directory");
}
private static long GetDirectorySize(CloudBlobDirectory directoryBlob) {
long size = 0;
foreach (var blobItem in directoryBlob.ListBlobs()) {
if (blobItem is CloudBlockBlob)
size += ((CloudBlockBlob)blobItem).Properties.Length;
if (blobItem is CloudBlobDirectory)
size += GetDirectorySize((CloudBlobDirectory)blobItem);
}
return size;
}
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Linq;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
namespace Orchard.Azure.Services.FileSystems {
public static class CloudBlobContainerExtensions {
public static bool BlobExists(this CloudBlobContainer container, string path) {
if (String.IsNullOrEmpty(path) || path.Trim() == String.Empty)
throw new ArgumentException("Path can't be empty");
try {
var blob = container.GetBlockBlobReference(path);
blob.FetchAttributes();
return true;
}
catch (StorageException) {
return false;
}
}
public static void EnsureBlobExists(this CloudBlobContainer container, string path) {
if (!BlobExists(container, path)) {
throw new ArgumentException("File " + path + " does not exist");
}
}
public static void EnsureBlobDoesNotExist(this CloudBlobContainer container, string path) {
if (BlobExists(container, path)) {
throw new ArgumentException("File " + path + " already exists");
}
}
public static bool DirectoryExists(this CloudBlobContainer container, string path) {
if (String.IsNullOrEmpty(path) || path.Trim() == String.Empty)
throw new ArgumentException("Path can't be empty");
return container.GetDirectoryReference(path).ListBlobs().Any();
}
public static void EnsureDirectoryExists(this CloudBlobContainer container, string path) {
if (!DirectoryExists(container, path)) {
throw new ArgumentException("Directory " + path + " does not exist");
}
}
public static void EnsureDirectoryDoesNotExist(this CloudBlobContainer container, string path) {
if (DirectoryExists(container, path)) {
throw new ArgumentException("Directory " + path + " already exists");
}
}
}
}

View File

@@ -0,0 +1,58 @@
using System.IO;
using Orchard.Environment.Configuration;
using Orchard.FileSystems.Media;
using Orchard.Environment.Extensions;
namespace Orchard.Azure.Services.FileSystems.Media {
[OrchardFeature("Orchard.Azure.MediaStorage")]
[OrchardSuppressDependency("Orchard.FileSystems.Media.FileSystemStorageProvider")]
public class AzureBlobStorageProvider : AzureFileSystem, IStorageProvider {
public AzureBlobStorageProvider(ShellSettings shellSettings, IMimeTypeProvider mimeTypeProvider) : base("media", shellSettings.Name, false, mimeTypeProvider)
{ }
public bool TrySaveStream(string path, Stream inputStream) {
try {
SaveStream(path, inputStream);
}
catch {
return false;
}
return true;
}
public void SaveStream(string path, Stream inputStream) {
// Create the file. The CreateFile() method will map the still relative path.
var file = CreateFile(path);
using (var outputStream = file.OpenWrite()) {
var buffer = new byte[8192];
while (true) {
var length = inputStream.Read(buffer, 0, buffer.Length);
if (length <= 0)
break;
outputStream.Write(buffer, 0, length);
}
}
}
/// <summary>
/// Retrieves the local path for a given URL within the storage provider.
/// </summary>
/// <param name="url">The public URL of the media.</param>
/// <returns>The corresponding local path.</returns>
public string GetStoragePath(string url) {
if (url.StartsWith(_absoluteRoot)) {
return url.Substring(Combine(_absoluteRoot, "/").Length);
}
return null;
}
public string GetRelativePath(string path) {
return GetPublicUrl(path);
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using Microsoft.WindowsAzure.Diagnostics;
using log4net.Appender;
using log4net.Core;
namespace Orchard.Azure.Services.Logging {
/// <summary>
/// Provides a Log4net appender implementation that sends log messages to Windows Azure Diagnostics.
/// </summary>
public class AzureDiagnosticsAppender : AppenderSkeleton {
private const string _wadStorageAccountConnectionStringSettingName = "Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString";
public AzureDiagnosticsAppender() {
var defaultDiagnostics = DiagnosticMonitor.GetDefaultInitialConfiguration();
var period = TimeSpan.FromMinutes(1d);
defaultDiagnostics.Directories.ScheduledTransferPeriod = period;
defaultDiagnostics.Logs.ScheduledTransferPeriod = period;
defaultDiagnostics.WindowsEventLog.ScheduledTransferPeriod = period;
DiagnosticMonitor.Start(_wadStorageAccountConnectionStringSettingName, defaultDiagnostics);
}
protected override void Append(LoggingEvent loggingEvent) {
var formattedMessage = RenderLoggingEvent(loggingEvent);
System.Diagnostics.Trace.WriteLine(formattedMessage);
}
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<remove name="host" />
<remove name="pages" />
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.WebPages" />
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
<add namespace="Orchard.Mvc.Html"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<system.web>
<compilation targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</assemblies>
</compilation>
</system.web>
</configuration>