How do I make MvcSiteMapProvider create 1 SiteMap per Area? - asp.net-mvc

How do you make 1 SiteMap per MVC area and use MvcSiteMapNodeAttribute at the same time?

Please have a look at this answer for help with setting up MvcSiteMapProvider with areas. The routes have to be configured using the correct conventions or it won't work right.
However, that alone isn't going to address this requirement, because there is no default assumption made that you want to have a different SiteMap per area.
The behavior of the internal DI container assumes that there will be 1 SiteMap per domain name, and that all of the SiteMaps in the application will be built using the same configuration. There is no way to change this behavior unless you use an external DI container and follow the instructions in Multiple SiteMaps in One Application to override it.
Option 1
You could continue using the internal DI container and a single SiteMap for the entire website and you could create a custom ISiteMapNodeVisibilityProvider that hides everything that is not in the current area by reading the area from the current request.
public class AreaSiteMapNodeVisibilityProvider
: SiteMapNodeVisibilityProviderBase
{
public AreaSiteMapNodeVisibilityProvider()
{
// NOTE: Accept this as a constructor parameter if using external DI and
// use a guard clause to ensure it is not null.
this.mvcContextFactory = new MvcSiteMapProvider.Web.Mvc.MvcContextFactory();
}
private readonly MvcSiteMapProvider.Web.Mvc.IMvcContextFactory mvcContextFactory;
#region ISiteMapNodeVisibilityProvider Members
public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var area = requestContext.RouteData.DataTokens["area"];
var areaName = area == null ? string.Empty : area.ToString();
return string.Equals(node.Area, areaName, StringComparison.OrdinalIgnoreCase);
}
#endregion
}
Then set it up as the default visibility provider.
<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MyNameSpace.AreaSiteMapNodeVisibilityProvider, MyAssemblyName" />
Using external DI (StructureMap example shown):
// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
.Ctor<string>("defaultProviderName").Is("MyNameSpace.AreaSiteMapNodeVisibilityProvider, MyAssemblyName");
Do note that you will still need to nest your area nodes below the non-area part of the site if you do this, so it might not behave as you would like. You need to ensure you set the parent key of the Admin area to a key of a node in the non-area part - there can only be 1 root node per SiteMap.
Also, if you go this route, be sure to set the MvcSiteMapProvider_VisibilityAffectsDescendants setting to "false" so your area nodes are not affected by the visibility of the non-area nodes.
Option 2
Inject a custom ISiteMapCacheKeyGenerator that is based on area and use the SiteMapCacheKey property of [MvcSiteMapNode] attribute to control which area the node belongs to.
public class AreaSiteMapCacheKeyGenerator
: ISiteMapCacheKeyGenerator
{
public AreaSiteMapCacheKeyGenerator(
IMvcContextFactory mvcContextFactory
)
{
if (mvcContextFactory == null)
throw new ArgumentNullException("mvcContextFactory");
this.mvcContextFactory = mvcContextFactory;
}
protected readonly IMvcContextFactory mvcContextFactory;
#region ISiteMapCacheKeyGenerator Members
public virtual string GenerateKey()
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var area = requestContext.RouteData.DataTokens["area"];
return area == null ? "default" : area.ToString();
}
#endregion
}
You need to inject this using external DI (StructureMap example shown):
this.For<ISiteMapCacheKeyGenerator>().Use<AreaSiteMapCacheKeyGenerator>();
And then configure your [MvcSiteMapNode] attributes:
[MvcSiteMapNode(Title = "title", Description = "desc", Key = "root", ParentKey = null, ImageUrl = "fa-home", Order = 0, SiteMapCacheKey = "Admin")]
[MvcSiteMapNode(Title = "title", Description = "desc", Key = "root", ParentKey = null, ImageUrl = "fa-home", Order = 0, SiteMapCacheKey = "default")]
Option 3
Rather than setting SiteMapCacheKey on every [MvcSiteMapNode] attribute, you could put each area in a separate assembly and configure it to only scan the pertinent area assembly for [MvcSiteMapNode] attribute.
public class AreaSiteMapCacheKeyGenerator
: ISiteMapCacheKeyGenerator
{
public AreaSiteMapCacheKeyGenerator(
IMvcContextFactory mvcContextFactory
)
{
if (mvcContextFactory == null)
throw new ArgumentNullException("mvcContextFactory");
this.mvcContextFactory = mvcContextFactory;
}
protected readonly IMvcContextFactory mvcContextFactory;
#region ISiteMapCacheKeyGenerator Members
public virtual string GenerateKey()
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var area = requestContext.RouteData.DataTokens["area"];
return area == null ? "default" : area.ToString();
}
#endregion
}
public class OneToOneSiteMapCacheKeyToBuilderSetMapper
: ISiteMapCacheKeyToBuilderSetMapper
{
public virtual string GetBuilderSetName(string cacheKey)
{
return cacheKey;
}
}
In the external DI module (StructureMap example shown):
// Setup the cache
var cacheDependency = this.For<ICacheDependency>().Use<NullCacheDependency>();
var cacheDetails = this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(cacheDependency);
// Register the ISiteMapNodeProvider instances
var defaultNodeProvider = this.For<ISiteMapNodeProvider>().Use<ReflectionSiteMapNodeProvider>()
.Ctor<bool>("includeAssemblies").Is(new string[] { "dllmain" });
var adminNodeProvider = this.For<ISiteMapNodeProvider>().Use<ReflectionSiteMapNodeProvider>()
.Ctor<bool>("includeAssemblies").Is(new string[] { "dll2" });
// Register the ISiteMapBuilder instances
var defaultBuilder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
.Ctor<ISiteMapNodeProvider>().Is(defaultNodeProvider);
var adminBuilder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
.Ctor<ISiteMapNodeProvider>().Is(adminNodeProvider);
// Register the builder sets
this.For<ISiteMapBuilderSetStrategy>().Use<SiteMapBuilderSetStrategy>()
.EnumerableOf<ISiteMapBuilderSet>().Contains(x =>
{
// SiteMap builder for the non-area part of the site
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("default")
.Ctor<bool>("securityTrimmingEnabled").Is(false)
.Ctor<bool>("enableLocalization").Is(false)
.Ctor<bool>("visibilityAffectsDescendants").Is(false)
.Ctor<bool>("useTitleIfDescriptionNotProvided").Is(true)
.Ctor<ISiteMapBuilder>().Is(defaultBuilder)
.Ctor<ICacheDetails>().Is(cacheDetails);
// SiteMap builder for the Admin area of the site
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("Admin")
.Ctor<bool>("securityTrimmingEnabled").Is(false)
.Ctor<bool>("enableLocalization").Is(false)
.Ctor<bool>("visibilityAffectsDescendants").Is(false)
.Ctor<bool>("useTitleIfDescriptionNotProvided").Is(true)
.Ctor<ISiteMapBuilder>().Is(adminBuilder)
.Ctor<ICacheDetails>().Is(cacheDetails);
});
// Register the custom ISiteMapCacheKeyGenerator and ISiteMapCacheKeyToBuilderSetMapper
this.For<ISiteMapCacheKeyGenerator>().Use<AreaSiteMapCacheKeyGenerator>();
this.For<ISiteMapCacheKeyToBuilderSetMapper>().Use<OneToOneSiteMapCacheKeyToBuilderSetMapper>();

Related

.NET Core WebAPI fall-back API-version in case of missing minor version

After many tries and read articles I decided to place my issue here. What I want is the following: I am working on api-versioning of an application. A supported version format by .NET Core (Microsoft.AspNetCore.Mvc.Versioning package) is Major.Minor, and this is what I want to use in the project I work on. What I want is to have is a fall-back version in case when the minor version is not specified by the client.
I am using .NET core 2.2, and using api-version specified in the header. The corresponding API versioning config looks like this:
services.AddApiVersioning(options => {
options.ReportApiVersions = true;
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
options.ErrorResponses = new ApiVersioningErrorResponseProvider();
});
I have the following two controllers for each version: (the controllers are simplified for the sake of this SO question):
[ApiVersion("1.0")]
[Route("api/[controller]")]
public class ValueControllerV10 : Controller
{
[HttpGet(Name = "collect")]
public String Collect()
{
return "Version 1.0";
}
}
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class ValueControllerV11 : Controller
{
[HttpGet(Name = "collect")]
public String Collect()
{
return "Version 1.1";
}
}
If the client specifies api-version=1.0 then the ValueControllerV10 is used. And of course if the client specifies api-version=1.1, then the ValueControllerV11 is used, as expected.
And now comes my problem. If the client specifies api-version=1 (so only the major version without the minor version), then the ValueControllerV10 is used. It is because ApiVersion.Parse("1") is equal to ApiVersion.Parse("1.0"), if i am not mistaken. But what I want in this case is to invoke the latest version of the given major version, which is 1.1 in my example.
My attempts:
First: Specifying [ApiVersion("1")] at ValueControllerV11
[ApiVersion("1")]
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class ValueControllerV11 : Controller
{
[HttpGet(Name = "collect")]
public String Collect()
{
return "Version 1.1";
}
}
It does not work, it leads
AmbiguousMatchException: The request matched multiple endpoints
To solve this, I have came up with the second approach:
Second: using custom IActionConstraint. For this I followed these articles:
https://stevenknox.net/aspnet-core-mvc-action-priority-using-actionconstraints/
https://www.strathweb.com/2017/06/using-iactionconstraints-in-asp-net-core-mvc/
I have then created the following class:
[AttributeUsage(AttributeTargets.Method)]
public class HttpRequestPriority : Attribute, IActionConstraint
{
public int Order
{
get
{
return 0;
}
}
public bool Accept(ActionConstraintContext context)
{
var requestedApiVersion = context.RouteContext.HttpContext.GetRequestedApiVersion();
if (requestedApiVersion.MajorVersion.Equals(1) && !requestedApiVersion.MinorVersion.HasValue)
{
return true;
}
return false;
}
}
And used at ValueControllerV11:
[ApiVersion("1")]
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class ValueControllerV11 : Controller
{
[HttpGet(Name = "collect")]
[HttpRequestPriority]
public String Collect()
{
return "Version 1.1";
}
}
Well, it solves the AmbiguousMatchException, but overrides the default behaviour of Microsoft.AspNetCore.Mvc.Versioning package so if the client uses api-version 1.1, then she get a 404 Not Found back, which is understandable according to the implementation of HttpRequestPriority
Third: Using MapSpaFallbackRoute in Startup.cs, conditionally:
app.MapWhen(x => x.GetRequestedApiVersion().Equals("1") && x.GetRequestedApiVersion().MinorVersion == null, builder =>
{
builder.UseMvc(routes =>
{
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new {controller = nameof(ValueControllerV11), action = "Collect"});
});
});
app.UseMvc();
It does not work either, no any impact. The name MapSpaFallbackRoute gives me also a feeling that it is not what I need to use...
So my question is: How can I introduce a fallback 'use latest' behaviour for the case when the minor version is not specified in api-version? Thanks in advance!
This is intrinsically not supported out-of-the-box. Floating versions, ranges, and so on are contrary to the principles of API Versioning. An API version does not, and cannot, imply any backward compatibility. Unless you control both sides in a closed system, assuming that a client can handle any contract change, even if you only add one new member, is a fallacy. Ultimately, if a client asks for 1/1.0 then that's what they should get or the server should say it's not supported.
My opinion aside, some people still want this type of behavior. It's not particularly straight forward, but you should be able to achieve your goal using a custom IApiVersionRoutePolicy or custom endpoint matcher - it depends on the style of routing you're using.
If you still using the legacy routing, this may be the easiest because you just create a new policy or extend the existing DefaultApiVersionRoutePolicy by overriding OnSingleMatch and register it in your service configuration. You'll know it's the scenario you're looking for because the incoming API version will not have the minor version. You are correct that 1 and 1.0 will equate as the same, but the minor version is not coalesced; therefore, ApiVersion.MinorVersion will be null in this scenario.
If you're using Endpoint Routing, you'll need to replace the ApiVersionMatcherPolicy. The following should be close to what you want to achieve:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static Microsoft.AspNetCore.Mvc.Versioning.ApiVersionMapping;
public sealed class MinorApiVersionMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
public MinorApiVersionMatcherPolicy(
IOptions<ApiVersioningOptions> options,
IReportApiVersions reportApiVersions,
ILoggerFactory loggerFactory )
{
DefaultMatcherPolicy = new ApiVersionMatcherPolicy(
options,
reportApiVersions,
loggerFactory );
Order = DefaultMatcherPolicy.Order;
}
private ApiVersionMatcherPolicy DefaultMatcherPolicy { get; }
public override int Order { get; }
public bool AppliesToEndpoints( IReadOnlyList<Endpoint> endpoints ) =>
DefaultMatcherPolicy.AppliesToEndpoints( endpoints );
public async Task ApplyAsync(
HttpContext httpContext,
EndpointSelectorContext context,
CandidateSet candidates )
{
var requestedApiVersion = httpContext.GetRequestedApiVersion();
var highestApiVersion = default( ApiVersion );
var explicitIndex = -1;
var implicitIndex = -1;
// evaluate the default policy
await DefaultMatcherPolicy.ApplyAsync( httpContext, context, candidates );
if ( requestedApiVersion.MinorVersion.HasValue )
{
// we're done because a minor version was specified
return;
}
var majorVersion = requestedApiVersion.MajorVersion;
for ( var i = 0; i < candidates.Count; i++ )
{
// make all candidates invalid by default
candidates.SetValidity( i, false );
var candidate = candidates[i];
var action = candidate.Endpoint.Metadata?.GetMetadata<ActionDescriptor>();
if ( action == null )
{
continue;
}
var model = action.GetApiVersionModel( Explicit | Implicit );
var maxApiVersion = model.DeclaredApiVersions
.Where( v => v.MajorVersion == majorVersion )
.Max();
// remember the candidate with the next highest api version
if ( highestApiVersion == null || maxApiVersion >= highestApiVersion )
{
highestApiVersion = maxApiVersion;
switch ( action.MappingTo( maxApiVersion ) )
{
case Explicit:
explicitIndex = i;
break;
case Implicit:
implicitIndex = i;
break;
}
}
}
if ( explicitIndex < 0 && ( explicitIndex = implicitIndex ) < 0 )
{
return;
}
var feature = httpContext.Features.Get<IApiVersioningFeature>();
// if there's a match:
//
// 1. make the candidate valid
// 2. clear any existing endpoint (ex: 400 response)
// 3. set the requested api version to the resolved value
candidates.SetValidity( explicitIndex, true );
context.Endpoint = null;
feature.RequestedApiVersion = highestApiVersion;
}
}
Then you'll need to update you service configuration like this:
// IMPORTANT: must be configured after AddApiVersioning
services.Remove( services.Single( s => s.ImplementationType == typeof( ApiVersionMatcherPolicy ) ) );
services.TryAddEnumerable( ServiceDescriptor.Singleton<MatcherPolicy, MinorApiVersionMatcherPolicy>() );
If we consider a controller like this:
[ApiController]
[ApiVersion( "2.0" )]
[ApiVersion( "2.1" )]
[ApiVersion( "2.2" )]
[Route( "api/values" )]
public class Values2Controller : ControllerBase
{
[HttpGet]
public string Get( ApiVersion apiVersion ) =>
$"Controller = {GetType().Name}\nVersion = {apiVersion}";
[HttpGet]
[MapToApiVersion( "2.1" )]
public string Get2_1( ApiVersion apiVersion ) =>
$"Controller = {GetType().Name}\nVersion = {apiVersion}";
[HttpGet]
[MapToApiVersion( "2.2" )]
public string Get2_2( ApiVersion apiVersion ) =>
$"Controller = {GetType().Name}\nVersion = {apiVersion}";
}
When you request api/values?api-version=2, you'll match 2.2.
I'll reiterate that this is generally not a good idea as clients should be able to rely on stable versions. Using the status in the version may be more appropriate if you want pre-release APIs (ex: 2.0-beta1).
I hope that helps.
Well, the credits for answering the question goes to #Chris Martinez, on the other hand I could figure out another way to solve my issue:
I have namely created an extension for RouteAttribute, implementing IActionConstraintFactory:
public class RouteWithVersionAttribute : RouteAttribute, IActionConstraintFactory
{
private readonly IActionConstraint _constraint;
public bool IsReusable => true;
public RouteWithVersionAttribute(string template, params string[] apiVersions) : base(template)
{
Order = -10; //Minus value means that the api-version specific route to be processed before other routes
_constraint = new ApiVersionHeaderConstraint(apiVersions);
}
public IActionConstraint CreateInstance(IServiceProvider services)
{
return _constraint;
}
}
Where the IActionContraint looks like the following:
public class ApiVersionHeaderConstraint : IActionConstraint
{
private const bool AllowRouteToBeHit = true;
private const bool NotAllowRouteToBeHit = false;
private readonly string[] _allowedApiVersions;
public ApiVersionHeaderConstraint(params string[] allowedApiVersions)
{
_allowedApiVersions = allowedApiVersions;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
var requestApiVersion = GetApiVersionFromRequest(context);
if (_allowedApiVersions.Contains(requestApiVersion))
{
return AllowRouteToBeHit;
}
return NotAllowRouteToBeHit;
}
private static string GetApiVersionFromRequest(ActionConstraintContext context)
{
return context.RouteContext.HttpContext.Request.GetTypedHeaders().Headers[CollectApiVersion.HeaderKey];
}
}
Then I can use the ApiVersionAttribute and my custom RouteWithVersionAttribute together, as follows:
[ApiVersion("1")]
[ApiVersion("1.1")]
[Route("collect", "1", "1.1")]
public class ValueControllerV11 : Controller
{
[HttpRequestPriority]
public String Collect()
{
return "Version 1.1";
}
}
Cheers!
what about the CurrentImplementationApiVersionSelector option, when registering the service? see here: https://github.com/microsoft/aspnet-api-versioning/wiki/API-Version-Selector
The CurrentImplementationApiVersionSelector selects the maximum API version available which does not have a version status. If no match is found, it falls back to the configured DefaultApiVersion. For example, if the versions "1.0", "2.0", and "3.0-Alpha" are available, then "2.0" will be selected because it's the highest, implemented or released API version.
services.AddApiVersioning(
options => options.ApiVersionSelector =
new CurrentImplementationApiVersionSelector( options ) );

Autofac Automocking in ASP.NET MVC

So I'm trying to use Autofac Automocking in ASP.NET MVC 5, but for some reason I can't get it to work.
Here's the test so far:
using (var mock = AutoMock.GetLoose())
{
const string mainUserID = "MainUserID";
const string otherUserID = "OtherUserID";
ApplicationUser user = new ApplicationUser()
{
Id = mainUserID,
UserName = "TestUser"
};
var dataProvider = mock.Mock<IDataProtectionProvider>();
dataProvider.DefaultValue = DefaultValue.Mock;
var userManagerMock = mock.Mock<ApplicationUserManager>();
}
The test fails when mocking the ApplicationUserManager. The error is this:
Result StackTrace:
at Autofac.Extras.Moq.AutoMock.Mock[T](Parameter[] parameters)
at AwenterWeb_NUnit.AccountControllerTest.<Deactivate_User>d__0.MoveNext() in C:\Users\Fabis\Documents\Docs\Kvalifikācijas darbs 2015\AwenterWeb\AwenterWeb-NUnit\AccountControllerTest.cs:line 51
at NUnit.Framework.AsyncInvocationRegion.AsyncTaskInvocationRegion.WaitForPendingOperationsToComplete(Object invocationResult)
at NUnit.Core.NUnitAsyncTestMethod.RunTestMethod()
Result Message: System.InvalidCastException : Unable to cast object of type 'AwenterWeb.ApplicationUserManager' to type 'Moq.IMocked`1[AwenterWeb.ApplicationUserManager]'.
The same thing happens when trying to automock the ApplicationDbContext and it has a very simple constructor, so there shouldn't even be any issues with it.
I'm new to Mocking - what should I do in this scenario?
Edit: Also kind of an unrelated question, maybe you guys know - I've noticed that when creating a Moq for a DbSet using a list created previously in the test, I have to do this:
var dbSetMock = new Mock<IDbSet<DbEntity>>();
dbSetMock.Setup(m => m.Provider).Returns(data.Provider);
dbSetMock.Setup(m => m.Expression).Returns(data.Expression);
dbSetMock.Setup(m => m.ElementType).Returns(data.ElementType);
dbSetMock.Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
It seems really unintuitive. Is there a way to just tell the mock to take the list? So something like:
dbSetMock.Setup(m => m).Returns(data);
Or any other way to create a DbSet Moq from an existing list quickly without having to write those 4 extra lines?
If you look at ligne 73 of MoqRegistrationHandler.cs you can see that only interface is moqable using Autofac.Extras.Moq
var typedService = service as TypedService;
if (typedService == null ||
!typedService.ServiceType.IsInterface ||
typedService.ServiceType.IsGenericType && typedService.ServiceType.GetGenericTypeDefinition() == typeof(IEnumerable<>) ||
typedService.ServiceType.IsArray ||
typeof(IStartable).IsAssignableFrom(typedService.ServiceType))
return Enumerable.Empty<IComponentRegistration>();
var rb = RegistrationBuilder.ForDelegate((c, p) => CreateMock(c, typedService))
.As(service)
.InstancePerLifetimeScope();
You can change the code but it may be quite difficult to make it works with non parameter less dependency.
Can your dependencies be changed to use an interface instead of a concrete class ? if it is not possible and/or if it doesn't make sense, you can use the MockRepository to create your non parameter-less component and then inject it on the AutoMock class.
class Program
{
static void Main(string[] args)
{
using (var mock = AutoMock.GetLoose())
{
/// configure your non interface component with constructor parameters
/// if foo need more complex parameters you can get them
/// using mock.Mock<T>().Object
var fooMock = mock.MockRepository.Create<Foo>((String)null);
fooMock.SetupGet(f => f.Value).Returns("test");
// insert your instance into the container
mock.Provide<Foo>(fooMock.Object);
var bar = mock.Create<Bar>();
Console.WriteLine(bar.GetValue());
}
}
}
public class Foo
{
public Foo(String value)
{
this._value = value;
}
private readonly String _value;
public virtual String Value
{
get
{
return this._value;
}
}
}
public interface IBar
{
String GetValue();
}
public class Bar : IBar
{
public Bar(Foo foo)
{
this._foo = foo;
}
private readonly Foo _foo;
public String GetValue()
{
return this._foo.Value;
}
}
It is not a perfect solution but without big refactoring of the Autofac.Extras.Moq project I can't see any simpler way to do it.

Custom dynamic dataannotation for field visibility c#

I am trying to create a dataannotations attribute that controls field visiblity based on settings in a database. The attribute will be used within a system that will be used by multiple clients. Further, the visibility of the field needs to be able to change on the fly. I know I could do an if statement around each field in the view, but I am trying to avoid that and keep the visibility control within the view model as follows:
[Visible(FirstName)]
public string FirstName { get; set; }
I have tried creating this custom attribute that gets the value from a method from a resource class called ResourceType (which is generated using T4 and contains the necessary code to hit the database):
public class VisibleAttribute : Attribute, IMetadataAware
{
/// <summary>
/// Whether this field is visible
/// </summary>
public bool Hidden { get; set; }
public VisibleAttribute(string theFieldName)
{
ResourceType resources = new ResourceType();
Type _resourceType = typeof(ResourceType);
MethodInfo getHidden = _resourceType.GetMethod("IsHidden");
object[] requiredParams = new object[] { theFieldName };
Hidden = (bool)getHidden.Invoke(resources, requiredParams);
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.ShowForEdit = !Hidden;
metadata.HideSurroundingHtml = Hidden;
}
}
Here is an excerpt of the ResourceType class:
public class ResourceType
{
public const string Creditors_SecondaryCreditorsPayOffYesNo_Require = "Prop_Creditors_SecondaryCreditorsPayOffYesNo_Require";
public static string Prop_FieldName_Require
{
get { return GetHiddenOption(FieldName) ? "true" : "false"; }
}
internal static bool GetHiddenOption(string fieldName)
{
< < Logic here to get the option from the database > >
}
I have also tried the same attribute but with the following constructor:
public VisibleAttribute(string theFieldName)
{
ResourceType resources = new ResourceType();
Type _resourceType = typeof(ResourceType);
PropertyInfo getHidden = _resourceType.GetProperty(theFieldName);
Hidden = (bool)getHidden.GetValue
}
The problem I have with these two attempts is that, since the code is in the constructor, it only runs the first time I load the page after an IIS reset. So, any further changes I make to the visibility settings are not reflected without amother IIS reset.
I also tried creating a custom DataAnnotationsModelMetadataProvider that attempts to only load the setting once per page request:
public class EGTDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType,
Func<object> modelAccessor, Type modelType, string propertyName)
{
var data = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var visibleAttributeMetadata = attributes.SingleOrDefault(a => typeof(VisibleAttribute) == a.GetType());
if (visibleAttributeMetadata != null)
{
VisibleAttribute visibleAttribte = (VisibleAttribute)visibleAttributeMetadata;
if (!visibleAttribte.VisibleIsSet)
{
PropertyInfo getHidden = visibleAttribte.ResourceType.GetProperty("Prop_" + WebUtils.RemoveSectionNameSpace(visibleAttribte.SectionName) + "_" + visibleAttribte.FieldName + "_Hide");
visibleAttribte.IsHidden = bool.Parse(getHidden.GetValue(null, null).ToString());
data.HideSurroundingHtml = visibleAttribte.IsHidden;
data.ShowForEdit = !visibleAttribte.IsHidden;
visibleAttribte.VisibleIsSet = true;
}
else
{
data.HideSurroundingHtml = visibleAttribte.IsHidden;
data.ShowForEdit = !visibleAttribte.IsHidden;
}
}
return data;
}
}
One issue I have with the ModelMetadataProvider is that the CreateMetadata method runs many times for a single field during a single request. It is very inefficient code, and a huge decrease in performace, to call the database 5-10+ times per request to get a setting that has not changed since the beginning of the request. If I try to set a flag indicating I've already loaded the setting, I'm back to the same scenario as above where I don't see the setting change until after an IIS reset.
I'm hoping someone can give me some pointers as to what methods I can employ to see the database changes real time. Or am I trying to do the impossible? Thanks in advance.
You could combine the metadata provider approach with caching the value just that single request.
For this you could use the Items dictionary in the current HttpContext. Be careful with this as a redirect will cause the items to be cleared:
string cacheKey = String.Format("IsVisible-{0}", propertyName)
if(!HttpContext.Current.Items.Contains(cacheKey))
HttpContext.Current.Items[cacheKey] = //get setting from db
bool isVisible = (bool)HttpContext.Current.Items[cacheKey];
You can also consider using the ASP .Net Cache in case that you prefer caching the value not just for the current request (Although you mentioned that within your metadata provider you were trying to load the setting once per request)

Orchard CMS : Creating module for OpenRasta, problems with dependency injection

I'm trying to create an Orchard CMS module that enables a RESTful web service using OpenRasta for a given route (/openrasta/* for example).
I need to get to the Orchard ContentManager to get the content for the service to return, so my OpenRasta handler (ContentHandler) uses a ContentService, which implements IContentService, which inherits IDependency. Normally this would work because Orchard will inject a ContentManager into the constructor:
public class ContentService : IContentService {
public IContentManager content;
public ContentService(IContentManager content) {
this.content = content;
}
public IEnumerable<string> GetContentTypeDefinitionNames() {
return content.GetContentTypeDefinitions().Select(d => d.Name);
}
}
But when I run it I get an error because OpenRasta doesn't know anything about the Orchard dependencies and it's trying to create ContentService, not Orchard, which is fair enough:
OpenRasta.DI.DependencyResolutionException: Could not resolve type
ContentService because its dependencies couldn't be fullfilled
Constructor: Orchard.ContentManagement.IContentManager
Is there a way to achieve this, can I go to an Orchard class somewhere and say "give me an instance of the ContentManager"?
Update: See my comments on #rfcdejong's answer for updates on my progress.
Are u using a ServiceRoute, added in a class implementing IRouteProvider
Look at the ServiceRoute summary, it says "Enables the creation of service routes over HTTP in support of REST scenarios."
public class Routes : IRouteProvider
{
public void GetRoutes(ICollection<RouteDescriptor> routes)
{
foreach (var routeDescriptor in GetRoutes())
routes.Add(routeDescriptor);
}
private static ServiceRoute _rastaService = new ServiceRoute(
"openrasta",
new MyServiceHostFactory<IOpenRastaService>(),
typeof(IOpenRastaService));
public IEnumerable<RouteDescriptor> GetRoutes()
{
return new[]
{
new RouteDescriptor
{
Priority = -1,
Route = _rastaService
}
};
}
}
And want to resolve ContentService? U might have to resolve the interface.
i guess u want the following to work:
var contentService = LifetimeScope.ResolveNew<IContentService>();
I have used HostContainer.Resolve directly and had issues as well. I will describe the solution i'm using at the moment in my own ServiceHostFactory
Do u have a own ServiceHostFactory deriven from OrchardServiceHostFactory?
In that case u can implement the following code to help u resolve instances
private ILifetimeScope _lifetimeScope = null;
private ILifetimeScope LifetimeScope
{
get
{
if (_lifetimeScope == null)
{
IHttpContextAccessor accessor = HostContainer.Resolve<IHttpContextAccessor>();
IRunningShellTable runningShellTable = HostContainer.Resolve<IRunningShellTable>();
ShellSettings shellSettings = runningShellTable.Match(accessor.Current());
IOrchardHost orchardHost = HostContainer.Resolve<IOrchardHost>();
ShellContext shellContext = orchardHost.GetShellContext(shellSettings);
_lifetimeScope = shellContext.LifetimeScope;
}
return _lifetimeScope;
}
}
I also created LifetimeScopeExtensions that has the following code
public static class LifetimeScopeExtensions
{
public static T ResolveNew<T>(this ILifetimeScope scope)
{
IWorkContextAccessor workContextAccessor = scope.Resolve<IWorkContextAccessor>();
WorkContext workContext = workContextAccessor.GetContext();
if (workContext == null)
{
using (IWorkContextScope workContextScope = workContextAccessor.CreateWorkContextScope())
{
ILifetimeScope lifetimeScope = workContextScope.Resolve<ILifetimeScope>();
return lifetimeScope.Resolve<T>();
}
}
else
{
ILifetimeScope lifetimeScope = workContext.Resolve<ILifetimeScope>();
return lifetimeScope.Resolve<T>();
}
}
public static object ResolveNew(this ILifetimeScope scope, Type type)
{
IWorkContextAccessor workContextAccessor = scope.Resolve<IWorkContextAccessor>();
WorkContext workContext = workContextAccessor.GetContext();
if (workContext == null)
{
using (IWorkContextScope workContextScope = workContextAccessor.CreateWorkContextScope())
{
ILifetimeScope lifetimeScope = workContextScope.Resolve<ILifetimeScope>();
return lifetimeScope.Resolve(type);
}
}
else
{
ILifetimeScope lifetimeScope = workContext.Resolve<ILifetimeScope>();
return lifetimeScope.Resolve(type);
}
}
}
var settingsService = LifetimeScope.ResolveNew<ITokenServiceSettingsService>();
So the issue is that your CMS uses its own IoC container. By default OpenRasta does that too.
This means that services that are present in Orchard won't be visible to OpenRasta.
For all other IoC containers, the answer is damn right simple: You use the IoC adaptation layer that lets OpenRasta live in whatever ioc container you want. We support unity, structuremap, castle and ninject. That said, autofac is not supported as no one ever built it.
The cleanest way for you to solve this problem (and any other you may encounter in the future for those issues) would be to build your own autofac ioc adaptation layer for openrasta. If you need help doing that, you can join the openeverything mailing list where the devs would be happy to help you.

Change lookup rule for views

I have an application that gets rolled out in multiple countries. There will be a setting in the web.config file, that defines the country.
The country will not be in the URL.
Some of the the views change depending on the country.
My first attempt is to use a folder inside the views folder that contains views, if they differ from the default view:
Default
/questions/ask.aspx
Spain
/questions/ESP/ask.aspx
If there is no view in the country-folder the default view is used. Is there a way to extend the ViewEngine to lookup views in the country folder first?
EDIT:
This is a poc only. To see a full implementation have a look at
http://pietschsoft.com/?tag=/mvc
private static string[] LocalViewFormats =
new string[] {
"~/Views/{1}/ESP/{0}.aspx",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
public LocalizationWebFormViewEngine()
{
ViewLocationFormats = LocalViewFormats;
}
public class MyViewEngine : WebFormViewEngine
{
private static string[] LocalViewFormats = new[] { "~/Views/ESP/{0}.aspx",
"~/Views/ESP/{0}.ascx" };
public MyViewEngine()
{
ViewLocationFormats = LocalViewFormats.Union(ViewLocationFormats).ToArray();
}
}
Obviously, you don't want to hardcode the location, but this should give you the general idea.

Resources