candidcontributions / canconcloud Goto Github PK
View Code? Open in Web Editor NEWThe Github home of CandidContributions.com (an Umbraco v9 website): a fortnightly podcast where four developers talk all things open source
License: MIT License
The Github home of CandidContributions.com (an Umbraco v9 website): a fortnightly podcast where four developers talk all things open source
License: MIT License
The CandidContributions.com website was upgraded from an Umbraco v8 'on-premise' site to a v9 Umbraco Cloud site during the Umbraco Hacktoberfest Hackathon in October 2021. It was a wonderful collaborative experience, and we're grateful to everyone who participated! You can read our write-up of the migration process on the Umbraco blog.
The main migration challenges we experienced are listed below as separate GitHub issues in case useful to others (and no doubt our future selves...). If you have any comments, or alternative suggestions about how we could have resolved the issues, please let us know!
If you only see numbers (not issue titles) in the list above please log in to GitHub and reload the page!
The resulting v9 code base is in this repository.
The v8 code base that we were migrating from can also be found on GitHub.
To retrieve the list of podcast episodes from our podcast service (Spreaker) we used code such as :
static HttpClient client = new HttpClient();
var episodesApiUrl = string.Format(spreakerApiEpisodesUrlFormat, options.ShowId);
var response = client.GetAsync(episodesApiUrl).Result;
See the complete code in our v8 code base: SpreakerComposer.cs
We switched to injecting IHttpClientFactory
public class SpreakerComponent : IComponent
{
private readonly IHttpClientFactory _clientFactory;
public SpreakerComponent(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public void SomeMethod()
{
var episodesApiUrl = string.Format(spreakerApiEpisodesUrlFormat, options.ShowId);
var request = new HttpRequestMessage(HttpMethod.Get, episodesApiUrl);
var client = _clientFactory.CreateClient();
var response = client.SendAsync(request).Result;
}
Refer to SpreakerComposer.cs in this code base for the complete code.
In v8, UmbracoHelper allows you to query for content using Xpath queries. We had one such static helper method:
private static T GetSingleByDocType<T>(this UmbracoHelper umbracoHelper, string docTypeAlias) where T : IPublishedContent
{
return (T)umbracoHelper.ContentSingleAtXPath($"//{docTypeAlias}");
}
Surprise! The above still works in v9 ๐
<add key="Umbraco.ModelsBuilder.Enable" value="true" />
<add key="Umbraco.ModelsBuilder.ModelsMode" value="AppData" />
<add key="Umbraco.ModelsBuilder.AcceptUnsafeModelsDirectory" value="true" />
<add key="Umbraco.ModelsBuilder.ModelsDirectory" value="~/../CandidContribs.Core/Models/Published" />
We chose the 'SourceCodeAuto' mode. Read about the different modes in the official docs.
"Umbraco": {
"CMS": {
"Global": { ... },
"Hosting": { ... },
"ModelsBuilder": {
"ModelsMode": "SourceCodeAuto",
"ModelsDirectory": "../CandidContributions.Core/Models/Content",
"AcceptUnsafeModelsDirectory": true
}
}
}
In v9, if no ModelsDirectory is specified, the models are created in /umbraco/models
in the web project. If you change your ModelsDirectory you will need to manually clear out that folder.
The main things changed in views:
v8 | v9 |
---|---|
@inherits Umbraco.Web.Mvc.UmbracoViewPage |
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage |
@Html.Partial("PartialName") |
<partial name="PartialName" /> |
or @await Html.PartialAsync("PartialName") |
|
@Html.Partial("PartialName", Model) |
<partial name="PartialName" model="Model" /> |
@RenderSection("Head", required: false) |
@await RenderSectionAsync("Head", required: false) |
But @RenderBody()
is still @RenderBody()
!
Be good to go through an upgrade process! Our Cloud account is on the starter plan, so interested to know if we can do this without needing to add another environment...
functions blocks in .cshtml still work, but helpers blocks don't ...
Whilst partial pages, taghelpers or view components might be alternatives to consider we wanted to change as little as possible during this initial migration. So after a bit of trial and error, we opted for this simple solution:
@helper DisplayMetaTags(IMetaTags metaTags)
{
var metaTitle = metaTags.Value<string>("metaTitle", fallback: Fallback.ToDefaultValue, defaultValue: Model.Name);
var metaDescription = metaTags.Value<string>("metaDescription", fallback: Fallback.ToDefaultValue, defaultValue: "...");
var metaKeywords = metaTags.Value<string>("metaKeywords", fallback: Fallback.ToDefaultValue, defaultValue: "...");
<meta name="title" content="@metaTitle" />
<meta name="description" content="@metaDescription" />
<meta name="keywords" content="@metaKeywords" />
}
@DisplayMetaTags(IMetaTags)Model)
@{
void DisplayMetaTags(IMetaTags metaTags)
{
var metaTitle = metaTags.Value<string>("metaTitle", fallback: Fallback.ToDefaultValue, defaultValue: Model.Name);
var metaDescription = metaTags.Value<string>("metaDescription", fallback: Fallback.ToDefaultValue, defaultValue: "...");
var metaKeywords = metaTags.Value<string>("metaKeywords", fallback: Fallback.ToDefaultValue, defaultValue: "...");
<meta name="title" content="@metaTitle" />
<meta name="description" content="@metaDescription" />
<meta name="keywords" content="@metaKeywords" />
}
}
DisplayMetaTags(IMetaTags)Model);
InvalidOperationException: No ClientId set.
Umbraco.Cloud.Identity.Cms.V9.B2CConfiguration.get_LocalDevelopmentScope()
The home page of our website features some content from elsewhere in the website. In our v8 site we had achieved this by creating a new model that inherits from the ModelsBuilder generated model, and then hijacked the Home route to add the querying of the other content.
public class HomeController : Umbraco.Web.Mvc.RenderMvcController
{
public override ActionResult Index(ContentModel model)
{
var homePageModel = new HomePageModel(model.Content)
{
PastEvents = new List<EventsPage>(),
UpcomingEvents = new List<EventsPage>()
};
// query for content
}
}
Seems a bit more complicated as need to include the constructor method. Note that the Index
method no longer takes a parameter:
public class HomeController : Umbraco.Cms.Web.Common.Controllers.RenderController
{
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ServiceContext _serviceContext;
public HomeController(ILogger<HomeController> logger,
ICompositeViewEngine compositeViewEngine,
IUmbracoContextAccessor umbracoContextAccessor,
IVariationContextAccessor variationContextAccessor,
ServiceContext context)
: base(logger, compositeViewEngine, umbracoContextAccessor)
{
_variationContextAccessor = variationContextAccessor;
_serviceContext = context;
}
public override IActionResult Index()
{
var homePageModel = new HomePageModel(CurrentPage, new PublishedValueFallback(_serviceContext, _variationContextAccessor))
{
PastEvents = new List<EventsPage>(),
UpcomingEvents = new List<EventsPage>()
};
// query for content
}
}
The view model also needed to change:
public class HomePageModel: Home
{
public HomePageModel(IPublishedContent content) : base(content) {
}
public List<EventsPage> PastEvents { get; set; }
public List<EventsPage> UpcomingEvents { get; set; }
}
public class HomePageModel : Home
{
public HomePageModel(IPublishedContent content, IPublishedValueFallback publishedValueFallback)
: base(content, publishedValueFallback)
{
}
public List<EventsPage> PastEvents { get; set; }
public List<EventsPage> UpcomingEvents { get; set; }
}
Check https://cultiv.nl/blog/using-hangfire-to-update-umbraco-content/ to see if the reported issues might effect our composer logic
CandidContribs.Core/Controllers/Api/DiscordBotController.cs
is currently excluded in the project.
It's fine, we can manage without our Bingo bot working in our Discord server for a little while longer!
If you try including this file (and the related models) then there are lots of errors that will need fixing. But the biggest difference is what to do about IHttpActionResult
as it appears to no longer be a thing.
var appDataFolder = UmbracoContext.HttpContext.Server.MapPath("~/App_Data");
The App_Data folder is no longer a special 'protected' folder. Steps can be taken to ensure that all its contents is protected, but for our purposes we actually don't need it to be protected. So we chose a different folder name to avoid future confusion about whether App_Data is actually protected or not.
public class GuestbookApiController : UmbracoApiController
{
private readonly IWebHostEnvironment _webHostEnvironment;
public GuestbookApiController(IWebHostEnvironment webHostEnvironment)
{
_webHostEnvironment = webHostEnvironment;
}
public string GetEntries(string id)
{
var githubFolder = _webHostEnvironment.ContentRootPath + "\\wwwroot\\github";
}
}
PS we know it is good practice to create paths using Path.Combine
instead of the approach above. Something else to do ;-)
Path.Combine(folder1,folder2)
is good (or is Path.Join
better?)
folder1 + folder2
, or folder1 + "\\" + folder2
is bad
Need to check for all instances of paths being created in the bad way and change.
Our website needs to make scheduled calls to the Spreaker API (Spreaker being the podcast hosting service we use), so that we can create a content node for each episode that we publish. Thus the latest episode appears automatically on our home page.
If v8 we achieved this using the BackgroundTaskRunner
feature.
Something similar should have been possible using RecurringHostedServiceBase
, according to the official docs.
However we decided that we would like more visibility and control over when our scheduled tasks run, so we opted for using Hangfire. Seb from Umbraco HQ was with us at the hackathon so walked us through what to do. First add Cultiv.Hangfire, the package that he created for adding Hangfire to Umbraco 9, from nuget into our 'core' project:
dotnet add package Cultiv.Hangfire
This added a new Hangfire dashboard to our Umbraco backoffice.
Then in our component we added and scheduled our retrieval task:
public void Initialize()
{
RecurringJob.AddOrUpdate(() => RetrieveEpisodes(null), Cron.Hourly());
}
public void RetrieveEpisodes(PerformContext context)
{
// do stuff!
}
Refer to SpreakerComposer.cs in this code base for the complete code.
In a few view models in the v8 site we used a IHtmlString
property (in System.Web
) usually to hold the content from a rich text editor.
public IHtmlString Text { get; set; }
And then the property displays correctly in a view using:
@Model.Text
IHtmlString doesn't exist any more, but we discovered IHtmlEncodedString
, in Umbraco.Cms.Core.Strings
so used:
public IHtmlEncodedString Text { get; set; }
which we figured should do the job. However @Model.Text
was now displaying the encoded version of the string, so we needed to use:
@Html.Raw(Model.Text)
With hindsight it feels a bit odd that the v8 version didn't need the Html.Raw method, but hey ho!
Oooh this was a "fun" one to solve!
In the v8 site we have an UmbracoApiController
that returns various content for the front-end scheduler tool for our events.
public class ScheduleApiController : UmbracoApiController
{
public IEnumerable<CheckBoxViewModel> GetDays(int id)
{
...
}
public IEnumerable<CheckBoxViewModel> GetActivities(int id)
{
...
}
}
Calls to this controller were being made with urls such as /umbraco/api/ScheduleApi/GetDays/1234.
The above code compiled in v9, but the methods weren't getting the correct id
parameter from the url. It appears that UmbracoApiController wasn't model binding properly for the id route parameter. We tried various things, including:
GetDays([FromQuery(Name="id")]int id)
- nope!GetDays([FromBody]int id)
- still no![Route("umbraco/api/ScheduleApi/GetDays/{id}")]
to the method but surely a better way![Route("umbraco/api/ScheduleApi")]
to the controller and then only [HttpGet("GetDays/{id:int}")]
to the method, but still not great[FromRoute]int id
, no route attributes required!The resulting code looks like this:
public class ScheduleApiController : UmbracoApiController
{
private readonly UmbracoHelper _umbracoHelper;
public ScheduleApiController(UmbracoHelper umbracoHelper)
{
_umbracoHelper = umbracoHelper;
}
public IEnumerable<CheckBoxViewModel> GetDays([FromRoute]int id)
{
...
}
public IEnumerable<CheckBoxViewModel> GetActivities([FromRoute] int id)
{
...
}
}
Paul from Umbraco HQ had joined us at this point (and helped us land upon our final solution). He also acknowledged that this looked like a bug so raised an issue, and subsequent pull request to fix it.
We used the appSettings section of web.config
to store to custom settings such as:
<add key="CandidContribs.SpreakerApi.Enabled" value="false" />
<add key="CandidContribs.SpreakerApi.ShowId" value="4200995" />
Retrieving these settings would be done with code such as
var enabled = bool.TryParse(ConfigurationManager.AppSettings["CandidContribs.SpreakerApi.Enabled"], out var val) && val;
var showId = ConfigurationManager.AppSettings["CandidContribs.SpreakerApi.ShowId"];
Equivalent settings added to appsettings.json
{
"SpreakerApi":
{
"Enabled": true,
"ShowId": 4200995
}
}
We wanted to work with these settings using a strongly typed approach.
This involved adding a model representing the new settings (Core\Models\Configuration\SpreakerApiOptions.cs):
public class SpreakerApiOptions
{
public bool Enabled { get; set; }
public int ShowId { get; set; }
}
Then in the composer for the component that needed these settings (Core\Composers\SpreakerComposer.cs):
public class SpreakerComposer : ComponentComposer<SpreakerComponent>
{
public override void Compose(IUmbracoBuilder builder)
{
base.Compose(builder);
builder.Services.Configure<SpreakerApiOptions>(builder.Config.GetSection("SpreakerApi"));
}
}
Finally in the component inject the config options:
public class SpreakerComponent : IComponent
{
private readonly SpreakerApiOptions _spreakerOptions;
public SpreakerComponent(IOptions<SpreakerApiOptions> spreakerOptions)
{
_spreakerOptions = spreakerOptions.Value;
}
}
We could then reference them as, for example, _spreakerOptions.Enabled
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.