I've found out today that for my site i could use a SiteMap provider which i've downloaded from Github for the MVC3, as my web application is MVC3.
Situation is following, my application is multilingual. I have a separate Library which contains all the resources. This Library is then added to my current project and everywhere where i need i use those resource files.
Now i've implemented the site map provider:
<mvcSiteMapNode title="$resources:Base,Home" controller="Home" action="Index" enableLocalization="true">
<mvcSiteMapNode title="Search" controller="Search" action="Index"/>
<mvcSiteMapNode title="Contact" controller="Contact" action="Index"/>
<mvcSiteMapNode title="About" controller="Home" action="About"/>
</mvcSiteMapNode>
But when i run it i get an error because it cannot find the resource with the key home. I think it is due to the fact that is outside of the application but in a separate Library.
How do i point to the resource file then, which is located din separate project?
The approach I would take would be to switch to external DI and then implement a custom IStringLocalizer class that can read the resources from another assembly. Here is a working example. I have created a demo application on GitHub as well.
using System;
using System.Collections.Specialized;
using System.Resources;
namespace MvcSiteMapProvider.Globalization
{
public class ResourceManagerStringLocalizer
: IStringLocalizer
{
public ResourceManagerStringLocalizer(
ResourceManager resourceManager
)
{
if (resourceManager == null)
throw new ArgumentNullException("resourceManager");
this.resourceManager = resourceManager;
}
protected readonly ResourceManager resourceManager;
/// <summary>
/// Gets the localized text for the supplied attributeName.
/// </summary>
/// <param name="attributeName">The name of the attribute (as if it were in the original XML file).</param>
/// <param name="value">The current object's value of the attribute.</param>
/// <param name="enableLocalization">True if localization has been enabled, otherwise false.</param>
/// <param name="classKey">The resource key from the ISiteMap class.</param>
/// <param name="implicitResourceKey">The implicit resource key.</param>
/// <param name="explicitResourceKeys">A <see cref="T:System.Collections.Specialized.NameValueCollection"/> containing the explicit resource keys.</param>
/// <returns></returns>
public virtual string GetResourceString(string attributeName, string value, bool enableLocalization, string classKey, string implicitResourceKey, NameValueCollection explicitResourceKeys)
{
if (attributeName == null)
{
throw new ArgumentNullException("attributeName");
}
if (enableLocalization)
{
string result = string.Empty;
if (explicitResourceKeys != null)
{
string[] values = explicitResourceKeys.GetValues(attributeName);
if ((values == null) || (values.Length <= 1))
{
result = value;
}
else if (this.resourceManager.BaseName.Equals(values[0]))
{
try
{
result = this.resourceManager.GetString(values[1]);
}
catch (MissingManifestResourceException)
{
if (!string.IsNullOrEmpty(value))
{
result = value;
}
}
}
}
if (!string.IsNullOrEmpty(result))
{
return result;
}
}
if (!string.IsNullOrEmpty(value))
{
return value;
}
return string.Empty;
}
}
}
Then you can inject it into your DI configuration module (StructureMap example shown, but any DI container will do).
First of all, you need to specify not to register the IStringLocalizer interface automatically by adding it to the excludeTypes variable.
var excludeTypes = new Type[] {
// Use this array to add types you wish to explicitly exclude from convention-based
// auto-registration. By default all types that either match I[TypeName] = [TypeName] or
// I[TypeName] = [TypeName]Adapter will be automatically wired up as long as they don't
// have the [ExcludeFromAutoRegistrationAttribute].
//
// If you want to override a type that follows the convention, you should add the name
// of either the implementation name or the interface that it inherits to this list and
// add your manual registration code below. This will prevent duplicate registrations
// of the types from occurring.
// Example:
// typeof(SiteMap),
// typeof(SiteMapNodeVisibilityProviderStrategy)
typeof(IStringLocalizer)
};
Then provide an explicit registration of the ResourceManagerStringLocalizer (and its dependencies) instead.
// Configure localization
// Fully qualified namespace.resourcefile (.resx) name without the extension
string resourceBaseName = "SomeAssembly.Resources.Resource1";
// A reference to the assembly where your resources reside.
Assembly resourceAssembly = typeof(SomeAssembly.Class1).Assembly;
// Register the ResourceManager (note that this is application wide - if you are
// using ResourceManager in your DI setup already you may need to use a named
// instance or SmartInstance to specify a specific object to inject)
this.For<ResourceManager>().Use(() => new ResourceManager(resourceBaseName, resourceAssembly));
// Register the ResourceManagerStringLocalizer (uses the ResourceManger)
this.For<IStringLocalizer>().Use<ResourceManagerStringLocalizer>();
Then it is just a matter of specifying the resources appropriately. You need to start them with the Base Name (in this case SomeAssembly.Resources.Resource1), and then specify the key of the resource as the second argument.
<mvcSiteMapNode title="$resources:SomeAssembly.Resources.Resource1,ContactTitle" controller="Home" action="Contact"/>
Note that getting the BaseName right is the key to making it work. See the following MSDN documentation: http://msdn.microsoft.com/en-us/library/yfsz7ac5(v=vs.110).aspx
I think it should be like this:
Assuming your:
ProjectName=MyProject
Folder which contains resources:LanguageFiles
Class in which resources are defined:Messaging
then.
It should be like this I believe..I am not sure but may be it works for you:
`title=#MyProject.LanguageFiles.Messaging.Home'
(assuming Home is the resource defined in the Messaging class)
Related
I am working on a WPF application and I want to architect my project using MEF (Manageable Extensibility Framework). But the problem is that I am getting this error when I try to run my application:
No exports were found that match the constraint: ContractName
MyFooPluginA RequiredTypeIdentity
namespace.of.my.core.project.IFooPlugin
Here is what I went ahead and created
The same concept applies to all plugin projects of type IBarPlugin. Here is how I am setting up my first plugin project:
The FooView.xaml.cs
/// <summary>
/// Interaction logic for FooView.xaml
/// </summary>
[Export(typeof(IFooPlugin)), PartCreationPolicy(CreationPolicy.Any)]
[ExportMetadata("Name", "MyFooPluginA")]
public partial class FooView: UserControl, IFooPlugin
{
[ImportingConstructor]
public FooView(FooViewModel viewModel) //we initialize the view first, then the view model
{
InitializeComponent();
DataContext = viewModel;
}
}
The FooViewModel:
[Export]
public class FooViewModel
{
[ImportingConstructor]
public FooViewModel(...) //Contains parameters for dependency injections
{
//doing some work here
}
}
Finally in the main application view model I am loading the plugins:
public class MainAppViewModel
{
/// <summary>
/// If one plugin of type IFooPlugin is found then it is loaded in this property
/// </summary>
public IFooPlugin FooPluginView
{
get
{
return _fooPluginView;
}
set
{
_fooPluginView= value;
RaisePropertyChanged(nameof(FooPluginView));
}
}
private IFooPlugin _fooPluginView;
/// <summary>
/// Stores the catalog of all exported dlls
/// </summary>
private AggregateCatalog catalog;
/// <summary>
/// Stores the catalog information and all its parts
/// </summary>
private CompositionContainer Container;
public MainAppViewModel()
{
InitPlugin();//first thing I want it to do
if (someConditon == true)
{
FooPluginView = Container.GetExport<IFooPlugin>("MyFooPluginA").Value;
}
else
{
FooPluginView = Container.GetExport<IFooPlugin>("MyFooPluginA").Value;
}
}
private void InitPlugin()
{
//First create a catalog of exports
//It can be TypeCatalog(typeof(ISomeView), typeof(SomeOtherImportType))
//to search for all exports by specified types
//DirectoryCatalog(pluginsPath, "App*.dll") to search specified directories
//and matching specified file name
//An aggregate catalog that combines multiple catalogs
catalog = new AggregateCatalog();
//Here we add all the parts found in all assemblies in directory of executing assembly directory
//with file name matching Plugin*.dll
string pluginsPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
catalog.Catalogs.Add(new DirectoryCatalog(pluginsPath, "*Plugin.dll"));
//also we add to a search path a subdirectory plugins
pluginsPath = Path.Combine(pluginsPath, "Plugins");
catalog.Catalogs.Add(new DirectoryCatalog(pluginsPath, "*Plugin.dll"));
//Create the CompositionContainer with the parts in the catalog.
Container = new CompositionContainer(catalog);
//Fill the imports of this object
//finds imports and fills in all preperties decorated
//with Import attribute in this instance
Container.ComposeParts(this);
}
}
I'm afraid this may not be the exact answer you want, But as far as I know Attribute values of [ExportMetadata] does not setting the ContractName. It's values are assigned as Metadata. So, you can change
FooPluginView = Container.GetExport<IFooPlugin>("MyFooPluginA").Value;
to
FooPluginView = Container.GetExports<IFooPlugin, IDictionary<string, object>>().Where(x => (string)x.Metadata["Name"] == "MyFooPluginA").FirstOrDefault().Value;
or similar. Also, It seems you didn't passed parameter to ImportingConstructor. I tried solution which explained in MEF Constructor Injection to your case like
[ImportingConstructor]
public UserControl1([Import("DI")]FooViewModel viewModel)
Container.ComposeExportedValue("DI",new FooViewModel(null));
FooPluginView = Container.GetExports<IFooPlugin, IDictionary<string, object>>().Where(x => (string)x.Metadata["Name"] == "MyFooPluginA").FirstOrDefault().Value;
and it works great in my environment. I hope this might help you.
I've read the Microsoft documentation of fundamentals for Options and Configuration, but still can't find the right way to extract configuration into an object while validating data annotations.
One approach I tried in Startup.ConfigureServices
services.AddOptions<EmailConfig>().Bind(Configuration.GetSection("Email")).ValidateDataAnnotations();
This "should" allow accessing the configuration by adding this in the class constructor: (IOptions<EmailConfig> emailConfig)
However it's not working.
Another approach is to add (IConfiguration configuration) to the constructor, but this doesn't allow me to call ValidateDataAnnotations.
configuration.GetSection("Email").Get<EmailConfig>();
First question: does the responsibility to bind and validate the configuration belong to the Startup class or to the class using it? If it's used by several classes I'd say it belongs to Startup; and the class could be used in another project with different configuration layout.
Second question: what is the correct syntax to bind and validate the configuration so it can be accessed from the class?
Third question: if I'm validating through data annotations in Startup, then the class using the configuration simply assumes the configuration is valid and I don't put any re-validation whatsoever?
UPDATE: After gaining more experience and reviewing the structure of all my code, I changed my approach to follow standard patterns.
The following code DOES work... but only validates it when used. This can be registered in a class library and won't throw any errors until the particular service is used.
services.AddOptions<EmailConfig>()
.Bind(configuration.GetSection("Email"))
.ValidateDataAnnotations();
Then, in Configure, I add this to force validation of needed configuration values at startup (CheckNotNull is a custom extension method, what matters is simply that you call IOptions.Value
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app?.ApplicationServices.GetService<IOptions<EmailConfig>>().Value.CheckNotNull("Config: Email");
app?.ApplicationServices.GetService<IOptions<OntraportConfig>>().Value.CheckNotNull("Config: Ontraport");
...
Then in the class using it
public class EmailService(IOptions<EmailConfig> config)
You can try validating the class yourself in start up before adding it to service collection.
Startup
var settings = Configuration.GetSection("Email").Get<EmailConfig>();
//validate
var validationResults = new List<ValidationResult>();
var validationContext = new ValidationContext(settings, serviceProvider: null, items: null);
if (!Validator.TryValidateObject(settings, validationContext, validationResults,
validateAllProperties: true)) {
//...Fail early
//will have the validation results in the list
}
services.AddSingleton(settings);
That way you are not coupled to IOptions and you also allow your code to fail early and you can explicitly inject the dependency where needed.
You could package the validation up into your own extension method like
public static T GetValid<T>(this IConfiguration configuration) {
var obj = configuration.Get<T>();
//validate
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
for calls like
EmailConfig emailSection = Configuration.GetSection("Email").GetValid<EmailConfig>();
services.AddSingleton(emailSection);
Internally, ValidateDataAnnotations is basically doing the same thing.
/// <summary>
/// Validates a specific named options instance (or all when name is null).
/// </summary>
/// <param name="name">The name of the options instance being validated.</param>
/// <param name="options">The options instance.</param>
/// <returns>The <see cref="ValidateOptionsResult"/> result.</returns>
public ValidateOptionsResult Validate(string name, TOptions options)
{
// Null name is used to configure all named options.
if (Name == null || name == Name)
{
var validationResults = new List<ValidationResult>();
if (Validator.TryValidateObject(options,
new ValidationContext(options, serviceProvider: null, items: null),
validationResults,
validateAllProperties: true))
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(String.Join(Environment.NewLine,
validationResults.Select(r => "DataAnnotation validation failed for members " +
String.Join(", ", r.MemberNames) +
" with the error '" + r.ErrorMessage + "'.")));
}
// Ignored if not validating this instance.
return ValidateOptionsResult.Skip;
}
Source Code
Update from the Future
Newer versions of .NET added more extension methods to simplify this.
Note: Technically these are all from Microsoft.Extensions.XYZ packages released alongside .NET. It's possible that these packages are compatible with earlier versions of .NET as well, but I haven't verified backward-compatibility.
OP's Example
services.AddOptions<EmailConfig>()
.Bind(configuration.GetSection("Email"))
.ValidateDataAnnotations();
Can now be simplified to:
// Requires .NET 5 extensions or greater
services.AddOptions<EmailConfig>()
.BindConfiguration("Email")
.ValidateDataAnnotations();
...and for eager validation at startup (rather than when options are used), we can add a single line:
// Requires .NET 6 extensions or greater
services.AddOptions<EmailConfig>()
.BindConfiguration("Email")
.ValidateDataAnnotations()
.ValidateOnStart();
Source/Credit
I learned about these updates from Andrew Lock's blog post. Credit and thanks go to him: Adding validation to strongly typed configuration objects in .NET 6
There is still no answer as to how ValidateDataAnnotations work, but based on Nkosi's answer, I wrote this class extension to easily run the validation on-demand. Because it's an extension on Object, I put it into a sub-namespace to only enable it when needed.
namespace Websites.Business.Validation {
/// <summary>
/// Provides methods to validate objects based on DataAnnotations.
/// </summary>
public static class ValidationExtensions {
/// <summary>
/// Validates an object based on its DataAnnotations and throws an exception if the object is not valid.
/// </summary>
/// <param name="obj">The object to validate.</param>
public static T ValidateAndThrow<T>(this T obj) {
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
/// <summary>
/// Validates an object based on its DataAnnotations and returns a list of validation errors.
/// </summary>
/// <param name="obj">The object to validate.</param>
/// <returns>A list of validation errors.</returns>
public static ICollection<ValidationResult> Validate<T>(this T obj) {
var Results = new List<ValidationResult>();
var Context = new ValidationContext(obj);
if (!Validator.TryValidateObject(obj, Context, Results, true))
return Results;
return null;
}
}
}
Then in Startup it's quite straightforward
EmailConfig EmailSection = Configuration.GetSection("Email").Get<EmailConfig>().ValidateAndThrow();
services.AddSingleton<EmailConfig>(EmailSection);
Works like a charm; actually works like I'd expect ValidateDataAnnotations to work.
You can also use a method to validate all IOptions in your IOC conainter
private void CheckConfiguration(IApplicationBuilder app, IServiceCollection services)
{
var optionsServiceDescriptors = services.Where(s => s.ServiceType.Name.Contains("IOptionsChangeTokenSource"));
foreach (var service in optionsServiceDescriptors)
{
var genericTypes = service.ServiceType.GenericTypeArguments;
if (genericTypes.Length > 0)
{
var optionsType = genericTypes[0];
var genericOptions = typeof(IOptions<>).MakeGenericType(optionsType);
dynamic instance = app.ApplicationServices.GetService(genericOptions);
var options = instance.Value;
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(options, new ValidationContext(options), results, true);
if (!isValid)
{
var messages = new List<string> { "Configuration issues" };
messages.AddRange(results.Select(r => r.ErrorMessage));
throw new Exception(string.Join("\n", messages));
}
}
}
}
You can find a example here : https://github.com/michelcedric/GetRequiredSectionSample/blob/feature/add-check-configuration/GetRequiredSectionSample/Startup.cs#L73
I am trying to add API versioning and my plan is to create a controller for each version in different namespace. My project structure looks like this (note: no separate area for each version)
Controllers
|
|---Version0
| |
| |----- ProjectController.cs
| |----- HomeController.cs
|
|---Version1
|
|----- ProjectController.cs
|----- HomeController.cs
I am using RoutingAttribute for the routes.
So, ProjectController in Version0 has function with route as
namespace MyProject.Controllers.Version0
{
class ProjectController : BaseController
{
...
[Route(api/users/project/getProjects/{projectId})]
public async GetProjects(string projectId)
{
...
}
}
}
and ProjectController in Version1 has function with route as
namespace MyProject.Controllers.Version1
{
class ProjectController : BaseController
{
...
[Route(api/v1/users/project/getProjects/{projectId})]
public async GetProjects(string projectId)
{
...
}
}
}
But, I get 404-NotFound when I am trying to hit the service.
If I rename the controllers to have unique name (Project1Controller and Project2Controller) the routing works. But, I am trying to avoid renaming for simplicity.
I followed this link to resolve the issue, but it didn't help. I did create areas but still no success. Adding routing logic in global.aspx file do not help. The namespace do not work either.
http://haacked.com/archive/2010/01/12/ambiguous-controller-names.aspx/
The above link suggest to create areas, but the attribute routing do not support areas as per link:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Is there another solution? A bug with RoutingAttributes?
Thank you!
First, Web API routing, and MVC routing doesn't work exactly in the same way.
Your first link points to MVC routing, with areas. Areas are not officially supported for Web API, although you can try to make something similar to them. However, even if you try to do something like that, you'll get the same error, because the way in wich Web API looks for a controller doesn't takes into account the controller's namespace.
So, out of the box, it will never work.
However, you can modify most Web API behaviors, and this is not an exception.
Web API uses a Controller Selector to get the desired controller. The behavior explained above is the behavior of the DefaultHttpControllerSelector, which comes with Web API, but you can implement your own selector to replace the default one, and support new behaviors.
If you google for "custom web api controller selector" you'll find many samples, but I find this the most interesting for exactly your problem:
ASP.NET Web API: Using Namespaces to Version Web APIs
This implementation is also interesting:
https://github.com/WebApiContrib/WebAPIContrib/pull/111/files (thank you to Robin van der Knaap for the update of this broken link)
As you see there, basically you need to:
implement your own IHttpControllerSelector, which takes into account namespaces to find the controllers, and the namespaces route variable, to choose one of them.
replace the original selector with this via Web API configuration.
I know this was answered a while a go and has already been accepted by the original poster. However if you are like me and require the use of attribute routing and have tried the suggested answer you will know that it wont quite work.
When I tried this I found out that it was actually missing the routing information that should have been generated by calling the extension method MapHttpAttributeRoutes of theHttpConfiguration class:
config.MapHttpAttributeRoutes();
This meant that the method SelectController of the replacement IHttpControllerSelector implementation never actually gets called and is why the request produces a http 404 response.
The issue is caused by an internal class called HttpControllerTypeCache which is an internal class in the System.Web.Http assembly under the System.Web.Http.Dispatcher namespace. The code in question is the following:
private Dictionary<string, ILookup<string, Type>> InitializeCache()
{
return this._configuration.Services.GetHttpControllerTypeResolver().GetControllerTypes(this._configuration.Services.GetAssembliesResolver()).GroupBy<Type, string>((Func<Type, string>) (t => t.Name.Substring(0, t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length)), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>((Func<IGrouping<string, Type>, string>) (g => g.Key), (Func<IGrouping<string, Type>, ILookup<string, Type>>) (g => g.ToLookup<Type, string>((Func<Type, string>) (t => t.Namespace ?? string.Empty), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase)), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
}
You will see in this code that it is grouping by the type name without the namespace. The DefaultHttpControllerSelector class uses this functionality when it builds up an internal cache of HttpControllerDescriptor for each controller. When using the MapHttpAttributeRoutes method it use another internal class called AttributeRoutingMapper which is part of the System.Web.Http.Routing namespace. This class uses the method GetControllerMapping of the IHttpControllerSelector in order to configure the routes.
So if you are going to write a custom IHttpControllerSelector then you need to overload the GetControllerMapping method for it to work. The reason I mention this is that none of the implementations I have seen on the internet does this.
Based on #JotaBe answer I've developed my own IHttpControllerSelector which allows controllers (in my case those which are tagged with [RoutePrefix] attribute) to be mapped with their full name (Namespace AND name).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Routing;
/// <summary>
/// Allows the use of multiple controllers with same name (obviously in different namespaces)
/// by prepending controller identifier with their namespaces (if they have [RoutePrefix] attribute).
/// Allows attribute-based controllers to be mixed with explicit-routes controllers without conflicts.
/// </summary>
public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _configuration;
private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
public NamespaceHttpControllerSelector(HttpConfiguration httpConfiguration) : base(httpConfiguration)
{
_configuration = httpConfiguration;
_controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
}
public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
{
return _controllers.Value; // just cache the list of controllers, so we load only once at first use
}
/// <summary>
/// The regular DefaultHttpControllerSelector.InitializeControllerDictionary() does not
/// allow 2 controller types to have same name even if they are in different namespaces (they are ignored!)
///
/// This method will map ALL controllers, even if they have same name,
/// by prepending controller names with their namespaces if they have [RoutePrefix] attribute
/// </summary>
/// <returns></returns>
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
// simple alternative? in case you want to map maybe "UserAPI" instead of "UserController"
// var controllerTypes = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
// .Where(t => t.IsClass && t.IsVisible && !t.IsAbstract && typeof(IHttpController).IsAssignableFrom(t));
var controllers = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
foreach (Type t in controllerTypes)
{
var controllerName = t.Name;
// ASP.NET by default removes "Controller" suffix, let's keep that convention
if (controllerName.EndsWith(ControllerSuffix))
controllerName = controllerName.Remove(controllerName.Length - ControllerSuffix.Length);
// For controllers with [RoutePrefix] we'll register full name (namespace+name).
// Those routes when matched they provide the full type name, so we can match exact controller type.
// For other controllers we'll register as usual
bool hasroutePrefixAttribute = t.GetCustomAttributes(typeof(RoutePrefixAttribute), false).Any();
if (hasroutePrefixAttribute)
controllerName = t.Namespace + "." + controllerName;
if (!controllers.Keys.Contains(controllerName))
controllers[controllerName] = new HttpControllerDescriptor(_configuration, controllerName, t);
}
return controllers;
}
/// <summary>
/// For "regular" MVC routes we will receive the "{controller}" value in route, and we lookup for the controller as usual.
/// For attribute-based routes we receive the ControllerDescriptor which gives us
/// the full name of the controller as registered (with namespace), so we can version our APIs
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor controller;
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
IDictionary<string, HttpControllerDescriptor> controllersWithoutAttributeBasedRouting =
GetControllerMapping().Where(kv => !kv.Value.ControllerType
.GetCustomAttributes(typeof(RoutePrefixAttribute), false).Any())
.ToDictionary(kv => kv.Key, kv => kv.Value);
var route = request.GetRouteData();
// regular routes are registered explicitly using {controller} route - and in case we'll match by the controller name,
// as usual ("CourseController" is looked up in dictionary as "Course").
if (route.Values != null && route.Values.ContainsKey("controller"))
{
string controllerName = (string)route.Values["controller"];
if (controllersWithoutAttributeBasedRouting.TryGetValue(controllerName, out controller))
return controller;
}
// For attribute-based routes, the matched route has subroutes,
// and we can get the ControllerDescriptor (with the exact name that we defined - with namespace) associated, to return correct controller
if (route.GetSubRoutes() != null)
{
route = route.GetSubRoutes().First(); // any sample route, we're just looking for the controller
// Attribute Routing registers a single route with many subroutes, and we need to inspect any action of the route to get the controller
if (route.Route != null && route.Route.DataTokens != null && route.Route.DataTokens["actions"] != null)
{
// if it wasn't for attribute-based routes which give us the ControllerDescriptor for each route,
// we could pick the correct controller version by inspecting version in accepted mime types in request.Headers.Accept
string controllerTypeFullName = ((HttpActionDescriptor[])route.Route.DataTokens["actions"])[0].ControllerDescriptor.ControllerName;
if (controllers.TryGetValue(controllerTypeFullName, out controller))
return controller;
}
}
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
I am experimenting with creating my own custom HTTP Context:
CustomHttpContext : HttpContextBase
{
public override HttpRequestBase Request { }
}
One thing i can't figure out is how to initialize the base class with
System.Web.HttpContext.Current
Does anyone have any ideas how i can initialise the custom context first with the Current Http then override certain Methods/Properties to serve my own purpose?
The simple answer is no, it's not possible. Also note that HttpContext does not inherit from HttpContextBase, instead, they both implement IServiceProvider. Finally, HttpContext is sealed, suggesting that the authors did not want people to do anything other than consume this class.
As you are no doubt annoyed by HttpContextBase has a parameterless constructor so does not even give you the option of instantiating it from the current request and response like HttpContext!
Let's use a 'decompiler' to take a look at the implementation of HttpContext.Current:
// System.Web.HttpContext
/// <summary>Gets or sets the <see cref="T:System.Web.HttpContext" /> object for the current HTTP request.</summary>
/// <returns>The <see cref="T:System.Web.HttpContext" /> for the current HTTP request.</returns>
public static HttpContext Current
{
get
{
return ContextBase.Current as HttpContext;
}
set
{
ContextBase.Current = value;
}
}
If we take a look at ContextBase.Current (from System.Web.Hosting.ContextBase):
// System.Web.Hosting.ContextBase
internal static object Current
{
get
{
return CallContext.HostContext;
}
[SecurityPermission(SecurityAction.Demand, Unrestricted = true)]
set
{
CallContext.HostContext = value;
}
}
and CallContext (in System.Runtime.Messaging):
// System.Runtime.Remoting.Messaging.CallContext
/// <summary>Gets or sets the host context associated with the current thread.</summary>
/// <returns>The host context associated with the current thread.</returns>
/// <exception cref="T:System.Security.SecurityException">The immediate caller does not have infrastructure permission. </exception>
public static object HostContext
{
[SecurityCritical]
get
{
IllogicalCallContext illogicalCallContext = Thread.CurrentThread.GetIllogicalCallContext();
object hostContext = illogicalCallContext.HostContext;
if (hostContext == null)
{
LogicalCallContext logicalCallContext = CallContext.GetLogicalCallContext();
hostContext = logicalCallContext.HostContext;
}
return hostContext;
}
[SecurityCritical]
set
{
if (value is ILogicalThreadAffinative)
{
IllogicalCallContext illogicalCallContext = Thread.CurrentThread.GetIllogicalCallContext();
illogicalCallContext.HostContext = null;
LogicalCallContext logicalCallContext = CallContext.GetLogicalCallContext();
logicalCallContext.HostContext = value;
return;
}
LogicalCallContext logicalCallContext2 = CallContext.GetLogicalCallContext();
logicalCallContext2.HostContext = null;
IllogicalCallContext illogicalCallContext2 = Thread.CurrentThread.GetIllogicalCallContext();
illogicalCallContext2.HostContext = value;
}
}
We start to get a feel for how the HttpContext is being retrieved. It's being packaged in with the thread the current user started when they visted the website (which makes perfect sense!). Delving further we can see it also gets recreated per request (see below).
We can also see, at the interface layer, HttpContext.Current cannot be changed to point at your own HttpContext as the property is not virtual. It also uses many BCL classes that are private or internal so you can't simply copy most of the implementation.
What would be easier, and also less prone to any other issues would be to simply wrap HttpContext with your own CustomContext object. You could simply wrap HttpContext.Current in a BaseContext property, then have your own properties on the class (and use whatever session, database, or request based state storage mechanism you want to store and retrieve your own properties).
Personally, I'd use my own class for storing my own information, as it belongs to my application and user etc and isn't really anything to do with the http pipeline or request/response processing.
See also:
ASP.NET MVC : How to create own HttpContext
How is HttpContext being maintained over request-response
Just to add on a bit to dash's answer, you can also use the [ThreadStatic] attribute with some static property. Initialize it on BeginRequest, either by using global.cs or by writing your own HttpModule/HttpHandler.
How to create a web module:
http://msdn.microsoft.com/en-us/library/ms227673(v=vs.100).aspx
Thread static:
http://msdn.microsoft.com/en-us/library/system.threadstaticattribute.aspx
I'm attempting to create a framework for allowing controllers and views to be dynamically imported into an MVC application. Here's how it works so far:
I'm using .NET 4, ASP.NET MVC 3 RC and the Razor ViewEngine
Controllers are exported and imported using MEF per project - I call a set of controllers and views from a given project a "Module"
Assemblies discovered using MEF are dynamically referenced by the BuildManager using a pre-application start method and BuildManager.AddReferencedAssembly.
Binaries (from exporting project) and Views are copied into the target project's folder structure using a build event
Controllers are selected using a custom controller factory which inherits from DefaultControllerFactory and overrides GetControllerType()
Views are selected using a custom view engine which inherits from RazorViewEngine and overrides GetView() and GetPartialView() to allow it to look for views in Module-specific view directories
Everything works so far except for views using a strongly typed model. Views that use the dynamic model work fine, but when I specify a model type using #model, I get a YSOD that says "The view 'Index' or its master was not found".
When debugging my ViewEngine implementation, I can see that:
this.VirtualPathProvider.FileExists(String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller"))) returns true, while
this.FileExists(controllerContext, String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller"))) returns false.
Looking in Reflector, the RazorViewEngine implementation of FileExists() ultimately winds up doing this:
return (BuildManager.GetObjectFactory(virtualPath, false) != null);
However, I can't view BuildManager.GetObjectFactory() from Reflector because it's hidden somehow.
I'm suspecting that it has something to do with the fact that the model type is a type that is loaded from MEF, but since I'm already referencing the assemblies discovered by MEF from BuildManager, I'm out of leads. Can anyone provide a little more insight into what might be going on?
Update:
Turns out I was using an outdated version of Reflector from before .NET 4. I can see GetObjectFactory() now, but I can't really seem to find anything helpful. I've tried adding this into my FindView() overload:
try
{
var path = String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller"));
var objFactory = System.Web.Compilation.BuildManager.GetObjectFactory(virtualPath: path, throwIfNotFound: true);
}
catch
{
}
Unfortunately, objFactory ends up null, and no exception gets thrown. All the bits that deal with compilation errors are part of private methods or types so I can't debug any of that, but it even seems like they'd end up throwing an exception, which doesn't seem to be happening. Looks like I'm at a dead end again. Help!
Update 2
I've discovered that at the point where FindView() is being called, if I call AppDomain.CurrentDomain.GetAssemblies(), the assembly that the model type is in is included. However, I cannot load the type using Type.GetType().
Update 3
Here's what I'm seeing:
Update 4
Here's the ViewEngine implementation:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Web.Hosting;
using System.Web.Compilation;
namespace Site.Admin.Portal
{
public class ModuleViewEngine : RazorViewEngine
{
private static readonly String[] viewLocationFormats = new String[]
{
"~/Views/{0}/{{1}}/{{0}}.aspx",
"~/Views/{0}/{{1}}/{{0}}.ascx",
"~/Views/{0}/{{1}}/{{0}}.cshtml",
"~/Views/{0}/Shared/{{0}}.aspx",
"~/Views/{0}/Shared/{{0}}.ascx",
"~/Views/{0}/Shared/{{0}}.cshtml"
};
public ModuleViewEngine(IModule module)
{
this.Module = module;
var formats = viewLocationFormats.Select(f => String.Format(f, module.Name)).ToArray();
this.ViewLocationFormats = formats;
this.PartialViewLocationFormats = formats;
this.AreaViewLocationFormats = formats;
this.AreaPartialViewLocationFormats = formats;
this.AreaMasterLocationFormats = formats;
}
public IModule Module { get; private set; }
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, String partialViewName, Boolean useCache)
{
var moduleName = controllerContext.RouteData.GetRequiredString("module");
if (moduleName.Equals(this.Module.Name, StringComparison.InvariantCultureIgnoreCase))
{
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
else return new ViewEngineResult(new String[0]);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, String viewName, String masterName, Boolean useCache)
{
var moduleName = controllerContext.RouteData.GetRequiredString("module");
if (moduleName.Equals(this.Module.Name, StringComparison.InvariantCultureIgnoreCase))
{
var baseResult = base.FindView(controllerContext, viewName, masterName, useCache);
return baseResult;
}
else return new ViewEngineResult(new String[0]);
}
}
}
Based on Update 2, I'm guessing what you've got is an explicitly loaded copy of your assembly (that is, it was loaded through some other method than Load, like LoadFrom). Explicitly loaded assemblies are set off aside into a special place, because they are not allowed to satisfy implicit type requirements. The rules for Fusion (the assembly loader) can be pretty arcane and hard to understand.
I agree with Matthew's assessment that, to get this to work, your DLL is going to have to be in /bin or else it will never be able to satisfy the implicit type requirement.
The imported libaries aren't in the /bin directory so aren't probed when trying to resolve references. I discovered a work around which I published in my MVC + MEF article (Part 2). Essentially you need to add your directories where your extensions sit to the probing path of the AppDomain.
Essentially where I am building my container:
/// <summary>
/// Creates the composition container.
/// </summary>
/// <returns></returns>
protected virtual CompositionContainer CreateCompositionContainer()
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(MapPath("~/bin")));
var config = CompositionConfigurationSection.GetInstance();
if (config != null && config.Catalogs != null) {
config.Catalogs
.Cast<CatalogConfigurationElement>()
.ForEach(c =>
{
if (!string.IsNullOrEmpty(c.Path)) {
string path = c.Path;
if (path.StartsWith("~"))
path = MapPath(path);
foreach (var directoryCatalog in GetDirectoryCatalogs(path)) {
// Register our path for probing.
RegisterPath(directoryCatalog.FullPath);
// Add the catalog.
catalog.Catalogs.Add(directoryCatalog);
}
}
});
}
var provider = new DynamicInstantiationExportProvider();
var container = new CompositionContainer(catalog, provider);
provider.SourceProvider = container;
return container;
}
I register all the directories of catalogs in the current domain:
/// <summary>
/// Registers the specified path for probing.
/// </summary>
/// <param name="path">The probable path.</param>
private void RegisterPath(string path)
{
AppDomain.CurrentDomain.AppendPrivatePath(path);
}
I believe the same should work for MVC3.
UPDATE: Correct me if I am wrong, but I don't believe that ViewEngines are instantiated once per request, you create a single instance that you register with MVC. Because of this only one IModule instance is ever used with your ViewEngine, so if a path doesn't match that first IModule.Name it won't be found? Does that make sense?