/// <summary> /// Creates the type of the configuration object. /// </summary> /// <param name="settings"> /// The settings used to control how configuration objects are created and the features they support. /// </param> /// <typeparam name="TConfigurationObject"> /// The type of the interface to be implemented by the configuration object to create. /// </typeparam> /// <returns> /// An implementation of the interface specified that can be used to interact with the configuration. /// </returns> /// <exception cref="InvalidOperationException"> /// Type specifies more than one 'Path' attribute and so cannot be processed. - or - Property specifies more /// than one 'Path' attribute and so cannot be processed. /// </exception> internal static Type GenerateConfigurationObjectType<TConfigurationObject>(ConfigurationObjectSettings settings) where TConfigurationObject : IConfigurationObject { var type = typeof(TConfigurationObject); return GenerateConfigurationObjectType(type, settings); }
/// <summary> /// Initializes a new instance of the <see cref="ConfigurationObjectTypeBuilder" /> class. /// </summary> /// <param name="interfaceType"> /// The type of the interface. /// </param> /// <param name="propertyDefs"> /// The definitions of the interface's properties. /// </param> /// <param name="settings"> /// Optional settings used to control how configuration objects are created and the features they support. /// </param> public ConfigurationObjectTypeBuilder(Type interfaceType, IEnumerable <PropertyDef> propertyDefs, ConfigurationObjectSettings settings) { // TODO: Make type builders cacheable. _interfaceType = interfaceType; _propertyDefs = propertyDefs.ToArray(); _baseClassType = typeof(ConfigurationObjectBase <>).MakeGenericType(_interfaceType); _settings = settings; settings.ConfigureEnvironment(); }
/// <summary> /// Creates the type of the configuration object. /// </summary> /// <param name="type"> /// The type of the interface to be implemented by the configuration object to create. /// </param> /// <param name="settings"> /// The settings used to control how configuration objects are created and the features they support. /// </param> /// <returns> /// An implementation of the interface specified that can be used to interact with the configuration. /// </returns> /// <exception cref="InvalidOperationException"> /// Type specifies more than one 'Path' attribute and so cannot be processed. - or - Property specifies more /// than one 'Path' attribute and so cannot be processed. /// </exception> internal static Type GenerateConfigurationObjectType(Type type, ConfigurationObjectSettings settings) { var propertyDefs = GetConfigurationObjectDefinition(type, settings); var builder = new ConfigurationObjectTypeBuilder(type, propertyDefs, settings); var generatedType = builder.Generate(); return generatedType; }
/// <summary> /// Add a new kind of configuration reader that represents values taken directly from the /// <see cref="IConfigurationRoot" /> object in the service collection. /// </summary> /// <typeparam name="TConfigurationObject"> /// The interface through which consumers will access the configuration. This must be derived from the /// <see cref="IConfigurationObject" /> interface. /// </typeparam> /// <param name="serviceCollection"> /// The service collection to which to add the configuration reader. This must not be <see langword="null" />. /// </param> /// <param name="configureOptions"> /// A method or lambda that will configure the settings used to control how configuration objects are /// created and the features they support. /// </param> /// <exception cref="ArgumentNullException"> /// <paramref name="configureOptions" /> is <see langword="null" />. /// </exception> public static IServiceCollection AddConfigurationReader<TConfigurationObject>(this IServiceCollection serviceCollection, Action<ConfigurationObjectSettings> configureOptions) where TConfigurationObject : IConfigurationObject { configureOptions.Validate(nameof(configureOptions), ObjectIs.NotNull); var settings = new ConfigurationObjectSettings(); configureOptions(settings); return AddConfigurationReader<TConfigurationObject>(serviceCollection, settings); }
/// <summary> /// Add a new kind of configuration reader that represents values taken directly from the /// <see cref="IConfigurationRoot" /> object in the service collection. /// </summary> /// <typeparam name="TConfigurationObject"> /// The interface through which consumers will access the configuration. This must be derived from the /// <see cref="IConfigurationObject" /> interface. /// </typeparam> /// <param name="serviceCollection"> /// The service collection to which to add the configuration reader. This must not be <see langword="null" />. /// </param> /// <param name="settings"> /// Optional settings used to control how configuration objects are created and the features they support. /// </param> public static IServiceCollection AddConfigurationReader<TConfigurationObject>(this IServiceCollection serviceCollection, ConfigurationObjectSettings? settings) where TConfigurationObject : IConfigurationObject { serviceCollection.Validate(nameof(serviceCollection), ObjectIs.NotNull); // Check to see if the collection has the relevant configuration reader registered, and if not, create and // add a new instance. if(ReferenceEquals(settings, null)) { // Use default values. settings = new ConfigurationObjectSettings(); } var serviceType = typeof(TConfigurationObject); if(serviceCollection.Any(d => d.ServiceType == serviceType)) { return serviceCollection; } var implementationType = GenerateConfigurationObjectType<TConfigurationObject>(settings); var descriptor = new ServiceDescriptor(serviceType, (provider) => { var configurationRoot = provider.GetService<IConfigurationRoot>(); if(_validators.Count <= 0) { foreach(var validator in provider.GetServices<IConfigurationObjectValidator>()) { _validators.Add(validator); } } var validators = GetValidatorsEx<TConfigurationObject>(); var configurationObject = (TConfigurationObject)Activator.CreateInstance(implementationType, null, configurationRoot, null, settings, validators); configurationObject.Load(); return configurationObject; }, ServiceLifetime.Singleton); serviceCollection.TryAdd(descriptor); return serviceCollection; }
/// <summary> /// Initializes a new instance of the <see cref="Implementation" /> class. /// </summary> /// <param name="underlyingType"> /// The underlying type of the property or collection represented. /// </param> /// <param name="settings"> /// The settings used to control how configuration objects are created and the features they support. /// </param> internal Implementation(Type underlyingType, ConfigurationObjectSettings settings) { ImplementationKind = GetImplementationKind(underlyingType, out var isReadOnlyColectionType); _settings = settings; Type elementType; switch (ImplementationKind) { case ImplementationKind.ConfigurationObject: ImplementationType = ServiceCollectionExtensions.GenerateConfigurationObjectType(underlyingType, settings); Type = underlyingType; break; case ImplementationKind.ConfigurationCollection: elementType = underlyingType.GenericTypeArguments.First(); if (isReadOnlyColectionType) { ImplementationType = typeof(ReadOnlyConfigurationCollection <>).MakeGenericType(elementType); } else { ImplementationType = typeof(ConfigurationCollection <>).MakeGenericType(elementType); } Type = elementType; break; case ImplementationKind.ConfigurationDictionary: elementType = underlyingType.GenericTypeArguments.First(); if (isReadOnlyColectionType) { ImplementationType = typeof(ReadOnlyConfigurationDictionary <>).MakeGenericType(elementType); } else { ImplementationType = typeof(ConfigurationDictionary <>).MakeGenericType(elementType); } Type = elementType; break; default: ImplementationType = null; Type = underlyingType; break; } }
/// <summary> /// Gets the configuration object definition. /// </summary> /// <param name="type"> /// The type of the object to define. /// </param> /// <param name="settings"> /// The settings used to control how configuration objects are created and the features they support. /// </param> /// <returns> /// A list of the property definitions for the type specified. /// </returns> /// <exception cref="InvalidOperationException"> /// Type '{type.Namespace}.{type.Name}' specifies more than one 'Path' attribute and so cannot be processed. /// </exception> internal static List<PropertyDef> GetConfigurationObjectDefinition(Type type, ConfigurationObjectSettings settings) { // TODO: Circular reference detection - for this version. var propertyDefs = new List<PropertyDef>(); foreach(var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if(!property.CanRead) { continue; } var propertyDef = new PropertyDef(property, settings); propertyDefs.Add(propertyDef); } return propertyDefs; }
/// <summary> /// Initializes a new instance of the <see cref="PropertyDef" /> class. /// </summary> /// <param name="propertyInfo"> /// The definition of the property. /// </param> /// <param name="settings"> /// The settings used to control how configuration objects are created and the features they support. /// </param> /// <exception cref="InvalidPropertyException"> /// 'ConfigurationValue' attribute on property specifies that persistence should only save values (not /// load), but no default value is provided. /// </exception> internal PropertyDef(PropertyInfo propertyInfo, ConfigurationObjectSettings settings) { PropertyInfo = propertyInfo; PropertyName = propertyInfo.Name; Type = propertyInfo.PropertyType; Settings = settings; if (Type.IsArray) { throw new InvalidPropertyException(PropertyName, $@"Property '{PropertyName}' is an array which is not supported. Use 'IConfigurationCollection' or 'IReadOnlyConfigurationCollection' instead."); } UnderlyingType = GetUnderlyingType(propertyInfo.PropertyType); IsReadOnly = !propertyInfo.CanWrite; var pathAttributes = propertyInfo.GetCustomAttributes(typeof(PathAttribute), true); if (pathAttributes.Length > 0) { var pathAttribute = ((PathAttribute)pathAttributes[0]); PathSection = pathAttribute.Path; PathModifier = pathAttribute.Usage; } else { PathModifier = PathIs.Relative; PathSection = PropertyName; } Implementation = new Implementation(UnderlyingType, Settings); switch (Implementation.ImplementationKind) { case ImplementationKind.ConfigurationCollection: case ImplementationKind.ConfigurationDictionary: case ImplementationKind.ConfigurationObject: ElementImplementation = new Implementation(Implementation.Type, Settings); break; } var configurationAttributes = propertyInfo.GetCustomAttributes <ConfigurationAttribute>(true).ToList(); if (configurationAttributes.Count > 0) { var attribute = configurationAttributes[0]; DefaultValue = attribute.DefaultValue; HasDefaultValue = attribute.IsDefaultValueSet; Persistence = attribute.Persistence; if (HasDefaultValue) { if (ReferenceEquals(DefaultValue, null)) { if (Type.IsValueType) { throw new ConfigurationException(null, PropertyName, string.Format(CultureInfo.InvariantCulture, "Property '{0}' has a default value of a type of 'null', which is incompatible with the type of the property.", PropertyName)); } } else { if (!Type.IsAssignableFrom(DefaultValue.GetType())) { throw new ConfigurationException(null, PropertyName, string.Format(CultureInfo.InvariantCulture, "Property '{0}' has a default value of a type incompatible with the type of the property.", PropertyName)); } } } } else { Persistence = ConfigurationPersistenceActions.LoadAndSave; } if (!Persistence.HasFlag(ConfigurationPersistenceActions.LoadOnly) && !HasDefaultValue) { var propertyName = $"[{propertyInfo.DeclaringType.Namespace}.{propertyInfo.DeclaringType.Name}].{propertyInfo.Name}"; throw new InvalidPropertyException(propertyName, $"'ConfigurationValue' attribute on property \"{propertyName}\" specifies that persistence should only save or ignore values (not load), but no default value is provided."); } }
/// <summary> /// Initializes a new instance of the <see cref="ConfigurationObjectBase{TInterface}" /> class. This is the /// interface used when creating the root instance for the service collection. /// </summary> /// <param name="propertyDef"> /// The definition of the property defined by this object. This can be <see lang="null" /> if this object is /// the root of the hierarchy. /// </param> /// <param name="configurationRoot"> /// The configuration root from which to read and write values. /// </param> /// <param name="parent"> /// The parent object to which this one belongs. <see langword="null" /> if this is a root object. /// </param> /// <param name="settings"> /// The settings used to control how configuration objects are created and the features they support. /// </param> /// <param name="validators"> /// The validators that can be applied to configuration objects of this type. /// </param> protected ConfigurationObjectBase(IPropertyDef?propertyDef, IConfigurationRoot configurationRoot, IConfigurationParent parent, ConfigurationObjectSettings settings, IEnumerable <IConfigurationObjectValidator <TInterface> > validators) : base(propertyDef, ServiceCollectionExtensions.GetConfigurationObjectDefinition(typeof(TInterface), settings), configurationRoot, parent) { _validators = validators?.ToArray() ?? Array.Empty <IConfigurationObjectValidator <TInterface> >(); DisablePropertyChangedEvents(); try { foreach (var value in Values) { if (value.PropertyDef.HasDefaultValue) { value.Value = value.PropertyDef.DefaultValue; value.Saved(); } } } finally { EnablePropertyChangedEvents(); Validate(); } }