Git Product home page Git Product logo

canconcloud's People

Contributors

lottepitcher avatar

Stargazers

 avatar

Watchers

 avatar

canconcloud's Issues

v8 to v9 Migration Notes

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.

v8-9: Replace HttpClient

v8

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

v9

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.

v8-9: XPath querying

v8

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}");
}

v9

Surprise! The above still works in v9 ๐ŸŽ‰

v8-9: ModelsBuilder

v8: web.config

<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" />

v9: appsettings.json

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
      }
    }
  }

Observations

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.

v8-9: Migrating Views

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()!

Upgrade to v9.1

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...

v8-9: Replace Razor Helpers

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:

v8

@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)

v9

@{
    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);

v8-9: Route Hijacking

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.

v8

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
    }
}

v9

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:

v8

public class HomePageModel: Home
{
    public HomePageModel(IPublishedContent content) : base(content) {
    }

    public List<EventsPage> PastEvents { get; set; }
    public List<EventsPage> UpcomingEvents { get; set; }
}

v9

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; }
}

View official docs for Custom Controllers

v8-9: IHttpActionResult not recognised

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.

v8-9: Replace Server.MapPath

v8

var appDataFolder = UmbracoContext.HttpContext.Server.MapPath("~/App_Data");

v9

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 ;-)

Avoid concatenating file/folder paths

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.

v8-9: Scheduled / Background tasks

v8

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.

v9

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.

v8-9: IHtmlString not recognised

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.

v8

public IHtmlString Text { get; set; }

And then the property displays correctly in a view using:

@Model.Text

v9

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!

v8-9: Api controllers using default 'id' parameter

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.

v8

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.

v9

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!
  • We could add [Route("umbraco/api/ScheduleApi/GetDays/{id}")] to the method but surely a better way!
  • Slightly better was adding [Route("umbraco/api/ScheduleApi")] to the controller and then only [HttpGet("GetDays/{id:int}")] to the method, but still not great
  • Finally we landed upon [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.

v8-9: Custom config sections in appsettings.json

v8

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"];

v9

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.