The much requested and highly anticipated "multiple blogs" feature has made it into BlogEngine.NET 2.5. BE.NET 2.5 will be released at the end of June. This post goes over how it works and some information on making themes, widgets, extensions and your code be multiple-blog compatible.
Blogs Management Page
Each installation of BE.NET will have a "primary" blog instance. By default this will be the normal blog you are accustomed to. The primary blog has a new "Blogs" admin page. That page looks like:
Here the blog instances (the primary blog instance and child blogs) can be managed. A blog instance can be made Inactive. An inactive blog instance will appear as if it does not exist. Other properties that can be configured for each blog instance can be seen in the following "Add New Blog" dialog window:
Let's look at the properties for each blog instance.
Any name you'd like to give the blog instance. The name does not appear anywhere outside this Blogs management page.
Storage Container Name
This is the name of the folder where the blog data will live. Basically a copy of the files and folders for each blog instance will be made -- i.e. each blog instance will have its own separate folder of data. This makes the blog instances portable and keeps the data separated. Databases can also be used with multiple blogs. Even when using a database, there is still some data (blog post images/files, and a few other small files) that is stored in the file system. So a separate folder will be created for each blog instance even when using the DbBlogProvider.
As in the past, the primary blog instance data is stored in App_Data. There's now a new Blogs folder under App_Data. Within App_Data\Blogs, a new folder matching the "Storage Container Name" will be created when a new blog instance is created. The data that is initially put into this child blog storage folder is explained next.
Existing Blog to Create New Blog from
This is partially related to the above Storage Container Name. When a new blog instance is created, the "Existing Blog to Create New Blog From" dropdown list will contain all of the existing blog instances. The files & folders from the existing blog instance you choose here will be copied into the storage folder for the new blog instance as "initial data". BE.NET 2.5 also includes a "Template" blog instance (which is inactive), and can be used as the existing blog instance to create new blog instances from. The Template blog instance has the same data and settings as the primary blog. You can optionally change the Template blog data, or even create multiple template blog instances to choose from when creating new blog instances.
If using a database, in addition to the storage container folder being copied for the new blog instance, the DB data from the existing blog instance will be copied in the DB for the new blog instance. So for both XML and DB, the new blog instance created will start off using the exact data, files, etc that the blog instance you copied from has.
Note, there is no Template blog included with the database. You can still create your own template blog for the DB by creating a new blog instance of your Primary blog, naming it "Template" (or any name) and customizing it to your liking.
Host Name & Virtual Path
These two properties along with the "Accept Any Text before Host Name" checkbox are what is used for BE.NET to determine which blog instance a request to the application is being made for. Virtual Path refers to the path following the root. Host name is the domain name (plus any subdomain). You can mix and match Hostname and Virtual Path to create many URL combinations. For example, the following URLs can be used:
... etc ...
The 2nd and 5th examples above would be given a Virtual Path of ~/blog1 and ~/blog2 respectively. Prefixing the virtual path with ~/ is required, and the ~/ will automatically be inserted for you in the Virtual Path field. In the 3rd example above, the Host Name would be "blog1.example.com", in the 4th example the host name would be "blog2.domain.com" and in the 5th example, the host name would be "blog.domain.com".
If you are only using a single hostname for your blog, you can omit the hostname. Entering it is really only required if you will be using multiple hostnames.
With the Virtual Paths above of blog1 and blog2 (examples 2 and 5), you do not need to create physical directories named blog1 and blog2. These are virtual directories that BE.NET will look for in the URL to treat that request to the web server as being on behalf of those blog instances. At the same time, you will want to make sure that physical directories matching the virtual paths (blog1, blog2, etc) do not already exist.
The "Accept Any Text before Host Name" checkbox is another option to allow any text (subdomains) to appear before the Host Name you enter. If you leave this box unchecked, then the Host Name you enter will need to match exactly what is in the browser address bar.
Multiple-Blog Capable Themes, Widgets and Extensions
If you will be running a single instance blog, all of the themes, extensions and widgets that run under BE.NET 2.0 will still work perfectly fine for BE.NET 2.5.
If you will be running multiple blog instances, existing themes, extensions and widgets may need some adjustments to work with this new multiple blogs feature. The following types of issues need to be addressed.
Caching - HttpRuntime.Cache
If an existing theme, widget or extension is storing data in the HttpRuntime.Cache, a likely needed change is to store that data separately for each blog instance. With multiple blog instances, the same set of (physical) themes, extensions and widgets are being used across all the blog instances. So if you have code in a widget that looks like:
HttpRuntime.Cache["recent-posts"] = recentPostList;
This same data you are storing will be shared across all blog instances that are using this widget. This is probably not what was intended. You can either change that code so it uses a dynamic cache key based on the "current blog instance", or as a convenient shortcut, you can use a new Caching provider built into BE.NET 2.5. That line of code above using the new caching provider would look like:
Blog.CurrentInstance.Cache["recent-posts"] = recentPostList;
The cache provider will use your "recent-posts" cache key, prefixing it with the ID of the current blog instance (a GUID). You can retrieve the current blog instance and/or the ID of the current blog instance via:
Blog currentInstance = Blog.CurrentInstance;
Guid currentInstanceId = Blog.CurrentInstance.Id;
Caching - Static
If your theme, widget or extension is storing data in static fields or properties, the cache provider described above cannot be used, however one approach is to convert your static data into a generic dictionary, with Guid as the key and your data type as the value. The Guid key will represent the current blog instance ID. You need to manage this dictionary by adding in each blog instance in the dictionary, ideally as the data needs to be stored the first time. That code might look like:
private static Dictionary<Guid, string> _recentPostsMarkup =
new Dictionary<Guid, string>();
private static string RecentPostsMarkup
_recentPostsMarkup[Blog.CurrentInstance.Id] = GetRecentPostsMarkup();
This is important and a problem you may notice, especially if you are using Virtual Paths for your blog instances. Let's look at a simple line of code in BE.NET 2.0 that worked fine there:
Simple enough. However, if you're using Virtual Paths and you have a blog instance with a Virtual Path of "blog1", the homepage for that blog instance will be something like:
If you have code that uses that redirect shown above, you will end up at:
When you probably wanted to end up at:
So instead of redirecting to the login page for "blog1", you've incorrectly redirected the person to the login page for the primary blog instance. That redirect should now look like the following to work well with multiple-blogs:
Response.Redirect(Utils.RelativeWebRoot + "Account/login.aspx");
In the past, Utils.RelativeWebRoot would resolve to the root of the blog. Starting with BE.NET 2.5, Utils.RelativeWebRoot will resolve to the homepage/root of the current blog instance. So when you are in "blog1", Utils.RelativeWebRoot will resolve to:
Similarly, Utils.AbsoluteWebRoot also takes the relative path of the current blog instance into consideration.
If you need the "real" web root of the application, you could use ~/ or you can use a new Utils.ApplicationRelativeWebRoot property.
While on the topic, a couple of other similar new properties are Blog.CurrentInstance.StorageLocation which will return a storage location beginning with ~/ to the physical location of the storage container for the current blog instance. So Blog.CurrentInstance.StorageLocation may return a value looking like:
BlogConfig.StorageLocation will return the same type of path, but to the direct App_Data folder (which equates to the storage location of the primary blog instance), so BlogConfig.StorageLocation will generally return:
With extensions, the primary blog instance has the "master" switch over whether extensions are enabled. If the primary blog instance disables an extension, that extension is no longer available to any of the child blogs. If the primary blog instance enables an extension, by default this extension will be enabled for all the child blogs. A child blog can disable an extension.
As long as the primary blog instance has an extension enabled, that extension will be loaded into memory and if it wires up any of the normal event handlers (Post.Serving, Commnet.Serving, etc), that extension is going to fire even if the event is occurring in a child blog that has explicilty disabled the extension. In this scenario, the extension is handling an event for the child blog, but it should not do anything since the child blog disabled the extension. In other words, we want the extension to "pretend" it was never called, or at least not take any action. This is where a change is required in extensions so they check to see if the blog instance the extension is firing for has the extension enabled or disabled. If it is disabled, then the extension should not run any of it's normal code. The extensions included with BE.NET 2.5 have a new line of code added to them for this. Taking the ResolveLinks extensions as an example, it has this new line of code (technically 2 lines of code) added to the beginning of PostCommentServing:
private static void PostCommentServing(object sender, ServingEventArgs e)
e.Body = LinkRegex.Replace(e.Body, new MatchEvaluator(Evaluator));
The new lines of code above are lines 3 and 4. It makes a call into the Extension Manager, passing to it the name of the extension (in this case, ResolveLinks). ExtensionEnabled() will return false if the extension is disabled for the current blog instance.
As mentioned earlier, if you will be running a single instance of BE.NET, these changes described above will not need to be made -- although they can be made. Overall, I think we maintained a lot of backwards compatibility as the changes you will need to make to have a multiple-blog capable BE.NET application are very minimal. The themes, widgets and extensions included with BE.NET 2.5 already have these changes made to them. Enjoy the new multiple blog capability and if you run into any issues or have any suggestions, the best place to pass along your thoughts is in the CodePlex discussion group.