From 40d58e997b84308a19a1e13243682cce8694f44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Thu, 18 Jan 2024 12:14:01 +0100 Subject: [PATCH 01/20] Upgrading to .NET 8 and OC 1.8.2 --- Controllers/DatabaseStorageController.cs | 2 +- Lombiq.TrainingDemo.csproj | 32 +++++++++---------- Migrations/BookMigrations.cs | 11 ++++--- Migrations/PersonMigrations.cs | 11 ++++--- .../Lombiq.TrainingDemo.Tests.csproj | 2 +- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Controllers/DatabaseStorageController.cs b/Controllers/DatabaseStorageController.cs index 28009925..e1d5a8cf 100644 --- a/Controllers/DatabaseStorageController.cs +++ b/Controllers/DatabaseStorageController.cs @@ -65,7 +65,7 @@ public async Task CreateBooksPost() foreach (var book in CreateDemoBooks()) { // So now you understand what will happen in the background when this service is being called. - _session.Save(book); + await _session.SaveAsync(book); } await _notifier.InformationAsync(H["Books have been created in the database."]); diff --git a/Lombiq.TrainingDemo.csproj b/Lombiq.TrainingDemo.csproj index 540d59c5..7af5659b 100644 --- a/Lombiq.TrainingDemo.csproj +++ b/Lombiq.TrainingDemo.csproj @@ -1,9 +1,9 @@ - + - net6.0 + net8.0 true Lombiq Training Demo for Orchard Core Orchard Core training demo module for teaching Orchard Core fundamentals primarily by going through its source code. @@ -25,27 +25,27 @@ - - + + - - - - - - - - - - + + + + + + + + + + - - + + diff --git a/Migrations/BookMigrations.cs b/Migrations/BookMigrations.cs index c4059009..ee4c0697 100644 --- a/Migrations/BookMigrations.cs +++ b/Migrations/BookMigrations.cs @@ -8,6 +8,7 @@ using Lombiq.TrainingDemo.Indexes; using OrchardCore.Data.Migration; +using System.Threading.Tasks; using YesSql.Sql; namespace Lombiq.TrainingDemo.Migrations; @@ -17,9 +18,9 @@ public class BookMigrations : DataMigration { // Migrations have Create() and UpdateFromX methods. When the module is first enabled the Create() is called so it // can set up DB tables. - public int Create() + public async Task CreateAsync() { - SchemaBuilder.CreateMapIndexTable(table => table + await SchemaBuilder.CreateMapIndexTableAsync(table => table .Column(nameof(BookIndex.Author)) // Titles of books can be really long sometimes (even as long as 26000 characters: // https://www.guinnessworldrecords.com/world-records/358711-longest-title-of-a-book) so we have to make @@ -31,7 +32,7 @@ public int Create() // Let's suppose that we'll store many books, tens of thousands in the database. In this case, it's also advised // to add SQL indices to columns that are frequently queried on. In our case, Author is like this so we add an // index below. Note that you can only add indices in AlterTable(). - SchemaBuilder.AlterTable(nameof(BookIndex), table => table + await SchemaBuilder.AlterTableAsync(nameof(BookIndex), table => table .CreateIndex($"IDX_{nameof(BookIndex)}_{nameof(BookIndex.Author)}", nameof(BookIndex.Author)) ); @@ -44,13 +45,13 @@ public int Create() // already enabled before and the create method was run (like when you update a module already running on an Orchard // site). The X in UpdateFromX is the version number of the update (the method's name is conventional). It means: // "run this update if the module's current migration version is X". This method will run if it's 1. - public int UpdateFrom1() + public async Task UpdateFrom1Async() { // The initial version of our module did not store the book's title. We quickly fix the issue by pushing out an // update that modifies the schema to add the Name. Remember, we've returned 2 in the Create method so this // update method won't be executed in a fresh setup. This is why you need to include all these changes in the // Create method as well. - SchemaBuilder.AlterTable(nameof(BookIndex), table => table + await SchemaBuilder.AlterTableAsync(nameof(BookIndex), table => table .AddColumn(nameof(BookIndex.Title)) ); diff --git a/Migrations/PersonMigrations.cs b/Migrations/PersonMigrations.cs index ec9e4409..d3806bfc 100644 --- a/Migrations/PersonMigrations.cs +++ b/Migrations/PersonMigrations.cs @@ -7,6 +7,7 @@ using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.Data.Migration; using System; +using System.Threading.Tasks; using YesSql.Sql; using static Lombiq.HelpfulLibraries.OrchardCore.Contents.ContentFieldEditorEnums; @@ -22,10 +23,10 @@ public class PersonMigrations : DataMigration public PersonMigrations(IContentDefinitionManager contentDefinitionManager) => _contentDefinitionManager = contentDefinitionManager; - public int Create() + public async Task CreateAsync() { // Now you can configure PersonPart. For example you can add content fields (as mentioned earlier) here. - _contentDefinitionManager.AlterPartDefinition(nameof(PersonPart), part => part + await _contentDefinitionManager.AlterPartDefinitionAsync(nameof(PersonPart), part => part // Each field has its own configuration. Here you will give a display name for it and add some additional // settings like a hint to be displayed in the editor. .WithField(nameof(PersonPart.Biography), field => field @@ -39,7 +40,7 @@ public int Create() ); // This one will create an index table for the PersonPartIndex as explained in the BookMigrations file. - SchemaBuilder.CreateMapIndexTable(table => table + await SchemaBuilder.CreateMapIndexTableAsync(table => table .Column(nameof(PersonPartIndex.BirthDateUtc)) .Column(nameof(PersonPartIndex.Handedness)) // The content item ID is always 26 characters. @@ -51,7 +52,7 @@ public int Create() // https://docs.orchardcore.net/en/latest/docs/glossary/#content-type. The content type's name is arbitrary but // choose a meaningful one. Notice how we use a class with constants to store the type name so we prevent risky // copy-pasting. - _contentDefinitionManager.AlterTypeDefinition(ContentTypes.PersonPage, type => type + await _contentDefinitionManager.AlterTypeDefinitionAsync(ContentTypes.PersonPage, type => type .Creatable() .Listable() // We attach parts by specifying their name. For our own parts we use nameof(): This is not mandatory but @@ -62,7 +63,7 @@ public int Create() // We can even create a widget with the same content part. Widgets are just content types as usual but with the // Stereotype set as "Widget". You'll notice that our site's configuration includes three zones on the frontend // that you can add widgets to, as well as two layers. Check them out on the admin! - _contentDefinitionManager.AlterTypeDefinition("PersonWidget", type => type + await _contentDefinitionManager.AlterTypeDefinitionAsync("PersonWidget", type => type .Stereotype("Widget") .WithPart(nameof(PersonPart)) ); diff --git a/Tests/Lombiq.TrainingDemo.Tests/Lombiq.TrainingDemo.Tests.csproj b/Tests/Lombiq.TrainingDemo.Tests/Lombiq.TrainingDemo.Tests.csproj index cadf76a0..b0c5696a 100644 --- a/Tests/Lombiq.TrainingDemo.Tests/Lombiq.TrainingDemo.Tests.csproj +++ b/Tests/Lombiq.TrainingDemo.Tests/Lombiq.TrainingDemo.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 From c7a85dea2a021fa77f04e95fd51392e01bba9e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Fri, 19 Jan 2024 14:07:13 +0100 Subject: [PATCH 02/20] Addressing warnings. --- Controllers/FileManagementController.cs | 2 +- Controllers/PersonListController.cs | 2 -- Controllers/SiteSettingsController.cs | 2 -- Manifest.cs | 12 ++++++------ Services/DemoSettingsConfiguration.cs | 1 - 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Controllers/FileManagementController.cs b/Controllers/FileManagementController.cs index 4b3abb7a..fe46b6a1 100644 --- a/Controllers/FileManagementController.cs +++ b/Controllers/FileManagementController.cs @@ -90,7 +90,7 @@ public async Task ReadFileFromMediaFolder() // If you want to extract the content of the file use a StreamReader to read the stream. using var stream = await _mediaFileStore.GetFileStreamAsync(TestFileRelativePath); using var streamReader = new StreamReader(stream); - var content = await streamReader.ReadToEndAsync(); + var content = await streamReader.ReadToEndAsync(HttpContext.RequestAborted); return $"File content: {content}"; } diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index 21153d48..29e00e35 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -111,10 +111,8 @@ public async Task FountainOfEternalYouth() part.BirthDateUtc = eighteenYearOld; // False alarm, this is not a loop. -#pragma warning disable S1643 // Strings should not be concatenated using '+' in a loop // You can also edit content fields: part.Biography.Text += " I'm young again!"; -#pragma warning restore S1643 // Strings should not be concatenated using '+' in a loop }); // If you happen to use reusable/named parts like BagPart (see the docs on it here: diff --git a/Controllers/SiteSettingsController.cs b/Controllers/SiteSettingsController.cs index 94c76a2d..5221ea5f 100644 --- a/Controllers/SiteSettingsController.cs +++ b/Controllers/SiteSettingsController.cs @@ -7,8 +7,6 @@ using Lombiq.TrainingDemo.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using OrchardCore.ContentManagement; -using OrchardCore.Entities; using OrchardCore.Settings; using System.Threading.Tasks; diff --git a/Manifest.cs b/Manifest.cs index 30a85400..6d25f36e 100644 --- a/Manifest.cs +++ b/Manifest.cs @@ -42,8 +42,8 @@ // feature then you need to include them in this list. Orchard Core will make sure to enable all dependent features // when you enable a feature that has dependencies. Without this some features would not work even if the assembly // is referenced in the project. - Dependencies = new[] - { + Dependencies = + [ "OrchardCore.Admin", "OrchardCore.Apis.GraphQL", "OrchardCore.BackgroundTasks", @@ -64,7 +64,7 @@ "OrchardCore.Themes", "OrchardCore.Users", "OrchardCore.Workflows", - } + ] )] // And we also have a second feature! @@ -77,10 +77,10 @@ Category = "Training", Description = "Demonstrates how to write middlewares in a separate feature.", // It's usual for sub-features to depend on the base feature but this is not mandatory. - Dependencies = new[] - { + Dependencies = + [ "Lombiq.TrainingDemo", - } + ] )] // How do you distinguish what's activated when turning on a feature? With the same Feature attribute! Check out the diff --git a/Services/DemoSettingsConfiguration.cs b/Services/DemoSettingsConfiguration.cs index 4b260ac4..81276f21 100644 --- a/Services/DemoSettingsConfiguration.cs +++ b/Services/DemoSettingsConfiguration.cs @@ -1,6 +1,5 @@ using Lombiq.TrainingDemo.Models; using Microsoft.Extensions.Options; -using OrchardCore.Entities; using OrchardCore.Settings; namespace Lombiq.TrainingDemo.Services; From b8a33abeb3a40796e391a0408c79b0a89296f60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Sat, 20 Jan 2024 12:15:22 +0100 Subject: [PATCH 03/20] Addressing warnings. --- Controllers/AuthorizationController.cs | 33 ++++------ Controllers/DisplayManagementController.cs | 6 +- Controllers/FileManagementController.cs | 26 +++----- Controllers/PersonListController.cs | 31 ++++------ Controllers/SiteSettingsController.cs | 12 +--- Controllers/YourFirstOrchardCoreController.cs | 30 +++------ Drivers/ColorFieldDisplayDriver.cs | 6 +- Drivers/DemoSettingsDisplayDriver.cs | 12 +--- Events/LoginGreeting.cs | 12 +--- Filters/ResourceFromShapeInjectingFilter.cs | 21 +++---- Filters/ResourceInjectionFilter.cs | 6 +- Filters/ShapeInjectionFilter.cs | 12 +--- GraphQL/Services/PersonAgeGraphQLFilter.cs | 6 +- Middlewares/RequestLoggingMiddleware.cs | 7 +-- Migrations/PersonMigrations.cs | 6 +- Navigation/DemoSettingsAdminMenu.cs | 6 +- Navigation/PersonsAdminMenu.cs | 6 +- Navigation/TrainingDemoNavigationProvider.cs | 11 +--- Services/DateTimeCachingService.cs | 61 +++++-------------- Services/DemoBackgroundTask.cs | 6 +- 20 files changed, 95 insertions(+), 221 deletions(-) diff --git a/Controllers/AuthorizationController.cs b/Controllers/AuthorizationController.cs index e850435c..86af0ef4 100644 --- a/Controllers/AuthorizationController.cs +++ b/Controllers/AuthorizationController.cs @@ -20,24 +20,13 @@ namespace Lombiq.TrainingDemo.Controllers; -public class AuthorizationController : Controller +public class AuthorizationController( + IAuthorizationService authorizationService, + IContentManager contentManager, + INotifier notifier, + IHtmlLocalizer htmlLocalizer) : Controller { - private readonly IAuthorizationService _authorizationService; - private readonly IContentManager _contentManager; - private readonly INotifier _notifier; - private readonly IHtmlLocalizer H; - - public AuthorizationController( - IAuthorizationService authorizationService, - IContentManager contentManager, - INotifier notifier, - IHtmlLocalizer htmlLocalizer) - { - _authorizationService = authorizationService; - _contentManager = contentManager; - _notifier = notifier; - H = htmlLocalizer; - } + private readonly IHtmlLocalizer H = htmlLocalizer; // Here we will create a Person content item and check if the user has permission to edit it. It's very common to // check if you can view or edit a specific item - it also happens if you use the built-in URLs like @@ -45,7 +34,7 @@ public AuthorizationController( public async Task CanEditPerson() { // Creating a content item for testing (won't be persisted). - var person = await _contentManager.NewAsync(ContentTypes.PersonPage); + var person = await contentManager.NewAsync(ContentTypes.PersonPage); // Check if the user has permission to edit the content item. When you check content-related permissions // (ViewContent, EditContent, PublishContent etc.) there is a difference between checking these for your content @@ -53,7 +42,7 @@ public async Task CanEditPerson() // ViewOwnContent, EditOwnContent, PublishOwnContent etc. permissions will be checked. This is automatic so you // don't need to use them directly. For this newly created Person item the owner is null so the EditContent // permission will be used. - if (!await _authorizationService.AuthorizeAsync(User, OrchardCore.Contents.CommonPermissions.EditContent, person)) + if (!await authorizationService.AuthorizeAsync(User, OrchardCore.Contents.CommonPermissions.EditContent, person)) { // Return 401 status code using this helper. Although it's a good practice to return 404 (NotFound()) // instead to prevent enumeration attacks. @@ -62,7 +51,7 @@ public async Task CanEditPerson() // To keep the demonstration short, only display a notification about the successful authorization and return to // the home page. - await _notifier.InformationAsync(H["You are authorized to edit Person content items."]); + await notifier.InformationAsync(H["You are authorized to edit Person content items."]); return Redirect("~/"); } @@ -74,12 +63,12 @@ public async Task CanManagePersons() // We've defined a ManagePersons earlier which is added to Administrator users by default. If the currently user // doesn't have the Administrator role then you can add it on the dashboard. Since this permission can be // checked without any object as a context the third parameter is left out. - if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) + if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) { return Unauthorized(); } - await _notifier.InformationAsync(H["You are authorized to manage persons."]); + await notifier.InformationAsync(H["You are authorized to manage persons."]); return Redirect("~/"); } diff --git a/Controllers/DisplayManagementController.cs b/Controllers/DisplayManagementController.cs index a8002a15..357fd495 100644 --- a/Controllers/DisplayManagementController.cs +++ b/Controllers/DisplayManagementController.cs @@ -16,14 +16,12 @@ namespace Lombiq.TrainingDemo.Controllers; -public class DisplayManagementController : Controller, IUpdateModel +public class DisplayManagementController(IDisplayManager bookDisplayManager) : Controller, IUpdateModel { // The core display management features can be used via the IDisplayManager service. The generic parameter will be // the object that needs to be displayed on the UI somehow. Don't forget to register this generic class with the // service provider (see: Startup.cs). - private readonly IDisplayManager _bookDisplayManager; - - public DisplayManagementController(IDisplayManager bookDisplayManager) => _bookDisplayManager = bookDisplayManager; + private readonly IDisplayManager _bookDisplayManager = bookDisplayManager; // Before we learn how shapes are generated using the display manager let's see what are these shapes actually. // Ad-hoc shapes can be created anywhere without the display manager. In this example we'll see how to create an diff --git a/Controllers/FileManagementController.cs b/Controllers/FileManagementController.cs index fe46b6a1..ba2ce5f1 100644 --- a/Controllers/FileManagementController.cs +++ b/Controllers/FileManagementController.cs @@ -27,29 +27,21 @@ namespace Lombiq.TrainingDemo.Controllers; -public class FileManagementController : Controller +public class FileManagementController( + IMediaFileStore mediaFileStore, + INotifier notifier, + IHtmlLocalizer htmlLocalizer, + ICustomFileStore customFileStore) : Controller { // Let's have the paths here in constants to avoid repeating ourselves. private const string TestFileRelativePath = "TrainingDemo/TestFile1.txt"; private const string UploadedFileFolderRelativePath = "TrainingDemo/Uploaded"; - private readonly IMediaFileStore _mediaFileStore; - private readonly INotifier _notifier; - private readonly IHtmlLocalizer H; - private readonly ICustomFileStore _customFileStore; - - public FileManagementController( - IMediaFileStore mediaFileStore, - INotifier notifier, - IHtmlLocalizer htmlLocalizer, - ICustomFileStore customFileStore) - { - _mediaFileStore = mediaFileStore; - _notifier = notifier; - _customFileStore = customFileStore; - H = htmlLocalizer; - } + private readonly IMediaFileStore _mediaFileStore = mediaFileStore; + private readonly INotifier _notifier = notifier; + private readonly IHtmlLocalizer H = htmlLocalizer; + private readonly ICustomFileStore _customFileStore = customFileStore; // This action will demonstrate how to create a file in the Media folder and read it from there. See it under // /Lombiq.TrainingDemo/FileManagement/CreateFileInMediaFolder. diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index 29e00e35..b033f2d3 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -26,27 +26,18 @@ namespace Lombiq.TrainingDemo.Controllers; -public class PersonListController : Controller +public class PersonListController( + ISession session, + IClock clock, + IContentItemDisplayManager contentItemDisplayManager, + IUpdateModelAccessor updateModelAccessor, + IContentManager contentManager) : Controller { - private readonly ISession _session; - private readonly IClock _clock; - private readonly IContentItemDisplayManager _contentItemDisplayManager; - private readonly IUpdateModelAccessor _updateModelAccessor; - private readonly IContentManager _contentManager; - - public PersonListController( - ISession session, - IClock clock, - IContentItemDisplayManager contentItemDisplayManager, - IUpdateModelAccessor updateModelAccessor, - IContentManager contentManager) - { - _session = session; - _clock = clock; - _contentItemDisplayManager = contentItemDisplayManager; - _updateModelAccessor = updateModelAccessor; - _contentManager = contentManager; - } + private readonly ISession _session = session; + private readonly IClock _clock = clock; + private readonly IContentItemDisplayManager _contentItemDisplayManager = contentItemDisplayManager; + private readonly IUpdateModelAccessor _updateModelAccessor = updateModelAccessor; + private readonly IContentManager _contentManager = contentManager; // See it under /Lombiq.TrainingDemo/PersonList/OlderThan30. public async Task OlderThan30() diff --git a/Controllers/SiteSettingsController.cs b/Controllers/SiteSettingsController.cs index 5221ea5f..9db3eb91 100644 --- a/Controllers/SiteSettingsController.cs +++ b/Controllers/SiteSettingsController.cs @@ -12,16 +12,10 @@ namespace Lombiq.TrainingDemo.Controllers; -public class SiteSettingsController : Controller +public class SiteSettingsController(ISiteService siteService, IOptionsSnapshot demoOptions) : Controller { - private readonly ISiteService _siteService; - private readonly DemoSettings _demoSettings; - - public SiteSettingsController(ISiteService siteService, IOptionsSnapshot demoOptions) - { - _siteService = siteService; - _demoSettings = demoOptions.Value; - } + private readonly ISiteService _siteService = siteService; + private readonly DemoSettings _demoSettings = demoOptions.Value; // Here's a quick simple demonstration about how to use ISiteService. Orchard Core stores basic settings that are // accessible right away in the ISite object. Here you will see how to access the site's name you gave when you set diff --git a/Controllers/YourFirstOrchardCoreController.cs b/Controllers/YourFirstOrchardCoreController.cs index dc2cadcb..e8c36008 100644 --- a/Controllers/YourFirstOrchardCoreController.cs +++ b/Controllers/YourFirstOrchardCoreController.cs @@ -17,31 +17,19 @@ namespace Lombiq.TrainingDemo.Controllers; -public class YourFirstOrchardCoreController : Controller +public class YourFirstOrchardCoreController( + INotifier notifier, + IStringLocalizer stringLocalizer, + IHtmlLocalizer htmlLocalizer, + ILogger logger) : Controller { - private readonly INotifier _notifier; - private readonly IStringLocalizer T; - private readonly IHtmlLocalizer H; + private readonly INotifier _notifier = notifier; + private readonly IStringLocalizer T = stringLocalizer; + private readonly IHtmlLocalizer H = htmlLocalizer; // You can use the non-generic counterpart of ILogger once injected just be sure to inject the generic one otherwise // the log entries won't contain the name of the class. - private readonly ILogger _logger; - - // Orchard Core uses the built in dependency injection feature coming with ASP.NET Core. You can use the module's - // Startup class to register your own services with the service provider. To learn more see: - // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection - public YourFirstOrchardCoreController( - INotifier notifier, - IStringLocalizer stringLocalizer, - IHtmlLocalizer htmlLocalizer, - ILogger logger) - { - _notifier = notifier; - _logger = logger; - - T = stringLocalizer; - H = htmlLocalizer; - } + private readonly ILogger _logger = logger; // Here's a simple action that will return some message. Nothing special here just demonstrates that this will work // in Orchard Core right after enabling the module. The route for this action will be diff --git a/Drivers/ColorFieldDisplayDriver.cs b/Drivers/ColorFieldDisplayDriver.cs index 10cd2368..59c61c63 100644 --- a/Drivers/ColorFieldDisplayDriver.cs +++ b/Drivers/ColorFieldDisplayDriver.cs @@ -15,11 +15,9 @@ namespace Lombiq.TrainingDemo.Drivers; // You shouldn't be surprised - content fields also have display drivers. ContentFieldDisplayDriver is specifically for // content fields. Don't forget to register this class with the service provider (see: Startup.cs). -public class ColorFieldDisplayDriver : ContentFieldDisplayDriver +public class ColorFieldDisplayDriver(IStringLocalizer stringLocalizer) : ContentFieldDisplayDriver { - private readonly IStringLocalizer T; - - public ColorFieldDisplayDriver(IStringLocalizer stringLocalizer) => T = stringLocalizer; + private readonly IStringLocalizer T = stringLocalizer; public override IDisplayResult Display(ColorField field, BuildFieldDisplayContext fieldDisplayContext) => // Same Display method for generating display shapes but this time the Initialize shape helper is being used. diff --git a/Drivers/DemoSettingsDisplayDriver.cs b/Drivers/DemoSettingsDisplayDriver.cs index b3c73b67..36f1b796 100644 --- a/Drivers/DemoSettingsDisplayDriver.cs +++ b/Drivers/DemoSettingsDisplayDriver.cs @@ -14,21 +14,15 @@ namespace Lombiq.TrainingDemo.Drivers; // Now this display driver abstraction is different from the one you've seen before. In Orchard Core you can connect // different objects to a master object that will be connected in the database when storing them. Site settings are // handled this way. -public class DemoSettingsDisplayDriver : SectionDisplayDriver +public class DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) : SectionDisplayDriver { // Since technically we have only one SiteSettings we have separate the editors using editor groups. It's a good // idea to store the editor group ID in a publicly accessibly constant (would be much better to store it in a static // class placed in a Constants folder). public const string EditorGroupId = "Demo"; - private readonly IAuthorizationService _authorizationService; - private readonly IHttpContextAccessor _hca; - - public DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) - { - _authorizationService = authorizationService; - _hca = hca; - } + private readonly IAuthorizationService _authorizationService = authorizationService; + private readonly IHttpContextAccessor _hca = hca; // Here's the EditAsync override to display editor for our site settings on the Dashboard. Note that it has a sync // version too. diff --git a/Events/LoginGreeting.cs b/Events/LoginGreeting.cs index f707dd30..3f39e203 100644 --- a/Events/LoginGreeting.cs +++ b/Events/LoginGreeting.cs @@ -16,16 +16,10 @@ namespace Lombiq.TrainingDemo.Events; // ILoginFormEvent exposes events of the, well, login form :). Useful to display a login greeting or anything even more // useful! The rest of it is pretty standard and we just use INotifier again. -public class LoginGreeting : ILoginFormEvent +public class LoginGreeting(INotifier notifier, IHtmlLocalizer htmlLocalizer) : ILoginFormEvent { - private readonly INotifier _notifier; - private readonly IHtmlLocalizer H; - - public LoginGreeting(INotifier notifier, IHtmlLocalizer htmlLocalizer) - { - _notifier = notifier; - H = htmlLocalizer; - } + private readonly INotifier _notifier = notifier; + private readonly IHtmlLocalizer H = htmlLocalizer; public Task IsLockedOutAsync(IUser user) => Task.CompletedTask; diff --git a/Filters/ResourceFromShapeInjectingFilter.cs b/Filters/ResourceFromShapeInjectingFilter.cs index 89717f6d..a7765b7b 100644 --- a/Filters/ResourceFromShapeInjectingFilter.cs +++ b/Filters/ResourceFromShapeInjectingFilter.cs @@ -6,25 +6,18 @@ namespace Lombiq.TrainingDemo.Filters; -public class ResourceFromShapeInjectingFilter : IAsyncResultFilter +public class ResourceFromShapeInjectingFilter( + IResourceManager resourceManager, + IShapeFactory shapeFactory, + IDisplayHelper displayHelper) : IAsyncResultFilter { // We've seen IResourceManager and IShapeFactory before. - private readonly IResourceManager _resourceManager; + private readonly IResourceManager _resourceManager = resourceManager; - private readonly IShapeFactory _shapeFactory; + private readonly IShapeFactory _shapeFactory = shapeFactory; // IDisplayHelper is new, however. We'll use it to execute a shape into HTML and inject that as a head script! - private readonly IDisplayHelper _displayHelper; - - public ResourceFromShapeInjectingFilter( - IResourceManager resourceManager, - IShapeFactory shapeFactory, - IDisplayHelper displayHelper) - { - _resourceManager = resourceManager; - _shapeFactory = shapeFactory; - _displayHelper = displayHelper; - } + private readonly IDisplayHelper _displayHelper = displayHelper; public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { diff --git a/Filters/ResourceInjectionFilter.cs b/Filters/ResourceInjectionFilter.cs index 2fbd34ad..29647ef6 100644 --- a/Filters/ResourceInjectionFilter.cs +++ b/Filters/ResourceInjectionFilter.cs @@ -6,12 +6,10 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. -public class ResourceInjectionFilter : IAsyncResultFilter +public class ResourceInjectionFilter(IResourceManager resourceManager) : IAsyncResultFilter { // To register resources you can use the IResourceManager service. - private readonly IResourceManager _resourceManager; - - public ResourceInjectionFilter(IResourceManager resourceManager) => _resourceManager = resourceManager; + private readonly IResourceManager _resourceManager = resourceManager; public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { diff --git a/Filters/ShapeInjectionFilter.cs b/Filters/ShapeInjectionFilter.cs index a0741a7d..f2d291ab 100644 --- a/Filters/ShapeInjectionFilter.cs +++ b/Filters/ShapeInjectionFilter.cs @@ -22,20 +22,14 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. -public class ShapeInjectionFilter : IAsyncResultFilter +public class ShapeInjectionFilter(ILayoutAccessor layoutAccessor, IShapeFactory shapeFactory) : IAsyncResultFilter { // To access the layout which contains the zones you need to use the ILayoutAccessor service. - private readonly ILayoutAccessor _layoutAccessor; + private readonly ILayoutAccessor _layoutAccessor = layoutAccessor; // To generate ad-hoc shapes the IShapeFactory can be used. This is the same which is behind the New property in // templates that you have previously seen in AdHocShape.cshtml. - private readonly IShapeFactory _shapeFactory; - - public ShapeInjectionFilter(ILayoutAccessor layoutAccessor, IShapeFactory shapeFactory) - { - _layoutAccessor = layoutAccessor; - _shapeFactory = shapeFactory; - } + private readonly IShapeFactory _shapeFactory = shapeFactory; public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { diff --git a/GraphQL/Services/PersonAgeGraphQLFilter.cs b/GraphQL/Services/PersonAgeGraphQLFilter.cs index df92df3f..ff375e65 100644 --- a/GraphQL/Services/PersonAgeGraphQLFilter.cs +++ b/GraphQL/Services/PersonAgeGraphQLFilter.cs @@ -14,11 +14,9 @@ namespace Lombiq.TrainingDemo.GraphQL.Services; // IGraphQLFilters can append conditions to the YesSql query, alter its result, or do both. -public class PersonAgeGraphQLFilter : IGraphQLFilter +public class PersonAgeGraphQLFilter(IClock clock) : IGraphQLFilter { - private readonly IClock _clock; - - public PersonAgeGraphQLFilter(IClock clock) => _clock = clock; + private readonly IClock _clock = clock; // While you can use this to execute some complex YesSql query it's best to stick with the IIndexAliasProvider // approach for such things. diff --git a/Middlewares/RequestLoggingMiddleware.cs b/Middlewares/RequestLoggingMiddleware.cs index 3b162a0c..e56cc4ec 100644 --- a/Middlewares/RequestLoggingMiddleware.cs +++ b/Middlewares/RequestLoggingMiddleware.cs @@ -21,12 +21,9 @@ namespace Lombiq.TrainingDemo.Middlewares; // By the way, do you remember that we have a separate module feature declared in the Manifest for this middleware? If // not, check out Manifest.cs again! -public class RequestLoggingMiddleware +public class RequestLoggingMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next; - - // You need to inject a RequestDelegate instance here. - public RequestLoggingMiddleware(RequestDelegate next) => _next = next; + private readonly RequestDelegate _next = next; // This method is the actual middleware. Note that apart from the first parameter obligatorily being HttpContext // further parameters can be injected Orchard services. diff --git a/Migrations/PersonMigrations.cs b/Migrations/PersonMigrations.cs index d3806bfc..fef471f8 100644 --- a/Migrations/PersonMigrations.cs +++ b/Migrations/PersonMigrations.cs @@ -17,11 +17,9 @@ namespace Lombiq.TrainingDemo.Migrations; // and configure content parts. Don't forget to register this class with the service provider (see Startup.cs). You can // also generate such migration steps with the Code Generation feature of our Helpful Extensions module, check it out // here: https://github.com/Lombiq/Helpful-Extensions -public class PersonMigrations : DataMigration +public class PersonMigrations(IContentDefinitionManager contentDefinitionManager) : DataMigration { - private readonly IContentDefinitionManager _contentDefinitionManager; - - public PersonMigrations(IContentDefinitionManager contentDefinitionManager) => _contentDefinitionManager = contentDefinitionManager; + private readonly IContentDefinitionManager _contentDefinitionManager = contentDefinitionManager; public async Task CreateAsync() { diff --git a/Navigation/DemoSettingsAdminMenu.cs b/Navigation/DemoSettingsAdminMenu.cs index 3b9b35d3..5edb5db9 100644 --- a/Navigation/DemoSettingsAdminMenu.cs +++ b/Navigation/DemoSettingsAdminMenu.cs @@ -8,11 +8,9 @@ namespace Lombiq.TrainingDemo.Navigation; // To actually see the menu item on the admin menu we need to add a navigation provider to it. -public class DemoSettingsAdminMenu : INavigationProvider +public class DemoSettingsAdminMenu(IStringLocalizer stringLocalizer) : INavigationProvider { - private readonly IStringLocalizer T; - - public DemoSettingsAdminMenu(IStringLocalizer stringLocalizer) => T = stringLocalizer; + private readonly IStringLocalizer T = stringLocalizer; public Task BuildNavigationAsync(string name, NavigationBuilder builder) { diff --git a/Navigation/PersonsAdminMenu.cs b/Navigation/PersonsAdminMenu.cs index fc3e675c..39399a4d 100644 --- a/Navigation/PersonsAdminMenu.cs +++ b/Navigation/PersonsAdminMenu.cs @@ -10,11 +10,9 @@ namespace Lombiq.TrainingDemo.Navigation; // INavigationProvider is used for building different kind of navigations (not just admin menus). Don't forget to // register this class with the service provider (see: Startup.cs). -public class PersonsAdminMenu : INavigationProvider +public class PersonsAdminMenu(IStringLocalizer stringLocalizer) : INavigationProvider { - private readonly IStringLocalizer T; - - public PersonsAdminMenu(IStringLocalizer stringLocalizer) => T = stringLocalizer; + private readonly IStringLocalizer T = stringLocalizer; public Task BuildNavigationAsync(string name, NavigationBuilder builder) { diff --git a/Navigation/TrainingDemoNavigationProvider.cs b/Navigation/TrainingDemoNavigationProvider.cs index cefe3e97..d77da237 100644 --- a/Navigation/TrainingDemoNavigationProvider.cs +++ b/Navigation/TrainingDemoNavigationProvider.cs @@ -17,15 +17,10 @@ namespace Lombiq.TrainingDemo.Navigation; // // For details on how to use them, see the Lombiq.BaseTheme.Samples project: // https://github.com/Lombiq/Orchard-Base-Theme/tree/issue/OSOE-62/Lombiq.BaseTheme.Samples -public class TrainingDemoNavigationProvider : MainMenuNavigationProviderBase +public class TrainingDemoNavigationProvider( + IHttpContextAccessor hca, + IStringLocalizer stringLocalizer) : MainMenuNavigationProviderBase(hca, stringLocalizer) { - public TrainingDemoNavigationProvider( - IHttpContextAccessor hca, - IStringLocalizer stringLocalizer) - : base(hca, stringLocalizer) - { - } - protected override void Build(NavigationBuilder builder) { var context = _hca.HttpContext; diff --git a/Services/DateTimeCachingService.cs b/Services/DateTimeCachingService.cs index b448b005..06871424 100644 --- a/Services/DateTimeCachingService.cs +++ b/Services/DateTimeCachingService.cs @@ -9,56 +9,25 @@ namespace Lombiq.TrainingDemo.Services; -public class DateTimeCachingService : IDateTimeCachingService +public class DateTimeCachingService( + IMemoryCache memoryCache, + ILocalClock localClock, + IDynamicCacheService dynamicCacheService, + ITagCache tagCache, + ISignal signal) : IDateTimeCachingService { public const string MemoryCacheKey = "Lombiq.TrainingDemo.MemoryCache.DateTime"; public const string DynamicCacheKey = "Lombiq.TrainingDemo.DynamicCache.DateTime"; public const string DynamicCacheTag = "Lombiq.TrainingDemo.DynamicCache.DateTime.Tag"; - // You've already seen the IClock service for getting the current UTC date. This service can be used to get the - // current local date based on the site settings. Also dates can be converted from or to UTC. - private readonly ILocalClock _localClock; - - // IMemoryCache service is a built-in service in ASP.NET Core. Use this if you want a fast cache that's local to the - // current process. Do note that if you app runs on multiple servers this cache won't be shared among nodes. To - // learn more about IMemoryCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory. - private readonly IMemoryCache _memoryCache; - - // Dynamic Cache is implemented primarily for caching shapes. It is based on the built-in ASP.NET Core - // IDistributedCache service which by default is implemented by DistributedMemoryCache. If you just want to cache - // simple values like you'd do with IMemoryCache but in a way that also shares cache entries between servers when - // your app runs on multiple servers then use IDistributedCache directly. To learn more about distributed caching - // and IDistributedCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed. - private readonly IDynamicCacheService _dynamicCacheService; - - // We're using ISignals to be able to send a signal to the memory cache to invalidate the given entry. - private readonly ISignal _signal; - - // Tag cache is a service for tagging cached data and invalidating cache by their tags. - private readonly ITagCache _tagCache; - - public DateTimeCachingService( - IMemoryCache memoryCache, - ILocalClock localClock, - IDynamicCacheService dynamicCacheService, - ITagCache tagCache, - ISignal signal) - { - _memoryCache = memoryCache; - _localClock = localClock; - _dynamicCacheService = dynamicCacheService; - _tagCache = tagCache; - _signal = signal; - } - // This method will get or create the cached DateTime object using the IMemoryCache. public async Task GetMemoryCachedDateTimeAsync() { - if (!_memoryCache.TryGetValue(MemoryCacheKey, out DateTime cachedDate)) + if (!memoryCache.TryGetValue(MemoryCacheKey, out DateTime cachedDate)) { - cachedDate = (await _localClock.LocalNowAsync).DateTime; + cachedDate = (await localClock.LocalNowAsync).DateTime; - _memoryCache.Set(MemoryCacheKey, cachedDate, GetMemoryCacheChangeToken()); + memoryCache.Set(MemoryCacheKey, cachedDate, GetMemoryCacheChangeToken()); } return cachedDate; @@ -91,33 +60,33 @@ public async Task InvalidateCachedDateTimeAsync() { // As mentioned ISignal service is used to invalidate the memory cache. This will invalidate all cache entries // if there are multiple ones related to the token. - await _signal.SignalTokenAsync(MemoryCacheKey); + await signal.SignalTokenAsync(MemoryCacheKey); // ITagCache.RemoveTagAsync will invalidate all the dynamic caches which are tagged with the given tag. - await _tagCache.RemoveTagAsync(DynamicCacheTag); + await tagCache.RemoveTagAsync(DynamicCacheTag); } // This change token is generated based on the cache key using the ISignal service. It is used to invalidate the // memory cache. You can use this not just as another way to invalidate specific entries but also a way to // invalidate many at the same time: You can use tie multiple cache entries to the same signal too. - private IChangeToken GetMemoryCacheChangeToken() => _signal.GetToken(MemoryCacheKey); + private IChangeToken GetMemoryCacheChangeToken() => signal.GetToken(MemoryCacheKey); private async Task GetOrCreateDynamicCachedDateTimeAsync(CacheContext cacheContext) { // Now that we have a cache context we try to acquire the object. The objects always need to be strings. - var cachedDateTimeText = await _dynamicCacheService.GetCachedValueAsync(cacheContext); + var cachedDateTimeText = await dynamicCacheService.GetCachedValueAsync(cacheContext); // If the date time text is not null then parse it to DateTime otherwise use the ILocalClock service to set it // to the current date. var cachedDateTime = cachedDateTimeText != null ? DateTime.Parse(cachedDateTimeText, CultureInfo.InvariantCulture) : - (await _localClock.LocalNowAsync).DateTime; + (await localClock.LocalNowAsync).DateTime; // If the date time text is null (meaning it wasn't cached) cache the DateTime object (which in this case is the // current date). if (cachedDateTimeText == null) { - await _dynamicCacheService.SetCachedValueAsync( + await dynamicCacheService.SetCachedValueAsync( cacheContext, cachedDateTime.ToString(CultureInfo.InvariantCulture)); } diff --git a/Services/DemoBackgroundTask.cs b/Services/DemoBackgroundTask.cs index 232e0e71..f30abfc8 100644 --- a/Services/DemoBackgroundTask.cs +++ b/Services/DemoBackgroundTask.cs @@ -27,19 +27,17 @@ namespace Lombiq.TrainingDemo.Services; // Tasks admin page. Also you can set Enabled to false if you don't want it start right after application start. These // settings can be updated entirely on the Background Tasks admin page. [BackgroundTask(Schedule = "*/2 * * * *", Description = "Demo background task that runs every 2 minutes.")] -public class DemoBackgroundTask : IBackgroundTask +public class DemoBackgroundTask(ILogger logger) : IBackgroundTask { // Setting a maximum time this background task will be executed. private const int MaxCount = 5; - private readonly ILogger _logger; + private readonly ILogger _logger = logger; // Storing execution times in a private field. Since background tasks are singleton objects this will keep its value // while the application runs. private int _count; - public DemoBackgroundTask(ILogger logger) => _logger = logger; - // Since background tasks are singletons we'll need this IServiceProvider instance to resolve every non-singleton // service. When in doubt, just use this IServiceProvider instance to resolve everything instead of injecting a // service via the constructor. From 804be1b4da7e1acae1fc5bfc3e4582c0e4f04b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Sat, 20 Jan 2024 12:50:48 +0100 Subject: [PATCH 04/20] Addressing warnings. --- .../ManagePersonsPermissionCheckerTask.cs | 31 +++++----------- Controllers/AdminController.cs | 33 +++++------------ Controllers/ApiController.cs | 15 ++------ Controllers/CacheController.cs | 15 +++----- Controllers/CrossTenantServicesController.cs | 10 +---- Controllers/DatabaseStorageController.cs | 37 ++++++------------- Drivers/DemoSettingsDisplayDriver.cs | 3 +- Services/CustomFileStore.cs | 9 +---- Services/DemoSettingsConfiguration.cs | 8 +--- Services/TestedService.cs | 8 +--- Startup.cs | 8 +--- 11 files changed, 51 insertions(+), 126 deletions(-) diff --git a/Activities/ManagePersonsPermissionCheckerTask.cs b/Activities/ManagePersonsPermissionCheckerTask.cs index 49f28aa6..5c81dbca 100644 --- a/Activities/ManagePersonsPermissionCheckerTask.cs +++ b/Activities/ManagePersonsPermissionCheckerTask.cs @@ -14,24 +14,13 @@ namespace Lombiq.TrainingDemo.Activities; // A simple workflow task that accepts a username as a TextField input and checks whether the user has ManagePersons // Permission or not. -public class ManagePersonsPermissionCheckerTask : TaskActivity +public class ManagePersonsPermissionCheckerTask( + IAuthorizationService authorizationService, + IUserService userService, + IWorkflowExpressionEvaluator expressionEvaluator, + IStringLocalizer localizer) : TaskActivity { - private readonly IAuthorizationService _authorizationService; - private readonly IUserService _userService; - private readonly IWorkflowExpressionEvaluator _expressionEvaluator; - private readonly IStringLocalizer S; - - public ManagePersonsPermissionCheckerTask( - IAuthorizationService authorizationService, - IUserService userService, - IWorkflowExpressionEvaluator expressionEvaluator, - IStringLocalizer localizer) - { - _authorizationService = authorizationService; - _userService = userService; - _expressionEvaluator = expressionEvaluator; - S = localizer; - } + private readonly IStringLocalizer S = localizer; // The technical name of the activity. Activities in a workflow definition reference this name. public override string Name => nameof(ManagePersonsPermissionCheckerTask); @@ -60,14 +49,14 @@ public override async Task ExecuteAsync( WorkflowExecutionContext workflowContext, ActivityContext activityContext) { - var userName = await _expressionEvaluator.EvaluateAsync(UserName, workflowContext, encoder: null); - var user = (User)await _userService.GetUserAsync(userName); + var userName = await expressionEvaluator.EvaluateAsync(UserName, workflowContext, encoder: null); + var user = (User)await userService.GetUserAsync(userName); if (user != null) { - var userClaim = await _userService.CreatePrincipalAsync(user); + var userClaim = await userService.CreatePrincipalAsync(user); - if (await _authorizationService.AuthorizeAsync(userClaim, PersonPermissions.ManagePersons)) + if (await authorizationService.AuthorizeAsync(userClaim, PersonPermissions.ManagePersons)) { return Outcomes("HasPermission"); } diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs index 5744dd91..255e24ab 100644 --- a/Controllers/AdminController.cs +++ b/Controllers/AdminController.cs @@ -19,25 +19,12 @@ namespace Lombiq.TrainingDemo.Controllers; // If you have multiple admin controllers then name them whatever you want but put an [Admin] attribute on them. -public class AdminController : Controller +public class AdminController( + IContentItemDisplayManager contentItemDisplayManager, + ISession session, + IAuthorizationService authorizationService, + IUpdateModelAccessor updateModelAccessor) : Controller { - private readonly IContentItemDisplayManager _contentItemDisplayManager; - private readonly ISession _session; - private readonly IAuthorizationService _authorizationService; - private readonly IUpdateModelAccessor _updateModelAccessor; - - public AdminController( - IContentItemDisplayManager contentItemDisplayManager, - ISession session, - IAuthorizationService authorizationService, - IUpdateModelAccessor updateModelAccessor) - { - _contentItemDisplayManager = contentItemDisplayManager; - _session = session; - _authorizationService = authorizationService; - _updateModelAccessor = updateModelAccessor; - } - // Let's see how it will be displayed, just type the default URL (/Lombiq.TrainingDemo/Admin/Index) into the browser // with an administrator account (or at least a user who has a role that has AccessAdmin permission). If you are // anonymous then a login page will automatically appear. The permission check (i.e. has AccessAdmin @@ -51,13 +38,13 @@ public async Task PersonListNewest() { // If the user needs to have a specific permission to access a page on the admin panel (besides the AccessAdmin // permission) you need to check it here. - if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) + if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) { return Unauthorized(); } // Nothing special here just display the last 10 Person Page content items. - var persons = await _session + var persons = await session .Query() .Where(index => index.ContentType == ContentTypes.PersonPage) .OrderByDescending(index => index.CreatedUtc) @@ -70,13 +57,13 @@ public async Task PersonListNewest() public async Task PersonListOldest() { - if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) + if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) { return Unauthorized(); } // Display the first 10 Person Page content items. - var persons = await _session + var persons = await session .Query() .Where(index => index.ContentType == ContentTypes.PersonPage) .OrderBy(index => index.CreatedUtc) @@ -90,7 +77,7 @@ private async Task> GetShapesAsync(IEnumerable // Notice the "SummaryAdmin" display type which is a built in display type specifically for listing items on the // dashboard. await persons.AwaitEachAsync(async person => - await _contentItemDisplayManager.BuildDisplayAsync(person, _updateModelAccessor.ModelUpdater, "SummaryAdmin")); + await contentItemDisplayManager.BuildDisplayAsync(person, updateModelAccessor.ModelUpdater, "SummaryAdmin")); } // NEXT STATION: Navigation/TrainingDemoNavigationProvider.cs diff --git a/Controllers/ApiController.cs b/Controllers/ApiController.cs index da0f3a5e..93668af4 100644 --- a/Controllers/ApiController.cs +++ b/Controllers/ApiController.cs @@ -29,17 +29,8 @@ namespace Lombiq.TrainingDemo.Controllers; // endpoints should most of the time use the "Api" authentication scheme: This is not the same that standard users are // authenticated with (via cookies). [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] -public class ApiController : Controller +public class ApiController(IAuthorizationService authorizationService, IContentManager contentManager) : Controller { - private readonly IAuthorizationService _authorizationService; - private readonly IContentManager _contentManager; - - public ApiController(IAuthorizationService authorizationService, IContentManager contentManager) - { - _authorizationService = authorizationService; - _contentManager = contentManager; - } - // You can look up the ID of a Person Page that you've created previously (when you open one from the admin content // item list the URL will contain it as /Admin/Contents/ContentItems/) and use it to access this // action under /api/Lombiq.TrainingDemo?contentItemId=. Note though that you'll only be able to @@ -52,13 +43,13 @@ public async Task Get(string contentItemId) // permission here. To authenticate with the API you can use any ASP.NET Core authentication scheme but Orchard // offers various OpenID-based options. If you just want to quickly check out the API then grant the permission // for the Anonymous role on the admin. - if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) + if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) { return this.ChallengeOrForbid("Api"); } // Just the usual stuff again. - var contentItem = await _contentManager.GetAsync(contentItemId); + var contentItem = await contentManager.GetAsync(contentItemId); // Only allow the retrieval of Person Page items. if (contentItem?.ContentType != ContentTypes.PersonPage) contentItem = null; diff --git a/Controllers/CacheController.cs b/Controllers/CacheController.cs index 88e165d2..5800e004 100644 --- a/Controllers/CacheController.cs +++ b/Controllers/CacheController.cs @@ -15,27 +15,22 @@ namespace Lombiq.TrainingDemo.Controllers; -public class CacheController : Controller +public class CacheController(IDateTimeCachingService dateTimeCachingService) : Controller { - // The actual caching is implemented in a service which we'll soon investigate. - private readonly IDateTimeCachingService _dateTimeCachingService; - - public CacheController(IDateTimeCachingService dateTimeCachingService) => _dateTimeCachingService = dateTimeCachingService; - // In this action we'll cache a DateTime three different ways. You can open it under // /Lombiq.TrainingDemo/Cache/Index public async Task Index() { // This one will be cached using the built-in ASP.NET Core IMemoryCache. - var memoryCachedDateTime = await _dateTimeCachingService.GetMemoryCachedDateTimeAsync(); + var memoryCachedDateTime = await dateTimeCachingService.GetMemoryCachedDateTimeAsync(); // This one will be using the DynamicCache provided by Orchard Core. It will have a 30 second expiration. var dynamicCachedDateTimeWith30SecondsExpiry = - await _dateTimeCachingService.GetDynamicCachedDateTimeWith30SecondsExpiryAsync(); + await dateTimeCachingService.GetDynamicCachedDateTimeWith30SecondsExpiryAsync(); // Finally this date will be cached only for this route. var dynamicCachedDateTimeVariedByRoutes = - await _dateTimeCachingService.GetDynamicCachedDateTimeVariedByRoutesAsync(); + await dateTimeCachingService.GetDynamicCachedDateTimeVariedByRoutesAsync(); // NEXT STATION: Services/DateTimeCachingService.cs @@ -57,7 +52,7 @@ public Task DifferentRoute() => // /Lombiq.TrainingDemo/Cache/InvalidateDateTimeCache public async Task InvalidateDateTimeCache() { - await _dateTimeCachingService.InvalidateCachedDateTimeAsync(); + await dateTimeCachingService.InvalidateCachedDateTimeAsync(); return RedirectToAction("Index"); } diff --git a/Controllers/CrossTenantServicesController.cs b/Controllers/CrossTenantServicesController.cs index 79fbd419..25543c0b 100644 --- a/Controllers/CrossTenantServicesController.cs +++ b/Controllers/CrossTenantServicesController.cs @@ -24,14 +24,8 @@ namespace Lombiq.TrainingDemo.Controllers; // This is a controller just for the sake of easy demonstration, you can do the same thing anywhere. In the Index // action, we'll fetch content items from another tenant with the IContentManager service that you already know. This is // just an example though, really you can access any other service as well. -public class CrossTenantServicesController : Controller +public class CrossTenantServicesController(IShellHost shellHost) : Controller { - private readonly IShellHost _shellHost; - - // We'll need IShellHost to access services from a currently running shell's dependency injection container (Service - // Provider). - public CrossTenantServicesController(IShellHost shellHost) => _shellHost = shellHost; - // A simple route for convenience. You can access this from under /CrossTenantServices?contentItemId=ID. Here ID // needs to be a content item ID that you can get e.g. from the URL when you open an item to edit from the admin (it // looks something like "4da2sme18cc2k2r5d4w23d4cwj" which is NOT made by a cat walking across the keyboard!). @@ -48,7 +42,7 @@ public async Task Index(string contentItemId) // First you have to retrieve the tenant's shell scope that contains the shell's Service Provider. Note that // there is also an IShellSettingsManager service that you can use to access the just shell settings for all // tenants (shell settings are a tenant's basic settings, like its technical name and its URL). - var shellScope = await _shellHost.GetScopeAsync("Default"); + var shellScope = await shellHost.GetScopeAsync("Default"); // We'll just return the title of the content item from this action but you can do anything else with the item // too, like displaying it. diff --git a/Controllers/DatabaseStorageController.cs b/Controllers/DatabaseStorageController.cs index e1d5a8cf..c897bb4b 100644 --- a/Controllers/DatabaseStorageController.cs +++ b/Controllers/DatabaseStorageController.cs @@ -24,27 +24,14 @@ namespace Lombiq.TrainingDemo.Controllers; -public class DatabaseStorageController : Controller +public class DatabaseStorageController( + ISession session, + IDisplayManager bookDisplayManager, + INotifier notifier, + IHtmlLocalizer htmlLocalizer, + IUpdateModelAccessor updateModelAccessor) : Controller { - private readonly ISession _session; - private readonly IDisplayManager _bookDisplayManager; - private readonly INotifier _notifier; - private readonly IHtmlLocalizer H; - private readonly IUpdateModelAccessor _updateModelAccessor; - - public DatabaseStorageController( - ISession session, - IDisplayManager bookDisplayManager, - INotifier notifier, - IHtmlLocalizer htmlLocalizer, - IUpdateModelAccessor updateModelAccessor) - { - _session = session; - _bookDisplayManager = bookDisplayManager; - _notifier = notifier; - _updateModelAccessor = updateModelAccessor; - H = htmlLocalizer; - } + private readonly IHtmlLocalizer H = htmlLocalizer; // A page with a button that will call the CreateBooks POST action. See it under // /Lombiq.TrainingDemo/DatabaseStorage/CreateBooks. @@ -65,10 +52,10 @@ public async Task CreateBooksPost() foreach (var book in CreateDemoBooks()) { // So now you understand what will happen in the background when this service is being called. - await _session.SaveAsync(book); + await session.SaveAsync(book); } - await _notifier.InformationAsync(H["Books have been created in the database."]); + await notifier.InformationAsync(H["Books have been created in the database."]); return RedirectToAction(nameof(CreateBooks)); } @@ -78,7 +65,7 @@ public async Task CreateBooksPost() public async Task JKRosenzweigBooks() { // ISession service is used for querying items. - var jkRosenzweigBooks = await _session + var jkRosenzweigBooks = await session // First, we define what object (document) we want to query and what index should be used for filtering. .Query() // In the .Where() method you can describe a lambda where the object will be the index object. @@ -92,7 +79,7 @@ public async Task JKRosenzweigBooks() var bookShapes = await jkRosenzweigBooks.AwaitEachAsync(async book => // We'll need to pass an IUpdateModel (used for model validation) to the method, which we can access via its // accessor service. Later you'll also see how we'll use this to run validations in drivers. - await _bookDisplayManager.BuildDisplayAsync(book, _updateModelAccessor.ModelUpdater)); + await bookDisplayManager.BuildDisplayAsync(book, updateModelAccessor.ModelUpdater)); // You can check out Views/DatabaseStorage/JKRosenzweigBooks.cshtml and come back here. return View(bookShapes); @@ -102,7 +89,7 @@ public async Task JKRosenzweigBooks() // NEXT STATION: Models/PersonPart.cs - private static IEnumerable CreateDemoBooks() => + private static Book[] CreateDemoBooks() => new[] { new Book diff --git a/Drivers/DemoSettingsDisplayDriver.cs b/Drivers/DemoSettingsDisplayDriver.cs index 36f1b796..bd9391ff 100644 --- a/Drivers/DemoSettingsDisplayDriver.cs +++ b/Drivers/DemoSettingsDisplayDriver.cs @@ -14,7 +14,8 @@ namespace Lombiq.TrainingDemo.Drivers; // Now this display driver abstraction is different from the one you've seen before. In Orchard Core you can connect // different objects to a master object that will be connected in the database when storing them. Site settings are // handled this way. -public class DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) : SectionDisplayDriver +public class DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) + : SectionDisplayDriver { // Since technically we have only one SiteSettings we have separate the editors using editor groups. It's a good // idea to store the editor group ID in a publicly accessibly constant (would be much better to store it in a static diff --git a/Services/CustomFileStore.cs b/Services/CustomFileStore.cs index 12cf20e8..d0463bee 100644 --- a/Services/CustomFileStore.cs +++ b/Services/CustomFileStore.cs @@ -14,15 +14,8 @@ public interface ICustomFileStore : IFileStore // You can add additional methods if you want. } -public class CustomFileStore : FileSystemStore, ICustomFileStore +public class CustomFileStore(string fileSystemPath) : FileSystemStore(fileSystemPath), ICustomFileStore { - // Since FileSystemStore requires a base path we also need to have it. If you have a very specific absolute path - // then you don't need it to be injected but for demonstration purposes we'll inject it from Startup.cs because it - // will be in the tenant's folder. - public CustomFileStore(string fileSystemPath) - : base(fileSystemPath) - { - } } // NEXT STATION: Startup.cs and find the File System comment line in the ConfigureServices method. diff --git a/Services/DemoSettingsConfiguration.cs b/Services/DemoSettingsConfiguration.cs index 81276f21..23578f50 100644 --- a/Services/DemoSettingsConfiguration.cs +++ b/Services/DemoSettingsConfiguration.cs @@ -5,12 +5,8 @@ namespace Lombiq.TrainingDemo.Services; // This is a configuration class that'll load the options from site settings. -public class DemoSettingsConfiguration : IConfigureOptions +public class DemoSettingsConfiguration(ISiteService siteService) : IConfigureOptions { - private readonly ISiteService _siteService; - - public DemoSettingsConfiguration(ISiteService siteService) => _siteService = siteService; - public void Configure(DemoSettings options) { // The method parameter comes from the other configuration options, like the appsettings.json file if it's set, @@ -38,7 +34,7 @@ public void Configure(DemoSettings options) // out what we have there related to settings and come back! // Unfortunately, no async here so we need to run this synchronously. - var settings = _siteService.GetSiteSettingsAsync() + var settings = siteService.GetSiteSettingsAsync() .GetAwaiter().GetResult() .As(); diff --git a/Services/TestedService.cs b/Services/TestedService.cs index 1e817741..534075bb 100644 --- a/Services/TestedService.cs +++ b/Services/TestedService.cs @@ -31,12 +31,8 @@ public interface ITestedService } // The implementation of the service follows. -public class TestedService : ITestedService +public class TestedService(IContentManager contentManager) : ITestedService { - private readonly IContentManager _contentManager; - - public TestedService(IContentManager contentManager) => _contentManager = contentManager; - public Task GetContentItemOrThrowAsync(string id) { // As you can see we rigorously check the input. Something we'll surely need to test later! @@ -51,7 +47,7 @@ public Task GetContentItemOrThrowAsync(string id) private async Task GetContentItemOrThrowInternalAsync(string id) => // You already know how this works :). - await _contentManager.GetAsync(id) + await contentManager.GetAsync(id) ?? throw new InvalidOperationException($"The content item with the ID {id} doesn't exist."); } diff --git a/Startup.cs b/Startup.cs index 145983d5..4873fba4 100644 --- a/Startup.cs +++ b/Startup.cs @@ -54,12 +54,8 @@ namespace Lombiq.TrainingDemo; // While the startup class doesn't need to derive from StartupBase and can just use conventionally named methods it's a // bit less of a magic this way, and code analysis won't tell us to make it static. -public class Startup : StartupBase +public class Startup(IShellConfiguration shellConfiguration) : StartupBase { - private readonly IShellConfiguration _shellConfiguration; - - public Startup(IShellConfiguration shellConfiguration) => _shellConfiguration = shellConfiguration; - public override void ConfigureServices(IServiceCollection services) { // NEXT STATION: Views/PersonPart.Edit.cshtml @@ -96,7 +92,7 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); // Demo Settings - services.Configure(_shellConfiguration.GetSection("Lombiq_TrainingDemo")); + services.Configure(shellConfiguration.GetSection("Lombiq_TrainingDemo")); services.AddTransient, DemoSettingsConfiguration>(); services.AddScoped, DemoSettingsDisplayDriver>(); services.AddScoped(); From 3763d3fe608a4843699d87ece54cf16c838a2009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Sat, 20 Jan 2024 15:28:40 +0100 Subject: [PATCH 05/20] Addressing warnings. --- Controllers/PersonListController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index b033f2d3..eee84aa4 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -33,7 +33,10 @@ public class PersonListController( IUpdateModelAccessor updateModelAccessor, IContentManager contentManager) : Controller { +#pragma warning disable CA2213 // Disposable fields should be disposed + // We don't need a dispose method in a controller. private readonly ISession _session = session; +#pragma warning restore CA2213 // Disposable fields should be disposed private readonly IClock _clock = clock; private readonly IContentItemDisplayManager _contentItemDisplayManager = contentItemDisplayManager; private readonly IUpdateModelAccessor _updateModelAccessor = updateModelAccessor; @@ -127,7 +130,7 @@ public async Task FountainOfEternalYouth() // IContentManager.GetAsync() instead. return "People modified: " + - (oldPeople.Any() ? + (oldPeople.Count != 0 ? string.Join(", ", oldPeople.Select(person => person.As().Name)) : "Nobody. Did you create people older than 90?"); From 32b3d3cc37b61ab1fc0ef975aa7a7ddd2eb462a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Mon, 29 Jan 2024 14:26:57 +0100 Subject: [PATCH 06/20] Removing fields. --- Controllers/DisplayManagementController.cs | 12 +++++------ Controllers/FileManagementController.cs | 23 ++++++++++------------ 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Controllers/DisplayManagementController.cs b/Controllers/DisplayManagementController.cs index 357fd495..6b80114c 100644 --- a/Controllers/DisplayManagementController.cs +++ b/Controllers/DisplayManagementController.cs @@ -16,13 +16,11 @@ namespace Lombiq.TrainingDemo.Controllers; +// The core display management features can be used via the IDisplayManager service. The generic parameter will be +// the object that needs to be displayed on the UI somehow. Don't forget to register this generic class with the +// service provider (see: Startup.cs). public class DisplayManagementController(IDisplayManager bookDisplayManager) : Controller, IUpdateModel { - // The core display management features can be used via the IDisplayManager service. The generic parameter will be - // the object that needs to be displayed on the UI somehow. Don't forget to register this generic class with the - // service provider (see: Startup.cs). - private readonly IDisplayManager _bookDisplayManager = bookDisplayManager; - // Before we learn how shapes are generated using the display manager let's see what are these shapes actually. // Ad-hoc shapes can be created anywhere without the display manager. In this example we'll see how to create an // ad-hoc shape inside a view (or could be another shape). Later we'll see how to do it from a filter too. Open from @@ -39,7 +37,7 @@ public async Task DisplayBook() var book = CreateDemoBook(); // This method will generate a shape primarily for displaying information about the given object. - var shape = await _bookDisplayManager.BuildDisplayAsync(book, this); + var shape = await bookDisplayManager.BuildDisplayAsync(book, this); // We will see how this display shape is generated and what will contain but first let's see how is this // rendered in the MVC view. NEXT STATION: Go to Views/DisplayManagement/DisplayBook.cshtml. @@ -56,7 +54,7 @@ public async Task DisplayBookDescription() // This time give an additional parameter which is the display type. If display type is given then Orchard Core // will search a cshtml file with a name [ClassName].[DisplayType].cshtml. - var shape = await _bookDisplayManager.BuildDisplayAsync(book, this, "Description"); + var shape = await bookDisplayManager.BuildDisplayAsync(book, this, "Description"); // NEXT STATION: Go to Views/Book.Description.cshtml diff --git a/Controllers/FileManagementController.cs b/Controllers/FileManagementController.cs index ba2ce5f1..dab10063 100644 --- a/Controllers/FileManagementController.cs +++ b/Controllers/FileManagementController.cs @@ -38,10 +38,7 @@ public class FileManagementController( private const string UploadedFileFolderRelativePath = "TrainingDemo/Uploaded"; - private readonly IMediaFileStore _mediaFileStore = mediaFileStore; - private readonly INotifier _notifier = notifier; private readonly IHtmlLocalizer H = htmlLocalizer; - private readonly ICustomFileStore _customFileStore = customFileStore; // This action will demonstrate how to create a file in the Media folder and read it from there. See it under // /Lombiq.TrainingDemo/FileManagement/CreateFileInMediaFolder. @@ -52,16 +49,16 @@ public async Task CreateFileInMediaFolder() using (var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hi there!"))) { // The third parameter here is optional - if true, it will override the file if already exists. - await _mediaFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); + await mediaFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); } // Use this method to check if the file exists (it will be null if the file doesn't exist). It's similar to the // built-in FileInfo class but not that robust. - var fileInfo = await _mediaFileStore.GetFileInfoAsync(TestFileRelativePath); + var fileInfo = await mediaFileStore.GetFileInfoAsync(TestFileRelativePath); // The IMediaFileStore has its own specific methods such as mapping the file path to a public URL. Since the // files in the Media folder are accessible from the outside this can be handy. - var publicUrl = _mediaFileStore.MapPathToPublicUrl(TestFileRelativePath); + var publicUrl = mediaFileStore.MapPathToPublicUrl(TestFileRelativePath); return $"Successfully created file! File size: {fileInfo.Length} bytes. Public URL: {publicUrl}"; } @@ -74,13 +71,13 @@ public async Task CreateFileInMediaFolder() public async Task ReadFileFromMediaFolder() { // This way you can check if the given file exists. - if (await _mediaFileStore.GetFileInfoAsync(TestFileRelativePath) == null) + if (await mediaFileStore.GetFileInfoAsync(TestFileRelativePath) == null) { return "Create the file first!"; } // If you want to extract the content of the file use a StreamReader to read the stream. - using var stream = await _mediaFileStore.GetFileStreamAsync(TestFileRelativePath); + using var stream = await mediaFileStore.GetFileStreamAsync(TestFileRelativePath); using var streamReader = new StreamReader(stream); var content = await streamReader.ReadToEndAsync(HttpContext.RequestAborted); @@ -99,15 +96,15 @@ public async Task UploadFileToMediaPost(IFormFile file) if (file == null) return BadRequest(); // You can use the Combine method to combine paths which is pretty much equivalent to the built-in method. - var mediaFilePath = _mediaFileStore.Combine(UploadedFileFolderRelativePath, file.FileName); + var mediaFilePath = mediaFileStore.Combine(UploadedFileFolderRelativePath, file.FileName); // In this case you already have a stream so use it to create the file. using (var stream = file.OpenReadStream()) { - await _mediaFileStore.CreateFileFromStreamAsync(mediaFilePath, stream); + await mediaFileStore.CreateFileFromStreamAsync(mediaFilePath, stream); } - await _notifier.InformationAsync(H["Successfully uploaded file!"]); + await notifier.InformationAsync(H["Successfully uploaded file!"]); return RedirectToAction(nameof(UploadFileToMedia)); } @@ -121,10 +118,10 @@ public async Task CreateFileInCustomFolder() // time. The files will be created inside our CustomFiles folder as it was defined in Startup.cs. using (var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hi there in the custom file storage!"))) { - await _customFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); + await customFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); } - var fileInfo = await _customFileStore.GetFileInfoAsync(TestFileRelativePath); + var fileInfo = await customFileStore.GetFileInfoAsync(TestFileRelativePath); return $"Successfully created file in the custom file storage! File size: {fileInfo.Length} bytes."; } From 96eb057d1229eb454b7379c1b337a17f78d46da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Mon, 29 Jan 2024 14:28:39 +0100 Subject: [PATCH 07/20] Removing fields. --- Controllers/PersonListController.cs | 41 +++++++++++------------------ 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index eee84aa4..06486f90 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -33,20 +33,11 @@ public class PersonListController( IUpdateModelAccessor updateModelAccessor, IContentManager contentManager) : Controller { -#pragma warning disable CA2213 // Disposable fields should be disposed - // We don't need a dispose method in a controller. - private readonly ISession _session = session; -#pragma warning restore CA2213 // Disposable fields should be disposed - private readonly IClock _clock = clock; - private readonly IContentItemDisplayManager _contentItemDisplayManager = contentItemDisplayManager; - private readonly IUpdateModelAccessor _updateModelAccessor = updateModelAccessor; - private readonly IContentManager _contentManager = contentManager; - // See it under /Lombiq.TrainingDemo/PersonList/OlderThan30. public async Task OlderThan30() { - var thresholdDate = _clock.UtcNow.AddYears(-30); - var people = await _session + var thresholdDate = clock.UtcNow.AddYears(-30); + var people = await session // This will query for content items where the related PersonPartIndex.BirthDateUtc is lower than the // threshold date. Notice that there is no Where method. The Query method has an overload for that which can // be useful if you don't want to filter in multiple indexes. @@ -64,9 +55,9 @@ public async Task OlderThan30() // When you retrieve content items via ISession then you also need to run LoadAsync() on them to initialize // everything. This foremost includes running handlers, which are pretty much event handlers for content // items (you'll see them in a minute with PersonPartHandler). - await _contentManager.LoadAsync(person); + await contentManager.LoadAsync(person); - return await _contentItemDisplayManager.BuildDisplayAsync(person, _updateModelAccessor.ModelUpdater, "Summary"); + return await contentItemDisplayManager.BuildDisplayAsync(person, updateModelAccessor.ModelUpdater, "Summary"); }); // Now assuming that you've already created a few Person content items on the dashboard and some of these @@ -83,8 +74,8 @@ public async Task FountainOfEternalYouth() // Again we'll fetch content items with PersonPart but this time we'll retrieve old people and we'll make them // younger! - var thresholdDate = _clock.UtcNow.AddYears(-90); - var oldPeople = (await _session + var thresholdDate = clock.UtcNow.AddYears(-90); + var oldPeople = (await session .Query(index => index.BirthDateUtc < thresholdDate) .ListAsync()) .ToList(); @@ -92,9 +83,9 @@ public async Task FountainOfEternalYouth() foreach (var person in oldPeople) { // Have to run LoadAsync() here too. - await _contentManager.LoadAsync(person); + await contentManager.LoadAsync(person); - var eighteenYearOld = _clock.UtcNow.AddYears(-18); + var eighteenYearOld = clock.UtcNow.AddYears(-18); // Don't just overwrite the part's property directly! That'll change the index record but not the document! // Don't just do this: @@ -119,11 +110,11 @@ public async Task FountainOfEternalYouth() // Once you're done you have to save the content item explicitly. Remember when we saved Books with // ISession.Save()? This is something similar for content items. - await _contentManager.UpdateAsync(person); + await contentManager.UpdateAsync(person); // After saving the content item with UpdateAsync() you also need to publish it to make sure that even a // draftable content item gets updated. - await _contentManager.PublishAsync(person); + await contentManager.PublishAsync(person); } // If you want to manage just one content item or a couple of them that you know by ID then fetch them with @@ -146,17 +137,17 @@ public async Task CreateAnAndroid() // First you need to instantiate the new content item. This just creates an object for now, nothing is yet // persisted into the database. - var person = await _contentManager.NewAsync(ContentTypes.PersonPage); + var person = await contentManager.NewAsync(ContentTypes.PersonPage); // We add some identity to our new synthetic friend. - var serialNumber = _clock.UtcNow.Ticks; + var serialNumber = clock.UtcNow.Ticks; var name = $"X Doe #{serialNumber.ToTechnicalString()}"; // Again we can save date into parts like this: person.Alter(part => { part.Name = name; - part.BirthDateUtc = _clock.UtcNow; + part.BirthDateUtc = clock.UtcNow; part.Handedness = Handedness.Right; // Actually ambidextrous. }); @@ -169,9 +160,9 @@ public async Task CreateAnAndroid() // You could also publish it right away but if you want to be safe and make sure that every part runs nicely // (like AutoroutePart, which we don't use on PersonPage, generates the permalink) it's safer to first create a // draft, then also update it, and finally publish it explicitly. - await _contentManager.CreateAsync(person, VersionOptions.Draft); - await _contentManager.UpdateAsync(person); - await _contentManager.PublishAsync(person); + await contentManager.CreateAsync(person, VersionOptions.Draft); + await contentManager.UpdateAsync(person); + await contentManager.PublishAsync(person); // You should see a new content item, check it out from the admin! From b3b4f0a44642052e9622f8fa95de4dba6b4d2f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Mon, 29 Jan 2024 14:42:02 +0100 Subject: [PATCH 08/20] Readding comments and removing fields. --- Controllers/CacheController.cs | 1 + Controllers/CrossTenantServicesController.cs | 2 ++ Controllers/SiteSettingsController.cs | 5 ++--- Controllers/YourFirstOrchardCoreController.cs | 12 +++++------- Drivers/DemoSettingsDisplayDriver.cs | 7 ++----- Events/LoginGreeting.cs | 3 +-- Filters/ResourceFromShapeInjectingFilter.cs | 14 ++++---------- Filters/ResourceInjectionFilter.cs | 6 ++---- Filters/ShapeInjectionFilter.cs | 14 +++++--------- GraphQL/Services/PersonAgeGraphQLFilter.cs | 4 +--- Middlewares/RequestLoggingMiddleware.cs | 4 +--- Services/CustomFileStore.cs | 3 +++ 12 files changed, 29 insertions(+), 46 deletions(-) diff --git a/Controllers/CacheController.cs b/Controllers/CacheController.cs index 5800e004..c20c2895 100644 --- a/Controllers/CacheController.cs +++ b/Controllers/CacheController.cs @@ -15,6 +15,7 @@ namespace Lombiq.TrainingDemo.Controllers; +// The actual caching is implemented in a service which we'll soon investigate. public class CacheController(IDateTimeCachingService dateTimeCachingService) : Controller { // In this action we'll cache a DateTime three different ways. You can open it under diff --git a/Controllers/CrossTenantServicesController.cs b/Controllers/CrossTenantServicesController.cs index 25543c0b..f0308c12 100644 --- a/Controllers/CrossTenantServicesController.cs +++ b/Controllers/CrossTenantServicesController.cs @@ -24,6 +24,8 @@ namespace Lombiq.TrainingDemo.Controllers; // This is a controller just for the sake of easy demonstration, you can do the same thing anywhere. In the Index // action, we'll fetch content items from another tenant with the IContentManager service that you already know. This is // just an example though, really you can access any other service as well. +// We'll need IShellHost to access services from a currently running shell's dependency injection container (Service +// Provider). public class CrossTenantServicesController(IShellHost shellHost) : Controller { // A simple route for convenience. You can access this from under /CrossTenantServices?contentItemId=ID. Here ID diff --git a/Controllers/SiteSettingsController.cs b/Controllers/SiteSettingsController.cs index 9db3eb91..6b2fbfa8 100644 --- a/Controllers/SiteSettingsController.cs +++ b/Controllers/SiteSettingsController.cs @@ -14,14 +14,13 @@ namespace Lombiq.TrainingDemo.Controllers; public class SiteSettingsController(ISiteService siteService, IOptionsSnapshot demoOptions) : Controller { - private readonly ISiteService _siteService = siteService; private readonly DemoSettings _demoSettings = demoOptions.Value; // Here's a quick simple demonstration about how to use ISiteService. Orchard Core stores basic settings that are // accessible right away in the ISite object. Here you will see how to access the site's name you gave when you set // up your website. public async Task SiteName() => - (await _siteService.GetSiteSettingsAsync()).SiteName; + (await siteService.GetSiteSettingsAsync()).SiteName; // NEXT STATION: Models/DemoSettings.cs @@ -31,7 +30,7 @@ public async Task DemoSettings() { // As mentioned the custom settings objects are serialized into the ISite object so use the .As<>() helper to // access it as you see below. - var messageFromSiteSettings = (await _siteService.GetSiteSettingsAsync()).As().Message; + var messageFromSiteSettings = (await siteService.GetSiteSettingsAsync()).As().Message; // But as you've seen in DemoSettings.cs our site settings are also Options so we can use it as such too: var messageFromOptions = _demoSettings.Message; diff --git a/Controllers/YourFirstOrchardCoreController.cs b/Controllers/YourFirstOrchardCoreController.cs index e8c36008..36c79cc5 100644 --- a/Controllers/YourFirstOrchardCoreController.cs +++ b/Controllers/YourFirstOrchardCoreController.cs @@ -17,20 +17,18 @@ namespace Lombiq.TrainingDemo.Controllers; +// Orchard Core uses the built in dependency injection feature coming with ASP.NET Core. You can use the module's +// Startup class to register your own services with the service provider. To learn more see: +// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection public class YourFirstOrchardCoreController( INotifier notifier, IStringLocalizer stringLocalizer, IHtmlLocalizer htmlLocalizer, ILogger logger) : Controller { - private readonly INotifier _notifier = notifier; private readonly IStringLocalizer T = stringLocalizer; private readonly IHtmlLocalizer H = htmlLocalizer; - // You can use the non-generic counterpart of ILogger once injected just be sure to inject the generic one otherwise - // the log entries won't contain the name of the class. - private readonly ILogger _logger = logger; - // Here's a simple action that will return some message. Nothing special here just demonstrates that this will work // in Orchard Core right after enabling the module. The route for this action will be // /Lombiq.TrainingDemo/YourFirstOrchardCore/Index. @@ -57,12 +55,12 @@ public async Task NotifyMe() // Extension it'll provide you a handy Error Log Watcher which lights up if there's a new error! Check it out // here: // https://marketplace.visualstudio.com/items?itemName=LombiqVisualStudioExtension.LombiqOrchardVisualStudioExtension - _logger.LogError("You have been notified about some error!"); + logger.LogError("You have been notified about some error!"); // INotifier is an Orchard Core service to send messages to the user. This service can be used almost everywhere // in the code base not only in Controllers. This service requires a LocalizedHtmlString object so the // IHtmlLocalizer service needs to be used for localization. - await _notifier.InformationAsync(H["Congratulations! You have been notified! Check the error log too!"]); + await notifier.InformationAsync(H["Congratulations! You have been notified! Check the error log too!"]); return View(); diff --git a/Drivers/DemoSettingsDisplayDriver.cs b/Drivers/DemoSettingsDisplayDriver.cs index bd9391ff..2c9246b2 100644 --- a/Drivers/DemoSettingsDisplayDriver.cs +++ b/Drivers/DemoSettingsDisplayDriver.cs @@ -22,9 +22,6 @@ public class DemoSettingsDisplayDriver(IAuthorizationService authorizationServic // class placed in a Constants folder). public const string EditorGroupId = "Demo"; - private readonly IAuthorizationService _authorizationService = authorizationService; - private readonly IHttpContextAccessor _hca = hca; - // Here's the EditAsync override to display editor for our site settings on the Dashboard. Note that it has a sync // version too. public override async Task EditAsync(DemoSettings section, BuildEditorContext context) @@ -76,9 +73,9 @@ private async Task IsAuthorizedToManageDemoSettingsAsync() { // Since the User object is not accessible here (as it was accessible in the Controller) we need to grab it from // the HttpContext. - var user = _hca.HttpContext?.User; + var user = hca.HttpContext?.User; - return user != null && await _authorizationService.AuthorizeAsync(user, DemoSettingsPermissions.ManageDemoSettings); + return user != null && await authorizationService.AuthorizeAsync(user, DemoSettingsPermissions.ManageDemoSettings); } } diff --git a/Events/LoginGreeting.cs b/Events/LoginGreeting.cs index 3f39e203..7e4ecf00 100644 --- a/Events/LoginGreeting.cs +++ b/Events/LoginGreeting.cs @@ -18,14 +18,13 @@ namespace Lombiq.TrainingDemo.Events; // useful! The rest of it is pretty standard and we just use INotifier again. public class LoginGreeting(INotifier notifier, IHtmlLocalizer htmlLocalizer) : ILoginFormEvent { - private readonly INotifier _notifier = notifier; private readonly IHtmlLocalizer H = htmlLocalizer; public Task IsLockedOutAsync(IUser user) => Task.CompletedTask; public async Task LoggedInAsync(IUser user) { - await _notifier.SuccessAsync(H["Hi {0}!", user.UserName]); + await notifier.SuccessAsync(H["Hi {0}!", user.UserName]); return; } diff --git a/Filters/ResourceFromShapeInjectingFilter.cs b/Filters/ResourceFromShapeInjectingFilter.cs index a7765b7b..8b753bdb 100644 --- a/Filters/ResourceFromShapeInjectingFilter.cs +++ b/Filters/ResourceFromShapeInjectingFilter.cs @@ -6,19 +6,13 @@ namespace Lombiq.TrainingDemo.Filters; +// We've seen IResourceManager and IShapeFactory before. +// IDisplayHelper is new, however. We'll use it to execute a shape into HTML and inject that as a head script! public class ResourceFromShapeInjectingFilter( IResourceManager resourceManager, IShapeFactory shapeFactory, IDisplayHelper displayHelper) : IAsyncResultFilter { - // We've seen IResourceManager and IShapeFactory before. - private readonly IResourceManager _resourceManager = resourceManager; - - private readonly IShapeFactory _shapeFactory = shapeFactory; - - // IDisplayHelper is new, however. We'll use it to execute a shape into HTML and inject that as a head script! - private readonly IDisplayHelper _displayHelper = displayHelper; - public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // Similar to ResourceInjectionFilter only run this if we're in a full view and the "alert" query string @@ -33,12 +27,12 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE // We use the shape factory again to instantiate the AlertScriptShape. Check out Views/AlertScriptShape.cshtml // because there's something curious there too, then come back! Next, we also use the display helper to execute // the shape and generate its HTML content. - var shapeContent = await _displayHelper.ShapeExecuteAsync(await _shapeFactory.New.AlertScriptShape()); + var shapeContent = await displayHelper.ShapeExecuteAsync(await shapeFactory.New.AlertScriptShape()); // Did you know that you can inject inline scripts with resource manager too? You can use this technique not // just like this to generate inline scripts but also e.g. to generate HTML output from shapes in background // tasks (like when sending e-mails and you want to have proper templates for them). - _resourceManager.RegisterFootScript(shapeContent); + resourceManager.RegisterFootScript(shapeContent); await next(); } diff --git a/Filters/ResourceInjectionFilter.cs b/Filters/ResourceInjectionFilter.cs index 29647ef6..49747b8c 100644 --- a/Filters/ResourceInjectionFilter.cs +++ b/Filters/ResourceInjectionFilter.cs @@ -6,11 +6,9 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. +// To register resources you can use the IResourceManager service. public class ResourceInjectionFilter(IResourceManager resourceManager) : IAsyncResultFilter { - // To register resources you can use the IResourceManager service. - private readonly IResourceManager _resourceManager = resourceManager; - public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // Let's decide when the filter should be executed. It wouldn't make sense to inject resources if this is a @@ -26,7 +24,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE // You can register "stylesheet" or "script" resources. You can also set where they'll be rendered with the // .AtHead() or .AtFoot() methods chained on the RegisterResource() method which obviously makes sense only if // the resource is a script. - _resourceManager.RegisterResource("stylesheet", "Lombiq.TrainingDemo.Filtered"); + resourceManager.RegisterResource("stylesheet", "Lombiq.TrainingDemo.Filtered"); await next(); } diff --git a/Filters/ShapeInjectionFilter.cs b/Filters/ShapeInjectionFilter.cs index f2d291ab..cbb7a284 100644 --- a/Filters/ShapeInjectionFilter.cs +++ b/Filters/ShapeInjectionFilter.cs @@ -22,15 +22,11 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. +// To access the layout which contains the zones you need to use the ILayoutAccessor service. +// To generate ad-hoc shapes the IShapeFactory can be used. This is the same which is behind the New property in +// templates that you have previously seen in AdHocShape.cshtml. public class ShapeInjectionFilter(ILayoutAccessor layoutAccessor, IShapeFactory shapeFactory) : IAsyncResultFilter { - // To access the layout which contains the zones you need to use the ILayoutAccessor service. - private readonly ILayoutAccessor _layoutAccessor = layoutAccessor; - - // To generate ad-hoc shapes the IShapeFactory can be used. This is the same which is behind the New property in - // templates that you have previously seen in AdHocShape.cshtml. - private readonly IShapeFactory _shapeFactory = shapeFactory; - public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // You can decide when the filter should be executed here. If this is not a ViewResult or PageResult the shape @@ -43,7 +39,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE } // We first retrieve the layout. - var layout = await _layoutAccessor.GetLayoutAsync(); + var layout = await layoutAccessor.GetLayoutAsync(); // The Layout object will contain a Zones dictionary that you can use to access a zone. The Content zone is // usually available in all themes and is the main zone in the middle of each page. @@ -51,7 +47,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE // Here you can add an ad-hoc generated shape to the Content zone. This works in the same way as we've seen // previously when we talked about display management. You can find the template that'll render this shape under // Views/InjectedShape.cshtml. - await contentZone.AddAsync(await _shapeFactory.CreateAsync("InjectedShape")); + await contentZone.AddAsync(await shapeFactory.CreateAsync("InjectedShape")); await next(); } diff --git a/GraphQL/Services/PersonAgeGraphQLFilter.cs b/GraphQL/Services/PersonAgeGraphQLFilter.cs index ff375e65..6c5bfda5 100644 --- a/GraphQL/Services/PersonAgeGraphQLFilter.cs +++ b/GraphQL/Services/PersonAgeGraphQLFilter.cs @@ -16,8 +16,6 @@ namespace Lombiq.TrainingDemo.GraphQL.Services; // IGraphQLFilters can append conditions to the YesSql query, alter its result, or do both. public class PersonAgeGraphQLFilter(IClock clock) : IGraphQLFilter { - private readonly IClock _clock = clock; - // While you can use this to execute some complex YesSql query it's best to stick with the IIndexAliasProvider // approach for such things. public Task> PreQueryAsync(IQuery query, IResolveFieldContext context) => @@ -35,7 +33,7 @@ public Task> PostQueryAsync( if (name != null && value.Value is int age) { - var now = _clock.UtcNow; + var now = clock.UtcNow; if (name == "age") name = "age_eq"; var filterType = name[^2..]; // The name operator like gt, le, etc. diff --git a/Middlewares/RequestLoggingMiddleware.cs b/Middlewares/RequestLoggingMiddleware.cs index e56cc4ec..90cb28aa 100644 --- a/Middlewares/RequestLoggingMiddleware.cs +++ b/Middlewares/RequestLoggingMiddleware.cs @@ -23,8 +23,6 @@ namespace Lombiq.TrainingDemo.Middlewares; // not, check out Manifest.cs again! public class RequestLoggingMiddleware(RequestDelegate next) { - private readonly RequestDelegate _next = next; - // This method is the actual middleware. Note that apart from the first parameter obligatorily being HttpContext // further parameters can be injected Orchard services. public async Task InvokeAsync( @@ -34,7 +32,7 @@ public async Task InvokeAsync( { // We let the next middleware run, but this is not mandatory: if this middleware would return a cached page for // example then we would write the cached response to the HttpContext and leave this out. - await _next(context); + await next(context); // Think twice when wrapping this call into a try-catch: here you'd catch all exceptions from the next // middleware that would normally result in a 404 or an 503, so it's maybe better to always let them bubble up. // But keep in mind that any uncaught exception here in your code will result in an error page. diff --git a/Services/CustomFileStore.cs b/Services/CustomFileStore.cs index d0463bee..63e0b4d2 100644 --- a/Services/CustomFileStore.cs +++ b/Services/CustomFileStore.cs @@ -14,6 +14,9 @@ public interface ICustomFileStore : IFileStore // You can add additional methods if you want. } +// Since FileSystemStore requires a base path we also need to have it. If you have a very specific absolute path +// then you don't need it to be injected but for demonstration purposes we'll inject it from Startup.cs because it +// will be in the tenant's folder. public class CustomFileStore(string fileSystemPath) : FileSystemStore(fileSystemPath), ICustomFileStore { } From 75e378bf714ee3b60e70a2f1a36ad19c506af80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Mon, 29 Jan 2024 14:48:50 +0100 Subject: [PATCH 09/20] Readding comments and removing fields. --- Middlewares/RequestLoggingMiddleware.cs | 1 + Services/DateTimeCachingService.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/Middlewares/RequestLoggingMiddleware.cs b/Middlewares/RequestLoggingMiddleware.cs index 90cb28aa..22127510 100644 --- a/Middlewares/RequestLoggingMiddleware.cs +++ b/Middlewares/RequestLoggingMiddleware.cs @@ -21,6 +21,7 @@ namespace Lombiq.TrainingDemo.Middlewares; // By the way, do you remember that we have a separate module feature declared in the Manifest for this middleware? If // not, check out Manifest.cs again! +// You need to inject a RequestDelegate instance here. public class RequestLoggingMiddleware(RequestDelegate next) { // This method is the actual middleware. Note that apart from the first parameter obligatorily being HttpContext diff --git a/Services/DateTimeCachingService.cs b/Services/DateTimeCachingService.cs index 06871424..409dc459 100644 --- a/Services/DateTimeCachingService.cs +++ b/Services/DateTimeCachingService.cs @@ -9,6 +9,18 @@ namespace Lombiq.TrainingDemo.Services; +// You've already seen the IClock service for getting the current UTC date. This service can be used to get the +// current local date based on the site settings. Also dates can be converted from or to UTC. +// IMemoryCache service is a built-in service in ASP.NET Core. Use this if you want a fast cache that's local to the +// current process. Do note that if you app runs on multiple servers this cache won't be shared among nodes. To +// learn more about IMemoryCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory. +// Dynamic Cache is implemented primarily for caching shapes. It is based on the built-in ASP.NET Core +// IDistributedCache service which by default is implemented by DistributedMemoryCache. If you just want to cache +// simple values like you'd do with IMemoryCache but in a way that also shares cache entries between servers when +// your app runs on multiple servers then use IDistributedCache directly. To learn more about distributed caching +// and IDistributedCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed. +// We're using ISignals to be able to send a signal to the memory cache to invalidate the given entry. +// Tag cache is a service for tagging cached data and invalidating cache by their tags. public class DateTimeCachingService( IMemoryCache memoryCache, ILocalClock localClock, From 750b4aed360181d3bf3991d58ffab69a49dcf007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:11:23 +0100 Subject: [PATCH 10/20] Revert "Addressing warnings." This reverts commit b8a33abeb3a40796e391a0408c79b0a89296f60e. # Conflicts: # Controllers/DisplayManagementController.cs # Controllers/FileManagementController.cs # Controllers/PersonListController.cs # Controllers/SiteSettingsController.cs # Controllers/YourFirstOrchardCoreController.cs # Drivers/DemoSettingsDisplayDriver.cs # Events/LoginGreeting.cs # Filters/ResourceFromShapeInjectingFilter.cs # Filters/ResourceInjectionFilter.cs # Filters/ShapeInjectionFilter.cs # GraphQL/Services/PersonAgeGraphQLFilter.cs # Middlewares/RequestLoggingMiddleware.cs # Services/DateTimeCachingService.cs --- Controllers/AuthorizationController.cs | 33 ++++++--- Controllers/DisplayManagementController.cs | 6 +- Controllers/FileManagementController.cs | 43 +++++++---- Controllers/PersonListController.cs | 61 ++++++++++------ Controllers/SiteSettingsController.cs | 15 +++- Controllers/YourFirstOrchardCoreController.cs | 38 +++++++--- Drivers/ColorFieldDisplayDriver.cs | 6 +- Drivers/DemoSettingsDisplayDriver.cs | 16 +++- Events/LoginGreeting.cs | 13 +++- Filters/ResourceFromShapeInjectingFilter.cs | 29 ++++++-- Filters/ResourceInjectionFilter.cs | 6 +- Filters/ShapeInjectionFilter.cs | 22 ++++-- GraphQL/Services/PersonAgeGraphQLFilter.cs | 8 +- Middlewares/RequestLoggingMiddleware.cs | 10 ++- Migrations/PersonMigrations.cs | 6 +- Navigation/DemoSettingsAdminMenu.cs | 6 +- Navigation/PersonsAdminMenu.cs | 6 +- Navigation/TrainingDemoNavigationProvider.cs | 11 ++- Services/DateTimeCachingService.cs | 73 ++++++++++++------- Services/DemoBackgroundTask.cs | 6 +- 20 files changed, 278 insertions(+), 136 deletions(-) diff --git a/Controllers/AuthorizationController.cs b/Controllers/AuthorizationController.cs index 86af0ef4..e850435c 100644 --- a/Controllers/AuthorizationController.cs +++ b/Controllers/AuthorizationController.cs @@ -20,13 +20,24 @@ namespace Lombiq.TrainingDemo.Controllers; -public class AuthorizationController( - IAuthorizationService authorizationService, - IContentManager contentManager, - INotifier notifier, - IHtmlLocalizer htmlLocalizer) : Controller +public class AuthorizationController : Controller { - private readonly IHtmlLocalizer H = htmlLocalizer; + private readonly IAuthorizationService _authorizationService; + private readonly IContentManager _contentManager; + private readonly INotifier _notifier; + private readonly IHtmlLocalizer H; + + public AuthorizationController( + IAuthorizationService authorizationService, + IContentManager contentManager, + INotifier notifier, + IHtmlLocalizer htmlLocalizer) + { + _authorizationService = authorizationService; + _contentManager = contentManager; + _notifier = notifier; + H = htmlLocalizer; + } // Here we will create a Person content item and check if the user has permission to edit it. It's very common to // check if you can view or edit a specific item - it also happens if you use the built-in URLs like @@ -34,7 +45,7 @@ public class AuthorizationController( public async Task CanEditPerson() { // Creating a content item for testing (won't be persisted). - var person = await contentManager.NewAsync(ContentTypes.PersonPage); + var person = await _contentManager.NewAsync(ContentTypes.PersonPage); // Check if the user has permission to edit the content item. When you check content-related permissions // (ViewContent, EditContent, PublishContent etc.) there is a difference between checking these for your content @@ -42,7 +53,7 @@ public async Task CanEditPerson() // ViewOwnContent, EditOwnContent, PublishOwnContent etc. permissions will be checked. This is automatic so you // don't need to use them directly. For this newly created Person item the owner is null so the EditContent // permission will be used. - if (!await authorizationService.AuthorizeAsync(User, OrchardCore.Contents.CommonPermissions.EditContent, person)) + if (!await _authorizationService.AuthorizeAsync(User, OrchardCore.Contents.CommonPermissions.EditContent, person)) { // Return 401 status code using this helper. Although it's a good practice to return 404 (NotFound()) // instead to prevent enumeration attacks. @@ -51,7 +62,7 @@ public async Task CanEditPerson() // To keep the demonstration short, only display a notification about the successful authorization and return to // the home page. - await notifier.InformationAsync(H["You are authorized to edit Person content items."]); + await _notifier.InformationAsync(H["You are authorized to edit Person content items."]); return Redirect("~/"); } @@ -63,12 +74,12 @@ public async Task CanManagePersons() // We've defined a ManagePersons earlier which is added to Administrator users by default. If the currently user // doesn't have the Administrator role then you can add it on the dashboard. Since this permission can be // checked without any object as a context the third parameter is left out. - if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) + if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) { return Unauthorized(); } - await notifier.InformationAsync(H["You are authorized to manage persons."]); + await _notifier.InformationAsync(H["You are authorized to manage persons."]); return Redirect("~/"); } diff --git a/Controllers/DisplayManagementController.cs b/Controllers/DisplayManagementController.cs index 6b80114c..3b5a084d 100644 --- a/Controllers/DisplayManagementController.cs +++ b/Controllers/DisplayManagementController.cs @@ -16,11 +16,13 @@ namespace Lombiq.TrainingDemo.Controllers; +public class DisplayManagementController : Controller, IUpdateModel // The core display management features can be used via the IDisplayManager service. The generic parameter will be // the object that needs to be displayed on the UI somehow. Don't forget to register this generic class with the // service provider (see: Startup.cs). -public class DisplayManagementController(IDisplayManager bookDisplayManager) : Controller, IUpdateModel -{ + private readonly IDisplayManager _bookDisplayManager; + + public DisplayManagementController(IDisplayManager bookDisplayManager) => _bookDisplayManager = bookDisplayManager; // Before we learn how shapes are generated using the display manager let's see what are these shapes actually. // Ad-hoc shapes can be created anywhere without the display manager. In this example we'll see how to create an // ad-hoc shape inside a view (or could be another shape). Later we'll see how to do it from a filter too. Open from diff --git a/Controllers/FileManagementController.cs b/Controllers/FileManagementController.cs index dab10063..fe46b6a1 100644 --- a/Controllers/FileManagementController.cs +++ b/Controllers/FileManagementController.cs @@ -27,18 +27,29 @@ namespace Lombiq.TrainingDemo.Controllers; -public class FileManagementController( - IMediaFileStore mediaFileStore, - INotifier notifier, - IHtmlLocalizer htmlLocalizer, - ICustomFileStore customFileStore) : Controller +public class FileManagementController : Controller { // Let's have the paths here in constants to avoid repeating ourselves. private const string TestFileRelativePath = "TrainingDemo/TestFile1.txt"; private const string UploadedFileFolderRelativePath = "TrainingDemo/Uploaded"; - private readonly IHtmlLocalizer H = htmlLocalizer; + private readonly IMediaFileStore _mediaFileStore; + private readonly INotifier _notifier; + private readonly IHtmlLocalizer H; + private readonly ICustomFileStore _customFileStore; + + public FileManagementController( + IMediaFileStore mediaFileStore, + INotifier notifier, + IHtmlLocalizer htmlLocalizer, + ICustomFileStore customFileStore) + { + _mediaFileStore = mediaFileStore; + _notifier = notifier; + _customFileStore = customFileStore; + H = htmlLocalizer; + } // This action will demonstrate how to create a file in the Media folder and read it from there. See it under // /Lombiq.TrainingDemo/FileManagement/CreateFileInMediaFolder. @@ -49,16 +60,16 @@ public async Task CreateFileInMediaFolder() using (var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hi there!"))) { // The third parameter here is optional - if true, it will override the file if already exists. - await mediaFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); + await _mediaFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); } // Use this method to check if the file exists (it will be null if the file doesn't exist). It's similar to the // built-in FileInfo class but not that robust. - var fileInfo = await mediaFileStore.GetFileInfoAsync(TestFileRelativePath); + var fileInfo = await _mediaFileStore.GetFileInfoAsync(TestFileRelativePath); // The IMediaFileStore has its own specific methods such as mapping the file path to a public URL. Since the // files in the Media folder are accessible from the outside this can be handy. - var publicUrl = mediaFileStore.MapPathToPublicUrl(TestFileRelativePath); + var publicUrl = _mediaFileStore.MapPathToPublicUrl(TestFileRelativePath); return $"Successfully created file! File size: {fileInfo.Length} bytes. Public URL: {publicUrl}"; } @@ -71,13 +82,13 @@ public async Task CreateFileInMediaFolder() public async Task ReadFileFromMediaFolder() { // This way you can check if the given file exists. - if (await mediaFileStore.GetFileInfoAsync(TestFileRelativePath) == null) + if (await _mediaFileStore.GetFileInfoAsync(TestFileRelativePath) == null) { return "Create the file first!"; } // If you want to extract the content of the file use a StreamReader to read the stream. - using var stream = await mediaFileStore.GetFileStreamAsync(TestFileRelativePath); + using var stream = await _mediaFileStore.GetFileStreamAsync(TestFileRelativePath); using var streamReader = new StreamReader(stream); var content = await streamReader.ReadToEndAsync(HttpContext.RequestAborted); @@ -96,15 +107,15 @@ public async Task UploadFileToMediaPost(IFormFile file) if (file == null) return BadRequest(); // You can use the Combine method to combine paths which is pretty much equivalent to the built-in method. - var mediaFilePath = mediaFileStore.Combine(UploadedFileFolderRelativePath, file.FileName); + var mediaFilePath = _mediaFileStore.Combine(UploadedFileFolderRelativePath, file.FileName); // In this case you already have a stream so use it to create the file. using (var stream = file.OpenReadStream()) { - await mediaFileStore.CreateFileFromStreamAsync(mediaFilePath, stream); + await _mediaFileStore.CreateFileFromStreamAsync(mediaFilePath, stream); } - await notifier.InformationAsync(H["Successfully uploaded file!"]); + await _notifier.InformationAsync(H["Successfully uploaded file!"]); return RedirectToAction(nameof(UploadFileToMedia)); } @@ -118,10 +129,10 @@ public async Task CreateFileInCustomFolder() // time. The files will be created inside our CustomFiles folder as it was defined in Startup.cs. using (var stream = new MemoryStream(Encoding.UTF8.GetBytes("Hi there in the custom file storage!"))) { - await customFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); + await _customFileStore.CreateFileFromStreamAsync(TestFileRelativePath, stream, overwrite: true); } - var fileInfo = await customFileStore.GetFileInfoAsync(TestFileRelativePath); + var fileInfo = await _customFileStore.GetFileInfoAsync(TestFileRelativePath); return $"Successfully created file in the custom file storage! File size: {fileInfo.Length} bytes."; } diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index 06486f90..29e00e35 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -26,18 +26,33 @@ namespace Lombiq.TrainingDemo.Controllers; -public class PersonListController( - ISession session, - IClock clock, - IContentItemDisplayManager contentItemDisplayManager, - IUpdateModelAccessor updateModelAccessor, - IContentManager contentManager) : Controller +public class PersonListController : Controller { + private readonly ISession _session; + private readonly IClock _clock; + private readonly IContentItemDisplayManager _contentItemDisplayManager; + private readonly IUpdateModelAccessor _updateModelAccessor; + private readonly IContentManager _contentManager; + + public PersonListController( + ISession session, + IClock clock, + IContentItemDisplayManager contentItemDisplayManager, + IUpdateModelAccessor updateModelAccessor, + IContentManager contentManager) + { + _session = session; + _clock = clock; + _contentItemDisplayManager = contentItemDisplayManager; + _updateModelAccessor = updateModelAccessor; + _contentManager = contentManager; + } + // See it under /Lombiq.TrainingDemo/PersonList/OlderThan30. public async Task OlderThan30() { - var thresholdDate = clock.UtcNow.AddYears(-30); - var people = await session + var thresholdDate = _clock.UtcNow.AddYears(-30); + var people = await _session // This will query for content items where the related PersonPartIndex.BirthDateUtc is lower than the // threshold date. Notice that there is no Where method. The Query method has an overload for that which can // be useful if you don't want to filter in multiple indexes. @@ -55,9 +70,9 @@ public async Task OlderThan30() // When you retrieve content items via ISession then you also need to run LoadAsync() on them to initialize // everything. This foremost includes running handlers, which are pretty much event handlers for content // items (you'll see them in a minute with PersonPartHandler). - await contentManager.LoadAsync(person); + await _contentManager.LoadAsync(person); - return await contentItemDisplayManager.BuildDisplayAsync(person, updateModelAccessor.ModelUpdater, "Summary"); + return await _contentItemDisplayManager.BuildDisplayAsync(person, _updateModelAccessor.ModelUpdater, "Summary"); }); // Now assuming that you've already created a few Person content items on the dashboard and some of these @@ -74,8 +89,8 @@ public async Task FountainOfEternalYouth() // Again we'll fetch content items with PersonPart but this time we'll retrieve old people and we'll make them // younger! - var thresholdDate = clock.UtcNow.AddYears(-90); - var oldPeople = (await session + var thresholdDate = _clock.UtcNow.AddYears(-90); + var oldPeople = (await _session .Query(index => index.BirthDateUtc < thresholdDate) .ListAsync()) .ToList(); @@ -83,9 +98,9 @@ public async Task FountainOfEternalYouth() foreach (var person in oldPeople) { // Have to run LoadAsync() here too. - await contentManager.LoadAsync(person); + await _contentManager.LoadAsync(person); - var eighteenYearOld = clock.UtcNow.AddYears(-18); + var eighteenYearOld = _clock.UtcNow.AddYears(-18); // Don't just overwrite the part's property directly! That'll change the index record but not the document! // Don't just do this: @@ -110,18 +125,18 @@ public async Task FountainOfEternalYouth() // Once you're done you have to save the content item explicitly. Remember when we saved Books with // ISession.Save()? This is something similar for content items. - await contentManager.UpdateAsync(person); + await _contentManager.UpdateAsync(person); // After saving the content item with UpdateAsync() you also need to publish it to make sure that even a // draftable content item gets updated. - await contentManager.PublishAsync(person); + await _contentManager.PublishAsync(person); } // If you want to manage just one content item or a couple of them that you know by ID then fetch them with // IContentManager.GetAsync() instead. return "People modified: " + - (oldPeople.Count != 0 ? + (oldPeople.Any() ? string.Join(", ", oldPeople.Select(person => person.As().Name)) : "Nobody. Did you create people older than 90?"); @@ -137,17 +152,17 @@ public async Task CreateAnAndroid() // First you need to instantiate the new content item. This just creates an object for now, nothing is yet // persisted into the database. - var person = await contentManager.NewAsync(ContentTypes.PersonPage); + var person = await _contentManager.NewAsync(ContentTypes.PersonPage); // We add some identity to our new synthetic friend. - var serialNumber = clock.UtcNow.Ticks; + var serialNumber = _clock.UtcNow.Ticks; var name = $"X Doe #{serialNumber.ToTechnicalString()}"; // Again we can save date into parts like this: person.Alter(part => { part.Name = name; - part.BirthDateUtc = clock.UtcNow; + part.BirthDateUtc = _clock.UtcNow; part.Handedness = Handedness.Right; // Actually ambidextrous. }); @@ -160,9 +175,9 @@ public async Task CreateAnAndroid() // You could also publish it right away but if you want to be safe and make sure that every part runs nicely // (like AutoroutePart, which we don't use on PersonPage, generates the permalink) it's safer to first create a // draft, then also update it, and finally publish it explicitly. - await contentManager.CreateAsync(person, VersionOptions.Draft); - await contentManager.UpdateAsync(person); - await contentManager.PublishAsync(person); + await _contentManager.CreateAsync(person, VersionOptions.Draft); + await _contentManager.UpdateAsync(person); + await _contentManager.PublishAsync(person); // You should see a new content item, check it out from the admin! diff --git a/Controllers/SiteSettingsController.cs b/Controllers/SiteSettingsController.cs index 6b2fbfa8..5221ea5f 100644 --- a/Controllers/SiteSettingsController.cs +++ b/Controllers/SiteSettingsController.cs @@ -12,15 +12,22 @@ namespace Lombiq.TrainingDemo.Controllers; -public class SiteSettingsController(ISiteService siteService, IOptionsSnapshot demoOptions) : Controller +public class SiteSettingsController : Controller { - private readonly DemoSettings _demoSettings = demoOptions.Value; + private readonly ISiteService _siteService; + private readonly DemoSettings _demoSettings; + + public SiteSettingsController(ISiteService siteService, IOptionsSnapshot demoOptions) + { + _siteService = siteService; + _demoSettings = demoOptions.Value; + } // Here's a quick simple demonstration about how to use ISiteService. Orchard Core stores basic settings that are // accessible right away in the ISite object. Here you will see how to access the site's name you gave when you set // up your website. public async Task SiteName() => - (await siteService.GetSiteSettingsAsync()).SiteName; + (await _siteService.GetSiteSettingsAsync()).SiteName; // NEXT STATION: Models/DemoSettings.cs @@ -30,7 +37,7 @@ public async Task DemoSettings() { // As mentioned the custom settings objects are serialized into the ISite object so use the .As<>() helper to // access it as you see below. - var messageFromSiteSettings = (await siteService.GetSiteSettingsAsync()).As().Message; + var messageFromSiteSettings = (await _siteService.GetSiteSettingsAsync()).As().Message; // But as you've seen in DemoSettings.cs our site settings are also Options so we can use it as such too: var messageFromOptions = _demoSettings.Message; diff --git a/Controllers/YourFirstOrchardCoreController.cs b/Controllers/YourFirstOrchardCoreController.cs index 36c79cc5..dc2cadcb 100644 --- a/Controllers/YourFirstOrchardCoreController.cs +++ b/Controllers/YourFirstOrchardCoreController.cs @@ -17,17 +17,31 @@ namespace Lombiq.TrainingDemo.Controllers; -// Orchard Core uses the built in dependency injection feature coming with ASP.NET Core. You can use the module's -// Startup class to register your own services with the service provider. To learn more see: -// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection -public class YourFirstOrchardCoreController( - INotifier notifier, - IStringLocalizer stringLocalizer, - IHtmlLocalizer htmlLocalizer, - ILogger logger) : Controller +public class YourFirstOrchardCoreController : Controller { - private readonly IStringLocalizer T = stringLocalizer; - private readonly IHtmlLocalizer H = htmlLocalizer; + private readonly INotifier _notifier; + private readonly IStringLocalizer T; + private readonly IHtmlLocalizer H; + + // You can use the non-generic counterpart of ILogger once injected just be sure to inject the generic one otherwise + // the log entries won't contain the name of the class. + private readonly ILogger _logger; + + // Orchard Core uses the built in dependency injection feature coming with ASP.NET Core. You can use the module's + // Startup class to register your own services with the service provider. To learn more see: + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection + public YourFirstOrchardCoreController( + INotifier notifier, + IStringLocalizer stringLocalizer, + IHtmlLocalizer htmlLocalizer, + ILogger logger) + { + _notifier = notifier; + _logger = logger; + + T = stringLocalizer; + H = htmlLocalizer; + } // Here's a simple action that will return some message. Nothing special here just demonstrates that this will work // in Orchard Core right after enabling the module. The route for this action will be @@ -55,12 +69,12 @@ public async Task NotifyMe() // Extension it'll provide you a handy Error Log Watcher which lights up if there's a new error! Check it out // here: // https://marketplace.visualstudio.com/items?itemName=LombiqVisualStudioExtension.LombiqOrchardVisualStudioExtension - logger.LogError("You have been notified about some error!"); + _logger.LogError("You have been notified about some error!"); // INotifier is an Orchard Core service to send messages to the user. This service can be used almost everywhere // in the code base not only in Controllers. This service requires a LocalizedHtmlString object so the // IHtmlLocalizer service needs to be used for localization. - await notifier.InformationAsync(H["Congratulations! You have been notified! Check the error log too!"]); + await _notifier.InformationAsync(H["Congratulations! You have been notified! Check the error log too!"]); return View(); diff --git a/Drivers/ColorFieldDisplayDriver.cs b/Drivers/ColorFieldDisplayDriver.cs index 59c61c63..10cd2368 100644 --- a/Drivers/ColorFieldDisplayDriver.cs +++ b/Drivers/ColorFieldDisplayDriver.cs @@ -15,9 +15,11 @@ namespace Lombiq.TrainingDemo.Drivers; // You shouldn't be surprised - content fields also have display drivers. ContentFieldDisplayDriver is specifically for // content fields. Don't forget to register this class with the service provider (see: Startup.cs). -public class ColorFieldDisplayDriver(IStringLocalizer stringLocalizer) : ContentFieldDisplayDriver +public class ColorFieldDisplayDriver : ContentFieldDisplayDriver { - private readonly IStringLocalizer T = stringLocalizer; + private readonly IStringLocalizer T; + + public ColorFieldDisplayDriver(IStringLocalizer stringLocalizer) => T = stringLocalizer; public override IDisplayResult Display(ColorField field, BuildFieldDisplayContext fieldDisplayContext) => // Same Display method for generating display shapes but this time the Initialize shape helper is being used. diff --git a/Drivers/DemoSettingsDisplayDriver.cs b/Drivers/DemoSettingsDisplayDriver.cs index 2c9246b2..b3c73b67 100644 --- a/Drivers/DemoSettingsDisplayDriver.cs +++ b/Drivers/DemoSettingsDisplayDriver.cs @@ -14,14 +14,22 @@ namespace Lombiq.TrainingDemo.Drivers; // Now this display driver abstraction is different from the one you've seen before. In Orchard Core you can connect // different objects to a master object that will be connected in the database when storing them. Site settings are // handled this way. -public class DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) - : SectionDisplayDriver +public class DemoSettingsDisplayDriver : SectionDisplayDriver { // Since technically we have only one SiteSettings we have separate the editors using editor groups. It's a good // idea to store the editor group ID in a publicly accessibly constant (would be much better to store it in a static // class placed in a Constants folder). public const string EditorGroupId = "Demo"; + private readonly IAuthorizationService _authorizationService; + private readonly IHttpContextAccessor _hca; + + public DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) + { + _authorizationService = authorizationService; + _hca = hca; + } + // Here's the EditAsync override to display editor for our site settings on the Dashboard. Note that it has a sync // version too. public override async Task EditAsync(DemoSettings section, BuildEditorContext context) @@ -73,9 +81,9 @@ private async Task IsAuthorizedToManageDemoSettingsAsync() { // Since the User object is not accessible here (as it was accessible in the Controller) we need to grab it from // the HttpContext. - var user = hca.HttpContext?.User; + var user = _hca.HttpContext?.User; - return user != null && await authorizationService.AuthorizeAsync(user, DemoSettingsPermissions.ManageDemoSettings); + return user != null && await _authorizationService.AuthorizeAsync(user, DemoSettingsPermissions.ManageDemoSettings); } } diff --git a/Events/LoginGreeting.cs b/Events/LoginGreeting.cs index 7e4ecf00..f707dd30 100644 --- a/Events/LoginGreeting.cs +++ b/Events/LoginGreeting.cs @@ -16,15 +16,22 @@ namespace Lombiq.TrainingDemo.Events; // ILoginFormEvent exposes events of the, well, login form :). Useful to display a login greeting or anything even more // useful! The rest of it is pretty standard and we just use INotifier again. -public class LoginGreeting(INotifier notifier, IHtmlLocalizer htmlLocalizer) : ILoginFormEvent +public class LoginGreeting : ILoginFormEvent { - private readonly IHtmlLocalizer H = htmlLocalizer; + private readonly INotifier _notifier; + private readonly IHtmlLocalizer H; + + public LoginGreeting(INotifier notifier, IHtmlLocalizer htmlLocalizer) + { + _notifier = notifier; + H = htmlLocalizer; + } public Task IsLockedOutAsync(IUser user) => Task.CompletedTask; public async Task LoggedInAsync(IUser user) { - await notifier.SuccessAsync(H["Hi {0}!", user.UserName]); + await _notifier.SuccessAsync(H["Hi {0}!", user.UserName]); return; } diff --git a/Filters/ResourceFromShapeInjectingFilter.cs b/Filters/ResourceFromShapeInjectingFilter.cs index 8b753bdb..89717f6d 100644 --- a/Filters/ResourceFromShapeInjectingFilter.cs +++ b/Filters/ResourceFromShapeInjectingFilter.cs @@ -6,13 +6,26 @@ namespace Lombiq.TrainingDemo.Filters; -// We've seen IResourceManager and IShapeFactory before. -// IDisplayHelper is new, however. We'll use it to execute a shape into HTML and inject that as a head script! -public class ResourceFromShapeInjectingFilter( - IResourceManager resourceManager, - IShapeFactory shapeFactory, - IDisplayHelper displayHelper) : IAsyncResultFilter +public class ResourceFromShapeInjectingFilter : IAsyncResultFilter { + // We've seen IResourceManager and IShapeFactory before. + private readonly IResourceManager _resourceManager; + + private readonly IShapeFactory _shapeFactory; + + // IDisplayHelper is new, however. We'll use it to execute a shape into HTML and inject that as a head script! + private readonly IDisplayHelper _displayHelper; + + public ResourceFromShapeInjectingFilter( + IResourceManager resourceManager, + IShapeFactory shapeFactory, + IDisplayHelper displayHelper) + { + _resourceManager = resourceManager; + _shapeFactory = shapeFactory; + _displayHelper = displayHelper; + } + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // Similar to ResourceInjectionFilter only run this if we're in a full view and the "alert" query string @@ -27,12 +40,12 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE // We use the shape factory again to instantiate the AlertScriptShape. Check out Views/AlertScriptShape.cshtml // because there's something curious there too, then come back! Next, we also use the display helper to execute // the shape and generate its HTML content. - var shapeContent = await displayHelper.ShapeExecuteAsync(await shapeFactory.New.AlertScriptShape()); + var shapeContent = await _displayHelper.ShapeExecuteAsync(await _shapeFactory.New.AlertScriptShape()); // Did you know that you can inject inline scripts with resource manager too? You can use this technique not // just like this to generate inline scripts but also e.g. to generate HTML output from shapes in background // tasks (like when sending e-mails and you want to have proper templates for them). - resourceManager.RegisterFootScript(shapeContent); + _resourceManager.RegisterFootScript(shapeContent); await next(); } diff --git a/Filters/ResourceInjectionFilter.cs b/Filters/ResourceInjectionFilter.cs index 49747b8c..b3f4397e 100644 --- a/Filters/ResourceInjectionFilter.cs +++ b/Filters/ResourceInjectionFilter.cs @@ -6,9 +6,11 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. -// To register resources you can use the IResourceManager service. -public class ResourceInjectionFilter(IResourceManager resourceManager) : IAsyncResultFilter +public class ResourceInjectionFilter : IAsyncResultFilter { + private readonly IResourceManager _resourceManager; + + public ResourceInjectionFilter(IResourceManager resourceManager) => _resourceManager = resourceManager; public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // Let's decide when the filter should be executed. It wouldn't make sense to inject resources if this is a diff --git a/Filters/ShapeInjectionFilter.cs b/Filters/ShapeInjectionFilter.cs index cbb7a284..a0741a7d 100644 --- a/Filters/ShapeInjectionFilter.cs +++ b/Filters/ShapeInjectionFilter.cs @@ -22,11 +22,21 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. -// To access the layout which contains the zones you need to use the ILayoutAccessor service. -// To generate ad-hoc shapes the IShapeFactory can be used. This is the same which is behind the New property in -// templates that you have previously seen in AdHocShape.cshtml. -public class ShapeInjectionFilter(ILayoutAccessor layoutAccessor, IShapeFactory shapeFactory) : IAsyncResultFilter +public class ShapeInjectionFilter : IAsyncResultFilter { + // To access the layout which contains the zones you need to use the ILayoutAccessor service. + private readonly ILayoutAccessor _layoutAccessor; + + // To generate ad-hoc shapes the IShapeFactory can be used. This is the same which is behind the New property in + // templates that you have previously seen in AdHocShape.cshtml. + private readonly IShapeFactory _shapeFactory; + + public ShapeInjectionFilter(ILayoutAccessor layoutAccessor, IShapeFactory shapeFactory) + { + _layoutAccessor = layoutAccessor; + _shapeFactory = shapeFactory; + } + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // You can decide when the filter should be executed here. If this is not a ViewResult or PageResult the shape @@ -39,7 +49,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE } // We first retrieve the layout. - var layout = await layoutAccessor.GetLayoutAsync(); + var layout = await _layoutAccessor.GetLayoutAsync(); // The Layout object will contain a Zones dictionary that you can use to access a zone. The Content zone is // usually available in all themes and is the main zone in the middle of each page. @@ -47,7 +57,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE // Here you can add an ad-hoc generated shape to the Content zone. This works in the same way as we've seen // previously when we talked about display management. You can find the template that'll render this shape under // Views/InjectedShape.cshtml. - await contentZone.AddAsync(await shapeFactory.CreateAsync("InjectedShape")); + await contentZone.AddAsync(await _shapeFactory.CreateAsync("InjectedShape")); await next(); } diff --git a/GraphQL/Services/PersonAgeGraphQLFilter.cs b/GraphQL/Services/PersonAgeGraphQLFilter.cs index 6c5bfda5..df92df3f 100644 --- a/GraphQL/Services/PersonAgeGraphQLFilter.cs +++ b/GraphQL/Services/PersonAgeGraphQLFilter.cs @@ -14,8 +14,12 @@ namespace Lombiq.TrainingDemo.GraphQL.Services; // IGraphQLFilters can append conditions to the YesSql query, alter its result, or do both. -public class PersonAgeGraphQLFilter(IClock clock) : IGraphQLFilter +public class PersonAgeGraphQLFilter : IGraphQLFilter { + private readonly IClock _clock; + + public PersonAgeGraphQLFilter(IClock clock) => _clock = clock; + // While you can use this to execute some complex YesSql query it's best to stick with the IIndexAliasProvider // approach for such things. public Task> PreQueryAsync(IQuery query, IResolveFieldContext context) => @@ -33,7 +37,7 @@ public Task> PostQueryAsync( if (name != null && value.Value is int age) { - var now = clock.UtcNow; + var now = _clock.UtcNow; if (name == "age") name = "age_eq"; var filterType = name[^2..]; // The name operator like gt, le, etc. diff --git a/Middlewares/RequestLoggingMiddleware.cs b/Middlewares/RequestLoggingMiddleware.cs index 22127510..3b162a0c 100644 --- a/Middlewares/RequestLoggingMiddleware.cs +++ b/Middlewares/RequestLoggingMiddleware.cs @@ -21,9 +21,13 @@ namespace Lombiq.TrainingDemo.Middlewares; // By the way, do you remember that we have a separate module feature declared in the Manifest for this middleware? If // not, check out Manifest.cs again! -// You need to inject a RequestDelegate instance here. -public class RequestLoggingMiddleware(RequestDelegate next) +public class RequestLoggingMiddleware { + private readonly RequestDelegate _next; + + // You need to inject a RequestDelegate instance here. + public RequestLoggingMiddleware(RequestDelegate next) => _next = next; + // This method is the actual middleware. Note that apart from the first parameter obligatorily being HttpContext // further parameters can be injected Orchard services. public async Task InvokeAsync( @@ -33,7 +37,7 @@ public async Task InvokeAsync( { // We let the next middleware run, but this is not mandatory: if this middleware would return a cached page for // example then we would write the cached response to the HttpContext and leave this out. - await next(context); + await _next(context); // Think twice when wrapping this call into a try-catch: here you'd catch all exceptions from the next // middleware that would normally result in a 404 or an 503, so it's maybe better to always let them bubble up. // But keep in mind that any uncaught exception here in your code will result in an error page. diff --git a/Migrations/PersonMigrations.cs b/Migrations/PersonMigrations.cs index fef471f8..d3806bfc 100644 --- a/Migrations/PersonMigrations.cs +++ b/Migrations/PersonMigrations.cs @@ -17,9 +17,11 @@ namespace Lombiq.TrainingDemo.Migrations; // and configure content parts. Don't forget to register this class with the service provider (see Startup.cs). You can // also generate such migration steps with the Code Generation feature of our Helpful Extensions module, check it out // here: https://github.com/Lombiq/Helpful-Extensions -public class PersonMigrations(IContentDefinitionManager contentDefinitionManager) : DataMigration +public class PersonMigrations : DataMigration { - private readonly IContentDefinitionManager _contentDefinitionManager = contentDefinitionManager; + private readonly IContentDefinitionManager _contentDefinitionManager; + + public PersonMigrations(IContentDefinitionManager contentDefinitionManager) => _contentDefinitionManager = contentDefinitionManager; public async Task CreateAsync() { diff --git a/Navigation/DemoSettingsAdminMenu.cs b/Navigation/DemoSettingsAdminMenu.cs index 5edb5db9..3b9b35d3 100644 --- a/Navigation/DemoSettingsAdminMenu.cs +++ b/Navigation/DemoSettingsAdminMenu.cs @@ -8,9 +8,11 @@ namespace Lombiq.TrainingDemo.Navigation; // To actually see the menu item on the admin menu we need to add a navigation provider to it. -public class DemoSettingsAdminMenu(IStringLocalizer stringLocalizer) : INavigationProvider +public class DemoSettingsAdminMenu : INavigationProvider { - private readonly IStringLocalizer T = stringLocalizer; + private readonly IStringLocalizer T; + + public DemoSettingsAdminMenu(IStringLocalizer stringLocalizer) => T = stringLocalizer; public Task BuildNavigationAsync(string name, NavigationBuilder builder) { diff --git a/Navigation/PersonsAdminMenu.cs b/Navigation/PersonsAdminMenu.cs index 39399a4d..fc3e675c 100644 --- a/Navigation/PersonsAdminMenu.cs +++ b/Navigation/PersonsAdminMenu.cs @@ -10,9 +10,11 @@ namespace Lombiq.TrainingDemo.Navigation; // INavigationProvider is used for building different kind of navigations (not just admin menus). Don't forget to // register this class with the service provider (see: Startup.cs). -public class PersonsAdminMenu(IStringLocalizer stringLocalizer) : INavigationProvider +public class PersonsAdminMenu : INavigationProvider { - private readonly IStringLocalizer T = stringLocalizer; + private readonly IStringLocalizer T; + + public PersonsAdminMenu(IStringLocalizer stringLocalizer) => T = stringLocalizer; public Task BuildNavigationAsync(string name, NavigationBuilder builder) { diff --git a/Navigation/TrainingDemoNavigationProvider.cs b/Navigation/TrainingDemoNavigationProvider.cs index d77da237..cefe3e97 100644 --- a/Navigation/TrainingDemoNavigationProvider.cs +++ b/Navigation/TrainingDemoNavigationProvider.cs @@ -17,10 +17,15 @@ namespace Lombiq.TrainingDemo.Navigation; // // For details on how to use them, see the Lombiq.BaseTheme.Samples project: // https://github.com/Lombiq/Orchard-Base-Theme/tree/issue/OSOE-62/Lombiq.BaseTheme.Samples -public class TrainingDemoNavigationProvider( - IHttpContextAccessor hca, - IStringLocalizer stringLocalizer) : MainMenuNavigationProviderBase(hca, stringLocalizer) +public class TrainingDemoNavigationProvider : MainMenuNavigationProviderBase { + public TrainingDemoNavigationProvider( + IHttpContextAccessor hca, + IStringLocalizer stringLocalizer) + : base(hca, stringLocalizer) + { + } + protected override void Build(NavigationBuilder builder) { var context = _hca.HttpContext; diff --git a/Services/DateTimeCachingService.cs b/Services/DateTimeCachingService.cs index 409dc459..b448b005 100644 --- a/Services/DateTimeCachingService.cs +++ b/Services/DateTimeCachingService.cs @@ -9,37 +9,56 @@ namespace Lombiq.TrainingDemo.Services; -// You've already seen the IClock service for getting the current UTC date. This service can be used to get the -// current local date based on the site settings. Also dates can be converted from or to UTC. -// IMemoryCache service is a built-in service in ASP.NET Core. Use this if you want a fast cache that's local to the -// current process. Do note that if you app runs on multiple servers this cache won't be shared among nodes. To -// learn more about IMemoryCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory. -// Dynamic Cache is implemented primarily for caching shapes. It is based on the built-in ASP.NET Core -// IDistributedCache service which by default is implemented by DistributedMemoryCache. If you just want to cache -// simple values like you'd do with IMemoryCache but in a way that also shares cache entries between servers when -// your app runs on multiple servers then use IDistributedCache directly. To learn more about distributed caching -// and IDistributedCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed. -// We're using ISignals to be able to send a signal to the memory cache to invalidate the given entry. -// Tag cache is a service for tagging cached data and invalidating cache by their tags. -public class DateTimeCachingService( - IMemoryCache memoryCache, - ILocalClock localClock, - IDynamicCacheService dynamicCacheService, - ITagCache tagCache, - ISignal signal) : IDateTimeCachingService +public class DateTimeCachingService : IDateTimeCachingService { public const string MemoryCacheKey = "Lombiq.TrainingDemo.MemoryCache.DateTime"; public const string DynamicCacheKey = "Lombiq.TrainingDemo.DynamicCache.DateTime"; public const string DynamicCacheTag = "Lombiq.TrainingDemo.DynamicCache.DateTime.Tag"; + // You've already seen the IClock service for getting the current UTC date. This service can be used to get the + // current local date based on the site settings. Also dates can be converted from or to UTC. + private readonly ILocalClock _localClock; + + // IMemoryCache service is a built-in service in ASP.NET Core. Use this if you want a fast cache that's local to the + // current process. Do note that if you app runs on multiple servers this cache won't be shared among nodes. To + // learn more about IMemoryCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory. + private readonly IMemoryCache _memoryCache; + + // Dynamic Cache is implemented primarily for caching shapes. It is based on the built-in ASP.NET Core + // IDistributedCache service which by default is implemented by DistributedMemoryCache. If you just want to cache + // simple values like you'd do with IMemoryCache but in a way that also shares cache entries between servers when + // your app runs on multiple servers then use IDistributedCache directly. To learn more about distributed caching + // and IDistributedCache visit https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed. + private readonly IDynamicCacheService _dynamicCacheService; + + // We're using ISignals to be able to send a signal to the memory cache to invalidate the given entry. + private readonly ISignal _signal; + + // Tag cache is a service for tagging cached data and invalidating cache by their tags. + private readonly ITagCache _tagCache; + + public DateTimeCachingService( + IMemoryCache memoryCache, + ILocalClock localClock, + IDynamicCacheService dynamicCacheService, + ITagCache tagCache, + ISignal signal) + { + _memoryCache = memoryCache; + _localClock = localClock; + _dynamicCacheService = dynamicCacheService; + _tagCache = tagCache; + _signal = signal; + } + // This method will get or create the cached DateTime object using the IMemoryCache. public async Task GetMemoryCachedDateTimeAsync() { - if (!memoryCache.TryGetValue(MemoryCacheKey, out DateTime cachedDate)) + if (!_memoryCache.TryGetValue(MemoryCacheKey, out DateTime cachedDate)) { - cachedDate = (await localClock.LocalNowAsync).DateTime; + cachedDate = (await _localClock.LocalNowAsync).DateTime; - memoryCache.Set(MemoryCacheKey, cachedDate, GetMemoryCacheChangeToken()); + _memoryCache.Set(MemoryCacheKey, cachedDate, GetMemoryCacheChangeToken()); } return cachedDate; @@ -72,33 +91,33 @@ public async Task InvalidateCachedDateTimeAsync() { // As mentioned ISignal service is used to invalidate the memory cache. This will invalidate all cache entries // if there are multiple ones related to the token. - await signal.SignalTokenAsync(MemoryCacheKey); + await _signal.SignalTokenAsync(MemoryCacheKey); // ITagCache.RemoveTagAsync will invalidate all the dynamic caches which are tagged with the given tag. - await tagCache.RemoveTagAsync(DynamicCacheTag); + await _tagCache.RemoveTagAsync(DynamicCacheTag); } // This change token is generated based on the cache key using the ISignal service. It is used to invalidate the // memory cache. You can use this not just as another way to invalidate specific entries but also a way to // invalidate many at the same time: You can use tie multiple cache entries to the same signal too. - private IChangeToken GetMemoryCacheChangeToken() => signal.GetToken(MemoryCacheKey); + private IChangeToken GetMemoryCacheChangeToken() => _signal.GetToken(MemoryCacheKey); private async Task GetOrCreateDynamicCachedDateTimeAsync(CacheContext cacheContext) { // Now that we have a cache context we try to acquire the object. The objects always need to be strings. - var cachedDateTimeText = await dynamicCacheService.GetCachedValueAsync(cacheContext); + var cachedDateTimeText = await _dynamicCacheService.GetCachedValueAsync(cacheContext); // If the date time text is not null then parse it to DateTime otherwise use the ILocalClock service to set it // to the current date. var cachedDateTime = cachedDateTimeText != null ? DateTime.Parse(cachedDateTimeText, CultureInfo.InvariantCulture) : - (await localClock.LocalNowAsync).DateTime; + (await _localClock.LocalNowAsync).DateTime; // If the date time text is null (meaning it wasn't cached) cache the DateTime object (which in this case is the // current date). if (cachedDateTimeText == null) { - await dynamicCacheService.SetCachedValueAsync( + await _dynamicCacheService.SetCachedValueAsync( cacheContext, cachedDateTime.ToString(CultureInfo.InvariantCulture)); } diff --git a/Services/DemoBackgroundTask.cs b/Services/DemoBackgroundTask.cs index f30abfc8..232e0e71 100644 --- a/Services/DemoBackgroundTask.cs +++ b/Services/DemoBackgroundTask.cs @@ -27,17 +27,19 @@ namespace Lombiq.TrainingDemo.Services; // Tasks admin page. Also you can set Enabled to false if you don't want it start right after application start. These // settings can be updated entirely on the Background Tasks admin page. [BackgroundTask(Schedule = "*/2 * * * *", Description = "Demo background task that runs every 2 minutes.")] -public class DemoBackgroundTask(ILogger logger) : IBackgroundTask +public class DemoBackgroundTask : IBackgroundTask { // Setting a maximum time this background task will be executed. private const int MaxCount = 5; - private readonly ILogger _logger = logger; + private readonly ILogger _logger; // Storing execution times in a private field. Since background tasks are singleton objects this will keep its value // while the application runs. private int _count; + public DemoBackgroundTask(ILogger logger) => _logger = logger; + // Since background tasks are singletons we'll need this IServiceProvider instance to resolve every non-singleton // service. When in doubt, just use this IServiceProvider instance to resolve everything instead of injecting a // service via the constructor. From 9cc4af5086059db146559c89729d83c7b62716fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:11:49 +0100 Subject: [PATCH 11/20] Revert "Addressing warnings." This reverts commit 804be1b4da7e1acae1fc5bfc3e4582c0e4f04b48. # Conflicts: # Controllers/CacheController.cs # Controllers/CrossTenantServicesController.cs # Drivers/DemoSettingsDisplayDriver.cs # Services/CustomFileStore.cs --- .../ManagePersonsPermissionCheckerTask.cs | 31 +++++++++++----- Controllers/AdminController.cs | 33 ++++++++++++----- Controllers/ApiController.cs | 15 ++++++-- Controllers/CacheController.cs | 16 +++++--- Controllers/CrossTenantServicesController.cs | 12 ++++-- Controllers/DatabaseStorageController.cs | 37 +++++++++++++------ Drivers/DemoSettingsDisplayDriver.cs | 12 ++---- Services/CustomFileStore.cs | 12 ++++-- Services/DemoSettingsConfiguration.cs | 8 +++- Services/TestedService.cs | 8 +++- Startup.cs | 8 +++- 11 files changed, 128 insertions(+), 64 deletions(-) diff --git a/Activities/ManagePersonsPermissionCheckerTask.cs b/Activities/ManagePersonsPermissionCheckerTask.cs index 5c81dbca..49f28aa6 100644 --- a/Activities/ManagePersonsPermissionCheckerTask.cs +++ b/Activities/ManagePersonsPermissionCheckerTask.cs @@ -14,13 +14,24 @@ namespace Lombiq.TrainingDemo.Activities; // A simple workflow task that accepts a username as a TextField input and checks whether the user has ManagePersons // Permission or not. -public class ManagePersonsPermissionCheckerTask( - IAuthorizationService authorizationService, - IUserService userService, - IWorkflowExpressionEvaluator expressionEvaluator, - IStringLocalizer localizer) : TaskActivity +public class ManagePersonsPermissionCheckerTask : TaskActivity { - private readonly IStringLocalizer S = localizer; + private readonly IAuthorizationService _authorizationService; + private readonly IUserService _userService; + private readonly IWorkflowExpressionEvaluator _expressionEvaluator; + private readonly IStringLocalizer S; + + public ManagePersonsPermissionCheckerTask( + IAuthorizationService authorizationService, + IUserService userService, + IWorkflowExpressionEvaluator expressionEvaluator, + IStringLocalizer localizer) + { + _authorizationService = authorizationService; + _userService = userService; + _expressionEvaluator = expressionEvaluator; + S = localizer; + } // The technical name of the activity. Activities in a workflow definition reference this name. public override string Name => nameof(ManagePersonsPermissionCheckerTask); @@ -49,14 +60,14 @@ public override async Task ExecuteAsync( WorkflowExecutionContext workflowContext, ActivityContext activityContext) { - var userName = await expressionEvaluator.EvaluateAsync(UserName, workflowContext, encoder: null); - var user = (User)await userService.GetUserAsync(userName); + var userName = await _expressionEvaluator.EvaluateAsync(UserName, workflowContext, encoder: null); + var user = (User)await _userService.GetUserAsync(userName); if (user != null) { - var userClaim = await userService.CreatePrincipalAsync(user); + var userClaim = await _userService.CreatePrincipalAsync(user); - if (await authorizationService.AuthorizeAsync(userClaim, PersonPermissions.ManagePersons)) + if (await _authorizationService.AuthorizeAsync(userClaim, PersonPermissions.ManagePersons)) { return Outcomes("HasPermission"); } diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs index 255e24ab..5744dd91 100644 --- a/Controllers/AdminController.cs +++ b/Controllers/AdminController.cs @@ -19,12 +19,25 @@ namespace Lombiq.TrainingDemo.Controllers; // If you have multiple admin controllers then name them whatever you want but put an [Admin] attribute on them. -public class AdminController( - IContentItemDisplayManager contentItemDisplayManager, - ISession session, - IAuthorizationService authorizationService, - IUpdateModelAccessor updateModelAccessor) : Controller +public class AdminController : Controller { + private readonly IContentItemDisplayManager _contentItemDisplayManager; + private readonly ISession _session; + private readonly IAuthorizationService _authorizationService; + private readonly IUpdateModelAccessor _updateModelAccessor; + + public AdminController( + IContentItemDisplayManager contentItemDisplayManager, + ISession session, + IAuthorizationService authorizationService, + IUpdateModelAccessor updateModelAccessor) + { + _contentItemDisplayManager = contentItemDisplayManager; + _session = session; + _authorizationService = authorizationService; + _updateModelAccessor = updateModelAccessor; + } + // Let's see how it will be displayed, just type the default URL (/Lombiq.TrainingDemo/Admin/Index) into the browser // with an administrator account (or at least a user who has a role that has AccessAdmin permission). If you are // anonymous then a login page will automatically appear. The permission check (i.e. has AccessAdmin @@ -38,13 +51,13 @@ public async Task PersonListNewest() { // If the user needs to have a specific permission to access a page on the admin panel (besides the AccessAdmin // permission) you need to check it here. - if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) + if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) { return Unauthorized(); } // Nothing special here just display the last 10 Person Page content items. - var persons = await session + var persons = await _session .Query() .Where(index => index.ContentType == ContentTypes.PersonPage) .OrderByDescending(index => index.CreatedUtc) @@ -57,13 +70,13 @@ public async Task PersonListNewest() public async Task PersonListOldest() { - if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) + if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.AccessPersonListDashboard)) { return Unauthorized(); } // Display the first 10 Person Page content items. - var persons = await session + var persons = await _session .Query() .Where(index => index.ContentType == ContentTypes.PersonPage) .OrderBy(index => index.CreatedUtc) @@ -77,7 +90,7 @@ private async Task> GetShapesAsync(IEnumerable // Notice the "SummaryAdmin" display type which is a built in display type specifically for listing items on the // dashboard. await persons.AwaitEachAsync(async person => - await contentItemDisplayManager.BuildDisplayAsync(person, updateModelAccessor.ModelUpdater, "SummaryAdmin")); + await _contentItemDisplayManager.BuildDisplayAsync(person, _updateModelAccessor.ModelUpdater, "SummaryAdmin")); } // NEXT STATION: Navigation/TrainingDemoNavigationProvider.cs diff --git a/Controllers/ApiController.cs b/Controllers/ApiController.cs index 93668af4..da0f3a5e 100644 --- a/Controllers/ApiController.cs +++ b/Controllers/ApiController.cs @@ -29,8 +29,17 @@ namespace Lombiq.TrainingDemo.Controllers; // endpoints should most of the time use the "Api" authentication scheme: This is not the same that standard users are // authenticated with (via cookies). [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] -public class ApiController(IAuthorizationService authorizationService, IContentManager contentManager) : Controller +public class ApiController : Controller { + private readonly IAuthorizationService _authorizationService; + private readonly IContentManager _contentManager; + + public ApiController(IAuthorizationService authorizationService, IContentManager contentManager) + { + _authorizationService = authorizationService; + _contentManager = contentManager; + } + // You can look up the ID of a Person Page that you've created previously (when you open one from the admin content // item list the URL will contain it as /Admin/Contents/ContentItems/) and use it to access this // action under /api/Lombiq.TrainingDemo?contentItemId=. Note though that you'll only be able to @@ -43,13 +52,13 @@ public async Task Get(string contentItemId) // permission here. To authenticate with the API you can use any ASP.NET Core authentication scheme but Orchard // offers various OpenID-based options. If you just want to quickly check out the API then grant the permission // for the Anonymous role on the admin. - if (!await authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) + if (!await _authorizationService.AuthorizeAsync(User, PersonPermissions.ManagePersons)) { return this.ChallengeOrForbid("Api"); } // Just the usual stuff again. - var contentItem = await contentManager.GetAsync(contentItemId); + var contentItem = await _contentManager.GetAsync(contentItemId); // Only allow the retrieval of Person Page items. if (contentItem?.ContentType != ContentTypes.PersonPage) contentItem = null; diff --git a/Controllers/CacheController.cs b/Controllers/CacheController.cs index c20c2895..88e165d2 100644 --- a/Controllers/CacheController.cs +++ b/Controllers/CacheController.cs @@ -15,23 +15,27 @@ namespace Lombiq.TrainingDemo.Controllers; -// The actual caching is implemented in a service which we'll soon investigate. -public class CacheController(IDateTimeCachingService dateTimeCachingService) : Controller +public class CacheController : Controller { + // The actual caching is implemented in a service which we'll soon investigate. + private readonly IDateTimeCachingService _dateTimeCachingService; + + public CacheController(IDateTimeCachingService dateTimeCachingService) => _dateTimeCachingService = dateTimeCachingService; + // In this action we'll cache a DateTime three different ways. You can open it under // /Lombiq.TrainingDemo/Cache/Index public async Task Index() { // This one will be cached using the built-in ASP.NET Core IMemoryCache. - var memoryCachedDateTime = await dateTimeCachingService.GetMemoryCachedDateTimeAsync(); + var memoryCachedDateTime = await _dateTimeCachingService.GetMemoryCachedDateTimeAsync(); // This one will be using the DynamicCache provided by Orchard Core. It will have a 30 second expiration. var dynamicCachedDateTimeWith30SecondsExpiry = - await dateTimeCachingService.GetDynamicCachedDateTimeWith30SecondsExpiryAsync(); + await _dateTimeCachingService.GetDynamicCachedDateTimeWith30SecondsExpiryAsync(); // Finally this date will be cached only for this route. var dynamicCachedDateTimeVariedByRoutes = - await dateTimeCachingService.GetDynamicCachedDateTimeVariedByRoutesAsync(); + await _dateTimeCachingService.GetDynamicCachedDateTimeVariedByRoutesAsync(); // NEXT STATION: Services/DateTimeCachingService.cs @@ -53,7 +57,7 @@ public Task DifferentRoute() => // /Lombiq.TrainingDemo/Cache/InvalidateDateTimeCache public async Task InvalidateDateTimeCache() { - await dateTimeCachingService.InvalidateCachedDateTimeAsync(); + await _dateTimeCachingService.InvalidateCachedDateTimeAsync(); return RedirectToAction("Index"); } diff --git a/Controllers/CrossTenantServicesController.cs b/Controllers/CrossTenantServicesController.cs index f0308c12..79fbd419 100644 --- a/Controllers/CrossTenantServicesController.cs +++ b/Controllers/CrossTenantServicesController.cs @@ -24,10 +24,14 @@ namespace Lombiq.TrainingDemo.Controllers; // This is a controller just for the sake of easy demonstration, you can do the same thing anywhere. In the Index // action, we'll fetch content items from another tenant with the IContentManager service that you already know. This is // just an example though, really you can access any other service as well. -// We'll need IShellHost to access services from a currently running shell's dependency injection container (Service -// Provider). -public class CrossTenantServicesController(IShellHost shellHost) : Controller +public class CrossTenantServicesController : Controller { + private readonly IShellHost _shellHost; + + // We'll need IShellHost to access services from a currently running shell's dependency injection container (Service + // Provider). + public CrossTenantServicesController(IShellHost shellHost) => _shellHost = shellHost; + // A simple route for convenience. You can access this from under /CrossTenantServices?contentItemId=ID. Here ID // needs to be a content item ID that you can get e.g. from the URL when you open an item to edit from the admin (it // looks something like "4da2sme18cc2k2r5d4w23d4cwj" which is NOT made by a cat walking across the keyboard!). @@ -44,7 +48,7 @@ public async Task Index(string contentItemId) // First you have to retrieve the tenant's shell scope that contains the shell's Service Provider. Note that // there is also an IShellSettingsManager service that you can use to access the just shell settings for all // tenants (shell settings are a tenant's basic settings, like its technical name and its URL). - var shellScope = await shellHost.GetScopeAsync("Default"); + var shellScope = await _shellHost.GetScopeAsync("Default"); // We'll just return the title of the content item from this action but you can do anything else with the item // too, like displaying it. diff --git a/Controllers/DatabaseStorageController.cs b/Controllers/DatabaseStorageController.cs index c897bb4b..e1d5a8cf 100644 --- a/Controllers/DatabaseStorageController.cs +++ b/Controllers/DatabaseStorageController.cs @@ -24,14 +24,27 @@ namespace Lombiq.TrainingDemo.Controllers; -public class DatabaseStorageController( - ISession session, - IDisplayManager bookDisplayManager, - INotifier notifier, - IHtmlLocalizer htmlLocalizer, - IUpdateModelAccessor updateModelAccessor) : Controller +public class DatabaseStorageController : Controller { - private readonly IHtmlLocalizer H = htmlLocalizer; + private readonly ISession _session; + private readonly IDisplayManager _bookDisplayManager; + private readonly INotifier _notifier; + private readonly IHtmlLocalizer H; + private readonly IUpdateModelAccessor _updateModelAccessor; + + public DatabaseStorageController( + ISession session, + IDisplayManager bookDisplayManager, + INotifier notifier, + IHtmlLocalizer htmlLocalizer, + IUpdateModelAccessor updateModelAccessor) + { + _session = session; + _bookDisplayManager = bookDisplayManager; + _notifier = notifier; + _updateModelAccessor = updateModelAccessor; + H = htmlLocalizer; + } // A page with a button that will call the CreateBooks POST action. See it under // /Lombiq.TrainingDemo/DatabaseStorage/CreateBooks. @@ -52,10 +65,10 @@ public async Task CreateBooksPost() foreach (var book in CreateDemoBooks()) { // So now you understand what will happen in the background when this service is being called. - await session.SaveAsync(book); + await _session.SaveAsync(book); } - await notifier.InformationAsync(H["Books have been created in the database."]); + await _notifier.InformationAsync(H["Books have been created in the database."]); return RedirectToAction(nameof(CreateBooks)); } @@ -65,7 +78,7 @@ public async Task CreateBooksPost() public async Task JKRosenzweigBooks() { // ISession service is used for querying items. - var jkRosenzweigBooks = await session + var jkRosenzweigBooks = await _session // First, we define what object (document) we want to query and what index should be used for filtering. .Query() // In the .Where() method you can describe a lambda where the object will be the index object. @@ -79,7 +92,7 @@ public async Task JKRosenzweigBooks() var bookShapes = await jkRosenzweigBooks.AwaitEachAsync(async book => // We'll need to pass an IUpdateModel (used for model validation) to the method, which we can access via its // accessor service. Later you'll also see how we'll use this to run validations in drivers. - await bookDisplayManager.BuildDisplayAsync(book, updateModelAccessor.ModelUpdater)); + await _bookDisplayManager.BuildDisplayAsync(book, _updateModelAccessor.ModelUpdater)); // You can check out Views/DatabaseStorage/JKRosenzweigBooks.cshtml and come back here. return View(bookShapes); @@ -89,7 +102,7 @@ public async Task JKRosenzweigBooks() // NEXT STATION: Models/PersonPart.cs - private static Book[] CreateDemoBooks() => + private static IEnumerable CreateDemoBooks() => new[] { new Book diff --git a/Drivers/DemoSettingsDisplayDriver.cs b/Drivers/DemoSettingsDisplayDriver.cs index b3c73b67..36f1b796 100644 --- a/Drivers/DemoSettingsDisplayDriver.cs +++ b/Drivers/DemoSettingsDisplayDriver.cs @@ -14,21 +14,15 @@ namespace Lombiq.TrainingDemo.Drivers; // Now this display driver abstraction is different from the one you've seen before. In Orchard Core you can connect // different objects to a master object that will be connected in the database when storing them. Site settings are // handled this way. -public class DemoSettingsDisplayDriver : SectionDisplayDriver +public class DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) : SectionDisplayDriver { // Since technically we have only one SiteSettings we have separate the editors using editor groups. It's a good // idea to store the editor group ID in a publicly accessibly constant (would be much better to store it in a static // class placed in a Constants folder). public const string EditorGroupId = "Demo"; - private readonly IAuthorizationService _authorizationService; - private readonly IHttpContextAccessor _hca; - - public DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) - { - _authorizationService = authorizationService; - _hca = hca; - } + private readonly IAuthorizationService _authorizationService = authorizationService; + private readonly IHttpContextAccessor _hca = hca; // Here's the EditAsync override to display editor for our site settings on the Dashboard. Note that it has a sync // version too. diff --git a/Services/CustomFileStore.cs b/Services/CustomFileStore.cs index 63e0b4d2..12cf20e8 100644 --- a/Services/CustomFileStore.cs +++ b/Services/CustomFileStore.cs @@ -14,11 +14,15 @@ public interface ICustomFileStore : IFileStore // You can add additional methods if you want. } -// Since FileSystemStore requires a base path we also need to have it. If you have a very specific absolute path -// then you don't need it to be injected but for demonstration purposes we'll inject it from Startup.cs because it -// will be in the tenant's folder. -public class CustomFileStore(string fileSystemPath) : FileSystemStore(fileSystemPath), ICustomFileStore +public class CustomFileStore : FileSystemStore, ICustomFileStore { + // Since FileSystemStore requires a base path we also need to have it. If you have a very specific absolute path + // then you don't need it to be injected but for demonstration purposes we'll inject it from Startup.cs because it + // will be in the tenant's folder. + public CustomFileStore(string fileSystemPath) + : base(fileSystemPath) + { + } } // NEXT STATION: Startup.cs and find the File System comment line in the ConfigureServices method. diff --git a/Services/DemoSettingsConfiguration.cs b/Services/DemoSettingsConfiguration.cs index 23578f50..81276f21 100644 --- a/Services/DemoSettingsConfiguration.cs +++ b/Services/DemoSettingsConfiguration.cs @@ -5,8 +5,12 @@ namespace Lombiq.TrainingDemo.Services; // This is a configuration class that'll load the options from site settings. -public class DemoSettingsConfiguration(ISiteService siteService) : IConfigureOptions +public class DemoSettingsConfiguration : IConfigureOptions { + private readonly ISiteService _siteService; + + public DemoSettingsConfiguration(ISiteService siteService) => _siteService = siteService; + public void Configure(DemoSettings options) { // The method parameter comes from the other configuration options, like the appsettings.json file if it's set, @@ -34,7 +38,7 @@ public void Configure(DemoSettings options) // out what we have there related to settings and come back! // Unfortunately, no async here so we need to run this synchronously. - var settings = siteService.GetSiteSettingsAsync() + var settings = _siteService.GetSiteSettingsAsync() .GetAwaiter().GetResult() .As(); diff --git a/Services/TestedService.cs b/Services/TestedService.cs index 534075bb..1e817741 100644 --- a/Services/TestedService.cs +++ b/Services/TestedService.cs @@ -31,8 +31,12 @@ public interface ITestedService } // The implementation of the service follows. -public class TestedService(IContentManager contentManager) : ITestedService +public class TestedService : ITestedService { + private readonly IContentManager _contentManager; + + public TestedService(IContentManager contentManager) => _contentManager = contentManager; + public Task GetContentItemOrThrowAsync(string id) { // As you can see we rigorously check the input. Something we'll surely need to test later! @@ -47,7 +51,7 @@ public Task GetContentItemOrThrowAsync(string id) private async Task GetContentItemOrThrowInternalAsync(string id) => // You already know how this works :). - await contentManager.GetAsync(id) + await _contentManager.GetAsync(id) ?? throw new InvalidOperationException($"The content item with the ID {id} doesn't exist."); } diff --git a/Startup.cs b/Startup.cs index 4873fba4..145983d5 100644 --- a/Startup.cs +++ b/Startup.cs @@ -54,8 +54,12 @@ namespace Lombiq.TrainingDemo; // While the startup class doesn't need to derive from StartupBase and can just use conventionally named methods it's a // bit less of a magic this way, and code analysis won't tell us to make it static. -public class Startup(IShellConfiguration shellConfiguration) : StartupBase +public class Startup : StartupBase { + private readonly IShellConfiguration _shellConfiguration; + + public Startup(IShellConfiguration shellConfiguration) => _shellConfiguration = shellConfiguration; + public override void ConfigureServices(IServiceCollection services) { // NEXT STATION: Views/PersonPart.Edit.cshtml @@ -92,7 +96,7 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); // Demo Settings - services.Configure(shellConfiguration.GetSection("Lombiq_TrainingDemo")); + services.Configure(_shellConfiguration.GetSection("Lombiq_TrainingDemo")); services.AddTransient, DemoSettingsConfiguration>(); services.AddScoped, DemoSettingsDisplayDriver>(); services.AddScoped(); From 1b39dc2deb847787744f1993af56f5d1d5dab7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:14:08 +0100 Subject: [PATCH 12/20] Fix. --- Controllers/DisplayManagementController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Controllers/DisplayManagementController.cs b/Controllers/DisplayManagementController.cs index 3b5a084d..a8002a15 100644 --- a/Controllers/DisplayManagementController.cs +++ b/Controllers/DisplayManagementController.cs @@ -17,12 +17,14 @@ namespace Lombiq.TrainingDemo.Controllers; public class DisplayManagementController : Controller, IUpdateModel -// The core display management features can be used via the IDisplayManager service. The generic parameter will be -// the object that needs to be displayed on the UI somehow. Don't forget to register this generic class with the -// service provider (see: Startup.cs). +{ + // The core display management features can be used via the IDisplayManager service. The generic parameter will be + // the object that needs to be displayed on the UI somehow. Don't forget to register this generic class with the + // service provider (see: Startup.cs). private readonly IDisplayManager _bookDisplayManager; public DisplayManagementController(IDisplayManager bookDisplayManager) => _bookDisplayManager = bookDisplayManager; + // Before we learn how shapes are generated using the display manager let's see what are these shapes actually. // Ad-hoc shapes can be created anywhere without the display manager. In this example we'll see how to create an // ad-hoc shape inside a view (or could be another shape). Later we'll see how to do it from a filter too. Open from @@ -39,7 +41,7 @@ public async Task DisplayBook() var book = CreateDemoBook(); // This method will generate a shape primarily for displaying information about the given object. - var shape = await bookDisplayManager.BuildDisplayAsync(book, this); + var shape = await _bookDisplayManager.BuildDisplayAsync(book, this); // We will see how this display shape is generated and what will contain but first let's see how is this // rendered in the MVC view. NEXT STATION: Go to Views/DisplayManagement/DisplayBook.cshtml. @@ -56,7 +58,7 @@ public async Task DisplayBookDescription() // This time give an additional parameter which is the display type. If display type is given then Orchard Core // will search a cshtml file with a name [ClassName].[DisplayType].cshtml. - var shape = await bookDisplayManager.BuildDisplayAsync(book, this, "Description"); + var shape = await _bookDisplayManager.BuildDisplayAsync(book, this, "Description"); // NEXT STATION: Go to Views/Book.Description.cshtml From f27c4fdca769df42e96f4624d784d3b3aa6332db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:15:59 +0100 Subject: [PATCH 13/20] Fix. --- Drivers/DemoSettingsDisplayDriver.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Drivers/DemoSettingsDisplayDriver.cs b/Drivers/DemoSettingsDisplayDriver.cs index 36f1b796..b3c73b67 100644 --- a/Drivers/DemoSettingsDisplayDriver.cs +++ b/Drivers/DemoSettingsDisplayDriver.cs @@ -14,15 +14,21 @@ namespace Lombiq.TrainingDemo.Drivers; // Now this display driver abstraction is different from the one you've seen before. In Orchard Core you can connect // different objects to a master object that will be connected in the database when storing them. Site settings are // handled this way. -public class DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) : SectionDisplayDriver +public class DemoSettingsDisplayDriver : SectionDisplayDriver { // Since technically we have only one SiteSettings we have separate the editors using editor groups. It's a good // idea to store the editor group ID in a publicly accessibly constant (would be much better to store it in a static // class placed in a Constants folder). public const string EditorGroupId = "Demo"; - private readonly IAuthorizationService _authorizationService = authorizationService; - private readonly IHttpContextAccessor _hca = hca; + private readonly IAuthorizationService _authorizationService; + private readonly IHttpContextAccessor _hca; + + public DemoSettingsDisplayDriver(IAuthorizationService authorizationService, IHttpContextAccessor hca) + { + _authorizationService = authorizationService; + _hca = hca; + } // Here's the EditAsync override to display editor for our site settings on the Dashboard. Note that it has a sync // version too. From 5b0bf0ddc62ff044e49079b231c74cedee65ccc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:17:06 +0100 Subject: [PATCH 14/20] Fix. --- Controllers/PersonListController.cs | 2 +- Filters/ResourceInjectionFilter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index 29e00e35..f4aeb910 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -136,7 +136,7 @@ public async Task FountainOfEternalYouth() // IContentManager.GetAsync() instead. return "People modified: " + - (oldPeople.Any() ? + (oldPeople.Count != 0 ? string.Join(", ", oldPeople.Select(person => person.As().Name)) : "Nobody. Did you create people older than 90?"); diff --git a/Filters/ResourceInjectionFilter.cs b/Filters/ResourceInjectionFilter.cs index b3f4397e..19b9dc04 100644 --- a/Filters/ResourceInjectionFilter.cs +++ b/Filters/ResourceInjectionFilter.cs @@ -26,7 +26,7 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE // You can register "stylesheet" or "script" resources. You can also set where they'll be rendered with the // .AtHead() or .AtFoot() methods chained on the RegisterResource() method which obviously makes sense only if // the resource is a script. - resourceManager.RegisterResource("stylesheet", "Lombiq.TrainingDemo.Filtered"); + _resourceManager.RegisterResource("stylesheet", "Lombiq.TrainingDemo.Filtered"); await next(); } From 8f747e2fd1274bfd5c22ff80ce1e5906272b9e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:17:43 +0100 Subject: [PATCH 15/20] Fix. --- Filters/ResourceInjectionFilter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Filters/ResourceInjectionFilter.cs b/Filters/ResourceInjectionFilter.cs index 19b9dc04..2fbd34ad 100644 --- a/Filters/ResourceInjectionFilter.cs +++ b/Filters/ResourceInjectionFilter.cs @@ -8,9 +8,11 @@ namespace Lombiq.TrainingDemo.Filters; // Don't forget to add this filter to the filter collection in the Startup.cs file. public class ResourceInjectionFilter : IAsyncResultFilter { + // To register resources you can use the IResourceManager service. private readonly IResourceManager _resourceManager; public ResourceInjectionFilter(IResourceManager resourceManager) => _resourceManager = resourceManager; + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { // Let's decide when the filter should be executed. It wouldn't make sense to inject resources if this is a From 7e89aad195423b2381ddc08b159a4a7666ee373d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Tue, 30 Jan 2024 11:44:22 +0100 Subject: [PATCH 16/20] Fix. --- Controllers/DatabaseStorageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Controllers/DatabaseStorageController.cs b/Controllers/DatabaseStorageController.cs index e1d5a8cf..3878818b 100644 --- a/Controllers/DatabaseStorageController.cs +++ b/Controllers/DatabaseStorageController.cs @@ -102,7 +102,7 @@ public async Task JKRosenzweigBooks() // NEXT STATION: Models/PersonPart.cs - private static IEnumerable CreateDemoBooks() => + private static Book[] CreateDemoBooks() => new[] { new Book From d35246e4173633165e780916a96bb94b51936974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Leh=C3=B3czky?= Date: Thu, 1 Feb 2024 00:06:09 +0100 Subject: [PATCH 17/20] Removing leftover comment --- Controllers/PersonListController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Controllers/PersonListController.cs b/Controllers/PersonListController.cs index f4aeb910..58c3801a 100644 --- a/Controllers/PersonListController.cs +++ b/Controllers/PersonListController.cs @@ -110,7 +110,6 @@ public async Task FountainOfEternalYouth() { part.BirthDateUtc = eighteenYearOld; - // False alarm, this is not a loop. // You can also edit content fields: part.Biography.Text += " I'm young again!"; }); From f2888de7691a4e477e409dc2202142e0bd256865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Thu, 1 Feb 2024 11:28:27 +0100 Subject: [PATCH 18/20] Using RegexGenerator. --- Drivers/ColorFieldDisplayDriver.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Drivers/ColorFieldDisplayDriver.cs b/Drivers/ColorFieldDisplayDriver.cs index 10cd2368..7906ef7d 100644 --- a/Drivers/ColorFieldDisplayDriver.cs +++ b/Drivers/ColorFieldDisplayDriver.cs @@ -7,7 +7,6 @@ using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using System; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -15,7 +14,7 @@ namespace Lombiq.TrainingDemo.Drivers; // You shouldn't be surprised - content fields also have display drivers. ContentFieldDisplayDriver is specifically for // content fields. Don't forget to register this class with the service provider (see: Startup.cs). -public class ColorFieldDisplayDriver : ContentFieldDisplayDriver +public partial class ColorFieldDisplayDriver : ContentFieldDisplayDriver { private readonly IStringLocalizer T; @@ -75,11 +74,7 @@ public override async Task UpdateAsync(ColorField field, IUpdate // Also some custom validation for our ColorField hex value. var isInvalidHexColor = !string.IsNullOrWhiteSpace(viewModel.Value) && - !Regex.IsMatch( - viewModel.Value, - "^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", - RegexOptions.ExplicitCapture, - TimeSpan.FromSeconds(1)); + !RegexExpression().IsMatch(viewModel.Value); if (isInvalidHexColor) updater.ModelState.AddModelError(Prefix, T["The given color is invalid."]); @@ -89,6 +84,9 @@ public override async Task UpdateAsync(ColorField field, IUpdate return await EditAsync(field, context); } + + [GeneratedRegex("^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)] + private static partial Regex RegexExpression(); } // END OF TRAINING SECTION: Content Field development From 6d4bf668b547f08025fbf5a2242533872119da24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Thu, 1 Feb 2024 17:50:06 +0100 Subject: [PATCH 19/20] Adding form-label. --- Views/ColorField-ColorPicker.Edit.cshtml | 2 +- Views/ColorField.Edit.cshtml | 4 ++-- Views/ColorFieldSettings.Edit.cshtml | 8 ++++---- Views/DemoSettings.Edit.cshtml | 2 +- .../ManagePersonsPermissionCheckerTask.Fields.Edit.cshtml | 2 +- Views/PersonPart.Edit.cshtml | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Views/ColorField-ColorPicker.Edit.cshtml b/Views/ColorField-ColorPicker.Edit.cshtml index 4b93c2fc..3b66c5f2 100644 --- a/Views/ColorField-ColorPicker.Edit.cshtml +++ b/Views/ColorField-ColorPicker.Edit.cshtml @@ -24,7 +24,7 @@
@Model.PartFieldDefinition.DisplayName() - +
diff --git a/Views/ColorField.Edit.cshtml b/Views/ColorField.Edit.cshtml index 1a1ba373..70328799 100644 --- a/Views/ColorField.Edit.cshtml +++ b/Views/ColorField.Edit.cshtml @@ -12,10 +12,10 @@
@Model.PartFieldDefinition.DisplayName() - + - + @if (!string.IsNullOrEmpty(settings.Hint)) { diff --git a/Views/ColorFieldSettings.Edit.cshtml b/Views/ColorFieldSettings.Edit.cshtml index 3cccebba..a36cc583 100644 --- a/Views/ColorFieldSettings.Edit.cshtml +++ b/Views/ColorFieldSettings.Edit.cshtml @@ -3,13 +3,13 @@
- +
- + @T["The hint text to display for this field on the editor."]
@@ -17,8 +17,8 @@
- - + + @T["The text associated to the checkbox."]
diff --git a/Views/DemoSettings.Edit.cshtml b/Views/DemoSettings.Edit.cshtml index 07bcfe69..9227fe83 100644 --- a/Views/DemoSettings.Edit.cshtml +++ b/Views/DemoSettings.Edit.cshtml @@ -3,7 +3,7 @@ @using OrchardCore.Mvc.Core.Utilities
- + diff --git a/Views/Items/ManagePersonsPermissionCheckerTask.Fields.Edit.cshtml b/Views/Items/ManagePersonsPermissionCheckerTask.Fields.Edit.cshtml index 66cf00ea..543ba86d 100644 --- a/Views/Items/ManagePersonsPermissionCheckerTask.Fields.Edit.cshtml +++ b/Views/Items/ManagePersonsPermissionCheckerTask.Fields.Edit.cshtml @@ -1,7 +1,7 @@ @model ManagePersonsPermissionCheckerTaskViewModel
- + @T["The user's name to be checked for ManagePersons permission."] diff --git a/Views/PersonPart.Edit.cshtml b/Views/PersonPart.Edit.cshtml index 351726a3..55b77c6f 100644 --- a/Views/PersonPart.Edit.cshtml +++ b/Views/PersonPart.Edit.cshtml @@ -7,18 +7,18 @@ here, because (as mentioned) it has its own editor and display shape so no need to worry about that. *@
- + @T["Person's name"]
- +
- +
From cd7522e97148ae4bd7582266f7447a71f14ba3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20M=C3=A1rkus?= Date: Mon, 5 Feb 2024 15:13:02 +0100 Subject: [PATCH 20/20] Upgrading MS packages. --- Lombiq.TrainingDemo.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.TrainingDemo.csproj b/Lombiq.TrainingDemo.csproj index 7af5659b..7421e1d7 100644 --- a/Lombiq.TrainingDemo.csproj +++ b/Lombiq.TrainingDemo.csproj @@ -23,8 +23,8 @@ - - + +