pineblog

PineBlog v2

PineBlog has been updated to version 2. This new release adds the following new features and improvements:

  • MongoDb can now be used a data source
  • Extended Markdown support
  • Now targeting net5.0 and netcoreapp3.1
  • Removed the _ValidationScriptsPartial from Opw.PineBlog.RazorPages
  • Removed dependencies on Opw.Core and Opw.EntityFramework

Build Status NuGet Badge License: MIT

Using MongoDb

When you want to use MongoDb as your database, then you don't use the Opw.PineBlog metapackage but you need to install the required packages individually.

  • Opw.PineBlog.MongoDb package The PineBlog data provider that uses MongoDb. NuGet Badge

  • Opw.PineBlog.RazorPages package The PineBlog UI using ASP.NET Core MVC Razor Pages. NuGet Badge

  • Opw.PineBlog.Core package The PineBlog core package. This package is a dependency for Opw.PineBlog.RazorPages and Opw.PineBlog.MongoDb. NuGet Badge

Startup

You add the PineBlog services and the Razor Pages UI in the Startup.cs of your application.

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddPineBlogCore(Configuration);
    services.AddPineBlogMongoDb(Configuration);

    services.AddRazorPages().AddPineBlogRazorPages();
    // or services.AddMvcCore().AddPineBlogRazorPages();
    // or services.AddMvc().AddPineBlogRazorPages();
    ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{

    // Make sure you enable static file serving
    app.UseStaticFiles();

    ...
    app.UseEndpoints(endpoints =>
    {
        // make sure to add the endpoint mapping for both RazorPages and Controllers
        endpoints.MapRazorPages();
        endpoints.MapControllers();
    });
    ...
}

NOTE: Make sure you enable static file serving app.UseStaticFiles();, to enable the serving of the css and javascript from the Opw.PineBlog.RazorPages packages.

Configuration

And a few properties need to be configured before you can run your web application with PineBlog.

{
    "ConnectionStrings": {
        "MongoDbConnection": "inMemory" // MongoDb connection string
    },
    "PineBlogOptions": {
        "Title": "PineBlog",
        "Description": "A blogging engine based on ASP.NET Core MVC Razor Pages and MongoDb",
        "ItemsPerPage": 5,
        "CreateAndSeedDatabases": true,
        "ConnectionStringName": "MongoDbConnection",
        "MongoDbDatabaseName": "pineblog-db",
        "AzureStorageConnectionString": "UseDevelopmentStorage=true",
        "AzureStorageBlobContainerName": "pineblog",
        "FileBaseUrl": "http://127.0.0.1:10000/devstoreaccount1"
    }
}

Blog Settings ConfigurationProvider

To be able to update the blog settings from the admin pages, you need to add the PineBlog IConfigurationProviders to the IConfigurationBuilder in the Program.cs. Add config.AddPineBlogMongoDbConfiguration(reloadOnChange: true); to ConfigureAppConfiguration(..) on the IWebHostBuilder.

WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .ConfigureAppConfiguration((hostingContext, config) => {
        config.AddPineBlogMongoDbConfiguration(reloadOnChange: true);
    });

Markdown support

Markdig is used as the Markdown processor and the following markdown features are enabled by default:

If you want or require more advanced Markdown features, you can enable those by overriding the ~/Areas/Blog/Pages/Shared/_Post.cshtml partial (see source).

Tables

How to create the following table with styling, using pipe tables and generic attributes.

Company Contact Country
Alfreds Futterkiste Maria Anders Germany
Centro comercial Moctezuma Francisco Chang Mexico
Ernst Handel Roland Mendel Austria
Island Trading Helen Bennett UK
{.table .table-striped}
|Company|Contact|Country|
|-|-|-|
|Alfreds Futterkiste|Maria Anders|Germany|
|Centro comercial Moctezuma|Francisco Chang|Mexico|
|Ernst Handel|Roland Mendel|Austria|
|Island Trading|Helen Bennett|UK|

Using CSS

How to create the following blockquote with an bootstrap info-alert style, using generic attributes.

Normally the dangers inherent in the diverse hardware environment enhances the efficiency of the inductive associative dichotomy on a strictly limited basis.

{.alert .alert-info}
> Normally the dangers inherent in the diverse hardware environment enhances the efficiency of the inductive associative dichotomy on a strictly limited basis.

.Net 5.0

The PineBlog packages are now targeting targeting net5.0 and netcoreapp3.1.

Where can I get it?

You can install the Opw.PineBlog metapackage from the console.

> dotnet add package Opw.PineBlog

The Opw.PineBlog metapackage includes the following packages.

  • Opw.PineBlog.EntityFrameworkCore package The PineBlog data provider that uses Entity Framework Core. NuGet Badge

  • Opw.PineBlog.RazorPages package The PineBlog UI using ASP.NET Core MVC Razor Pages. NuGet Badge

  • Opw.PineBlog.Core package The PineBlog core package. This package is a dependency for Opw.PineBlog.RazorPages and Opw.PineBlog.EntityFrameworkCore. NuGet Badge

  • Opw.PineBlog.MongoDb package The PineBlog data provider that uses MongoDb. NuGet Badge

Demo website

Check out the PineBlog demo website. You can write and publish posts, upload files and test application before install. And no worries, it is just a sandbox and will clean itself.

Url: pineblog.azurewebsites.net
Username: pineblog@example.com
Password: demo

PineBlog is upgraded to .Net Core 3.0

In the latest release of PineBlog I've added support for .Net Core 3.0 (and 3.1-preview3) and removed support for .Net Core 2.2. There are no mayor changes in the package. But since .Net Core 3.x has some changes in the Startup.cs, you might need to update the registration of PineBlog there.

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddPineBlog(Configuration); 

    // when you want to add just the bare minimum
    services.AddRazorPages().AddPineBlogRazorPages();

    // or you can use the more familiar ways from .Net Core 2.x
    // or services.AddMvcCore().AddPineBlogRazorPages();
    // or services.AddMvc().AddPineBlogRazorPages();
    ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseEndpoints(endpoints =>
    {
        // make sure to add the endpoint mapping for both RazorPages and Controllers
        endpoints.MapRazorPages();
        endpoints.MapControllers();
    });
    ...
}

For more information, please check PineBlog on GitHub.

Creating a custom ConfigurationProvider for a Entity Framework Core source

ASP.NET Core has a lightweight configuration system that is designed to be highly extensible. It lets you aggregate many configuration values from multiple different sources, and then access those in a strongly typed fashion using the Options pattern.

Using packages in the Microsoft.Extensions.Configuration namespace, you can read configuration from:

  • Azure Key Vault
  • Azure App Configuration
  • Command-line arguments
  • Custom providers (installed or created)
  • Directory files
  • Environment variables
  • In-memory .NET objects
  • Settings files

For PineBlog I wanted some values that can be set in the appsettings.json to be set from the admin UI as well. These values are stored in the database and exposed through a Entity Framework Core (EF Core) DbContext.

In this post I'm going to describe creating a custom configuration provider that uses EF Core. For the sake of simplicity it is a more general description than the implementation in PineBlog.

Default configuration

Web apps based on the ASP.NET Core dotnet new templates call CreateDefaultBuilder when building a host. This provides default configuration for the app, for instance the appsettings.json using the File Configuration Provider. For more information see the official documentation on this topic.

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>();;

The ConfigOptions

In this example we will have a ConfigOptions class that maps to the configuration in appsettings.json. And a ConfigEntity class that overrides some of those configuration values from the database.

public class ConfigOptions
{
    public string BackgroundColor { get; set; }
    public int ItemsPerPage { get; set; }
    public bool ShowHeader { get; set; }

    // this property will not be overridden by ConfigEntity
    public string ApiKey { get; set; } 
}

public class ConfigEntity
{
    public string BackgroundColor { get; set; }
    public int ItemsPerPage { get; set; }
    public bool ShowHeader { get; set; }
}

The appsettings.json look like this:

{
  "ConfigOptions": {
    "BackgroundColor": "#ff0000",
    "ItemsPerPage": 2,
    "ShowHeader": true,
    "ApiKey": "h&Ww1vbFP6y2RW7Nx$$Q&&KW"
  }
}

Creating a custom configuration provider

With the basics out of the way, we can now start creating our custom configuration provider.

In order to create a custom provider, you need to implement the IConfigurationProvider and IConfigurationSource interfaces from the Microsoft.Extensions.Configuration.Abstractions package. Or you can use any of the provided base classes to get started.

Configuration source

The IConfigurationSource interface only has one method that needs implementing.

And because we will be using EF Core as a configuration source we need a DbContextOptionsBuilder action to use the DbContext from the configuration provider. We also have a ReloadDelay to avoid triggering a reload before a change is completely saved.

public class ConfigEntityConfigurationSource : IConfigurationSource
{
    public Action<DbContextOptionsBuilder> OptionsAction { get; set; }

    public bool ReloadOnChange { get; set; }

    // Number of milliseconds that reload will wait before calling Load. This helps avoid triggering a reload before a change is completely saved. Default is 500.
    public int ReloadDelay { get; set; } = 500;

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new ConfigEntityConfigurationProvider(this);
    }
}

Configuration provider

For our custom configuration provider we use the ConfigurationProvider base class, this is the most basic implementation of IConfigurationProvider. The configuration provider initializes the database when it's empty.

We override the Load method with our custom implementation, this method loads (or reloads) the data for the provider. Here we instantiate the DbContext using the OptionsAction from the configuration source. And we then get a ConfigEntity from the database and set its values to the configuration dictionary of the configuration provider.
And by using the same keys for the configuration values (e.g. ConfigOptions.BackgroundColor) as we used in the appsettings.json we will effectively override those values with the values from the database.

public class ConfigEntityConfigurationProvider : ConfigurationProvider
{
    private readonly ConfigEntityConfigurationSource _source;

    public ConfigEntityConfigurationProvider(ConfigEntityConfigurationSource source)
    {
        _source = source;
    }

    public override void Load()
    {
        var builder = new DbContextOptionsBuilder<EntityDbContext>();
        _source.OptionsAction(builder);

        using (var context = new CustomDbContext(builder.Options))
        {
            context.Database.EnsureCreated();

            var config = context.ConfigEntity.SingleOrDefault();
            if (config == null) return;

            Data = new Dictionary<string, string>();
            Data.Add($"{nameof(ConfigOptions)}.{nameof(ConfigOptions.BackgroundColor)}", config.BackgroundColor);
            Data.Add($"{nameof(ConfigOptions)}.{nameof(ConfigOptions.ItemsPerPage)}", config.ItemsPerPage);
            Data.Add($"{nameof(ConfigOptions)}.{nameof(ConfigOptions.ShowHeader)}", config.ShowHeader);
        }
    }
}

We now have a configuration provider that loads its values from the database. But it will only do this once, when the application is started, and we want it to reload as well when the user updates the ConfigEntity in the database.

Reloading configuration on entity changes

To trigger a reload of the configuration when the ConfigEntity in the database changes, we need to let the configuration provider know that the entity has changed. We'll solve this by triggering an event on a entity change observer class and listening for this event in our configuration provider.

Entity change observer

We create a singleton class that has an EventHandler for the entity changes. Our configuration provider can listen for this event to update the configuration.

I've made this class an old school singleton, since injecting it into the DbContext was a bit complicated. And we use ThreadPool.QueueUserWorkItem offload the invoking of the event to a background thread, so it doesn't block the DbContext.SaveChanges.

public class EntityChangeObserver
{
    public event EventHandler<EntityChangeEventArgs> Changed;

    public void OnChanged(EntityChangeEventArgs e)
    {
        ThreadPool.QueueUserWorkItem((_) => Changed?.Invoke(this, e));
    }

    #region singleton

    private static readonly Lazy<EntityChangeObserver> lazy = new Lazy<EntityChangeObserver>(() => new EntityChangeObserver());

    private EntityChangeObserver() { }

    public static EntityChangeObserver Instance => lazy.Value;

    #endregion singleton
}

Notify the observer

Create a custom (base) class that extends DbContext, and override the SaveChanges methods.

public abstract class EntityDbContext : DbContext
{
    public override int SaveChanges()
    {
        OnEntityChange();
        base.SaveChanges()
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken)
    {
        OnEntityChange();
        return await base.SaveChangesAsync(cancellationToken);
    }

    private void OnEntityChange()
    {
        foreach(var entry in ChangeTracker.Entries()
            .Where(i => i.State == EntityState.Modified || i.State == EntityState.Added))
        {
            EntityChangeObserver.Instance.OnChanged(new EntityChangeEventArgs(entry));
        }
    }
}

Trigger a reload

To let the configuration provider know the entity has changed and to trigger a reload, we need to listen for the EntityChangeObserver.Changed event. And because we only want to reload when the ConfigEntity changes, we add a check to see if that is the type of the changed entity.

public ConfigEntityConfigurationProvider(ConfigEntityConfigurationSource source)
{
    _source = source;

    if (_source.ReloadOnChange)
        EntityChangeObserver.Instance.Changed += EntityChangeObserver_Changed;
}

private void EntityChangeObserver_Changed(object sender, EntityChangeEventArgs e)
{
    if (e.Entry.Entity.GetType() != typeof(ConfigEntity))
        return;

    Thread.Sleep(_source.ReloadDelay);
    Load();
}

Add the configuration in the application

And finally in the Program.cs we can now configure the IConfigurationSource.

WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .ConfigureAppConfiguration((hostingContext, config) => {
        config.Add(new ConfigEntityConfigurationSource {
            OptionsAction = o => o.UseInMemoryDatabase("db", new InMemoryDatabaseRoot()),
            ReloadOnChange = true
        });
    });

Our application will now load the ConfigOptions from the appsettings.json, then use the ConfigEntityConfigurationSource to override some of the values (when present in the database). And when the user updates the ConfigEntity in the database the values will be reloaded.

Note: Use IOptionsSnapshot to support reloading options with minimal processing overhead. Options are computed once per request when accessed and cached for the lifetime of the request. See the official documentation on reload configuration data with IOptionsSnapshot.