Porting to .NET Core (Part 3)

Home / Porting to .NET Core (Part 3)

Continuing down the porting trail, there are a few more relevant things I want to share that I didn’t cover previously.


I’ve been using AutoMapper for the longest time. In most of my applications, I have been stuck on v3.x. Why you may ask? I was using this extension that would use the AfterMap function to handle mapping of custom IEnumerables. There is a PaginatedList<T> that I use in all of my apps, it seems, and there was no way use the newer versions of AutoMapper to achieve the same results as a simple extension. The extension itself was literally “IncludePaginatedMapping()” that you would attach to map (Source, Dest) mapping. Super useful. I definitely did not want to have to create that mapping manually for any paginate lists types.

Being stuck on v3.x was also a stumbling block in the way of the migration/porting to .NET Core. Today, though, using the new “Open Generics” in v6.0.2 of AutoMapper along with an ITypeConverter, I was finally able to move the v.Latest. The type converter looks pretty simple:

public class PaginatedListConverter<TSource, TDest> : ITypeConverter<PaginatedList<TSource>, PaginatedList<TDest>>
{
	public PaginatedList<TDest> Convert(PaginatedList<TSource> source, PaginatedList<TDest> dest, ResolutionContext context)
	{
		var list = (source as List<TSource>);
		dest = new PaginatedList<TDest>(context.Mapper.Map<List<TSource>, List<TDest>>(list),
			source.TotalCount, source.PageNumber, source.PageSize, source.SortBy, source.SortDirection);
		return dest;
	}
}

The key, though, is that with the latest changes in the OpenGenerics, you define a generic PaginiatedList mapping and simply tell it to use the converter:

protected AutoMapperProfile(string profileName) : base(profileName)
{
	CreateMap(typeof(PaginatedList<>), typeof(PaginatedList<>))
		.ConvertUsing(typeof(PaginatedListConverter<,>));
}

With that little bit of code in place, any PaginatedList gets converted and life is good. In the Dotnet Core ConfigureServices, the mapper is defined as a singleton to be injected. I like this approach a lot better than a static reference to IMapper.

// Add AutoMapper
var config = new AutoMapper.MapperConfiguration(cfg =>
{
	cfg.AddProfile(new AutoMapperProfile());
});

var mapper = config.CreateMapper();
services.AddSingleton(mapper);

There was a utility class that I was using since MVC3 came out that I could use to determine if a user had access to a Controller / Action. Yes, I had updated it to work with every iteration of MVC along the way. In Dotnet Core (MVC6), this is not easy to achieve. I literally could not find a working example of how to achieve this. A lot of people would simple code authorization in two places, but this is not DRY and, imho, introduces potentially discrepancies in authorization.

The method below is up to the task. It uses the factories and providers to find the controller/action that you’re interested in inspecting. The result of this inspection is an ControllerActionDescriptor. It then uses this information to create an ActionContext. An ActionContext lets you create the AuthorizationContext. With the AutorizationContext, we can see what AuthorizationFilters are applied to the action. The code will simply iterate over the filters and execute them. If the AuthorizationContext Result is non-null, the user doesn’t have access to the endpoint

Where is this useful? I use it in much of many of my applications for dynamic generation of links and such that a user should be able to see. Since I don’t, then, need to define security/authorization outside of my controller(s), everything stays DRY and succinct. This also means that those (UI) methods that generically check authorization and display widgets can be packaged and used in other projects regardless of that project/application’s authorization set-up.

/// <summary>
/// Determines if specified action is accessible to current user.
/// </summary>
/// <param name="htmlHelper">HtmlHelper object.</param>
/// <param name="actionName">Action name to test.</param>
/// <param name="controllerName">Controller name to test.</param>
/// <param name="areaName">Area name to test.</param>
/// <returns>True/false if action is accessible to current user.</returns>
public static bool ActionIsAccessibleToUser(this IHtmlHelper htmlHelper, string actionName, string controllerName = "", string areaName = "", string[] namespaces = null)
{
	// Ensure area name is specified.
	if (areaName == null)
	{
		throw new ArgumentException("Argument areaName cannot be null. If root area is desired, specify parameter as an empty string.");
	}

	// Ensure controller name is specified.
	if (string.IsNullOrWhiteSpace(controllerName))
	{
		throw new ArgumentException("Argument controllerName must be specified.");
	}

	var factory = ServiceProviderFactory.ServiceProvider.GetService(typeof(IControllerFactory)) as IControllerFactory;
	var provider = ServiceProviderFactory.ServiceProvider.GetService(typeof(IActionDescriptorCollectionProvider)) as IActionDescriptorCollectionProvider;
	var filterProvider = ServiceProviderFactory.ServiceProvider.GetService(typeof(IFilterProvider)) as IFilterProvider;

	var ctrlBase = (ControllerBase)factory.CreateController(new ControllerContext(htmlHelper.ViewContext));
	var ctrlName = string.IsNullOrEmpty(controllerName) ? controllerName : (string)htmlHelper.ViewContext.RouteData.Values["controller"];
	var actionDescriptor = provider.ActionDescriptors.Items.Where(x =>
	{
		var desc = (ControllerActionDescriptor)x;

		return (desc.ControllerName == ctrlName && desc.ActionName == actionName &&
			(string.IsNullOrWhiteSpace(areaName) || (desc.RouteValues.ContainsKey("area") && desc.RouteValues["area"] == areaName)) &&
			(namespaces == null || (namespaces.First() == desc.ControllerTypeInfo.Namespace)));
	}).First();

	RouteData routeData = new RouteData();
	routeData.Values.Add("action", actionName);
	routeData.Values.Add("controller", controllerName);

	if (!string.IsNullOrWhiteSpace("area"))
	{
		routeData.Values.Add("area", areaName);
	}

	if (namespaces != null)
	{
		routeData.DataTokens["namespaces"] = namespaces;
	}

	var actionContext = new ActionContext(ctrlBase.HttpContext, routeData, actionDescriptor);
	var controllerContext = new ControllerContext(actionContext);

	var controller = factory.CreateController(controllerContext);

	if (actionDescriptor == null)
		return false;

	var authFilterContext = new AuthorizationFilterContext(actionContext, actionDescriptor.FilterDescriptors.Select(x => x.Filter).ToList());

	foreach (AuthorizeFilter authorizationFilter in actionDescriptor.FilterDescriptors
		.Where(x => x.Filter.GetType() == typeof(AuthorizeFilter)).Select(x => (AuthorizeFilter)x.Filter))
	{
		authorizationFilter.OnAuthorizationAsync(authFilterContext).GetAwaiter().GetResult();
		if (authFilterContext.Result != null)
		{
			authFilterContext.Result = new OkResult();
			return false;
		}
	}

	return true;
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.