Skip to content

Commit

Permalink
#13 Fixed more bugs with GetOrAdd method and auto-save.
Browse files Browse the repository at this point in the history
Seems to works reliably in web and app, and will add missing configurations automatically.
  • Loading branch information
thargy authored and billings7 committed Oct 4, 2016
1 parent 851e850 commit 391a293
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 63 deletions.
114 changes: 62 additions & 52 deletions Utilities/Configuration/ConfigurationSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Web;
using System.Web.Configuration;
using System.Xml;
using System.Xml.Linq;
Expand All @@ -52,11 +51,6 @@ namespace WebApplications.Utilities.Configuration
public abstract partial class ConfigurationSection<T> : ConfigurationSection, IInternalConfigurationSection
where T : ConfigurationSection<T>, IConfigurationElement, new()
{
/// <summary>
/// The time in milliseconds that events are buffered for.
/// </summary>
public const int EventBufferMs = 250;

#region Delegates
/// <summary>
/// Handles changes in configuration.
Expand Down Expand Up @@ -88,21 +82,13 @@ public delegate void ConfigurationChangedEventHandler(
/// </summary>
private BufferedAction<string> _changeAction;

/// <summary>
/// <see langword="true"/> is library is running in ASP.NET.
/// </summary>
public static readonly bool IsWeb;
private string _filePath;

/// <summary>
/// Initializes static members of the <see cref="ConfigurationSection{T}" /> class.
/// </summary>
static ConfigurationSection()
{
// Detect whether we are running in an ASP.NET website, note that the context may not be propogated to
// background threads so cannot be relied upon exclusively.
IsWeb = HttpContext.Current != null ||
HttpRuntime.AppDomainAppId != null;

_activeChangeAction =
new BufferedAction<string>(
// ReSharper disable EventExceptionNotDocumented, AssignNullToNotNullAttribute
Expand All @@ -120,7 +106,7 @@ static ConfigurationSection()
}
},
// ReSharper restore EventExceptionNotDocumented, AssignNullToNotNullAttribute
EventBufferMs);
ConfigurationExtensions.EventBufferMs);

// Try to find attribute
ConfigurationSectionAttribute attribute =
Expand Down Expand Up @@ -172,17 +158,16 @@ static ConfigurationSection()
/// Initializes a new instance of the <see cref="ConfigurationSection{T}" /> class.
/// </summary>
/// <exception cref="ConfigurationErrorsException">The selected value conflicts with a value that is already defined.</exception>
// ReSharper disable once NotNullMemberIsNotInitialized
protected ConfigurationSection()
{
// Set up change buffer
_changeAction =
new BufferedAction<string>(
// ReSharper disable once EventExceptionNotDocumented, AssignNullToNotNullAttribute
changes => Changed?.Invoke((T)this, new ConfigurationChangedEventArgs((T)this, changes)),
EventBufferMs);
changes => Changed?.Invoke((T)this, new ConfigurationChangedEventArgs((T)this, changes)), ConfigurationExtensions.EventBufferMs);

// This will get set during initialization
LineNumber = -1;
((IInternalConfigurationElement)this).ConfigurationElementName = $"<{SectionName}>";

// As our system supports change notification, we can default the restart on external changes to false.
Expand Down Expand Up @@ -258,9 +243,8 @@ public static T Active
if (active != null && !active.IsDisposed) return active;

// ReSharper disable ExceptionNotDocumented
return _active = GetOrAdd(
IsWeb
? WebConfigurationManager.OpenWebConfiguration(null)
return _active = GetOrAdd(ConfigurationExtensions.IsWeb
? WebConfigurationManager.OpenWebConfiguration("~/")
: ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None));
// ReSharper restore ExceptionNotDocumented
}
Expand Down Expand Up @@ -328,15 +312,15 @@ public XNamespace Xmlns
/// <exception cref="UnauthorizedAccessException">The caller does not have the required permission. </exception>
/// <exception cref="IOException">An I/O error occurs. </exception>
[NotNull]
public static T GetOrAdd([NotNull] System.Configuration.Configuration configuration)
public static T GetOrAdd([CanBeNull] System.Configuration.Configuration configuration = null)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));

// Get the section.
T section;
try
{
section = configuration.GetSection(SectionName) as T;
section = configuration != null
? configuration.GetSection(SectionName) as T
: ConfigurationManager.GetSection(SectionName) as T;
}
// ReSharper disable once CatchAllClause
catch (Exception e)
Expand All @@ -351,18 +335,45 @@ public static T GetOrAdd([NotNull] System.Configuration.Configuration configurat
throw;
}

// Return if found.
if (section != null) return section;
// If we don't have a section create one.
if (section == null)
{
// Calculate the most appropriate configuration (if we don't have one already).
if (configuration == null)
configuration = ConfigurationExtensions.IsWeb
? WebConfigurationManager.OpenWebConfiguration("~/")
: ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);

// Create new configuration
section = Create();

// Add to existing configuration
configuration.Sections.Add(SectionName, section);

// Create new configuration and set it as the active one.
section = Create();
// If the configuration doesn't yet have a file we're done.
if (!configuration.HasFile) return section;

// Add to existing configuration and save
configuration.Sections.Add(SectionName, section);
try
{
// Attempt to save configuration and reload the section.
configuration.Save();

ConfigurationManager.RefreshSection(SectionName);
section = configuration.GetSection(SectionName) as T ?? section;
}
catch (Exception e)
{
// Intercept exception to throw
ExceptionDispatchInfo edi = ExceptionDispatchInfo.Capture(e);
// ReSharper disable once AssignNullToNotNullAttribute, EventExceptionNotDocumented
ConfigurationLoadError?.Invoke(new ConfigurationLoadEventArgs(SectionName, edi));
}
}

// If the section has an associated file, save and reload.
if (section.HasFile)
section = section.Save();
// If we don't have a file path, but our configuration does, then set it.
if (!section.HasFile &&
section.CurrentConfiguration?.HasFile == true)
section.FilePath = section.CurrentConfiguration.FilePath;
return section;
}

Expand Down Expand Up @@ -512,32 +523,30 @@ partial void DoChanged(string fullPath)
public bool HasFile => !string.IsNullOrWhiteSpace(FilePath);

/// <inheritdoc />
public string FilePath { get; private set; }
public string FilePath
{
get { return _filePath; }
private set
{
if (_filePath == value) return;
if (!string.IsNullOrWhiteSpace(_filePath))
ConfigurationFileWatcher.UnWatch(this);

/// <inheritdoc />
public int LineNumber { get; private set; }
_filePath = value;
if (!string.IsNullOrWhiteSpace(_filePath))
ConfigurationFileWatcher.Watch(this);
}
}

/// <inheritdoc />
public bool IsDisposed { get; private set; }

/// <inheritdoc />
protected override void DeserializeSection(XmlReader reader)
{
// Grab file info
// Grab file path
IConfigErrorInfo errorInfo = reader as IConfigErrorInfo;
if (errorInfo != null)
{
FilePath = errorInfo.Filename;
LineNumber = errorInfo.LineNumber;
ConfigurationFileWatcher.Watch(this);
}
else
{
// Take our best guess
FilePath = CurrentConfiguration?.FilePath;
LineNumber = 0;
ConfigurationFileWatcher.Watch(this);
}
FilePath = errorInfo != null ? errorInfo.Filename : CurrentConfiguration?.FilePath;

base.DeserializeSection(reader);
}
Expand Down Expand Up @@ -640,6 +649,7 @@ public class ConfigurationLoadEventArgs : EventArgs
/// The section name.
/// </summary>
[NotNull]
// ReSharper disable once MemberHidesStaticFromOuterClass
public readonly string SectionName;

/// <summary>
Expand Down
5 changes: 0 additions & 5 deletions Utilities/Configuration/IConfigurationSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,5 @@ public interface IConfigurationSection : IConfigurationElement, IDisposable
/// <value>The file path.</value>
[CanBeNull]
string FilePath { get; }
/// <summary>
/// Gets the line number of the configuration file path the section was loaded from (if any); otherwise -1.
/// </summary>
/// <value>The line number.</value>
int LineNumber { get; }
}
}
23 changes: 19 additions & 4 deletions Utilities/ConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
using System.Collections.Specialized;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using WebApplications.Utilities.Annotations;
using WebApplications.Utilities.Configuration;
using WebApplications.Utilities.Reflect;
Expand Down Expand Up @@ -92,8 +93,7 @@ public static class ConfigurationExtensions
/// Gets the associated configuration file paths.
/// </summary>
[NotNull]
private static readonly Func<System.Configuration.Configuration, IReadOnlyCollection<string>>
_getConfigFilePaths;
private static readonly Func<System.Configuration.Configuration, IReadOnlyCollection<string>> _getConfigFilePaths;

/// <summary>
/// Gets the configuration file paths for a given <see cref="System.Configuration.Configuration"/>.
Expand All @@ -111,6 +111,11 @@ public static IReadOnlyCollection<string> GetConfigFilePaths(
/// </summary>
static ConfigurationExtensions()
{
// Detect whether we are running in an ASP.NET website, note that the context may not be propogated to
// background threads so cannot be relied upon exclusively.
IsWeb = HttpContext.Current != null ||
HttpRuntime.AppDomainAppId != null;

ParameterExpression configuration = Expression.Parameter(typeof(System.Configuration.Configuration), "configuration");
ParameterExpression streamInfos = Expression.Parameter(typeof(ICollection), "streamInfos");
ParameterExpression index = Expression.Parameter(typeof(int), "index");
Expand Down Expand Up @@ -173,16 +178,26 @@ static ConfigurationExtensions()
[NotNull]
public static string GetFullPath([CanBeNull] this IConfigurationElement parent, [CanBeNull] string configurationElementName)
{
if (string.IsNullOrEmpty(configurationElementName))
if (String.IsNullOrEmpty(configurationElementName))
configurationElementName = "?";

// Short cut
if (parent == null) return configurationElementName;

if (char.IsLetterOrDigit(configurationElementName[0]))
if (Char.IsLetterOrDigit(configurationElementName[0]))
configurationElementName = "." + configurationElementName;
return parent.FullPath + configurationElementName;
}

/// <summary>
/// The time in milliseconds that events are buffered for.
/// </summary>
public const int EventBufferMs = 250;

/// <summary>
/// <see langword="true"/> if the library is running in ASP.NET.
/// </summary>
[PublicAPI]
public static readonly bool IsWeb;
}
}
108 changes: 108 additions & 0 deletions Utilities/Converters/XNamespaceConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#region © Copyright Web Applications (UK) Ltd, 2015. All rights reserved.
// Copyright (c) 2015, Web Applications UK Ltd
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of Web Applications UK Ltd nor the
// names of its contributors may be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL WEB APPLICATIONS UK LTD BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion

using NodaTime;
using System;
using System.ComponentModel;
using System.Globalization;
using System.Xml.Linq;
using WebApplications.Utilities.Annotations;

namespace WebApplications.Utilities.Converters
{
/// <summary>
/// Providers methods for converting types to/from the <see cref="Duration"/> type.
/// </summary>
public class XNamespaceConverter : TypeConverter
{
/// <summary>
/// Returns whether this converter can convert an object of the given type to the type of this converter, using the specified context.
/// </summary>
/// <returns>
/// true if this converter can perform the conversion; otherwise, false.
/// </returns>
/// <param name="context">An <see cref="T:System.ComponentModel.ITypeDescriptorContext"/> that provides a format context. </param>
/// <param name="sourceType">A <see cref="T:System.Type"/> that represents the type you want to convert from. </param>
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);

/// <summary>
/// Converts the given object to the type of this converter, using the specified context and culture information.
/// </summary>
/// <returns>
/// An <see cref="T:System.Object"/> that represents the converted value.
/// </returns>
/// <param name="context">An <see cref="T:System.ComponentModel.ITypeDescriptorContext"/> that provides a format context. </param>
/// <param name="culture">The <see cref="T:System.Globalization.CultureInfo"/> to use as the current culture. </param>
/// <param name="value">The <see cref="T:System.Object"/> to convert. </param>
/// <exception cref="T:System.NotSupportedException">The conversion cannot be performed. </exception>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string @string = value as string;
return @string != null ? (XNamespace)@string : base.ConvertFrom(context, culture, value);
}

/// <summary>
/// Returns whether this converter can convert the object to the specified type, using the specified context.
/// </summary>
/// <returns>
/// true if this converter can perform the conversion; otherwise, false.
/// </returns>
/// <param name="context">An <see cref="T:System.ComponentModel.ITypeDescriptorContext"/> that provides a format context. </param>
/// <param name="destinationType">A <see cref="T:System.Type"/> that represents the type you want to convert to. </param>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
=> destinationType == typeof(string) || base.CanConvertTo(context, destinationType);

/// <summary>
/// Converts the given value object to the specified type, using the specified context and culture information.
/// </summary>
/// <returns>
/// An <see cref="T:System.Object"/> that represents the converted value.
/// </returns>
/// <param name="context">An <see cref="T:System.ComponentModel.ITypeDescriptorContext"/> that provides a format context. </param>
/// <param name="culture">A <see cref="T:System.Globalization.CultureInfo"/>. If null is passed, the current culture is assumed. </param>
/// <param name="value">The <see cref="T:System.Object"/> to convert. </param>
/// <param name="destinationType">The <see cref="T:System.Type"/> to convert the <paramref name="value"/> parameter to. </param>
/// <exception cref="T:System.ArgumentNullException">The <paramref name="destinationType"/> parameter is null. </exception>
/// <exception cref="T:System.NotSupportedException">The conversion cannot be performed. </exception>
public override object ConvertTo(
ITypeDescriptorContext context,
CultureInfo culture,
[NotNull] object value,
Type destinationType)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (destinationType == null) throw new ArgumentNullException(nameof(destinationType));

XNamespace ns = value as XNamespace;

return destinationType == typeof(string)
? ns?.ToString()
: base.ConvertTo(context, culture, value, destinationType);
}
}
}
Loading

0 comments on commit 391a293

Please sign in to comment.