Blog posts by Kristoffer Lindén2022-02-07T08:50:09.0000000Z/blogs/kristoffer-linden/Optimizely WorldAttach local database using .NET Core and CMS 12/blogs/kristoffer-linden/dates/2022/1/attach-local-database-using--net-core-and-cms-12/2022-02-07T08:50:09.0000000Z<p>In my CMS 11 solution I used the variable <strong>|DataDirectory|</strong> to point out the App_Data folder where my database file is placed and that workd just fine. Is Net Core I tried to use the same:</p>
<pre class="language-markup"><code>"ConnectionStrings": {
"EPiServerDB": "Data Source=(LocalDb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\EPiServerDB_f6c7a112.mdf;Initial Catalog=EPiServerDB_f6c7a112;Connection Timeout=60;Integrated Security=True;MultipleActiveResultSets=True"
}</code></pre>
<p>In CMS 12 using Net Core, the |DataDictionary| does not work anymore and need to be replaced in some way. I found this that I thought was my solution:</p>
<pre class="language-csharp"><code>public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
string baseDir = env.ContentRootPath;
AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Path.Combine(baseDir, "App_Data"));
}</code></pre>
<p>What happend was that when calling <span>ConfigureCmsDefaults() which is done in Program.cs, Episerver tries to connect to the database but the connectionstring is not modified so that fails. A strange behavior was that the code then never reached the Configure(...) method and I could never understand why until I set the real physical path to the database in the connectionstring.</span></p>
<pre class="language-markup"><code>"ConnectionStrings": {
"EPiServerDB": "Data Source=(LocalDb)\\MSSQLLocalDB;AttachDbFilename=D:\\Projekt\\Web\\App_Data\\EPiServerDB_f6c7a112.mdf;Initial Catalog=EPiServerDB_f6c7a112;Connection Timeout=60;Integrated Security=True;MultipleActiveResultSets=True"
}</code></pre>
<p>Then everything worked just fine, but is doesn't look to good and finally I found a solution using <strong>PostConfigure</strong>.</p>
<p>Connectionstring looks like this:</p>
<pre class="language-markup"><code>"ConnectionStrings": {
"EPiServerDB": "Data Source=(LocalDb)\\MSSQLLocalDB;AttachDbFilename=App_Data\\EPiServerDB_f6c7a112.mdf;Initial Catalog=EPiServerDB_f6c7a112;Connection Timeout=60;Integrated Security=True;MultipleActiveResultSets=True",
}</code></pre>
<p>And ConfigureServices like this:</p>
<pre class="language-csharp"><code>public void ConfigureServices(IServiceCollection services)
{
services.PostConfigure<DataAccessOptions>(o =>
{
o.SetConnectionString(_configuration.GetConnectionString("EPiServerDB").Replace("App_Data", Path.GetFullPath("App_Data")));
});
services.PostConfigure<ApplicationOptions>(o =>
{
o.ConnectionStringOptions.ConnectionString = _configuration.GetConnectionString("EPiServerDB").Replace("App_Data", Path.GetFullPath("App_Data"));
});
}</code></pre>
<p>In this way the connectionstring is modified before Episerver is initiated and the database will be attached correctly.</p>
<p><strong></strong></p>Handle translations in ViewConfiguration for CMS 12/blogs/kristoffer-linden/dates/2021/12/handle-translations-in-viewconfiguration-for-cms-12/2021-12-30T12:30:04.0000000Z<p>In CMS 11 I had a view configuration that looked like this:</p>
<pre class="language-csharp"><code>[ServiceConfiguration(typeof(ViewConfiguration))]
public class SendToTranslationView : ViewConfiguration<IContentData>
{
public SendToTranslationView()
{
var localization = ServiceLocator.Current.GetInstance<LocalizationService>();
Key = "SendToTranslationView";
Name = localization.GetString("/Translations/ContentView/Name");
Description = localization.GetString("/Translations/ContentView/Description");
ControllerType = "epi-cms/widget/IFrameController";
ViewType = "/MyPlugin/ContentTranslationHistory/Index";
IconClass = "epi-iconCatalog epi-icon--medium";
}
}</code></pre>
<p>And after upgrading to CMS 12, the menu option no longer was translated even though the Thread.CurrentThread.CurrentUICulture was updated when changing the users UI language.<br />For some reason Name and Descrption no longer works in CMS 12 so the solution is to use LanguagePath instead.</p>
<p>This is how it should look:</p>
<pre class="language-csharp"><code>[ServiceConfiguration(typeof(ViewConfiguration))]
public class SendToTranslationView : ViewConfiguration<IContentData>
{
public SendToTranslationView()
{
Key = "SendToTranslationView";
ControllerType = "epi-cms/widget/IFrameController";
ViewType = "/MyPlugin/ContentTranslationHistory/Index";
IconClass = "epi-iconCatalog epi-icon--medium";
LanguagePath = "/Translations/ContentView";
}
}</code></pre>
<p>To get your menu option translated to English your language file then needs to contain:</p>
<pre class="language-markup"><code><language name="English" id="en">
<Translations>
<ContentView>
<Name>Translate content</Name>
<Description>Choose what properties you want to translate and then mark your content</Description>
</ContentView>
</Translations>
</language></code></pre>
<p>So from now on, use LanguagePath instead of Name and Description in you ViewConfigurations</p>How to simplify initialization for your custom plugin/blogs/kristoffer-linden/dates/2021/12/how-to-initialize-your-custom-plugin/2021-12-13T15:37:12.0000000Z<p>When your are creating a custom plugin you might want to initialize your plugin on startup. You could of course add everything you need in the Optimizely Startup.cs class but is is much nicer to have the user just adding one row of code using an extension method.</p>
<p>Your extension could look something like this:</p>
<pre class="language-csharp"><code>public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMyPlugin(this IServiceCollection services, string connectionString)
{
services.AddDbContext<MyDbContext>(x => x.UseSqlServer(connectionString))
.AddScoped<Microsoft.EntityFrameworkCore.DbContext, MyDbContext>()
.AddSingleton<IMyService1, MyService>()
.AddSingleton<IMyService2, MyService2>();
return services;
}
}</code></pre>
<p>And the only thing you need to add in Startup.cs is this:</p>
<pre class="language-csharp"><code>services.AddMyPlugin(_configuration.GetConnectionString("EpiserverDB"));</code></pre>
<p>I had som static content that generated 404 so I also had to create an extension to handle static content.</p>
<pre class="language-csharp"><code>public static IApplicationBuilder UseMyPluginStaticContent(this IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(env.ContentRootPath, "modules", "MyPlugin", "ClientResources")),
RequestPath = "/modules/MyPlugin/ClientResources"
});
return app;
}</code></pre>
<p>and the add this row in the Startup.cs Configure method:</p>
<pre class="language-csharp"><code>app.UseMyPluginStaticContent(env);</code></pre>
<p>This is an easy way to create advanced initialization but the user only needs to add two rows of code that clearly points out that they are use by My Plugin.</p>Create your own menu to handle your custom plugin /blogs/kristoffer-linden/dates/2021/12/create-your-own-menu-for-your-plugin-/2021-12-08T10:13:54.0000000Z<p>So in CMS 11 when you wrote your plugin you easily add settings to your plugin using <strong>GuiPlugin </strong>attribute and adding properties using the <strong>PlugInProperty </strong>attribute.<br />In CMS 12 that is not longer possible so the best is instead to create your own top menu item with a submenu that contains for example Dashboard and Settings.</p>
<p>The menu provider could look like this:</p>
<pre class="language-csharp"><code>[MenuProvider]
public class MyPluginMenuProvider : IMenuProvider
{
private readonly LocalizationService _localizationService;
public MyPluginMenuProvider(LocalizationService localizationService)
{
_localizationService = localizationService;
}
public IEnumerable<MenuItem> GetMenuItems()
{
var mainMenuItem =
new UrlMenuItem("My plugin", MenuPaths.Global + "/cms/myplugin", null)
{
IsAvailable = context => true,
SortIndex = SortIndex.Last + 100,
AuthorizationPolicy = CmsPolicyNames.CmsAdmin
};
var dashboardMenuItem =
new UrlMenuItem(_localizationService.GetString("/Plugin/DisplayName"),
MenuPaths.Global + "/cms/myplugin/dashboard", "/MyPlugin/Dashboard/Index")
{
IsAvailable = context => true,
SortIndex = 100,
AuthorizationPolicy = CmsPolicyNames.CmsAdmin
};
var settingsMenuItem =
new UrlMenuItem(_localizationService.GetString("/Plugin/Settings"),
MenuPaths.Global + "/cms/myplugin/settings", "/MyPlugin/Settings/Index")
{
IsAvailable = context => true,
SortIndex = 200,
AuthorizationPolicy = CmsPolicyNames.CmsAdmin
};
return new List<MenuItem>
{
mainMenuItem,
dashboardMenuItem,
settingsMenuItem
};
}
}</code></pre>
<p>That would give you this look:</p>
<p><img src="/link/b6d918c6448b4104af48fcc65b679f94.aspx" width="532" alt="" height="207" /></p>
<p>Your layout view should look something like this to have the Optimizely menu to render:</p>
<pre class="language-markup"><code>@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>My Plugin</title>
<!-- Shell -->
@ClientResources.RenderResources("ShellCore")
<!-- LightTheme -->
@ClientResources.RenderResources("ShellCoreLightTheme")
</head>
<body>
@Html.CreatePlatformNavigationMenu()
<div @Html.ApplyPlatformNavigation()>
@RenderBody()
</div>
</body>
</html></code></pre>
<p>When this is done you can easily work with your dashboard and settings functions.</p>
<p>Thanks Scott Reed for enlighting me in this forum post:<br /><a href="/link/35e5a606cf6844268a155413c0df8bff.aspx">https://world.optimizely.com/forum/developer-forum/CMS/Thread-Container/2021/12/admin-plugin--tool-in-cms-12/</a></p>
<p>You can read more here:<br /><a href="/link/973566f68f5440199168d0e23cba9f08.aspx">https://world.optimizely.com/documentation/developer-guides/CMS/user-interface/extending-the-navigation/using-menu-providers/</a></p>
<p>/Kristoffer</p>Create Episerver admin user by code/blogs/kristoffer-linden/dates/2017/12/create-episerver-login-account-by-code/2017-12-14T09:53:11.0000000Z<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>After installing an Alloy site I forgot the username and password to login to Episerver and creating a local user and adding it to the Administrators group did not work for me. Maybe someone can tell me how that should work and be done!?</p>
<p>The old fashion way to create an Episerver user is described here:</p>
<p><a href="/link/d6349083e9484f6c84de3fe47b244229.aspx">https://world.episerver.com/blogs/Henrik-Fransas/Dates/2015/10/how-to-create-a-admin-user-through-code/</a></p>
<p>But that does not work in CMS 10 or 11 so I decided to write some code to create a user in CMS 11 and ended up with this:</p>
<pre class="language-csharp"><code>[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class EpiserverInitialization : IInitializableModule
{
private static readonly string[] _roles = { "WebAdmins", "WebEditors" };
public void Initialize(InitializationEngine context)
{
using (UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(new ApplicationDbContext<ApplicationUser>("EPiServerDB")))
{
//If there's already a user, then we don't need a seed
if (!store.Users.Any(x => x.UserName == "sysadmin"))
{
var createdUser = CreateUser(store, "sysadmin", "p@ssword", "sysadmin@mysite.com");
AddUserToRoles(store, createdUser, _roles);
store.UpdateAsync(createdUser).GetAwaiter().GetResult();
}
}
}
private ApplicationUser CreateUser(UserStore<ApplicationUser> store, string username, string password, string email)
{
//We know that this Password hasher is used as it's configured
IPasswordHasher hasher = new PasswordHasher();
string passwordHash = hasher.HashPassword(password);
ApplicationUser applicationUser = new ApplicationUser
{
Email = email,
EmailConfirmed = true,
LockoutEnabled = true,
IsApproved = true,
UserName = username,
PasswordHash = passwordHash,
SecurityStamp = Guid.NewGuid().ToString()
};
store.CreateAsync(applicationUser).GetAwaiter().GetResult();
//Get the user associated with our username
ApplicationUser createdUser = store.FindByNameAsync(username).GetAwaiter().GetResult();
return createdUser;
}
private void AddUserToRoles(UserStore<ApplicationUser> store, ApplicationUser user, string[] roles)
{
IUserRoleStore<ApplicationUser, string> userRoleStore = store;
using (var roleStore = new RoleStore<IdentityRole>(new ApplicationDbContext<ApplicationUser>("EPiServerDB")))
{
IList<string> userRoles = userRoleStore.GetRolesAsync(user).GetAwaiter().GetResult();
foreach (string roleName in roles)
{
if (roleStore.FindByNameAsync(roleName).GetAwaiter().GetResult() == null)
{
roleStore.CreateAsync(new IdentityRole { Name = roleName }).GetAwaiter().GetResult();
}
if (!userRoles.Contains(roleName))
userRoleStore.AddToRoleAsync(user, roleName).GetAwaiter().GetResult();
}
}
}
public void Uninitialize(InitializationEngine context)
{
}
public void Preload(string[] parameters)
{
}
}</code></pre>
<p>After this code is run I can login with the created sysadmin user.</p>
<p>/Kristoffer</p>
</body>
</html>