简体   繁体   中英

ASP.NET CORE 1.0, AppSettings from another assembly

I have an application splittet into two projects: a web application and a class library. The Startup is only in the web application:

var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")

I wanna have my appsettings.json in that class library and being loaded from there. Is that possible? How can I do that?

Yes you could implement IConfigurationProvider

There is a base class ConfigurationProvider that you can inherit from then override all the virtual methods.

You can also see how the JsonConfigurationProvider is implemented.

So I guess your implementation could use the Json provider code internally against embedded json files.

Then you would also want to implement ConfigurationBuilder extension to register your provider similar as the code for using json config.

The best solution I have found requires creating a new IFileProvider and IFileInfo, and then embedding the JSON settings files in your assembly.

The solution reuses the existing AddJsonFile logic. This way you only need to tell the configuration system how and where to locate the JSON file, not how to parse it.

The solution is compatible with .NET Core 1.0.

Usage:

public class Startup
{
    private readonly AppSettings _appSettings;

    public Startup(IHostingEnvironment env)
    {
        Assembly assembly = GetType().GetTypeInfo().Assembly;

        new ConfigurationBuilder()
          .AddEmbeddedJsonFile(assembly, "appsettings.json")
          .AddEmbeddedJsonFile(assembly, $"appsettings.{env.EnvironmentName.ToLower()}.json")
          .Build();
    }

    ...
}

Embed the files by updating the project.json for the class library:

...
"buildOptions": {
  "embed": [
    "appsettings.json",
    "appsettings.development.json",
    "appsettings.production.json"
  ]
}
...

IEmbeddedFileInfo implementation:

public class EmbeddedFileInfo : IFileInfo
{
    private readonly Stream _fileStream;

    public EmbeddedFileInfo(string name, Stream fileStream)
    {
        if (name == null) throw new ArgumentNullException(nameof(name));
        if (fileStream == null) throw new ArgumentNullException(nameof(fileStream));

        _fileStream = fileStream;

        Exists = true;
        IsDirectory = false;
        Length = fileStream.Length;
        Name = name;
        PhysicalPath = name;
        LastModified = DateTimeOffset.Now;
    }

    public Stream CreateReadStream()
    {
        return _fileStream;
    }

    public bool Exists { get; }
    public bool IsDirectory { get; }
    public long Length { get; }
    public string Name { get; }
    public string PhysicalPath { get; }
    public DateTimeOffset LastModified { get; }
}

IFileInfo implementation:

public class EmbeddedFileProvider : IFileProvider
{
    private readonly Assembly _assembly;

    public EmbeddedFileProvider(Assembly assembly)
    {
        if (assembly == null) throw new ArgumentNullException(nameof(assembly));

        _assembly = assembly;
    }

    public IFileInfo GetFileInfo(string subpath)
    {
        string fullFileName = $"{_assembly.GetName().Name}.{subpath}";

        bool isFileEmbedded = _assembly.GetManifestResourceNames().Contains(fullFileName);

        return isFileEmbedded
          ? new EmbeddedFileInfo(subpath, _assembly.GetManifestResourceStream(fullFileName))
          : (IFileInfo) new NotFoundFileInfo(subpath);
    }

    public IDirectoryContents GetDirectoryContents(string subpath)
    {
        throw new NotImplementedException();
    }

    public IChangeToken Watch(string filter)
    {
        throw new NotImplementedException();
    }
}

Create the easy to use AddEmbeddedJsonFile extension method.

public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddEmbeddedJsonFile(this IConfigurationBuilder cb,
        Assembly assembly, string name, bool optional = false)
    {
        // reload on change is not supported, always pass in false
        return cb.AddJsonFile(new EmbeddedFileProvider(assembly), name, optional, false);
    }
}

Someone else can correct me, but I don't think what you are looking for exists. App Configs and AppSettings files are read at runtime by the application that is running.

The Class Library cannot see any AppSettings specific to itself, because when it runs at run time, it is in the folder of the running application.

The only potential way I can see for you to get your class library contain the json file, is to have the json file as an embedded resource. Eg: In the solution, select the json file, and set it to Embedded Resource instead of 'content'.

The problem becomes getting the embedded config file out of your assembly, and then loaded.

AddJsonFile accepts a path to the json file.

You could however extract the Json file to a temp directory, then load from there.

static byte[] StreamToBytes(Stream input)
            {

                int capacity = input.CanSeek ? (int)input.Length : 0; 
                using (MemoryStream output = new MemoryStream(capacity))
                {
                    int readLength;
                    byte[] buffer = new byte[capacity/*4096*/];  //An array of bytes
                    do
                    {
                        readLength = input.Read(buffer, 0, buffer.Length);   //Read the memory data, into the buffer
                        output.Write(buffer, 0, readLength);
                    }
                    while (readLength != 0); //Do all this while the readLength is not 0
                    return output.ToArray();  //When finished, return the finished MemoryStream object as an array.
                }

            }



Assembly yourAssembly = Assembly.GetAssembly(typeof(MyTypeWithinAssembly));
using (Stream input = yourAssembly.GetManifestResourceStream("NameSpace.Resources.Config.json")) // Acquire the dll from local memory/resources.
                {
                    byte[] byteData  = StreamToBytes(input);
                    System.IO.File.WriteAllBytes(Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)+"//Config.json",new byte[]{});
                }



var builder = new ConfigurationBuilder()
    .AddJsonFile(Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData)+"//Config.json");

You should in theory be able to specify a type from the class library, in order to help the c# code target that class library specifically. Then you just need to provide the namespace and path to the embedded json file.

Here is my solution, thanks Baaleos and Joe for your advices.

project.json

"resource": [
    "appsettings.json"
  ]

startup.cs

var builder = new ConfigurationBuilder()
    .Add(new SettingsConfigurationProvider("appsettings.json"))
    .AddEnvironmentVariables();

this.Configuration = builder.Build();

namespace ClassLibrary
{
    using Microsoft.Extensions.Configuration;
    using Newtonsoft.Json;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;

    /// <summary>
    /// A JSON file based <see cref="ConfigurationProvider"/> for embedded resources.
    /// </summary>
    public class SettingsConfigurationProvider : ConfigurationProvider
    {
        /// <summary>
        /// Initializes a new instance of <see cref="SettingsConfigurationProvider"/>.
        /// </summary>
        /// <param name="name">Name of the JSON configuration file.</param>
        /// <param name="optional">Determines if the configuration is optional.</param>
        public SettingsConfigurationProvider(string name)
            : this(name, false)
        {
        }

        /// <summary>
        /// Initializes a new instance of <see cref="SettingsConfigurationProvider"/>.
        /// </summary>
        /// <param name="name">Name of the JSON configuration file.</param>
        /// <param name="optional">Determines if the configuration is optional.</param>
        public SettingsConfigurationProvider(string name, bool optional)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new ArgumentException("Name must be a non-empty string.", nameof(name));
            }

            this.Optional = optional;
            this.Name = name;
        }

        /// <summary>
        /// Gets a value that determines if this instance of <see cref="SettingsConfigurationProvider"/> is optional.
        /// </summary>
        public bool Optional { get; }

        /// <summary>
        /// The name of the file backing this instance of <see cref="SettingsConfigurationProvider"/>.
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// Loads the contents of the embedded resource with name <see cref="Path"/>.
        /// </summary>
        /// <exception cref="FileNotFoundException">If <see cref="Optional"/> is <c>false</c> and a
        /// resource does not exist with name <see cref="Path"/>.</exception>
        public override void Load()
        {
            Assembly assembly = Assembly.GetAssembly(typeof(SettingsConfigurationProvider));
            var resourceName = $"{assembly.GetName().Name}.{this.Name}";
            var resources = assembly.GetManifestResourceNames();

            if (!resources.Contains(resourceName))
            {
                if (Optional)
                {
                    Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                }
                else
                {
                    throw new FileNotFoundException($"The configuration file with name '{this.Name}' was not found and is not optional.");
                }
            }
            else
            {
                using (Stream settingsStream = assembly.GetManifestResourceStream(resourceName))
                {
                    Load(settingsStream);
                }
            }
        }

        internal void Load(Stream stream)
        {
            JsonConfigurationFileParser parser = new JsonConfigurationFileParser();
            try
            {
                Data = parser.Parse(stream);
            }
            catch (JsonReaderException e)
            {
                string errorLine = string.Empty;
                if (stream.CanSeek)
                {
                    stream.Seek(0, SeekOrigin.Begin);

                    IEnumerable<string> fileContent;
                    using (var streamReader = new StreamReader(stream))
                    {
                        fileContent = ReadLines(streamReader);
                        errorLine = RetrieveErrorContext(e, fileContent);
                    }
                }

                throw new FormatException($"Could not parse the JSON file. Error on line number '{e.LineNumber}': '{e}'.");
            }
        }

        private static string RetrieveErrorContext(JsonReaderException e, IEnumerable<string> fileContent)
        {
            string errorLine;
            if (e.LineNumber >= 2)
            {
                var errorContext = fileContent.Skip(e.LineNumber - 2).Take(2).ToList();
                errorLine = errorContext[0].Trim() + Environment.NewLine + errorContext[1].Trim();
            }
            else
            {
                var possibleLineContent = fileContent.Skip(e.LineNumber - 1).FirstOrDefault();
                errorLine = possibleLineContent ?? string.Empty;
            }

            return errorLine;
        }

        private static IEnumerable<string> ReadLines(StreamReader streamReader)
        {
            string line;
            do
            {
                line = streamReader.ReadLine();
                yield return line;
            } while (line != null);
        }
    }
}

You need also the JsonConfigurationFileParser :

namespace ClassLibrary
{
    using Microsoft.Extensions.Configuration;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;

    internal class JsonConfigurationFileParser
    {
        private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        private readonly Stack<string> _context = new Stack<string>();
        private string _currentPath;

        private JsonTextReader _reader;

        public IDictionary<string, string> Parse(Stream input)
        {
            _data.Clear();
            _reader = new JsonTextReader(new StreamReader(input));
            _reader.DateParseHandling = DateParseHandling.None;

            var jsonConfig = JObject.Load(_reader);

            VisitJObject(jsonConfig);

            return _data;
        }

        private void VisitJObject(JObject jObject)
        {
            foreach (var property in jObject.Properties())
            {
                EnterContext(property.Name);
                VisitProperty(property);
                ExitContext();
            }
        }

        private void VisitProperty(JProperty property)
        {
            VisitToken(property.Value);
        }

        private void VisitToken(JToken token)
        {
            switch (token.Type)
            {
                case JTokenType.Object:
                    VisitJObject(token.Value<JObject>());
                    break;

                case JTokenType.Array:
                    VisitArray(token.Value<JArray>());
                    break;

                case JTokenType.Integer:
                case JTokenType.Float:
                case JTokenType.String:
                case JTokenType.Boolean:
                case JTokenType.Bytes:
                case JTokenType.Raw:
                case JTokenType.Null:
                    VisitPrimitive(token);
                    break;

                default:
                    throw new FormatException($@"
                        Unsupported JSON token '{_reader.TokenType}' was found. 
                        Path '{_reader.Path}', 
                        line {_reader.LineNumber} 
                        position {_reader.LinePosition}.");
            }
        }

        private void VisitArray(JArray array)
        {
            for (int index = 0; index < array.Count; index++)
            {
                EnterContext(index.ToString());
                VisitToken(array[index]);
                ExitContext();
            }
        }

        private void VisitPrimitive(JToken data)
        {
            var key = _currentPath;

            if (_data.ContainsKey(key))
            {
                throw new FormatException($"A duplicate key '{key}' was found.");
            }
            _data[key] = data.ToString();
        }

        private void EnterContext(string context)
        {
            _context.Push(context);
            _currentPath = string.Join(Constants.KeyDelimiter, _context.Reverse());
        }

        private void ExitContext()
        {
            _context.Pop();
            _currentPath = string.Join(Constants.KeyDelimiter, _context.Reverse());
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM