Last week I was struggling to find out how to make good use of metadata within NancyFx. I was reading through their extensive documentation along with searching Google for answers. The closest I could find was a single blog post written by Jim Liddell and unfortunately it did not cover the specifics of what I wanted to try and achieve.
I wanted to provide metadata for specific routes within specific modules, in a clean and structured manner. I did not want to add any extra layers of complexity, I just wanted something simple that just worked. I spent a few hours trying to get it working with how I believed it should be working, based off of the methods in the framework but unfortunately kept on hitting runtime errors. So in the end, I created a gist of what I was trying to do, and sent it to Andreas Håkansson via email, asking what I was doing wrong. A couple of days later, he came back to me with the answer and of course at that moment I instantly realized how it all worked. This was still not what I wanted as an end result, but by knowing how the framework worked, I could make it work the way I wanted.
I wanted to be able to create a MetadataModule file for each Module that I wanted to provide Metadata for, and have it work with paths that were relative (the base path) to what was already found my existing Nancy Modules. So this is what I did…
I created an interface named IDataModule, that would allow me to have a structured way of retrieving my information from each of my MetadataModule files.
public interface IDataModule<T>
{
T Get(string path);
}
Along with a simple object structure that would serve as my Metadata itself.
public class MetadataModel
{
public int Index { get; set; }
}
I then created an instance of a MetadataModule file, but I decided to try and keep a bit of structure to this, just to make my life a little easier. So currently all my Nancy Modules live in a folder named Modules so I decided my Metadata files should live in a folder named Metadata. I also decided that since my Modules have the naming convention of NameModule that my Metadata should have a matching convention, so a matching file would be NameMetadataModule. So for HomeModule I would then have a matching HomeMetadataModule file.
public class HomeMetadataModule : IDataModule<MetadataModel>
{
private readonly Dictionary<string, MetadataModel> _paths = new Dictionary<string, MetadataModel>
{
{"/", new MetadataModel {Index = 1}}
};
public MetadataModel Get(string path)
{
return _paths.ContainsKey(path) ? _paths[path] : null;
}
}
Now for this all to work, I needed an IRouteMetadataProvider instance, which supported all these rules I put into place. It is perhaps not the cleanest implementation, but it works perfectly for my needs.
public class RouteMetadataProvider : IRouteMetadataProvider
{
public Type GetMetadataType(INancyModule module, RouteDescription routeDescription)
{
return typeof (MetadataModel);
}
public object GetMetadata(INancyModule module, RouteDescription routeDescription)
{
var moduleType = module.GetType();
var moduleName = moduleType.FullName;
var parts = moduleName.Split('.').ToArray();
if (parts[0] != GetType().FullName.Split('.')[0]) return null;
if (parts[parts.Length - 2] != "Modules") return null;
parts[parts.Length - 2] = "Metadata";
parts[parts.Length - 1] = ReplaceModuleWithMetadataModule(parts[parts.Length - 1]);
var metadataModuleName = string.Join(".", parts);
var type = Type.GetType(metadataModuleName);
var dataModuleType = type == null ? null : TinyIoCContainer.Current.Resolve(type) as IDataModule<MetadataModel>;
if (dataModuleType == null) return null;
var requestPath = routeDescription.Path.Substring(module.ModulePath.Length) + "/";
return dataModuleType.Get(requestPath);
}
private string ReplaceModuleWithMetadataModule(string moduleName)
{
var i = moduleName.LastIndexOf("Module", StringComparison.Ordinal);
return moduleName.Substring(0, i) + "MetadataModule";
}
}
Now in my instance usage case, I wanted this to store an index position so that I could create an object for generating a navbar. I used a very simple object for storing my navbar.
public class LinkModel
{
public string Link { get; set; }
public string Text { get; set; }
}
I then created a simple method for reading the data for me, including the Metadata, from the IRouteCache instance.
public IEnumerable<LinkModel> GetMainLinks(IRouteCache routeCache)
{
return routeCache
.SelectMany(routes =>
{
return routes.Value
.Where(route => route.Item1 == 0 &&
route.Item2.Method == "GET" &&
route.Item2.Name != string.Empty)
.Select(route => new
{
route.Item2.Metadata.Retrieve<MetadataModel>().Index,
route.Item2.Path,
route.Item2.Name
});
})
.OrderBy(route => route.Index)
.Select(route => new LinkModel
{
Link = route.Path,
Text = route.Name
});
}
I will most likely create some improvements on this in the near future, but for now, it works perfectly for what I need and perhaps this will serve as useful for others.