If you ever wanted an efficient and easy way to use the APIs and resources of an Optimizely CMS-project this is for you. No more database hacks to fetch or manipulate data.
The sources are available on GitHub at https://github.com/EricHerlitz/optimizely-cms-terminal. Big thanks to Johan Petersson at Optimizely for helping out!
Some requirements
- Initialize as much as possible of Optimizely CMS
- Beeing able to use DI/IoC
- As few dependencies as possible
I'll be using Alloy as an example, I do most of my development in Linux but this will work on Windows just as well. I'm using the Optimizely CLI tools to get started, read more here: Optimizely CMS 12 CLI tools, getting started.
Creating the Alloy web
This step isn't strictly needed if you already have an existing CMS site you are willing to try on.
dotnet new epi-alloy-mvc -n Optimizely.CMS.Alloy
Since I'm on Linux the LocalDB won't work, next step is to create a database on SQL Server. Mine is located in a docker container.
dotnet-episerver create-cms-database \ -S localhost \ -U sa \ -P YOURSAPASSWORD \ -dn EpiserverDB_CmsAlloyDatabase \ -du EpiserverDB_CmsAlloyUser \ -dp aVeryStrongDatabasePassword123! \ Optimizely.CMS.Alloy.csproj
Check here for a more detailed explanation of the Optimizely CLI: Creating a new CMS database using the Optimizely CLI.
That should be all you need to have an environment setup, start Alloy in all its glory, and create an admin user either by using the CLI or by using the Create admin start page in Alloy.
Creating the Optimizely Terminal (console app)
The console app can be created wherever you like. If you want to use it standalone to read data you may want to create it as a separate solution. If you want to be able to use the custom content objects like typed ContentData objects (PageData, BlockData etc) I'd strongly suggest you create it as a project in your solution sharing the ContentData objects as abstractions between the CMS project and the Terminal project.
Create the terminal to your needs, here's what I do
dotnet new console -n Optimizely.CMS.Terminal -f net8.0 --use-program-main
Ensure you have the Optimizely Nuget feed in your sources
dotnet nuget list source
If not present add it by typing
dotnet nuget add source https://api.nuget.optimizely.com/v3/index.json -n Optimizely
Add required packages. In this example, I'll add specific versions. Adjust to your needs.
dotnet add package EPiServer.CMS.Core -v 12.19.0 dotnet add package EPiServer.Hosting -v 12.19.0 dotnet add package EPiServer.ImageLibrary.ImageSharp -v 2.0.1 dotnet add package Microsoft.Extensions.Hosting -v 8.0.0 dotnet add package Microsoft.Extensions.Http -v 8.0.0
The host builder
I've added a CreateHostBuilder method to the Program.cs file where I configure and bootstrap the console and the CMS API's. The CreateHostBuilder method is executed and adds the RunConsoleAsync() extension method which enables console support, builds and starts the host.
public static async Task Main(string[] args) => await CreateHostBuilder(args).RunConsoleAsync();
The HostBuilder is constructed like this
- First, there's a ConfigureHostConfiguration where we need to specify which config files to use and to help the host builder to select the correct environment.
- The ConfigureCmsDefaults implementation is the default from Optimizely. It sets up the service provider factories.
- The ConfigureLogging implementation is, well, there for logging configuration if you need it.
- ConfigureServices is where we have our Dependency Injections and the place where we start the good stuff.
- The AddHostedService inside the ConfigureServices is where the Startup file is and it is inside this file you will perform your operations.
private static IHostBuilder CreateHostBuilder(string[] args) { // ... return Host.CreateDefaultBuilder(args) .ConfigureHostConfiguration(host => { host.AddEnvironmentVariables("ASPNETCORE_"); host.AddJsonFile("appsettings.json", false); host.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", true); }) .ConfigureCmsDefaults() .ConfigureLogging(logging => { logging.ClearProviders(); logging.AddConsole(); }) .ConfigureServices((hostContext, services) => { services.BootstrapEpi(hostContext); services.AddTransient<InstanceCounter>(); // sample implementation #1 services.AddHostedService<Startup>(); }); }
I've also added an appsettings.json. Some properties in this file are not initialized by default of the Optimizely initialization, such properties are bootstraped using the custom BootstrapEpi() extension method. Ensure that the appsettings files are copied to the output directory by using the "copy always" setting on the file.
<ItemGroup> <None Update="appsettings.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> <None Update="appsettings.Development.json"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup>
As of the date of publication, I've added DataAccessOption and LicensingOptions in the BootstrapEpi implementation to ensure that the database can be found and the license can be loaded. An alternative to my approach is that you add the license file to the project and copy it the same way as the appsettings files.
public static void BootstrapEpi(this IServiceCollection services, HostBuilderContext hostContext) { services.Configure<DataAccessOptions>(o => { o.ConnectionStrings.Add(new ConnectionStringOptions { ConnectionString = hostContext.Configuration.GetConnectionString("EPiServerDB"), Name = o.DefaultConnectionStringName }); }); services.Configure<LicensingOptions>(o => { o.LicenseFilePath = hostContext.Configuration.GetSection("EPiServer:CMS:LicensePath:Path").Value; }); services.AddCmsHost(); }
You do however need a license file when working with the IContent APIs.
Sample implementation #1
A simple counter that shows all content types and how many times they have been created.
For the full implementation see the InstanceCounter.cs file
public class InstanceCounter( IContentTypeRepository contentTypeRepository, IContentModelUsage contentModelUsage) { public void CountInstances() { var contentTypes = contentTypeRepository.List(); var items = contentTypes.ToList().Select(contentType => new { Name = contentType.Name, Count = contentModelUsage.ListContentOfContentType(contentType).DistinctBy(x => x.ContentLink.ID).Count() }).ToList(); items.ForEach(item => Console.WriteLine($"{item.Name} has {item.Count} instances")); } }
Inject in the builder
services.AddTransient<InstanceCounter>();
add in Startup
public class Startup( InstanceCounter instanceCounter, IHostApplicationLifetime hostApplicationLifetime) : IHostedService { public Task StartAsync(CancellationToken cancellationToken) { // this will run the implementation instanceCounter.CountInstances(); hostApplicationLifetime.StopApplication(); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { hostApplicationLifetime.StopApplication(); return Task.CompletedTask; } }
Run either in your IDE or by typing dotnet run in the console
Sample implementation #2
To fetch a page and some property data inject the IContentLoader and read properties like this
var startPage = contentLoader.Get(ContentReference.StartPage); var teaserText = startPage.GetPropertyValue ("TeaserText");
Since we don't have the content types in this project we'll need to rely on the methods from the ContentDataExtensions class to work with the content objects.
The sky is the limit to what type of services you can build from here on. The sources are available on GitHub at https://github.com/EricHerlitz/optimizely-cms-terminal.