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?
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);
}
}
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.
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());
}
}
}
Related
i want to use enum as a key to get value from an resource file
in my project i use and method to get value from resx file:
using System;
using System.Reflection;
using System.Resources;
using System.Web;
using Utility.Tools.ExtensionMethods;
using Utility.Tools.Infrastructure;
using Utility.Tools.Utilities.Storage;
namespace MultiLanguage
{
public enum LocaleConfig_Type : byte
{
FA,
AR,
DARI
}
public class MultiLangToolkit
{
public static string GetTranslate(MainKeys key)
{
var lang = LanguegesToolkit.GetCurentLang();
return GetTranslate(key, lang);
}
public static string GetTranslate(MainKeys key, LocaleConfig_Type locale)
{
try
{
string value;
switch (locale)
{ case LocaleConfig_Type.FA:
{
var fa_resx = new ResourceManager("MultiLanguage.FA", Assembly.GetExecutingAssembly());
value = fa_resx.GetString(key.ToString());
}
break;
case LocaleConfig_Type.AR:
{
var ar_resx = new ResourceManager("MultiLanguage.AR", Assembly.GetExecutingAssembly());
value = ar_resx.GetString(key.ToString());
}
break;
case LocaleConfig_Type.DARI:
{
var dari_resx = new ResourceManager("MultiLanguage.DARI", Assembly.GetExecutingAssembly());
value = dari_resx.GetString(key.ToString());
}
break;
default:
value = "No Language Selected!";
break;
}
if (!string.IsNullOrEmpty(value))
return value;
return "Key Or Value NotFound!";
}
catch (Exception)
{
return "Exception when translate";
}
}
}
public class LanguegesToolkit
{
public static LocaleConfig_Type GetCurentLang()
{
try
{
object sessionDefalutLocale = null;
if (HttpContext.Current.Session != null)
{
sessionDefalutLocale = HttpContext.Current.Session[SessionManager.SESSION_USER_DEFAULT_LOCAL];
}
if (sessionDefalutLocale != null)
{
return sessionDefalutLocale.ToString().ToEnum<LocaleConfig_Type>();
}
else
{
var defaultLocale = RequestGlobalProperties.CurrentUserRequest.DefaultLocale;
if (defaultLocale.IsNullOrEmpty())
return Constants.ServiceDefaultLocale.ToEnum<LocaleConfig_Type>();
return defaultLocale.ToEnum<LocaleConfig_Type>();
}
}
catch (Exception)
{
return LocaleConfig_Type.FA;
}
}
}
}
and i have a enum class to store enums:
namespace MultiLanguage
{
public enum MainKeys
{
/// <summary>
/// login to account
/// </summary>
Sign_in,
/// <summary>
/// password
/// </summary>
Password,
/// <summary>
/// user name
/// </summary>
Username
}
}
and i use this code to get value from my resx files
MultiLanguage.MultiLangToolkit.GetTranslate(MultiLanguage.MainKeys.Username)
but this type of code is too many for getting a value from my resx file
i just want to write this code instants the code above:
MultiLanguage.MainKeys.Username
simply just call the enum and get translated value from resx file
i use this code in a dll file to use that in my other projects and use and enum as a key instants of string because strings is not a good choice and my cause and runtime issue and it's hard to debug that. and using summery on the enums to see that is the value in resx file(yes i copy the value frome my rex file into summery)
how can i do that?
is that possible ?
If you really want to stick with your enumerations then you could just create an extension method to simplify the usage. Something like
public static class EnumExtensions
{
public static string Translate(this MainKeys key)
{
return MultiLanguage.MultiLangToolkit.GetTranslate(key);
}
}
then you can just use
MultiLanguage.MainKeys.Username.Translate();
How do I separate my code into their own classes and still have it function the same? This is currently what my code looks like.
using System;
using System.Collections.Generic;
using System.Xml;
using XCENT.JobServer.JobPlugIn;
using System.IO;
using HPD.API.Utility.DataAccess;
namespace DataPurge
{
public class Purge : IJob, IJobControl {
public IJobControl JobControl { get { return ( this ); } }
public int MaxInstanceCount { get { return 1; } }
public string Name { get { return "DataPurge"; } }
public Purge() { }
public void Run( string XmlFragment ) {
XmlNode xmlNode = null;
try
{
xmlNode = Common.ConstructXmlNodeFromString(XmlFragment, "Params");
var list = DataList();
foreach (var item in list)
{
var factory = new PurgerFactory(item);
IPurger purge = factory.Purger;
purge.Purge();
purge = null;
factory = null;
}
}
catch (Exception ex)
{
throw;
}
}
public interface IPurger
{
void Purge();
}
public enum PurgeType
{
File,
Database,
}
public class FilePurger : IPurger
{
private Parameters parameter;
public FilePurger(Parameters parameter)
{
this.parameter = parameter;
}
public void Purge()
{
var files = new DirectoryInfo(parameter.FilePath).GetFiles();
foreach (var file in files)
{
if (DateTime.Now - file.CreationTime > TimeSpan.FromDays(7))
{
File.Delete(file.FullName);
}
}
}
}
public class DbPurger : IPurger
{
private Parameters parameter;
public DbPurger(Parameters parameter)
{
this.parameter = parameter;
}
public void Purge()
{
var access = new SqlDataAccess();
var sqlParams = new Dictionary<string, object>();
sqlParams.Add("#OlderThanDays", parameter.OlderThanDays);
access.ExecuteNonQuery(parameter.CString, parameter.SPName, sqlParams, 30, false);
}
}
private List<Parameters> DataList()
{
var sqlParams = new SqlDataAccess();
var list = sqlParams.GetDataTableAsList<Parameters>("Data Source = MYSERVER; Initial Catalog = MYDATABASE; User ID = UID; Password = PASSWORD;", "purge.spoDataTable", null);
return list;
}
public class PurgerFactory
{
public IPurger Purger { get; set; }
public PurgerFactory(Parameters parameter)
{
PurgeType type = (PurgeType)Enum.Parse(typeof(PurgeType), parameter.PurgeType);
switch (type)
{
case PurgeType.File:
Purger = new FilePurger(parameter);
break;
case PurgeType.Database:
Purger = new DbPurger(parameter);
break;
default:
throw new NotImplementedException();
}
}
}
/// <summary>
/// Used to submit a job via the job monitor
/// </summary>
public XmlNode JobXMLNode => Common.ConstructXmlNodeFromString("" +
"<JobParams>" +
" <Param Name=\"InfrastructureAPI\" DataType=\"String\">" +
" <Description>Infrastructure API URL.</Description>" +
" </Param>" +
" <Param Name=\"EnvironmentName\" DataType=\"String\">" +
" <Description>The current environment.</Description>" +
" </Param>" +
"</JobParams>",
"JobParams");
}
}
Currently all parts of the program are stuffed into this one single class. I want to separate them out into their own separate classes to make the code much cleaner but still have it function the same. I'm still a beginner coder and don't know the first place to start. Any help would be much appreciated!
You should create a file IPurger.cs for the interface IPurger, then a file FilePurger.cs for the class FilePurger, the file DbPurger.cs for the class DbPurger and lastly PurgerFactory.cs for the class PurgerFactory.
That should clean up your code quite well.
If that enum is used from multiple places, you may want to place it in its own class too, perhaps a generic Enums.cs.
I have a class LanguagePopupMessage which is used all over the application (and external libraries). If this class is constructed it fetches the namespace where it's created and adds a suffix to be unique.
The Question is: How can get all LanguagePopupMessage definitions including the fieldname parameter?
Im using structuremap in my application. It's also scanning all libraries at startup, so maybe there is a possiblity how to automaticate it. 👀
using System;
using System.Diagnostics;
namespace ConsoleApp1
{
/// <summary>
/// Creates the namespace for a popup window and has an additional flag for the caption
/// </summary>
public class LanguagePopupMessage
{
public string Namespace { get; }
public string Caption => $"{Namespace}Caption";
public LanguagePopupMessage(string fieldName)
{
if(string.IsNullOrEmpty(fieldName))
throw new ArgumentNullException(nameof(fieldName));
if (_GetNamespace() is Type type)
{
Namespace = $"{type}.{fieldName}";
}
else
{
throw new InvalidOperationException("could not fetch the namespace");
}
}
private Type _GetNamespace()
{
StackTrace st = new StackTrace();
foreach (var sf in st.GetFrames())
{
var type = sf.GetMethod().DeclaringType;
if (type != GetType())
{
return type;
}
}
return null;
}
public override string ToString()
{
return $"Namespace '{Namespace}' Caption '{Caption}'";
}
}
class Program
{
//ConsoleApp1.Program.PopupMessage.ConfigNotLoaded
//ConsoleApp1.Program.PopupMessage.ConfigNotLoadedCaption
private static readonly LanguagePopupMessage _CONFIG_NOT_LOADED_POPUP_MESSAGE = new LanguagePopupMessage("ConfigNotLoaded");
static void Main(string[] args)
{
Console.ReadKey();
}
}
}
namespace ConsoleApp1.Subfolder
{
public class SubfolderClass
{
/// <summary>
/// ConsoleApp1.Subfolder.SubfolderClass.FooMessage
/// ConsoleApp1.Subfolder.SubfolderClass.FooMessageCaption
/// </summary>
public static readonly LanguagePopupMessage Message = new LanguagePopupMessage("FooMessage");
}
}
I made a custom IRegistrationConvention - FindAllLanguagePopupMessages for structuremap. During runtime a new container scans all libraries -> all Types if there are any FieldInfo of type LanguagePopupMessage and adding it into a collection.
To get better performance, I made an Attribute - ContainsTranslationDefinition to filter the classes.
Sourcecode
public class ContainsTranslationDefinition : Attribute
{ }
/// <summary>
/// Creates the namespace for a popup window and has an additional flag for the caption
/// </summary>
public class LanguagePopupMessage
{
public string Namespace { get; }
public string Caption => $"{Namespace}Caption";
public LanguagePopupMessage(string fieldName)
{
if(string.IsNullOrEmpty(fieldName))
throw new ArgumentNullException(nameof(fieldName));
if (_GetNamespace() is Type type)
{
Namespace = $"{type}.{fieldName}";
}
else
{
throw new InvalidOperationException("could not fetch the namespace");
}
}
private Type _GetNamespace()
{
StackTrace st = new StackTrace();
foreach (var sf in st.GetFrames())
{
var type = sf.GetMethod().DeclaringType;
if (type != GetType())
{
return type;
}
}
return null;
}
public override string ToString()
{
return $"Namespace '{Namespace}' Caption '{Caption}'";
}
}
/// <summary>
/// Add <see cref="LanguagePopupMessage"/> into the <see cref="Container.Model"/> type lifecycle
/// </summary>
public class FindAllLanguagePopupMessages : IRegistrationConvention
{
private readonly ILifecycle _Lifecycle = new UniquePerRequestLifecycle();
public void ScanTypes(TypeSet types, Registry registry)
{
List<LanguagePopupMessage> dec = new List<LanguagePopupMessage>();
foreach (Type type in types.AllTypes())
{
if (!Attribute.IsDefined(type, typeof(ContainsTranslationDefinition)))
{
continue;
}
_FindConfigDeclarations(type, dec);
}
foreach (LanguagePopupMessage languagePopupMessage in dec)
{
Console.WriteLine($"{languagePopupMessage}");
}
}
private static void _FindConfigDeclarations(Type type, List<LanguagePopupMessage> declarations)
{
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy);
declarations.AddRange(fields
.Where(info => info.IsInitOnly && typeof(LanguagePopupMessage).IsAssignableFrom(info.FieldType))
.Select(info => (LanguagePopupMessage)info.GetValue(null)));
// find all nested class types and run method recursively
foreach (var nestedType in type.GetNestedTypes(BindingFlags.Public))
{
_FindConfigDeclarations(nestedType, declarations);
}
}
}
[ContainsTranslationDefinition]
public class TestClass
{
private static readonly LanguagePopupMessage _CONFIG_1 = new LanguagePopupMessage("ConfigNotLoaded1");
private static readonly LanguagePopupMessage _CONFIG_2 = new LanguagePopupMessage("ConfigNotLoaded2");
}
[ContainsTranslationDefinition]
public class Program
{
//ConsoleApp1.Program.PopupMessage.ConfigNotLoaded
//ConsoleApp1.Program.PopupMessage.ConfigNotLoadedCaption
private static readonly LanguagePopupMessage _CONFIG_NOT_LOADED_POPUP_MESSAGE = new LanguagePopupMessage("ConfigNotLoaded3");
static void Main(string[] args)
{
// Create container and tell where to look for depencies
IContainer container = new Container(c => c.Scan(scanner =>
{
scanner.TheCallingAssembly();
scanner.WithDefaultConventions();
scanner.AssembliesFromApplicationBaseDirectory();
scanner.With(new FindAllLanguagePopupMessages());
}));
Console.ReadKey();
}
}
Preview
In my project I have implemented custom routing constraints to allow for API versioning via a custom header variable (api-version), similar to this sample project on Codeplex, although I modified the constraint to allow for a major.minor convention.
This involves creating two separate controllers whose routes are differentiated via a FullVersionedRoute attribute:
Sample1Controller.cs
/// <summary>
/// v1.0 Controller
/// </summary>
public class Sample1Controller : ApiController
{
[FullVersionedRoute("api/test", "1.0")]
public IEnumerable<string> Get()
{
return new[] { "This is version 1.0 test!" };
}
}
Sample2Controller.cs
/// <summary>
/// v2.0 Controller
/// </summary>
public class Sample2Controller : ApiController
{
[FullVersionedRoute("api/test", "2.0")]
public IEnumerable<string> Get()
{
return new[] { "This is version 2.0 test!" };
}
}
FullVersionedRoute.cs
using System.Collections.Generic;
using System.Web.Http.Routing;
namespace HelperClasses.Versioning
{
/// <summary>
/// Provides an attribute route that's restricted to a specific version of the api.
/// </summary>
internal class FullVersionedRoute : RouteFactoryAttribute
{
public FullVersionedRoute(string template, string allowedVersion) : base(template)
{
AllowedVersion = allowedVersion;
}
public string AllowedVersion
{
get;
private set;
}
public override IDictionary<string, object> Constraints
{
get
{
var constraints = new HttpRouteValueDictionary();
constraints.Add("version", new FullVersionConstraint(AllowedVersion));
return constraints;
}
}
}
}
FullVersionConstraint.cs
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
namespace HelperClasses.Versioning
{
/// <summary>
/// A Constraint implementation that matches an HTTP header against an expected version value.
/// </summary>
internal class FullVersionConstraint : IHttpRouteConstraint
{
public const string VersionHeaderName = "api-version";
private const string DefaultVersion = "1.0";
public FullVersionConstraint(string allowedVersion)
{
AllowedVersion = allowedVersion;
}
public string AllowedVersion
{
get;
private set;
}
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
if (routeDirection == HttpRouteDirection.UriResolution)
{
var version = GetVersionHeader(request) ?? DefaultVersion;
return (version == AllowedVersion);
}
return false;
}
private string GetVersionHeader(HttpRequestMessage request)
{
IEnumerable<string> headerValues;
if (request.Headers.TryGetValues(VersionHeaderName, out headerValues))
{
// enumerate the list once
IEnumerable<string> headers = headerValues.ToList();
// if we find once instance of the target header variable, return it
if (headers.Count() == 1)
{
return headers.First();
}
}
return null;
}
}
}
This works just fine, but the auto-generated help files can't differentiate between the actions in the two controllers as they look like the same route (if you only pay attention to the url route, whic it does by default). As such, the action from Sample2Controller.cs overwrites the action from Sample1Controller.cs so only the Sample2 API is displayed on the help pages.
Is there a way to configure the Web API Help Page package to recognize a Custom Constraint and recognize that there are two, separate APIs, and subsequently display them as separate API groups on Help Pages?
I found this article which describes how to achieve this by implementing IApiExplorer.
In short, what you'll want to do is add a new VersionedApiExplorer class implementing IApiExplorer like so
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Description;
using System.Web.Http.Routing;
namespace HelperClasses.Versioning
{
public class VersionedApiExplorer<TVersionConstraint> : IApiExplorer
{
private IApiExplorer _innerApiExplorer;
private HttpConfiguration _configuration;
private Lazy<Collection<ApiDescription>> _apiDescriptions;
private MethodInfo _apiDescriptionPopulator;
public VersionedApiExplorer(IApiExplorer apiExplorer, HttpConfiguration configuration)
{
_innerApiExplorer = apiExplorer;
_configuration = configuration;
_apiDescriptions = new Lazy<Collection<ApiDescription>>(
new Func<Collection<ApiDescription>>(Init));
}
public Collection<ApiDescription> ApiDescriptions
{
get { return _apiDescriptions.Value; }
}
private Collection<ApiDescription> Init()
{
var descriptions = _innerApiExplorer.ApiDescriptions;
var controllerSelector = _configuration.Services.GetHttpControllerSelector();
var controllerMappings = controllerSelector.GetControllerMapping();
var flatRoutes = FlattenRoutes(_configuration.Routes);
var result = new Collection<ApiDescription>();
foreach (var description in descriptions)
{
result.Add(description);
if (controllerMappings != null && description.Route.Constraints.Any(c => c.Value is TVersionConstraint))
{
var matchingRoutes = flatRoutes.Where(r => r.RouteTemplate == description.Route.RouteTemplate && r != description.Route);
foreach (var route in matchingRoutes)
GetRouteDescriptions(route, result);
}
}
return result;
}
private void GetRouteDescriptions(IHttpRoute route, Collection<ApiDescription> apiDescriptions)
{
var actionDescriptor = route.DataTokens["actions"] as IEnumerable<HttpActionDescriptor>;
if (actionDescriptor != null && actionDescriptor.Count() > 0)
GetPopulateMethod().Invoke(_innerApiExplorer, new object[] { actionDescriptor.First(), route, route.RouteTemplate, apiDescriptions });
}
private MethodInfo GetPopulateMethod()
{
if (_apiDescriptionPopulator == null)
_apiDescriptionPopulator = _innerApiExplorer.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(
m => m.Name == "PopulateActionDescriptions" && m.GetParameters().Length == 4);
return _apiDescriptionPopulator;
}
public static IEnumerable<IHttpRoute> FlattenRoutes(IEnumerable<IHttpRoute> routes)
{
var flatRoutes = new List<HttpRoute>();
foreach (var route in routes)
{
if (route is HttpRoute)
yield return route;
var subRoutes = route as IReadOnlyCollection<IHttpRoute>;
if (subRoutes != null)
foreach (IHttpRoute subRoute in FlattenRoutes(subRoutes))
yield return subRoute;
}
}
}
}
and then add this to your WebAPIConfig
var apiExplorer = config.Services.GetApiExplorer();
config.Services.Replace(typeof(IApiExplorer), new VersionedApiExplorer<FullVersionConstraint>(apiExplorer, config));
You should then see both your Sample1 and Sample2 APIs on your Web API Help Page.
I have a class which needs to be a Singleton.
It must also be able to load and save its field data in an xml file.
The following method will return a new instance, which breaks my Singleton pattern, leaving potential bugs in my code.
public Settings Load()
{
using (Stream stream = File.OpenRead(FileName))
{
XmlSerializer serializer = new XmlSerializer(typeof(Settings));
return (Settings)serializer.Deserialize(stream);
}
}
What method can I use in order to update the data in my existing instance, instead of returning a entirely new instance?
I've studied a bit of Linq to Xml, but haven't found any good example of this.
Is it necessary for me to keep all my field data in a Dictionary?
I used to run into all sorts of bugs making an Xml Singleton class and ended up scrapping it as I had handles all over the place. I replaced it with using two ways. One a read-only version that was for reading data, and a second Using method/statement for writing changes.
This in general is the pattern I use:
public class Settings : IDisposable
{
string file = "my settings file";
XElement root;
private Settings()
{
root = XElement.Load(file);
}
private void Dispose()
{
root.Save(file);
}
public static Settings Read { get { return new Settings(); } } // return read-only version
public static void Write(Action<Settings> handler)
{
using(Setting settings = new Settings())
handler(settings);
}
// below here is implentation specific
public XElement Root { get { return root; } }
public string SettingA
{
get { return (string)(Root.Attribute("SettingA") ?? (object)string.Empty); }
set { Set(Root, "SettingsA", value, true); }
}
// I wrote this for another StackOverflow thread
/// <summary>
/// Set any value via its .ToString() method.
/// <para>Returns XElement of source or the new XElement if is an ELEMENT</para>
/// </summary>
/// <param name="isAttribute">true for ATTRIBUTE or false for ELEMENT</param>
/// <returns>source or XElement value</returns>
private XElement Set(XElement source, string name, object value, bool isAttribute)
{
string sValue = value.ToString();
XElement eValue = source.Element(name), result = source;
XAttribute aValue = source.Attribute(name);
if (null != eValue)
eValue.ReplaceWith(result = new XElement(name, sValue));
else if (null != aValue)
aValue.ReplaceWith(new XAttribute(name, sValue));
else if (isAttribute)
source.Add(new XAttribute(name, sValue));
else
source.Add(result = new XElement(name, sValue));
return result;
}
/// <summary>
/// Replace with for XAttribute
/// </summary>
/// <param name="source"></param>
/// <param name="value"></param>
/// <returns></returns>
public static XAttribute ReplaceWith(this XAttribute source, XAttribute value)
{
XElement parent = source.Parent;
if (null == parent)
throw new Exception("Source has no parent");
source.Remove();
parent.Add(value);
return value;
}
}
I've not used the serializer, so don't know if my pattern will fit for you. I prefer XElement.
So to use this you'd probably write a singleton class that makes use of your non-singleton XmlSerialize class. You'd only access it through the singleton.
But this is how I'd end up using it as is:
string settingA = Settings.Read.SettingA;
To save a value it would be:
Settings.Write(s => s.SettingA = "new value");
why dont you have something like
public Class TheClassHoldingYourObject
{
private static XmlSerializer _instance;
public static Settings Load()
{
if(_instance != null) return _instance
using (Stream stream = File.OpenRead(FileName))
{
XmlSerializer serializer = new XmlSerializer(typeof(Settings));
return (Settings)serializer.Deserialize(stream);
}
}
}
Now you will always get the same instance