Our blog contains the activity stream of Orchard Dojo: general news, new resources or tutorials are announced here.

Featured tags

IIS
API
SMS
SEO
All tags >

Reimplement batching in YesSql, Azure Application Insights module - This week in Orchard (26/02/2021)

Refactoring the Content zone, reimplement batching in YesSql, hiding Setup recipes, adding more indexes to index tables, Azure Application Insights module and a lot more is waiting just for you in this post! Orchard Core updates Do not display Setup recipes in the admin UI You can easily list all of the existing recipes of your site under Configuration -> Recipes. If you create a recipe, like myrecipe.recipe.json, and put it in a Recipes folder of one of your modules, that recipe will be listed on this page. Let's add a simple one and check out the content of that page. You can see that our recipe with the display name My Recipe is on the list but wait! What happened with the other recipes? We have several other built-in recipes like the Blog, the Agency, and so on. Where are they? From now the logic in the Index method of the AdminController in the OrchardCore.Recipes module is slightly changed. If the recipe is a setup recipe (defined in the issetuprecipe of the JSON file) or has the hidden tag, the recipe will not appear in that list. You can see that our myrecipe has the false value for the issetuprecipe but the Blog recipe has the true value. Admin Dashboard Widgets We mentioned the Admin Dashboard widgets two times in This week in Orchard that allows you to add cards to the homepage of the dashboard, which is about to represent a piece of functionality of a given feature or module. You can find the first post here and the second one here. And now this feature has been merged to the dev branch of Orchard Core and there is also a new page in the Orchard Core Documentation about this module. It hasn't got too much information yet but the embedded recording could help you to see this feature in action. If you haven't heard about this feature, don't forget to check out the previous blog posts and the demo video! Use DocumentId in indexes This fix is also about using the correct combination of fields for each index that we use. For example, the index for the UserIndex table now contains every field of the table. So, from now you will find indexes for every index table. Refactor ContentZone - Tabs, Card, Column containers With more and more dynamic shapes coming into Orchard Core (all the tab/card grouping shapes, and also shapes like the ContentCard) it would be potentially useful to have a ShapesViewModel that could be used as a hard type abstract class to create specific targetted ViewModels, i.e. a TabViewModel. The problem with the ShapeViewModel is it doesn't support a list of positioned Items, so right now we are using Shape because many of these tab/card/content card shapes require a list of positioned items as well. So we was not intending that a ShapesViewModel was actually an IShape, just an abstract that could be used to build shapes with, and the mixin would still turn it into an IShape, by mixing in the ShapeViewModel which has all the required IShape properties. The idea being you could then use the IShapeFactory to create a shape, then add items to it. var shape = ShapeFactory.CreateAsync<TabViewModel>("Tab", m =>{ m.TabId = tabId});foreach(var item in thetabs){ shape.Add(item);} The current usage builds 3 different shapes, even when displaying on the front end, and there are no groups. Here we do a quick check first, to see if there are any groups, and if there are none, just render the zone directly. No extra shapes. Also Removes uses a dynamic, and moves to hard typed models. Uses ToLookup instead of GroupBy (better usage). Uses fewer dynamics for injected properties, i.e. DisplayAsync can be resolved directly to its interface instead of being dynamic (this needs evaluation, as we might do it for the other C# Shape Attributes, or not). If you check out the modifications of the ZoneShapes class, you will find the implementation of the ContentZone shape and building the TabContainer shape by using the new GroupingsViewModel. And by having the GroupingsViewModel ViewModel we can use that instead of a dynamic one as you can see in the TabContainer.cshtml file. Demos YesSql: Reimplement batching When we do updates on the content items and when one deletes a document all the index tables corresponding to the document type get a DELETE query. And this DELETE query is actually a single DELETE query. If we see a benchmark we can realize that the batching isn't working. So, if you would like to delete multiple content items, the code is just about to send multiple DELETE requests instead of sending one. If you run the query locally using SQL Server and you have 1000 indexes, it takes one second. So, you might not notice that it's slow. It's slow but it could look like that's because we have several indexes. Extra Indexes: 1, Elapsed 00:00:00.0371806Extra Indexes: 100, Elapsed 00:00:00.1317780Extra Indexes: 200, Elapsed 00:00:00.2826214Extra Indexes: 500, Elapsed 00:00:00.6213360Extra Indexes: 1000, Elapsed 00:00:01.2628232 But when you have the SQL Server in Azure, the deletion of 1000 indexes could take 28 seconds. At this point, you will start noticing the performance just after 100 indexes. Apparently, we have around 20 indexes in Orchard Core but you can quickly arrive at 100. Extra Indexes: 1, Elapsed 00:00:00.0838498Extra Indexes: 100, Elapsed 00:00:02.5644737Extra Indexes: 200, Elapsed 00:00:05.0814065Extra Indexes: 500, Elapsed 00:00:12.6558409Extra Indexes: 1000, Elapsed 00:00:28.6622291 In YesSql, when you update a document, it needs to update all the indexes, which means it will rebuild them. And an index can return multiple records like if I'm indexing the name of someone, I might want to index the first name, the last name, the middle name. These are three records, one per name. So, what it does is, it builds the three records in memory and it will send a query to delete any record that was associated with the document and then sends three inserts for the new records. So you have one for the delete and three for the inserts. But it sends a delete even for the indexes that didn't return anything because the fact that you didn't return anything might mean that there is nothing anymore associated. So you need to delete and send nothing. This way we get a delete per index. That's not optimal but that's how YesSql works and that's optimal for reads and not writes. To goal is to make reads faster than writes. Still, we should not have to send one independent query to do the deletes per index. That's what this issue is about. And Sébastien Ros managed to fix it. And here are some numbers. These are all for dummy indexes that never perform any writes. Just cause deletes. What we can see in the profiler is that everything easy to batch is batched together. The big batch with mostly deletes, and a couple of inserts, is now long-running, instead of many many short runs. Probably from the look of it, the insert is still expensive, so which pushes up the time run. Before batchingLocal Indexes 1, Elapsed 00:00:00.3004236 Indexes 100, Elapsed 00:00:00.2375774 Indexes 200, Elapsed 00:00:00.3583902 Indexes 500, Elapsed 00:00:00.7695818 Indexes 1000, Elapsed 00:00:01.2836934Remote Indexes 1, Elapsed 00:00:00.7207663 Indexes 100, Elapsed 00:00:03.3552247 Indexes 200, Elapsed 00:00:05.5547927 Indexes 500, Elapsed 00:00:13.8364514 Indexes 1000, Elapsed 00:00:27.2306443After BatchingLocal Indexes 1, Elapsed 00:00:00.2207824 Indexes 100, Elapsed 00:00:00.0910920 Indexes 200, Elapsed 00:00:00.1632908 Indexes 500, Elapsed 00:00:00.4007200 Indexes 1000, Elapsed 00:00:00.4559752RemoteIndexes 1, Elapsed 00:00:03.6326000Indexes 100, Elapsed 00:00:04.9639312Indexes 200, Elapsed 00:00:09.8273422Indexes 500, Elapsed 00:00:16.1340951Indexes 1000, Elapsed 00:00:15.2008296 And how it looks like is the following. If you create a blog post in Orchard Core and after the change, there is a single communication to the database that contains everything that you can see here. This contains creating the Document, creating the ContentItemIndex, updating the ContentItemIndex with the DocumentId, and so on. These are three indexes to update (ContentItemIndex, ContainedPartIndex and AutoroutePartIndex). insert into [Document] ([Id], [Type], [Content], [Version]) values (19, 'OrchardCore.ContentManagement.ContentItem, OrchardCore.ContentManagement.Abstractions', '{"ContentItemId":"4cpw0fnmjb1kp07dmzxx8n8ecg","ContentItemVersionId":"4953p18bj3gyy5yy82f7mj7w4y","ContentType":"BlogPost","DisplayText":"The title","Latest":true,"Published":false,"ModifiedUtc":"2020-12-31T00:31:34.3346095Z","PublishedUtc":null,"CreatedUtc":"2020-12-31T00:31:34.3346095Z","Owner":"48v9vt5vxznr5z9m1df9zmvjm8","Author":"admin","TitlePart":{"Title":"The title"},"AutoroutePart":{"Path":"blog/the-title","SetHomepage":false,"Disabled":false,"RouteContainedItems":false,"Absolute":false},"BlogPost":{"Subtitle":{"Text":"Subtitle"},"Image":{"Anchors":[],"Paths":[],"MediaTexts":[]},"Tags":{"TagNames":["Space"],"TaxonomyContentItemId":"4ykev5wxfcny7tvsahz9y64mwe","TermContentItemIds":["4nv0z7r24r1vw3sfpq7t6xws59"]},"Category":{"TaxonomyContentItemId":"4tpy2wv97bkbf0zkx8tyd1bm4q","TermContentItemIds":["4bsstr09f29rp0sgy85n9f07wj"]}},"MarkdownBodyPart":{"Markdown":"Some text"},"ContainedPart":{"ListContentItemId":"491emynv0kavbzhy40xmqv1wds","Order":0}}', 1);insert into [ContentItemIndex] ([ContentItemId], [ContentItemVersionId], [Published], [Latest], [ContentType], [ModifiedUtc], [PublishedUtc], [CreatedUtc], [Owner], [Author], [DisplayText]) values ('4cpw0fnmjb1kp07dmzxx8n8ecg', '4953p18bj3gyy5yy82f7mj7w4y', 0, 1, 'BlogPost', '2020-12-31T00:31:34', '', '2020-12-31T00:31:34', '48v9vt5vxznr5z9m1df9zmvjm8', 'admin', 'The title') ; select last_insert_rowid() [Id];update [ContentItemIndex] set [DocumentId] = 19 where [Id] = (last_insert_rowid());insert into [ContainedPartIndex] ([ListContentItemId], [Order]) values ('491emynv0kavbzhy40xmqv1wds', 0) ; select last_insert_rowid() [Id];update [ContainedPartIndex] set [DocumentId] = 19 where [Id] = (last_insert_rowid());insert into [AutoroutePartIndex] ([ContentItemId], [Path], [Published], [Latest], [ContainedContentItemId], [JsonPath]) values ('4cpw0fnmjb1kp07dmzxx8n8ecg', 'blog/the-title', 0, 1, '', '') ; select last_insert_rowid() [Id];update [AutoroutePartIndex] set [DocumentId] = 19 where [Id] = (last_insert_rowid()); When we create a blog post there aren't any delete requests, because there is nothing before but when we update we need to delete all the indexes and these calls contains unnecessary deletes because for example there is no LayerMetaDataIndex on the blog post but the logic here is hey, we are dealing with a content item and there is an index for all of the content items called LayerMetadataIndex. So it will send the delete just in case which is dumb. But you can see here for each index we have a delete and it used to be a single query communication query with the database. Now let's divide by two the part where we insert and update the indexes. Instead of having to insert something then update something for each index now, we have just inserted it. We still have all the deletes and some of them are still useless but these are quick. So we have just this request now when we update a single blog post. insert into [Document] ([Id], [Type], [Content], [Version]) values (23, 'OrchardCore.ContentManagement.ContentItem, OrchardCore.ContentManagement.Abstractions', '{"ContentItemId":"4cpw0fnmjb1kp07dmzxx8n8ecg","ContentItemVersionId":"4ypdrxm7xbndr0dvcpwaraa95g","ContentType":"BlogPost","DisplayText":"The title","Latest":true,"Published":false,"ModifiedUtc":"2020-12-31T05:55:56.8113646Z","PublishedUtc":"2020-12-31T01:22:37.7926461Z","CreatedUtc":"2020-12-31T00:31:34.3346095Z","Owner":"48v9vt5vxznr5z9m1df9zmvjm8","Author":"admin","TitlePart":{"Title":"The title"},"AutoroutePart":{"Path":"blog/the-title","SetHomepage":false,"Disabled":false,"RouteContainedItems":false,"Absolute":false},"BlogPost":{"Subtitle":{"Text":"Subtitle"},"Image":{"Anchors":[],"Paths":[],"MediaTexts":[]},"Tags":{"TagNames":["Space"],"TaxonomyContentItemId":"4ykev5wxfcny7tvsahz9y64mwe","TermContentItemIds":["4nv0z7r24r1vw3sfpq7t6xws59"]},"Category":{"TaxonomyContentItemId":"4tpy2wv97bkbf0zkx8tyd1bm4q","TermContentItemIds":["4bsstr09f29rp0sgy85n9f07wj"]}},"MarkdownBodyPart":{"Markdown":"Some text"},"ContainedPart":{"ListContentItemId":"491emynv0kavbzhy40xmqv1wds","Order":0}}', 1);delete from [ContentItemIndex] where [DocumentId] = 22;delete from [AliasPartIndex] where [DocumentId] = 22;delete from [LayerMetadataIndex] where [DocumentId] = 22;delete from [ContainedPartIndex] where [DocumentId] = 22;delete from [AutoroutePartIndex] where [DocumentId] = 22;delete from [TaxonomyIndex] where [DocumentId] = 22;insert into [ContentItemIndex] ([ContentItemId], [ContentItemVersionId], [Published], [Latest], [ContentType], [ModifiedUtc], [PublishedUtc], [CreatedUtc], [Owner], [Author], [DisplayText], [DocumentId]) values ('4cpw0fnmjb1kp07dmzxx8n8ecg', '4q3271jp1705etwnf52c0nnbwz', 1, 0, 'BlogPost', '2020-12-31T01:22:37', '2020-12-31T01:22:37', '2020-12-31T00:31:34', '48v9vt5vxznr5z9m1df9zmvjm8', 'admin', 'The title', 22) ; select last_insert_rowid() [Id];insert into [ContainedPartIndex] ([ListContentItemId], [Order], [DocumentId]) values ('491emynv0kavbzhy40xmqv1wds', 0, 22) ; select last_insert_rowid() [Id];insert into [AutoroutePartIndex] ([ContentItemId], [Path], [Published], [Latest], [ContainedContentItemId], [JsonPath], [DocumentId]) values ('4cpw0fnmjb1kp07dmzxx8n8ecg', 'blog/the-title', 1, 0, '', '', 22) ; select last_insert_rowid() [Id];insert into [TaxonomyIndex] ([TaxonomyContentItemId], [ContentItemId], [ContentType], [ContentPart], [ContentField], [TermContentItemId], [DocumentId]) values ('4ykev5wxfcny7tvsahz9y64mwe', '4cpw0fnmjb1kp07dmzxx8n8ecg', 'BlogPost', 'BlogPost', 'Tags', '4nv0z7r24r1vw3sfpq7t6xws59', 22) ; select last_insert_rowid() [Id];insert into [TaxonomyIndex] ([TaxonomyContentItemId], [ContentItemId], [ContentType], [ContentPart], [ContentField], [TermContentItemId], [DocumentId]) values ('4tpy2wv97bkbf0zkx8tyd1bm4q', '4cpw0fnmjb1kp07dmzxx8n8ecg', 'BlogPost', 'BlogPost', 'Category', '4bsstr09f29rp0sgy85n9f07wj', 22) ; select last_insert_rowid() [Id];update [Document] set [Content] = '{"ContentItemId":"4cpw0fnmjb1kp07dmzxx8n8ecg","ContentItemVersionId":"4q3271jp1705etwnf52c0nnbwz","ContentType":"BlogPost","DisplayText":"The title","Latest":false,"Published":true,"ModifiedUtc":"2020-12-31T01:22:37.6390174Z","PublishedUtc":"2020-12-31T01:22:37.7926461Z","CreatedUtc":"2020-12-31T00:31:34.3346095Z","Owner":"48v9vt5vxznr5z9m1df9zmvjm8","Author":"admin","TitlePart":{"Title":"The title"},"AutoroutePart":{"Path":"blog/the-title","SetHomepage":false,"Disabled":false,"RouteContainedItems":false,"Absolute":false},"BlogPost":{"Subtitle":{"Text":"Subtitle"},"Image":{"Anchors":[],"Paths":[],"MediaTexts":[]},"Tags":{"TagNames":["Space"],"TaxonomyContentItemId":"4ykev5wxfcny7tvsahz9y64mwe","TermContentItemIds":["4nv0z7r24r1vw3sfpq7t6xws59"]},"Category":{"TaxonomyContentItemId":"4tpy2wv97bkbf0zkx8tyd1bm4q","TermContentItemIds":["4bsstr09f29rp0sgy85n9f07wj"]}},"MarkdownBodyPart":{"Markdown":"Some text"},"ContainedPart":{"ListContentItemId":"491emynv0kavbzhy40xmqv1wds","Order":0}}', [Version] = 1 where [Id] = 22;insert into [ContentItemIndex] ([ContentItemId], [ContentItemVersionId], [Published], [Latest], [ContentType], [ModifiedUtc], [PublishedUtc], [CreatedUtc], [Owner], [Author], [DisplayText], [DocumentId]) values ('4cpw0fnmjb1kp07dmzxx8n8ecg', '4ypdrxm7xbndr0dvcpwaraa95g', 0, 1, 'BlogPost', '2020-12-31T05:55:56', '2020-12-31T01:22:37', '2020-12-31T00:31:34', '48v9vt5vxznr5z9m1df9zmvjm8', 'admin', 'The title', 23) ; select last_insert_rowid() [Id];insert into [ContainedPartIndex] ([ListContentItemId], [Order], [DocumentId]) values ('491emynv0kavbzhy40xmqv1wds', 0, 23) ; select last_insert_rowid() [Id];insert into [AutoroutePartIndex] ([ContentItemId], [Path], [Published], [Latest], [ContainedContentItemId], [JsonPath], [DocumentId]) values ('4cpw0fnmjb1kp07dmzxx8n8ecg', 'blog/the-title', 0, 1, '', '', 23) ; select last_insert_rowid() [Id]; Using the other PR we are able to teach the index when not to send deletes as there will never be any index related to this Document, so don't send deletes. After that, you can see there is no more LayerMetaDataIndex and no more AliasPartIndex calls because the blog post doesn't have these. delete from [ContentItemIndex] where [DocumentId] = 23;delete from [ContainedPartIndex] where [DocumentId] = 23;delete from [AutoroutePartIndex] where [DocumentId] = 23;delete from [TaxonomyIndex] where [DocumentId] = 23; This is by having a new method on the IndexProvider to explain when to not use the IndexProvider. So it would not even go to the Map method. In this case, we say if you don't have the AliasPart, don't use the IndexProvider. context.For<AliasPartIndex>() .When(c => c.Has<AliasPart>()) .Map(contentItem => Check out the following recording on YouTube to know more about this YesSql improvement! News from the community Lombiq Hosting - Azure Application Insights This new Orchard Core module from Lombiq enables easy integration of Azure Application Insights telemetry into Orchard. Just install the module, configure the instrumentation key from a configuration source (like the appsettings.json file) as normally for AI, and collected data will start appearing in the Azure Portal. Would like to learn more about our new module? Then head to the repository now where you can find every detail about how to set up and use that module in your site! Orchard Dojo Newsletter Lombiq's Orchard Dojo Newsletter has 191 subscribers! We have started this newsletter to inform the community around Orchard with the latest news about the platform. By subscribing to this newsletter, you will get an e-mail whenever a new post published to Orchard Dojo, including This week in Orchard of course. Do you know of other Orchard enthusiasts who you think would like to read our weekly articles? Tell them to subscribe here! If you are interested in more news around Orchard and the details of the topics above, don't forget to check out the recording of this week's Orchard meeting!

Custom User Settings, Advanced Markdown Extensions - This week in Orchard (27/11/2020)

Now it's easier to extend the user setting than ever thanks to the new custom user settings feature! Create advanced Markdown with the new extensions, use the new isInRole layer rule, and many other great things in Orchard Core! Check out our current post for more! Orchard Core updates Use Advanced Markdown extensions Set up your site using the Blog recipe and navigate to the admin UI to edit the predefined blog post. You can use a Markdown editor to edit the body of your blog post. To convert the Markdown string to HTML, Orchard Core using Markdig, which is a fast, powerful, CommonMark compliant, extensible Markdown processor for .NET. This processor has a great extensible architecture with more than 20 built-in extensions to allow footnotes, support emojis, and smileys, adding figures, enable Bootstrap classes, and so on. You can check out the features section of the readme.md file to know more about these extensions. And the good news is now you can select which extension you want to enable by just setting different configuration values in your appsettings.json file. The default option is to turn on the nohtml extension (that disables HTML support) and the one called advanced, which enables advanced Markdown extensions. By turning on the advanced extension, Markdig will use every extension except the BootStrap, Emoji, SmartyPants and soft-line as hard-line breaks extensions. On the screen above you can see we also turn on the emoji extension. Let's try this out quickly! Here you can see we added some Emoji shortcodes and smileys to the blog post and they are converted to their respective Unicode characters. And we also added a task list at the end of the blog post that comes from the TaskLists extension that is enabled by default because the TaskList extension will be enabled if you say you want to use the Advanced Markdown extension. You can read more about Markdown configuration in the Orchard Core documentation too! Add isInRole layer rule Let's say we would like to create a layer that contains widgets that are only available for users who are in the Editor role. We cannot do that easily until now, so let's try out the newly added isInRole layer rule! Set up your site using the Blog recipe then head to Design -> Widgets in the admin UI. Here you can see the two predefined layers: Always and Homepage. Click on the Add button to create a new layer. The only thing we have to do here is to set the Rule to isInRole("editor"). Now put a widget to this layer and create a new user with the editor role. If you log in with that user you can see the widget in the Content zone of the theme. Add CloneContent Permission as Content Type dynamic permission Navigate to the admin UI of your site then head to Security -> Roles and hit the Edit button near an arbitrary role. Now type the clone word in the search box that will list the new clone content permissions. This means now you can set the clone content permission for every securable content type (a content type that can have custom permissions). Improves Document Options and Compression MessagePack for C# is an extremely fast MessagePack serializer for C#. It is 10x faster than MsgPack-Cli and outperforms other C# serializers. MessagePack for C# also ships with built-in support for LZ4 compression - an extremely fast compression algorithm. Performance is important, particularly in applications like games, distributed computing, microservices, or data caches. And now this package is part of Orchard Core. Orchard Core using MessagePack to GZIP content, because there is an option in MessagePack to also compress it. There is a new IDocumentSerialiser to say how to serialize into a sequence of bytes or deserialize from a byte array. JSON.NET can serialize anything, but if there is an issue with a custom document, we can use MessagePack to customize the way how to serialize content from the distributed cache. Demos Custom User Settings Set up your site using the Blog recipe. Now navigate to the admin UI and head to Configuration -> Features to enable the new Custom User Settings feature that allows content types to become custom user settings. After we can create a new content type that will be responsible to store the custom user settings. To do that, head to Content -> Content Definition -> Content Types and create a new content type. The most important thing to do here is to make the stereotype CustomUserSettings and remove the ticks from the Creatable, Listable, Draftable, etc, checkboxes. We named our content type as User Profile and attached three fields to it: a media field to store the avatar of the user and two text fields to store the first name and the last name of the user. We haven't added any content parts to this content type. Let's navigate to Security -> Users and edit one of the users in the system. Here you will notice a new tab near the Content one with the name User Profile. That's because we named our content type User Profile. Here we specified the first and the last name of the user and attached an image to it. If you hit Save, you can persist your changes for this given user. Now it's time to use our custom user settings from code. The Orchard Core documentation has a great detailed page about how you can get the custom user settings using Liquid, how to adjust the placement, and many more. Notice the brand new users_by_id Liquid Filter that you can use to get the User object from the database by a user ID. But how can you get your custom user settings in C#? Well, that's also quite simple! The User class in Orchard Core implements the Entity class, which has a JObject property called Properties. And inside the Properties, you can find the whole UserProfile content type with it's attached content parts and fields. { "Email": "[email protected]", "EmailConfirmed": true, "Id": 18, "IsEnabled": true, "LoginInfos": [], "NormalizedEmail": "[email protected]", "NormalizedUserName": "ADMIN", "PasswordHash": "AQAAAAEAACcQAAAAECfRTVdPZm1meRWrxja3vzbJlslet72QzrqSsPKxOeeGKQs7bcMVeTYRSNrhc2yjpw==", "Properties": { "UserProfile": { "Author": "admin", "ContentItemId": "4k7hmhyywz1ettg2hkv5xpcfxz", "ContentItemVersionId": null, "ContentType": "UserProfile", "CreatedUtc": null, "DisplayText": null, "Latest": false, "ModifiedUtc": "2020-11-24T14:36:36.8629171Z", "Owner": null, "Published": false, "PublishedUtc": null, "UserProfile": { "Firstname": { "Text": "Admin" }, "Image": { "MediaTexts": [ "" ], "Paths": [ "c827782e12851cd2cf4c5161c4f5445a.jpg" ] }, "Lastname": { "Text": "Orchard" } } } }, "ResetToken": null, "RoleNames": [ "Administrator" ], "SecurityStamp": "FAALQXTJCDTD5EOVTLCW6N75CW3BOVYU", "UserClaims": [], "UserId": "4gq8jagmrtxrwvg21csa0b6y4y", "UserName": "admin", "UserTokens": []} Navigate to the CustomUserSettingsDisplayDriver and check out the GetUserSettingsAsync private method. Here you can see the way about getting the custom user settings content item from the Properties array. And after you can easily work with the properties of your content item. And that's not all of it! Check out this recording on YouTube to know more about this great, useful feature! News from the community Introducing 40 days of Orchard Core: Dojo Course 3 is here! We've been hard at work to bring you the updated, Orchard Core version of our legendary Dojo Course tutorial series, Dojo Course 3! If you want to become an Orchard Core developer, Dojo Course 3 is for you! Starting with the 1st of December we'll publish a new Orchard Core tutorial every day for 40 days on our YouTube channel. Be sure to set a reminder for the premiere video and subscribe if you want to see them just as they come out! If you're looking for our previous Orchard 1.x tutorial series check out Dojo Course 2. Orchard Dojo Newsletter Lombiq's Orchard Dojo Newsletter has 169 subscribers! We have started this newsletter to inform the community around Orchard with the latest news about the platform. By subscribing to this newsletter, you will get an e-mail whenever a new post published to Orchard Dojo, including This week in Orchard of course. Do you know of other Orchard enthusiasts who you think would like to read our weekly articles? Tell them to subscribe here! If you are interested in more news around Orchard and the details of the topics above, don't forget to check out the recording of this week's Orchard meeting!

4 ways to display something from your module nested within a page in Orchard Core - Orchard Core Nuggets

A common question during Orchard Core development, something that came up again recently, is how to display something within the context of an Orchard page when that piece of data comes from your module? How can you "inject" something into the Orchard layout when you want to display e.g. a list of products retrieved from an external API? There are a couple of ways to do this depending on what exactly you need. All are fairly straightforward so let's see a quick rundown! Creating a whole page from you module If you want a whole page served by just your module then it's really simple: Create a module, add a controller as you'd do in standard ASP.NET Core MVC, make an action produce a view and that's it! The view will be wrapped into the Orchard layout so the theme you've selected will be visible around it: The basic styling will be there, the header and menu, any widgets you have put onto layers... Our Training Demo module has a simple sample exactly for this, just check it out and you'll see what we're talking about. The above screenshot comes from our Open-Source Orchard Core Extensions solution BTW. Creating a widget Widgets (see official docs) are basically little boxes of content or other functionality that you can put anywhere on the site. For example, an infobox about the site, a search box, a recent articles box, a footer can all be widgets. You can use them in two ways: Add a widget to a content item with Flow Part: Flow Part can be used to build flexible layouts out of various widgets, including nesting them (like putting widgets into a Container Widget). When you set up Orchard with the built-in Agency recipe and theme then you'll get a Page content type that has Flow Part out of the box: You can get the same content type from our Helpful Extensions Orchard Core module too. Another option to use widgets is to put them onto a layer, a sort of container of widgets. There you can place the widget into a global zone, an area of the Orchard layout, like the header, footer, or sidebars. The widgets on a layer will be displayed whenever the layer rule of that layer matches (you can think of it as a logic expression producing a boolean value), like on every page except the home page or on every page but only for authenticated visitors. For more info check out the docs. OK, but how can you create a widget? A widget is just a content item whose content type has its stereotype set as "Widget". You can change this value from the admin from under Content / Content Definitions and also from code. So, basically, the task is to create a content part of yours that'll display the data you want to show from its driver, then create a widget content type where that part is attached. Seems like a lot? It isn't, check out the relevant content part development tutorial again from the Training Demo, including creating a widget. Injecting a shape into the layout This can be a lot easier than developing a widget but also less flexible to use. You can also write your own code in a template (like a cshtml Razor file) and inject that into the Orchard layout directly. You can see an example of injecting a shape in the Training Demo module too. Displaying a shape from a Liquid Widget The Liquid Widget is a widget that can render a piece of Liquid markup. While there is such a widget in the Agency theme and you can throw it together from the admin quickly too the Helpful Libraries module has it built-in as well. With this widget and Orchard's pretty advanced Liquid support you can of course just write Liquid directly. However, for more complex apps maintaining templates editable from the admin quickly becomes an issue so we'd recommend keeping code in your modules and themes. The good news is that once you have a piece of Razor code in a cshtml template (or Liquid in a .liquid template) then you can just display it from a Liquid Widget! For example, if you have a WeatherData.cshtml template fetching some weather information from an external API then you can display it from a Liquid Widget, and thus use it just as any widget with this little piece of code: {% shape "WeatherData" %} There's more to it, check out the docs on the shape Liquid tag. Conclusion Pretty much that's it. There are other ways too but these are the most straightforward and most flexible ones. Do you have another technique you'd like others to know? Add below in the comments! Did you like this post? It's part of our Orchard Core Nuggets series where we answer common Orchard questions, be it about user-facing features or developer-level issues. Check out the other posts for more such bite-sized Orchard Core tips and let us know if you have another question!