Skip to content

Commit 466158c

Browse files
committed
Merge branch 'release/4.4.0'
This update primarily focuses on introducing new extension methods for registering topic areas (66c53c9). In addition, it also shores up the implementation of Oroborus Configuration, helping ensure that updates to `ContentTypeDescriptor` and `AttributeDescriptor` instances are reflected immediately in e.g. the OnTopic Editor. These were caused by version conflicts introduced by multiple in-memory instances of the same topic entities stored in e.g., `TopicRepositoryBase._contentTypeDescriptors` and `CachedTopicRepository._cache` (85216c4). Finally, this separates out the verbose sitemap into an `Extended()` action—which includes all relationships and attribute metadata; the default `Index()` now outputs a shorter form sitemap, based on the data most search agents utilize (86df229). Features - Introduced a series of extension methods for configuring topic areas—including `MapTopicAreaRoute()`, `MapDefaultAreaControllerRoute()`, and `MapImplicitAreaControllerRoute()` (66c53c9) - Introduced a new `Extended()` action on `SitemapController` to display extended metadata, while setting the default `Index()` action to display a standard—and much smaller—sitemap (86df229) - Containers are used to organize topics, but aren't intended to be displayed to users. They are now hidden from the Sitemap (87b6d1b) and will return a 403 Unauthorized if accessed (e4130cf) Improvements - Significantly improved real-time updates of Oroborus Configuration via OnTopic by introducing code to centralize references of `GetContentTypeDescriptors()` in order to avoid version conflicts with e.g. `CachedTopicRepository` (85216c4) - Extended the search criteria for mapping routing variables to topic paths in order to support implicit controllers (74a8a76) - Implementing .NET Core 3.x's `{**path}` catch-all route handling for the `path` variable, ensuring it is not encoded (30c6691) Maintenance - Updated development and build dependencies in NuGet (21a9ad7)
2 parents 6ec3b60 + 21a9ad7 commit 466158c

22 files changed

+526
-144
lines changed

OnTopic.AspNetCore.Mvc.Host/SampleActivator.cs

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public object Create(ControllerContext context) {
102102
new TopicController(_topicRepository, _topicMappingService),
103103
nameof(SitemapController) =>
104104
new SitemapController(_topicRepository),
105+
nameof(RedirectController) =>
106+
new RedirectController(_topicRepository),
105107
_ => throw new Exception($"Unknown controller {type.Name}")
106108
};
107109

OnTopic.AspNetCore.Mvc.Host/Startup.cs

+2-4
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,9 @@ public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
112112
| Configure: MVC
113113
\-----------------------------------------------------------------------------------------------------------------------*/
114114
app.UseEndpoints(endpoints => {
115-
endpoints.MapControllerRoute(
116-
name: "default",
117-
pattern: "{controller}/{action=Index}/"
118-
);
119115
endpoints.MapTopicRoute("Web");
116+
endpoints.MapTopicSitemap();
117+
endpoints.MapTopicRedirect();
120118
endpoints.MapControllers();
121119
});
122120

OnTopic.AspNetCore.Mvc.Tests/OnTopic.AspNetCore.Mvc.Tests.csproj

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
10-
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
11-
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
10+
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
11+
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
1212
</ItemGroup>
1313

1414
<ItemGroup>

OnTopic.AspNetCore.Mvc/Controllers/SitemapController.cs

+33-11
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ public SitemapController(ITopicRepository topicRepository) {
9191
/// Provides the Sitemap.org sitemap for the site.
9292
/// </summary>
9393
/// <param name="indent">Optionally enables indentation of XML elements in output for human readability.</param>
94-
/// <returns>The site's homepage view.</returns>
95-
public virtual ActionResult Index(bool indent = false) {
94+
/// <param name="includeMetadata">Optionally enables extended metadata associated with each topic.</param>
95+
/// <returns>A Sitemap.org sitemap.</returns>
96+
public virtual ActionResult Index(bool indent = false, bool includeMetadata = false) {
9697

9798
/*------------------------------------------------------------------------------------------------------------------------
9899
| Ensure topics are loaded
@@ -109,7 +110,7 @@ public virtual ActionResult Index(bool indent = false) {
109110
| Establish sitemap
110111
\-----------------------------------------------------------------------------------------------------------------------*/
111112
var declaration = new XDeclaration("1.0", "utf-8", "no");
112-
var sitemap = GenerateSitemap(rootTopic);
113+
var sitemap = GenerateSitemap(rootTopic, includeMetadata);
113114
var settings = indent? SaveOptions.None : SaveOptions.DisableFormatting;
114115

115116
/*------------------------------------------------------------------------------------------------------------------------
@@ -119,19 +120,36 @@ public virtual ActionResult Index(bool indent = false) {
119120

120121
}
121122

123+
/*==========================================================================================================================
124+
| GET: /SITEMAP/EXTENDED
125+
\-------------------------------------------------------------------------------------------------------------------------*/
126+
/// <summary>
127+
/// Provides the Sitemap.org sitemap for the site, including extended metadata attributes.
128+
/// </summary>
129+
/// <remarks>
130+
/// Introducing the metadata makes the sitemap considerably larger. However, it also means that some agents will index the
131+
/// additional information and make it available for querying. For instance, the (now defunct) Google Custom Search Engine
132+
/// (CSE) would previously allow queries to be filtered based on metadata attributes exposed via the sitemap.
133+
/// </remarks>
134+
/// <param name="indent">Optionally enables indentation of XML elements in output for human readability.</param>
135+
/// <returns>A Sitemap.org sitemap.</returns>
136+
public virtual ActionResult Extended(bool indent = false) => Index(indent, true);
137+
122138
/*==========================================================================================================================
123139
| METHOD: GENERATE SITEMAP
124140
\-------------------------------------------------------------------------------------------------------------------------*/
125141
/// <summary>
126142
/// Given a root topic, generates an XML-formatted sitemap.
127143
/// </summary>
128-
/// <returns>The site's homepage view.</returns>
144+
/// <param name="topic">The topic to add to the sitemap.</param>
145+
/// <param name="includeMetadata">Optionally enables extended metadata associated with each topic.</param>
146+
/// <returns>A Sitemap.org sitemap.</returns>
129147
[Obsolete("The GenerateSitemap() method should not be public. It will be marked private in OnTopic Library 5.0.")]
130-
public virtual XDocument GenerateSitemap(Topic rootTopic) =>
148+
public virtual XDocument GenerateSitemap(Topic rootTopic, bool includeMetadata = false) =>
131149
new XDocument(
132150
new XElement(_sitemapNamespace + "urlset",
133151
from topic in rootTopic?.Children
134-
select AddTopic(topic)
152+
select AddTopic(topic, includeMetadata)
135153
)
136154
);
137155

@@ -141,8 +159,10 @@ select AddTopic(topic)
141159
/// <summary>
142160
/// Given a <see cref="Topic"/>, adds it to a given <see cref="XmlNode"/>.
143161
/// </summary>
162+
/// <param name="topic">The topic to add to the sitemap.</param>
163+
/// <param name="includeMetadata">Optionally enables extended metadata associated with each topic.</param>
144164
[Obsolete("The AddTopic() method should not be public. It will be marked private in OnTopic Library 5.0.")]
145-
public IEnumerable<XElement> AddTopic(Topic topic) {
165+
public IEnumerable<XElement> AddTopic(Topic topic, bool includeMetadata = false) {
146166

147167
/*------------------------------------------------------------------------------------------------------------------------
148168
| Establish return collection
@@ -171,21 +191,23 @@ public IEnumerable<XElement> AddTopic(Topic topic) {
171191
new XElement(_sitemapNamespace + "changefreq", "monthly"),
172192
new XElement(_sitemapNamespace + "lastmod", lastModified.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
173193
new XElement(_sitemapNamespace + "priority", 1),
174-
new XElement(_pagemapNamespace + "PageMap",
194+
includeMetadata? new XElement(_pagemapNamespace + "PageMap",
175195
new XElement(_pagemapNamespace + "DataObject",
176196
new XAttribute("type", topic.ContentType?? "Page"),
177197
getAttributes()
178198
),
179199
getRelationships()
180-
)
200+
) : null
181201
);
182-
topics.Add(topicElement);
202+
if (!topic.ContentType!.Equals("Container", StringComparison.InvariantCultureIgnoreCase)) {
203+
topics.Add(topicElement);
204+
}
183205

184206
/*------------------------------------------------------------------------------------------------------------------------
185207
| Iterate over children
186208
\-----------------------------------------------------------------------------------------------------------------------*/
187209
foreach (var childTopic in topic.Children) {
188-
topics.AddRange(AddTopic(childTopic));
210+
topics.AddRange(AddTopic(childTopic, includeMetadata));
189211
}
190212

191213
return topics;

OnTopic.AspNetCore.Mvc/OnTopic.AspNetCore.Mvc.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939

4040
<ItemGroup>
4141
<FrameworkReference Include="Microsoft.AspNetCore.App" />
42-
<PackageReference Include="GitVersionTask" Version="5.3.3">
42+
<PackageReference Include="GitVersionTask" Version="5.3.7">
4343
<PrivateAssets>all</PrivateAssets>
4444
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4545
</PackageReference>
46-
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.0.0">
46+
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0">
4747
<PrivateAssets>all</PrivateAssets>
4848
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
4949
</PackageReference>

OnTopic.AspNetCore.Mvc/ServiceCollectionExtensions.cs

+138-8
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
| Client Ignia, LLC
44
| Project Topics Library
55
\=============================================================================================================================*/
6-
using OnTopic.AspNetCore.Mvc.Controllers;
7-
using OnTopic.Internal.Diagnostics;
6+
using System;
87
using Microsoft.AspNetCore.Builder;
98
using Microsoft.AspNetCore.Mvc.Infrastructure;
9+
using Microsoft.AspNetCore.Mvc.TagHelpers;
1010
using Microsoft.AspNetCore.Routing;
1111
using Microsoft.Extensions.DependencyInjection;
1212
using Microsoft.Extensions.DependencyInjection.Extensions;
13+
using OnTopic.AspNetCore.Mvc.Controllers;
14+
using OnTopic.Internal.Diagnostics;
1315

1416
namespace OnTopic.AspNetCore.Mvc {
1517

@@ -38,6 +40,7 @@ public static IMvcBuilder AddTopicSupport(this IMvcBuilder services) {
3840
| Register services
3941
\-----------------------------------------------------------------------------------------------------------------------*/
4042
services.Services.TryAddSingleton<IActionResultExecutor<TopicViewResult>, TopicViewResultExecutor>();
43+
services.Services.TryAddSingleton<TopicRouteValueTransformer>();
4144

4245
/*------------------------------------------------------------------------------------------------------------------------
4346
| Configure services
@@ -64,6 +67,12 @@ public static IMvcBuilder AddTopicSupport(this IMvcBuilder services) {
6467
/// <summary>
6568
/// Adds an MVC route for handling OnTopic related requests, and maps it to the <see cref="TopicController"/> by default.
6669
/// </summary>
70+
/// <remarks>
71+
/// For ASP.NET Core 3, prefer instead <see cref="MapTopicRoute(IEndpointRouteBuilder, String, String, String)"/>, as
72+
/// endpoint routing is preferred in ASP.NET Core 3. OnTopic also offers far more extension methods for endpoint routing,
73+
/// while this method is provided exclusively for backward compatibility.
74+
/// </remarks>
75+
[Obsolete("This method is deprecated and will be removed in OnTopic 5. Callers should migrate to endpoint routing.", false)]
6776
public static IRouteBuilder MapTopicRoute(
6877
this IRouteBuilder routes,
6978
string rootTopic,
@@ -80,12 +89,10 @@ public static IRouteBuilder MapTopicRoute(
8089
| EXTENSION: MAP TOPIC ROUTE (IENDPOINTROUTEBUILDER)
8190
\-------------------------------------------------------------------------------------------------------------------------*/
8291
/// <summary>
83-
/// Adds an MVC route for handling OnTopic related requests, and maps it to the <see cref="TopicController"/> by default.
92+
/// Adds the <c>{rootTopic}/{**path}</c> endpoint route for a specific root topic, which enables that root to be mapped to
93+
/// specific topics via the <see cref="TopicRepositoryExtensions.Load(Repositories.ITopicRepository, RouteData)"/>
94+
/// extension method used by <see cref="TopicController"/>.
8495
/// </summary>
85-
/// <remarks>
86-
/// This is functionally identical to <see cref="MapTopicRoute(IRouteBuilder, String, String, String)"/>, except that it
87-
/// targets the <see cref="IEndpointRouteBuilder"/>, which is preferred in ASP.NET Core 3.
88-
/// </remarks>
8996
public static ControllerActionEndpointConventionBuilder MapTopicRoute(
9097
this IEndpointRouteBuilder routes,
9198
string rootTopic,
@@ -94,10 +101,133 @@ public static ControllerActionEndpointConventionBuilder MapTopicRoute(
94101
) =>
95102
routes.MapControllerRoute(
96103
name: $"{rootTopic}Topic",
97-
pattern: rootTopic + "/{*path}",
104+
pattern: rootTopic + "/{**path}",
98105
defaults: new { controller, action, rootTopic }
99106
);
100107

108+
/*==========================================================================================================================
109+
| EXTENSION: MAP TOPIC AREA ROUTE (IENDPOINTROUTEBUILDER)
110+
\-------------------------------------------------------------------------------------------------------------------------*/
111+
/// <summary>
112+
/// Adds the <c>{areaName}/{**path}</c> endpoint route for a specific area, which enables the areas to be mapped to
113+
/// specific topics via the <see cref="TopicRepositoryExtensions.Load(Repositories.ITopicRepository, RouteData)"/>
114+
/// extension method used by <see cref="TopicController"/>.
115+
/// </summary>
116+
/// <remarks>
117+
/// If there are multiple routes that fit this description, you can instead opt to use the <see cref=
118+
/// "MapTopicAreaRoute(IEndpointRouteBuilder)"/> extension, which will register all areas.
119+
/// </remarks>
120+
public static ControllerActionEndpointConventionBuilder MapTopicAreaRoute(
121+
this IEndpointRouteBuilder routes,
122+
string areaName,
123+
string? controller = null,
124+
string action = "Index"
125+
) =>
126+
routes.MapAreaControllerRoute(
127+
name: $"TopicAreas",
128+
areaName: areaName,
129+
pattern: areaName + "/{**path}",
130+
defaults: new { controller = controller?? areaName, action, rootTopic = areaName }
131+
);
132+
133+
/// <summary>
134+
/// Adds the <c>{area:exists}/{**path}</c> endpoint route for all areas, which enables the areas to be mapped to specific
135+
/// topics via the <see cref="TopicRepositoryExtensions.Load(Repositories.ITopicRepository, RouteData)"/> extension method
136+
/// used by <see cref="TopicController"/>.
137+
/// </summary>
138+
/// <remarks>
139+
/// Be aware that this method uses the <see cref="ControllerEndpointRouteBuilderExtensions.MapDynamicControllerRoute{
140+
/// TTransformer}(IEndpointRouteBuilder, String)"/> method. In .NET 3.x, this is incompatible with both the <see cref=
141+
/// "AnchorTagHelper"/> and <see cref="LinkGenerator"/> classes. This means that e.g. <c>@Url.Action()</c> references
142+
/// in views won't be properly formed. If these are required, prefer registering each route individually using <see cref=
143+
/// "MapTopicAreaRoute(IEndpointRouteBuilder, String, String?, String)"/>.
144+
/// </remarks>
145+
public static void MapTopicAreaRoute(this IEndpointRouteBuilder routes) =>
146+
routes.MapDynamicControllerRoute<TopicRouteValueTransformer>("{area:exists}/{**path}");
147+
148+
/*==========================================================================================================================
149+
| EXTENSION: MAP DEFAULT AREA CONTROLLER ROUTES (IENDPOINTROUTEBUILDER)
150+
\-------------------------------------------------------------------------------------------------------------------------*/
151+
/// <summary>
152+
/// Adds the fully-qualified <c>{area:exists}/{controller}/{action=Index}/{id?}</c> endpoint route for all areas.
153+
/// </summary>
154+
/// <remarks>
155+
/// This is analogous to the standard <see cref="ControllerEndpointRouteBuilderExtensions.MapDefaultControllerRoute(
156+
/// IEndpointRouteBuilder)"/> method that ships with ASP.NET, except that it works with areas.
157+
/// </remarks>
158+
public static void MapDefaultAreaControllerRoute(this IEndpointRouteBuilder routes) =>
159+
routes.MapControllerRoute(
160+
name: "TopicAreas",
161+
pattern: "{area:exists}/{controller}/{action=Index}/{id?}"
162+
);
163+
164+
/*==========================================================================================================================
165+
| EXTENSION: MAP IMPLICIT AREA CONTROLLER ROUTES (IENDPOINTROUTEBUILDER)
166+
\-------------------------------------------------------------------------------------------------------------------------*/
167+
/// <summary>
168+
/// Adds the <c>{areaName}/{action=Index}</c> endpoint route for a specific area where the controller has the same name as
169+
/// the area.
170+
/// </summary>
171+
/// <remarks>
172+
/// <para>
173+
/// This extension method implicitly assigns the controller name based on the area name. This is advantageous when an
174+
/// area has a single controller which is named after the area—e.g., <c>[Area("Forms")]</c> and <c>FormsController</c>—
175+
/// as this allows the redundant <c>Controller</c> to be ommited from the route (e.g., <c>/Forms/Forms/{action}</c>.
176+
/// </para>
177+
/// <para>
178+
/// If there are multiple routes that fit this description, you can instead opt to use the <see cref=
179+
/// "MapImplicitAreaControllerRoute(IEndpointRouteBuilder)"/> overload, which will register all areas.
180+
/// </para>
181+
/// </remarks>
182+
public static void MapImplicitAreaControllerRoute(this IEndpointRouteBuilder routes, string areaName) =>
183+
routes.MapAreaControllerRoute(
184+
name: $"{areaName}TopicArea",
185+
areaName: areaName,
186+
pattern: $"{areaName}/{{action}}",
187+
defaults: new { controller = areaName }
188+
);
189+
190+
/// <summary>
191+
/// Adds the <c>{area:exists}/{action=Index}</c> endpoint route for all areas where the controller has the same name as
192+
/// the area.
193+
/// </summary>
194+
/// <remarks>
195+
/// <para>
196+
/// This extension method implicitly assigns the controller name based on the area name. This is advantageous when there
197+
/// are multiple areas which have a single controller which is named after the area—e.g., <c>[Area("Forms")]</c> and
198+
/// <c>FormsController: Controller</c>—as this allows those to be collectively registered under a single route, without
199+
/// needing the redundant <c>Controller</c> value to be defined in the route (e.g., <c>/Forms/Forms/{action}</c>.
200+
/// </para>
201+
/// <para>
202+
/// Be aware that this method uses the <see cref="ControllerEndpointRouteBuilderExtensions.MapDynamicControllerRoute{
203+
/// TTransformer}(IEndpointRouteBuilder, String)"/> method. In .NET 3.x, this is incompatible with both the <see cref=
204+
/// "AnchorTagHelper"/> and <see cref="LinkGenerator"/> classes. This means that e.g. <c>@Url.Action()</c> references
205+
/// in views won't be properly formed. If these are required, prefer registering each route individually using <see
206+
/// cref="MapImplicitAreaControllerRoute(IEndpointRouteBuilder, String)"/>.
207+
/// </para>
208+
/// </remarks>
209+
public static void MapImplicitAreaControllerRoute(this IEndpointRouteBuilder routes) =>
210+
routes.MapDynamicControllerRoute<TopicRouteValueTransformer>("{area:exists}/{action=Index}");
211+
212+
/*==========================================================================================================================
213+
| EXTENSION: MAP TOPIC SITEMAP (IENDPOINTROUTEBUILDER)
214+
\-------------------------------------------------------------------------------------------------------------------------*/
215+
/// <summary>
216+
/// Adds the <c>Sitemap/{action=Index}</c> endpoint route for the OnTopic sitemap.
217+
/// </summary>
218+
/// <remarks>
219+
/// For most implementations, this will be covered by the default route, such as that implemented by the standard <see
220+
/// cref="ControllerEndpointRouteBuilderExtensions.MapDefaultControllerRoute(IEndpointRouteBuilder)"/> method that ships
221+
/// with ASP.NET. This extension method is provided as a convenience method for implementations that aren't using the
222+
/// standard route, for whatever reason, and want a specific route setup for the sitemap.
223+
/// </remarks>
224+
public static ControllerActionEndpointConventionBuilder MapTopicSitemap(this IEndpointRouteBuilder routes) =>
225+
routes.MapControllerRoute(
226+
name: "TopicSitemap",
227+
pattern: "Sitemap/{action=Index}",
228+
defaults: new { controller = "Sitemap" }
229+
);
230+
101231
/*==========================================================================================================================
102232
| EXTENSION: MAP TOPIC REDIRECT (IENDPOINTROUTEBUILDER)
103233
\-------------------------------------------------------------------------------------------------------------------------*/

0 commit comments

Comments
 (0)