Fanray v1.1.0-preview3 - Extensible Architecture

Today I'm releasing Fanray v1.1.0-preview3 and I want to dedicate this release to the innocent students and citizens who died in Beijing on June 4th, 1989!

Remembering Beijing on June 4th, 1989
Remembering Beijing on June 4th, 1989

Architecture

Preview 3 brings an architecture that allows you to extend Fanray by creating Plugins, Widgets and Themes in a clean and maintainable way. This new extensibility will keep the system core from being bloated and let others build their own features and applications easily. I have revamped the project docs on GitHub wiki where I explained the differences between plugin, widget and application as well as how to develop widget, plugin and theme.

Extensible Architecture
Extensible Architecture

Extensible design in software engineering is to accept that not everything can be designed in advance. A light software framework which allows for changes is provided instead. - Wikipedia

Upgrade

For this release there is no db schema change, but if you are running a previous version then go to Admin > Plugins to activate the Shortcodes plugin, if you have source code in your post currently this still is the way to render them.

Admin - Plugins
Admin - Plugins

Fanray Solution

There is a major revamp on the Fanray solution. The Fan base class library provides the essential infrastructure services, Fan.Blog is the blog application, Fan.Web becomes the web infrastructure project, and the new Fan.WebApp is now the web app that hosts everything. Notice, each plugin, theme or widget has their own project. The widgets pertaining to the blog have "Blog" in their project name like "BlogTags" etc, while generic widget SocialIcons does not.

Fanray Solution
Fanray Solution

Limitation

One limitation is that currently, Fan.WebApp still requires project dependencies on those extension projects to get their DLLs. Ideally extensions should be deployed to the main web app by a matter of just dropping in their DLLs at run time, but this requires hot loading and unloading of DLLs. This capability is coming in .NET Core 3.0, so how it would work out remains to be seen. The current capability is enough for a starter kit IMO, but the ability to develop all plugins, widgets and themes independently will be a game changer and make Fanray magical :)

Roadmap

So far I've put in major effort on architectural aspect of the project, while it still needs more work I'm going to get back to improving the basic functionality of the blog.  Some of the features I will be working on are

  • Pages
  • Navigation
  • A second theme
  • REST API
  • Full Text Search

I do want to release the v1.1 final before .NET Core 3 comes out this fall, depending on my progress I may or may not finish all the features listed. Please let me know if you'd like to help, also report to me any bugs you've encountered. Thanks and happy coding!

Fanray v1.1.0-preview2 released

I’m releasing preview 2 today.  For details please refer to preview2 milestone. Here I list some notable changes.

Upgrade from preview 1

Preview 2 does not have any database schema changes. But I did update widget’s unique identifier from its type to its folder name.  Therefore, before you run the preview 2 code, first go to Admin > Widgets and delete your widgets from the widget areas.  Then, run preview 2 and manually add your widgets back. Through refactoring the widget’s code, I realized there would be even more refactoring coming to widgets, so using type (namespace + assembly) of a widget is not good idea to identify a widget. Using its folder name is more less likely to change, but I’ll see.

Image processing update

There have been a few changes related to image processing. I’ll write separate posts to touch on these. I switched from Magick.NET to ImageSharp for better performance. Each uploaded image, depending on its dimension, now could have up to 4 resized images, small, medium, medium large and large, in addition to the original which is always saved. For a gif, depending on its dimension, it could have a small resized version.  Responsive Images has also been implemented to display the best image based on the client’s screen size and resolution.

Upgrade CKEditor5 to latest

I upgraded CKEditor5 to v12.  I was two versions behind. Probably the most notable change is now you can copy paste your Microsoft Word document content directly into the editor. The editor knows what content it can accept and will leave the rest as plain text. I tried a complex resume doc and was impressed by how much the editor could take in.

I support 996.icu

While this project is still licensed under Apache 2.0, I did update the licensing section of the README file to show my support for the 996.icu campaign. Software developers in Mainland China created a 996.icu repo and started a campaign against the 996 working hour system implemented by certain high tech companies, like Huawei and Alibaba, in Mainland China. Personally I think working 72hr-week is inhumane, on top of that I heard these overtime hours are without pay, that is not only wrong it is against the law.  I can understand to rush a project out the door sometimes overtime is necessary, but doing it once in a while may be OK doing it persistently can severely impact your health. I have a very bad back to prove it.  While it's easy for me to say that your health is the most important thing and just quit if you company treats you like a slave, everyone's situation may be different.  Especially when the job market is not good and you have a family to support, the choice between doing 996 or quit is not a so easy one. I support my fellow software developers to defend their labor rights against unlawful working conditions. As such any entity, whether individual or corporation, that is in violation of the local labor laws against their employees shall not use my repo.

Fanray v1.1-preview1 - Widgets

I haven't posted in a while due to some health issues, I've been battling with chronic lower back pain for years and it elevated to a new level last year. I tried my best to rehab myself till I was convinced the pain was not going anywhere, earlier this month I opted for surgery. I'm still recovering and hopefully as I get better I'll be able to put more time into the project. 

Version Changes Back to 1.x

I started this project with the goal to learn new technologies by building something useful we can all benefit from and learn from. Inevitably I will make mistakes along the way as part of the learning process. I originally intended for the next release to be v2.0 because it's been a year since I released v1.0 and thought it was a good time to start anew with Asp.net Core 2.x. But then I realized the existing feature set is still relatively small, it'd be more appropriate to version it as 1.x. Therefore I'm making the next release 1.1 instead of 2.0. Also I'm abandoning the "alpha" and "beta" release convention and will use "preview" instead. So for each release there will be a number of previews followed by a final release. Any issues after a final release will go to the patch number.  Today I'm releasing v1.1-preview1, please let me know of any issues you run into with this release. Note that previews like alphas are not recommended for production. My apologies for the version confusion!

Introducing Widgets

This preview incorporates what I worked on for the past few months: fixes, improvements but most of all a brand new widget system. Widgets provide a way for theme designers to optionally define regions (widget areas) on their theme for end users to drag and drop pieces of UI (widgets) onto these areas to customize their site. Note that widgets are optional, developers / designers still have full control when they develop theme and they don't have to include widgets if they don't want to. 

Widgets
Widgets

Having said that, widget does give developers a way to extend the functionality of the application. For example it's common for a site to have social icons that link to the user's social media pages. A Social Icons widget allows a user to input her social media links and social icons will appear accordingly. The user can also sort the icons in any order by drag and drop them. In the coming days and weeks I'll provide more info on how to create your own widgets.

Social Icons Widget
Social Icons Widget

Upgrade to v1.1

If you start fresh, this section does not apply to you, you can just run the app and the database will be created for you.  But if you are running a previous version, here is how to upgrade your existing database. Always remember to backup your database first!

From v2.0-a4

Run the following script before launching app.

[code lang=sql]

UPDATE [dbo].[__EFMigrationsHistory] 
  SET [MigrationId] = '20171120170827_FanV1' 
WHERE MigrationId like '%FanSchemaV1%' 
GO 

UPDATE [dbo].[__EFMigrationsHistory] 
  SET [MigrationId] = '20190318202954_FanV1_1' 
     ,[ProductVersion] = '2.2.3-servicing-35854' 
WHERE MigrationId like '%FanV2_0%' 
GO 

DROP INDEX [IX_Core_Meta_Key] ON [Core_Meta] 
GO 

ALTER TABLE [Core_Meta] ADD [Type] int NOT NULL DEFAULT 0 
GO 

CREATE UNIQUE CLUSTERED INDEX [IX_Core_Meta_Type_Key] ON [Core_Meta] ([Type], [Key]) 
GO 

INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'clarity', N'', 1) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'blog-after-post', N'{"id":"blog-after-post","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'blog-after-post-list', N'{"id":"blog-after-post-list","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'blog-before-post', N'{"id":"blog-before-post","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'blog-before-post-list', N'{"id":"blog-before-post-list","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'blog-sidebar1', N'{"id":"blog-sidebar1","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'blog-sidebar2', N'{"id":"blog-sidebar2","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'footer1', N'{"id":"footer1","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'footer2', N'{"id":"footer2","widgetIds":[]}', 2) 
GO 
INSERT [dbo].[Core_Meta] ([Key], [Value], [Type]) VALUES (N'footer3', N'{"id":"footer3","widgetIds":[]}', 2) 
GO

[/code]

After that you will not see your sidebar anymore, go to the Admin Panel's Widgets page, add your widgets back.

From v1.0

First run the following sql.

[code lang=sql]

UPDATE [dbo].[__EFMigrationsHistory] 
  SET [MigrationId] = '20171120170827_FanV1' 
WHERE MigrationId like '%FanSchemaV1%' 
GO

[/code]

Then you have a choice of either just run the app and let EF migrations update your database, or go to the solution in the sql folder run 1.0-1.1.sql to update your database.

 

ASP.NET Core TempData and Error 400 The size of the request headers is too long

If you develop with ASP.NET Core 2.0 or later and you are using TempData, you may run into a 400 error saying your request headers is too long.  I've recently encountered this while writing a blog post, it happens when I try to preview the post.  Instead of seeing the preview of my content, I see this error :(

Bad Request - Request Too Long

HTTP Error 400. The size of the request headers is too long.

Bad Request - Request Too Long
Bad Request - Request Too Long

The error message itself and the fact that this error happens only on this post does not make sense to me. After googling it, it becomes clear that the cause of this particular error is that you have too many cookies!  Here is a good StackOverflow question on this issue.

To investigate I open up the Chrome devtool and see a list of 5 cookies associated with TempData, from .AspNetCore.Mvc.CookieTempDataProvider to .AspNetCore.Mvc.CookieTempDataProviderC4.  The first cookie has a value of "chunks-4" suggesting there are 4 other cookies, and almost all of these 4 cookies have a size of 4016 bytes which is the max cookie size allowed in Chrome browser.  Furthermore I've learned that to fix this issue you can simply delete these cookies.  However deleting the cookies does not really solve my problem, as soon as I hit the preview button again the error and cookies come back.

TempData Cookies
ASP.NET Core TempData Cookies

Digging deeper into the Compose page where I initiate the preview, I see that I am using TempData to transfer a blog post from the Compose page to the Preview page.  This reminds me of a fact I read about TempData when I first implemented this feature.  In the previous versions of ASP.NET and prior to ASP.NET Core 2.0, the TempData was implemented with session, but starting with ASP.NET Core 2.0 TempData by default is implemented with a cookie provider. Hence when I preview a post ASP.NET serializes my post content via cookies.  As my post gets long the sheer size of my content exceeds the max headers limit.  

The simplest way to fix this issue is to configure TempData using the session provider instead.  For scalability reasons I'm not normally a fan of session, I prefer and have been using IDistributedCache, but here I'm making an exception for just this one feature.  Once the object stored in the TempData is accessed it is removed from TempData, so it should not affect the performance.

ASP.NET does have two other similar mechanisms that could be used to pass data from controller to view, the ViewData and ViewBag.  But these two are used to carry data to the same view which would not work in my situation, because the preview happens on a different page.

Once I figured out why, the fix is very simple.  Open Startup.cs and add 3 lines of code to turn the session provider on and the preview issue is solved.

Upgrade from ASP.NET Core 2.1 to 2.2 and the IIS InProcess Hosting Model

ASP.NET Core 2.2 recently came out and the one thing that caught my attention was the new IIS InProcess Hosting Model.  I saw the demo during this week's Community Standup and couldn't help but agree that this could be a big help to development efficiency.  As web developers we all do build solution and refresh browsers a lot and any improvement to shorten the time between making a change and seeing that change is welcoming.

So I followed the migration guide, upgraded my blog from 2.1 to 2.2 locally (at the writing of this post this site is not updated yet). The upgrade was surprisingly easy compared to last time when I upgraded from 2.0 to 2.1, that was a lot more changes needed.

Here are things I did to upgrade to v2.2

  1. Install the 2.2 SDK.
  2. Update all .csproj files (class libs, web and test projects) TargetFramework from netcoreapp2.1 to netcoreapp2.2
  3. For the Fan.Web.csproj, also add <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>.  This is the new IIS in-process hosting model that will make development more efficient.
  4. Update Startup.cs CompatibilityVersion.Version_2_1 to CompatibilityVersion.Version_2_2
  5. After that I built my solution and found one incompatibilities with a nuget package and I ended up updating all nuget packages to latest and everything worked fine from that point.

To try the IIS InProcess Hosting Model, first I made sure my site is running in-process with IIS Express after upgrading to ASP.NET Core 2.2 by looking at the Process Explorer.  As you can see my DLLs are listed under the IIS Express process.

Process Explorer showing my DLLs in process with IIS Express and ASP.NET Core 2.2
Process Explorer showing my DLLs in process with IIS Express and ASP.NET Core 2.2

Then I tried to load my site on localhost with OutOfProcess, it took about 6.5 seconds and after that InProcess and only took about 4 seconds.

As noted by the migration guide the VS auto build does not currently work with InProcess.  Remember how we could update C# code and then just refresh the browser and see our changes, now with v2.2 InProcess if we make a code change then we have to do a manual build.  Honestly I'm OK with that.  Razor changes are still automatically picked up and there is performance improvements there as well.

 

Fanray v2.0-a4 released!

I released Fanray v2.0-a4, building on v2.0-a3 released from two weeks ago. Now you can drag and drop images directly in Composer and they will be uploaded and inserted in the editor.

dnd images
Drag and drop images to editor directly!

Currently you can only upload image files of type .jpg, .jpeg, .png and .gif, and the max file size allowed is 5MB. Error handling has been re-worked to support client side checking on these file constraints first and only sending valid files to the server side.  Server side validation remains of course.

Upload file error checking
Upload file error checking

All images uploaded are resized to different sizes, small, medium, large and original, the only exception is .gif files, only originals are saved and no resizes.  This is due to a remaining technical issue I yet to be able to solve with Magick.NET, when resizing gif files it takes a long time and the resized file size is larger than the original file. I put in a temporary fix till I figure out how to resize animated gif with Magick.NET or another library. 

Uploading files by clicking the UPLOAD button has been re-worked for Microsoft Edge.  Edge does not work with file input onchange event, so I implemented it with addEventListener instead.  I also added file input tag in the html markup instead of dynamically creating it in js.

Finally I improved on slug generation to make duplicates unique work better.  Posts and files uploaded are siloed with time, for a post you have year/month/day as part of its slug and for files you have year/month in its path.  So the chance of file name and post name collisions should be small.  But still it could happen, for example, you uploaded a file earlier this month and later you upload another file with the same name, what do you expect should happen?  Rest assured, I don't replace the existing file, instead I append a number to the file name, like my-file-2.png, to resolve the conflict. This number starts at 2, so if you upload another one you'll get my-file-3.png and so on.

Fanray v2.0-a3 released!

I released Fanray v2.0-a3, building on v2.0-a2 released from two weeks ago. Thanks to CKEditor5's latest release, now we can embed in our posts YouTube and Vimeo videos.

Simply copy a YouTube URL and paste it into your editor.  You will see a placeholder of the video show up in your editor.  This placeholder image is not playable directly inside the editor, however you can play the video when you preview the post.

What’s inside the editor is an oEmbed tag.  From the oEmbed's website

oEmbed is a format for allowing an embedded representation of a URL on third party sites. The simple API allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource, without having to parse the resource directly.

To show the actual video, what I did was I parsed the oembed tag and swapped it out with the YouTube’s own embed code. And to keep it simple I wrote the parser on the server side, take a look at the OembedParser.cs for more details.

Secondly, YouTube has a few different URL formats, most commonly are the following three. 

  • youtu.be/ 
  • youtube.com/watch?v= 
  • youtube.com/embed/

I did run into one edge case on an URL with the v param that did not start in the normal order, like this

https://www.youtube.com/watch?time_continue=75&v=MNor4dYXa6U

In this one case the editor didn’t like it, but for vast majority of the scenarios you are covered.  

I wrote some unit tests for my OembedParser that shows you the parsing of the different URL formats, you can check out OembedParserTest.cs for more info. 

Also if you take a look at the CKEditor’s documentation, you will see that it supports a variety of media embeds like a tweet, google map etc., but for this release I only support YouTube and Vimeo videos and will support others down the road. 

For other things I did for this release check out the milestone.  Thank you!
 

Implementing the Mediator Pattern with MediatR

Thus far I have a single service in Fan.Blog that handles everything, it's called BlogService.  As my code grows this becomes less maintainable, with each new feature I want to add to the blog I have to pile on top of this class, not to mention it's less than ideal if there are others working on the same project and everyone modifies one file.  So I wanted to break this giant service class into smaller more focused services.

In a YouTube video I want to show you how I refactored my BlogService with the Mediator Pattern using the MediatR library, please check it out.

Fanray v2.0-a2 released!

Today I release Fanray v2.0-a2, building on v2.0-a1 released from last week.  This release provides the ability to preview a post (issue #208) and other fixes.

Preview post
Preview your post

The preview will show you exactly how your post will look before you publish it, and it is fast and convenient.  It happens right inside the composer, when you click on the Preview button a dialog pops up full screen to show you the post as if it's published.  The URL of the post is printed on top of the preview window.  To exit you can press Esc or hit Close button.

One thing I want to point out, by design I want each Fanray theme to provide a content.css.  This file is meant for the editor to consume, it includes things like the typography and the content width styles of your particular theme. It enables the users to see a similar styling to the final published post while they are still editing.

But the content.css does not make your content 100% resemble the eventual published post.  This is due to some of the limitations of the editor, for example it currently does not support block source code which is a known issue and will be addressed in the future.  In the meantime, I have come up with a shortcode for you to input block source code into your post.

The ability to preview a post before publish is a much needed feature for the increased usability of the blog composer.  It'd be even nicer if it could have a keyboard shortcut assigned to the preview to help user get into the preview quickly.  Also if you are writing a long post it'd be helpful if there is an option to sync the preview and the editor so you don't always start from the top and have to scroll down.  These nice-to-have additions I don't have time to pursue right now, if anyone in the community is interested please feel free to improve on it.

I hope you find this feature useful :)

Using Vuex

I implemented Vuex in my project this week, I'm new to Vuex and this is what I learned.  In a Vue app when you need communication between two components, you have the following choices

One recommendation I read from the Vue Forum when someone asked about Bus vs Vuex is 

My advice would be as follows: use vuex for everything as a default, and drop back to an event bus when vuex becomes a hurdle.

The basic idea is your Vue app is a tree of components and the tree could be N layers deep, and to pass data between any two components in the tree becomes a challenge.  Events when used here and there is OK but in a large app if you add a lot of them your code becomes cumbersome.  So what the app need is a global object store to contain shared data accessible to all components and that is what Vuex is.

Here I have an issue #252, it describes that when you insert some selected images from the gallery dialog into your post and then you re-open the gallery two things should happen 1. the previously selected images should be de-selected and 2. the Edit, Delete and Insert buttons which only show up when there are selected images should be hidden, neither is happening. Not only that both should happen when the dialog is closed in most cases.

Composer with gallery open
Composer with gallery open

There are 4 ways to close the dialog

  1. Click on Insert, this should insert and de-select the images and close it
  2. Click on Close, this should de-select images and close it
  3. Press "Esc" key on keyboard, this should de-select images and close it
  4. Click anywhere outside the gallery, this should close dialog but without de-selecting the images. I find this scenario often happens by mistake not by intention.

The gallery is the child component, it contains the toolbar of buttons and a list of images.  When you select images they are stored in an array named selectedImages on this child component.  The gallery is then contained inside a dialog component which is part of the parent composer. Whether the selectedImages is empty is what controls the visibility of the three buttons on the toolbar.  When I click on Insert I pass the selected images to a method of the parent which then writes out html to the editor.  

When I click on insert I actually could easily de-select the images and empty out selectedImages because the Insert button is part of the gallery component, same with clicking the Close button.  When I press Esc on my keyboard to close the dialog though it is not as simple because that is part of the parent.  In other words the parent needs to access selectedImages on the child. 

I feel this is a great example of when you could use Vuex.

[code lang=js]

let store = new Vuex.Store({ 
   strict: true, 
   state: { 
       selectedImages: [], 
   }, 
   mutations: { 
       setSelectedImages(state, newSelectedImages) { 
           state.selectedImages = newSelectedImages; 
       }, 
       addSelectedImage(state, image) { 
           state.selectedImages.push(image); 
       }, 
       removeSelectedImage(state, idx) { 
           state.selectedImages.splice(idx, 1); 
       }, 
   }, 
   actions: { 
       selectImage: function ({ commit }, image) { 
           commit('addSelectedImage', image); 
       }, 
       deselectImage: function ({ commit }, idx) { 
           commit('removeSelectedImage', idx); 
       }, 
       emptySelectedImages({ commit, state }) { 
           commit('setSelectedImages', []); 
       }, 
   }, 
});

[/code]

Then the selectedImages array breaks loose and accessible globally, thus the parent can take a hold of it and empty it when necessary

[code lang=js]this.$store.dispatch('emptySelectedImages'); [/code]

Older posts