Getting Web API Help Pages to Work with Custom Routing Constraints - c#

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.

Related

Alias a model's property to a different name in asp.net-core

I wish to alias the name of my request object properties, so that these requests both work and both go to the same controller:
myapi/cars?colors=red&colors=blue&colors=green and myapi/cars?c=red&c=blue&c=green
for request object:
public class CarRequest {
Colors string[] { get; set; }
}
Has anyone been able to use the new ModelBinders to solve this without having to write ModelBindings from scratch?
Here is a similar problem for an older version of asp.net and also here
I wrote a model binder to do this:
EDIT:
Here's the repo on github. There are two nuget packages you can add to your code that solve this problem. Details in the readme
It basically takes the place of the ComplexTypeModelBinder (I'm too cowardly to replace it, but I slot it in front with identical criteria), except that it tries to use my new attribute to expand the fields it's looking for.
Binder:
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MYDOMAIN.Client;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
namespace MYDOMAIN.Web.AliasModelBinder
{
public class AliasModelBinder : ComplexTypeModelBinder
{
public AliasModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders, ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes)
: base(propertyBinders, loggerFactory, allowValidatingTopLevelNodes)
{
}
protected override Task BindProperty(ModelBindingContext bindingContext)
{
var containerType = bindingContext.ModelMetadata.ContainerType;
if (containerType != null)
{
var propertyType = containerType.GetProperty(bindingContext.ModelMetadata.PropertyName);
var attributes = propertyType.GetCustomAttributes(true);
var aliasAttributes = attributes.OfType<BindingAliasAttribute>().ToArray();
if (aliasAttributes.Any())
{
bindingContext.ValueProvider = new AliasValueProvider(bindingContext.ValueProvider,
bindingContext.ModelName, aliasAttributes.Select(attr => attr.Alias));
}
}
return base.BindProperty(bindingContext);
}
}
}
Provider:
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Logging;
namespace MYDOMAIN.Web.AliasModelBinder
{
public class AliasModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
foreach (var property in context.Metadata.Properties)
{
propertyBinders.Add(property, context.CreateBinder(property));
}
return new AliasModelBinder(propertyBinders,
(ILoggerFactory) context.Services.GetService(typeof(ILoggerFactory)), true);
}
return null;
}
/// <summary>
/// Setup the AliasModelBinderProvider Mvc project to use BindingAlias attribute, to allow for aliasing property names in query strings
/// </summary>
public static void Configure(MvcOptions options)
{
// Place in front of ComplexTypeModelBinderProvider to replace this binder type in practice
for (int i = 0; i < options.ModelBinderProviders.Count; i++)
{
if (options.ModelBinderProviders[i] is ComplexTypeModelBinderProvider)
{
options.ModelBinderProviders.Insert(i, new AliasModelBinderProvider());
return;
}
}
options.ModelBinderProviders.Add(new AliasModelBinderProvider());
}
}
}
Value Provider:
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Primitives;
namespace MYDOMAIN.Web.AliasModelBinder
{
public class AliasValueProvider : IValueProvider
{
private readonly IValueProvider _provider;
private readonly string _originalName;
private readonly string[] _allNamesToBind;
public AliasValueProvider(IValueProvider provider, string originalName, IEnumerable<string> aliases)
{
_provider = provider;
_originalName = originalName;
_allNamesToBind = new[] {_originalName}.Concat(aliases).ToArray();
}
public bool ContainsPrefix(string prefix)
{
if (prefix == _originalName)
{
return _allNamesToBind.Any(_provider.ContainsPrefix);
}
return _provider.ContainsPrefix(prefix);
}
public ValueProviderResult GetValue(string key)
{
if (key == _originalName)
{
var results = _allNamesToBind.Select(alias => _provider.GetValue(alias)).ToArray();
StringValues values = results.Aggregate(values, (current, r) => StringValues.Concat(current, r.Values));
return new ValueProviderResult(values, results.First().Culture);
}
return _provider.GetValue(key);
}
}
}
And an attribute to go in / be referenced by the client project
using System;
namespace MYDOMAIN.Client
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class BindingAliasAttribute : Attribute
{
public string Alias { get; }
public BindingAliasAttribute(string alias)
{
Alias = alias;
}
}
}
Configured in the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services
...
.AddMvcOptions(options =>
{
AliasModelBinderProvider.Configure(options);
...
})
...
Usage:
public class SomeRequest
{
[BindingAlias("f")]
public long[] SomeVeryLongNameForSomeKindOfFoo{ get; set; }
}
leading to a request that looks either like this:
api/controller/action?SomeVeryLongNameForSomeKindOfFoo=1&SomeVeryLongNameForSomeKindOfFoo=2
or
api/controller/action?f=1&f=2
I put most things in my web project, and the attribute in my client project.

Find all references/declarations of an object at runtime in c# | structuremap

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

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?
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());
}
}
}

Why am I receiving the exception "A public action method was not found on controller"?

I am using the following article Addition to ASP.NET MVC Localization - Using routing to support multi-culture routes.
If you look at section "Registering routes" you will see that current routes are updated (in the "RegisterRoutes" method) with the "{culture}" segment.
The difference is that I want to keep the current routes and add a duplicate for each one with a "{culture}" segment, so for a route like "foo/bar" I would get a duplicate "{culture}/foo/bar".
You can see I'm also making sure the new route comes first .
public static void MapMvcMultiCultureAttributes(this RouteCollection routes, bool inheritedRoutes = true, string defaultCulture = "en-US", string cultureCookieName = "culture")
{
routes.MapMvcAttributeRoutes(inheritedRoutes ? new InheritedRoutesProvider() : null);
var multiCultureRouteHandler = new MultiCultureMvcRouteHandler(defaultCulture, cultureCookieName);
var initialList = routes.ToList();
routes.Clear();
foreach (var routeBase in initialList)
{
var route = routeBase as Route;
if (route != null)
{
if (route.Url.StartsWith("{culture}"))
{
continue;
}
var cultureUrl = "{culture}";
if (!String.IsNullOrWhiteSpace(route.Url))
{
cultureUrl += "/" + route.Url;
}
var cultureRoute = routes.MapRoute(null, cultureUrl, null, new
{
culture = "^\\D{2,3}(-\\D{2,3})?$"
});
cultureRoute.Defaults = route.Defaults;
cultureRoute.DataTokens = route.DataTokens;
foreach (var constraint in route.Constraints)
{
cultureRoute.Constraints.Add(constraint.Key, constraint.Value);
}
cultureRoute.RouteHandler = multiCultureRouteHandler;
route.RouteHandler = multiCultureRouteHandler;
}
routes.Add(routeBase);
}
}
The "InheritedRoutesProvider" looks like this:
private class InheritedRoutesProvider : DefaultDirectRouteProvider
{
protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(ActionDescriptor actionDescriptor)
{
return actionDescriptor.GetCustomAttributes(typeof(IDirectRouteFactory), true)
.Cast<IDirectRouteFactory>()
.ToArray();
}
}
My controller looks like this:
public class MyBaseController: Controller
{
[HttpGet]
[Route("bar")]
public virtual ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
[RoutePrefix("foo")]
public class MyController: MyBaseController
{
}
My "RegisterRoutes" method looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcMultiCultureAttributes();
routes.LowercaseUrls = true;
}
Now, if I do:
/foo/bar - WORKS!
/en-US/foo/bar - HttpException A public action method 'MyAction' was not found on controller 'MyController'
I can give you an example of how I would do it. That example you are using is quite old.
Implement in your controllers (use inheritance) the BeginExecuteCore method as below:
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
string cultureName = RouteData.Values["culture"] as string;
if (cultureName == null)
cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
Request.UserLanguages[0] : // obtain it from HTTP header AcceptLanguages
null;
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
if (RouteData.Values["culture"] as string != cultureName)
{
// Force a valid culture in the URL
RouteData.Values["culture"] = cultureName.ToLower(); // lower case too
// Redirect user
Response.RedirectToRoute(RouteData.Values);
}
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
Add some routes
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Custom",
url: "{controller}/{action}/{culture}",
defaults: new { culture = CultureHelper.GetDefaultCulture(), controller = "Coordinate", action = "Index" }
Implement a culture helper class
public static class CultureHelper
{
private static readonly List _cultures = new List {
"listOfClutures"
};
public static bool IsRighToLeft()
{
return System.Threading.Thread.CurrentThread.CurrentCulture.TextInfo.IsRightToLeft;
}
public static string GetImplementedCulture(string name)
{
if (string.IsNullOrEmpty(name))
return GetDefaultCulture(); // return Default culture
if (!CultureInfo.GetCultures(CultureTypes.SpecificCultures).Any(c => c.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
return GetDefaultCulture(); // return Default culture if it is invalid
if (_cultures.Any(c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
return name; // accept it
var n = GetNeutralCulture(name);
foreach (var c in _cultures)
if (c.StartsWith(n))
return c;
return GetDefaultCulture(); // return Default culture as no match found
}
public static string GetDefaultCulture()
{
return "en-GB"; // return Default culture, en-GB
}
public static string GetCurrentCulture()
{
return Thread.CurrentThread.CurrentCulture.Name;
}
public static string GetCurrentNeutralCulture()
{
return GetNeutralCulture(Thread.CurrentThread.CurrentCulture.Name);
}
public static string GetNeutralCulture(string name)
{
if (!name.Contains("-")) return name;
return name.Split('-')[0]; // Read first part only. E.g. "en", "es"
}
public static List<KeyValuePair<string, string>> GetImplementedLanguageNames()
{
List<KeyValuePair<string, string>> languageNames = new List<KeyValuePair<string, string>>();
foreach (string culture in _cultures)
{
languageNames.Add(new KeyValuePair<string, string>(culture, CultureInfo.GetCultureInfo(culture).EnglishName));
}
languageNames.Sort((firstPair, nextPair) =>
{
return firstPair.Value.CompareTo(nextPair.Value);
});
string currCulture = GetCurrentCulture();
languageNames.Remove(new KeyValuePair<string, string>(currCulture, CultureInfo.GetCultureInfo(currCulture).EnglishName));
languageNames.Insert(0, new KeyValuePair<string, string>(currCulture, CultureInfo.GetCultureInfo(currCulture).EnglishName));
return languageNames;
}
public static string GetDateTimeUsingCurrentCulture(DateTime dateToConvert)
{
CultureInfo ci = new CultureInfo(GetCurrentCulture());
return dateToConvert.ToString(ci.DateTimeFormat.ShortDatePattern + ' ' + ci.DateTimeFormat.ShortTimePattern);
}
}
I don't think you'll be able to achieve this with attribute routing because the RouteCollection passed to RegisterRoutes and its subsequent RouteData is quite different and uses internal classes that you can't get to.
I got as far as determining that RouteData expects a key called "MS_DirectRouteMatches" whose value is a collection of RouteData. Google MS_DirectRouteMatches if you want to dig further.
Ultimately, I assume this isn't intended to be an extension point.
You will get more success if you stick to conventional routing. I found your code worked pretty well in this scenario, but I expect you already knew that. Sorry I couldn't give you a better solution.
From what I can see it's not using the parent ActionResult. I'm not sure why this is happening. You could override the ActionResult in the derived class, but to me it seems that there's something amiss in the inheritance.. a fault in how you are managing your classes.
public class MyBaseController: Controller
{
[HttpGet]
[Route("bar")]
public virtual ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
So assuming this will not work:
[RoutePrefix("foo")]
public class MyController: MyBaseController
{
[HttpGet]
[Route("foo")]
public override ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
I'd suggest doing this:
[RoutePrefix("foo")]
public class MyController: MyBaseController
{
[HttpGet]
[Route("foo")]
public ActionResult MyAction(){
{
return Content("Hello stranger!");
}
}
This at least will tell you if the inheritance is flawed and you can review your code.
Another way around this would be to use the one controller with two methods for the differing formats.

Caching WebAPI 2

EDIT: For each request, a new instance of controller is created. However, this is not true with Attribute classes. Once they are created, it is used for multiple requests. I hope it helps.
I wrote my own WebAPI (using latest version of WebAPI and .net framework) caching action filter. I am aware about CacheCow & this. However, i wanted mine anyways.
However, there is some issue with my code because i don't get exepected output when i use it in my project on live server. On local machine everything works fine.
I used below code in my blog RSS generator and i cache the data for each category. There are around 5 categories (food, tech, personal etc).
Issue: When i navigate to say api/GetTech it returns me the rss feed items from personal blog category. When i navigate to say api/GetPersonal , it returns me api/Food
I am not able to find the root cause but I think this is due to use of static method/variable. I have double checked that my _cachekey has unique value for each category of my blog.
Can someone point out any issues with this code esp when we have say 300 requests per minute ?
public class WebApiOutputCacheAttribute : ActionFilterAttribute
{
// Cache timespan
private readonly int _timespan;
// cache key
private string _cachekey;
// cache repository
private static readonly MemoryCache _webApiCache = MemoryCache.Default;
/// <summary>
/// Initializes a new instance of the <see cref="WebApiOutputCacheAttribute"/> class.
/// </summary>
/// <param name="timespan">The timespan in seconds.</param>
public WebApiOutputCacheAttribute(int timespan)
{
_timespan = timespan;
}
public override void OnActionExecuting(HttpActionContext ac)
{
if (ac != null)
{
_cachekey = ac.Request.RequestUri.PathAndQuery.ToUpperInvariant();
if (!_webApiCache.Contains(_cachekey)) return;
var val = (string)_webApiCache.Get(_cachekey);
if (val == null) return;
ac.Response = ac.Request.CreateResponse();
ac.Response.Content = new StringContent(val);
var contenttype = (MediaTypeHeaderValue)_webApiCache.Get("response-ct") ?? new MediaTypeHeaderValue("application/rss+xml");
ac.Response.Content.Headers.ContentType = contenttype;
}
else
{
throw new ArgumentNullException("ac");
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (_webApiCache.Contains(_cachekey)) return;
var body = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
if (actionExecutedContext.Response.StatusCode == HttpStatusCode.OK)
{
lock (WebApiCache)
{
_wbApiCache.Add(_cachekey, body, DateTime.Now.AddSeconds(_timespan));
_webApiCache.Add("response-ct", actionExecutedContext.Response.Content.Headers.ContentType, DateTimeOffset.UtcNow.AddSeconds(_timespan));
}
}
}
}
The same WebApiOutputCacheAttribute instance can be used to cache multiple simultaneous requests, so you should not store cache keys on the instance of the attribute. Instead, regenerate the cache key during each request / method override. The following attribute works to cache HTTP GET requests.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using Newtonsoft.Json;
// based on strathweb implementation
// http://www.strathweb.com/2012/05/output-caching-in-asp-net-web-api/
public class CacheHttpGetAttribute : ActionFilterAttribute
{
public int Duration { get; set; }
public ILogExceptions ExceptionLogger { get; set; }
public IProvideCache CacheProvider { get; set; }
private bool IsCacheable(HttpRequestMessage request)
{
if (Duration < 1)
throw new InvalidOperationException("Duration must be greater than zero.");
// only cache for GET requests
return request.Method == HttpMethod.Get;
}
private CacheControlHeaderValue SetClientCache()
{
var cachecontrol = new CacheControlHeaderValue
{
MaxAge = TimeSpan.FromSeconds(Duration),
MustRevalidate = true,
};
return cachecontrol;
}
private static string GetServerCacheKey(HttpRequestMessage request)
{
var acceptHeaders = request.Headers.Accept;
var acceptHeader = acceptHeaders.Any() ? acceptHeaders.First().ToString() : "*/*";
return string.Join(":", new[]
{
request.RequestUri.AbsoluteUri,
acceptHeader,
});
}
private static string GetClientCacheKey(string serverCacheKey)
{
return string.Join(":", new[]
{
serverCacheKey,
"response-content-type",
});
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext == null) throw new ArgumentNullException("actionContext");
var request = actionContext.Request;
if (!IsCacheable(request)) return;
try
{
// do NOT store cache keys on this attribute because the same instance
// can be reused for multiple requests
var serverCacheKey = GetServerCacheKey(request);
var clientCacheKey = GetClientCacheKey(serverCacheKey);
if (CacheProvider.Contains(serverCacheKey))
{
var serverValue = CacheProvider.Get(serverCacheKey);
var clientValue = CacheProvider.Get(clientCacheKey);
if (serverValue == null) return;
var contentType = clientValue != null
? JsonConvert.DeserializeObject<MediaTypeHeaderValue>(clientValue.ToString())
: new MediaTypeHeaderValue(serverCacheKey.Substring(serverCacheKey.LastIndexOf(':') + 1));
actionContext.Response = actionContext.Request.CreateResponse();
// do not try to create a string content if the value is binary
actionContext.Response.Content = serverValue is byte[]
? new ByteArrayContent((byte[])serverValue)
: new StringContent(serverValue.ToString());
actionContext.Response.Content.Headers.ContentType = contentType;
actionContext.Response.Headers.CacheControl = SetClientCache();
}
}
catch (Exception ex)
{
ExceptionLogger.Log(ex);
}
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
try
{
var request = actionExecutedContext.Request;
// do NOT store cache keys on this attribute because the same instance
// can be reused for multiple requests
var serverCacheKey = GetServerCacheKey(request);
var clientCacheKey = GetClientCacheKey(serverCacheKey);
if (!CacheProvider.Contains(serverCacheKey))
{
var contentType = actionExecutedContext.Response.Content.Headers.ContentType;
object serverValue;
if (contentType.MediaType.StartsWith("image/"))
serverValue = actionExecutedContext.Response.Content.ReadAsByteArrayAsync().Result;
else
serverValue = actionExecutedContext.Response.Content.ReadAsStringAsync().Result;
var clientValue = JsonConvert.SerializeObject(
new
{
contentType.MediaType,
contentType.CharSet,
});
CacheProvider.Add(serverCacheKey, serverValue, new TimeSpan(0, 0, Duration));
CacheProvider.Add(clientCacheKey, clientValue, new TimeSpan(0, 0, Duration));
}
if (IsCacheable(actionExecutedContext.Request))
actionExecutedContext.ActionContext.Response.Headers.CacheControl = SetClientCache();
}
catch (Exception ex)
{
ExceptionLogger.Log(ex);
}
}
}
Just replace the CacheProvider with your MemoryCache.Default. In fact, the code above uses the same by default during development, and uses azure cache when deployed to a live server.
Even though your code resets the _cachekey instance field during each request, these attributes are not like controllers where a new one is created for each request. Instead, the attribute instance can be repurposed to service multiple simultaneous requests. So don't use an instance field to store it, regenerate it based on the request each and every time you need it.

Categories

Resources