But with overriding the page like this I can't change it from /product?id=123 to /product-with-longer-name?id=123 or /product-with-longer-name/123 that is used when adding the page in Backoffice.
Also, above setup requires me to implement FindContent (CurrentPage is always null) which is not something I need.
I mean that you should be able to, in the Umbraco Backoffice, to supply any initial routing to this page, then just add a magical route /{id} at the end and it maps to the supplied material. Otherwise, it's not flexible. And query strings are ugly.
Current Setup that works as it should, but only with Query Strings:
public class ProductPageController : DefaultRenderController<ProductPageController>
{
private readonly IServiceProvider serviceProvider;
public ProductPageController(IServiceProvider serviceProvider, ILogger<RenderController> logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor, IVariationContextAccessor variationContextAccessor, ServiceContext context) : base(logger, compositeViewEngine, umbracoContextAccessor, variationContextAccessor, context)
{
this.serviceProvider = serviceProvider;
}
// Tried working with these with no success
//[Route("[controller]/[action]/{id?}")]
[HttpGet]
public IActionResult Index([FromQuery(Name="id")] string id)
{
var product = serviceProvider
.GetRequiredService<ProductDatabaseService>()
.GetProduct(id);
return CurrentTemplate(new ProductPageModel(product, CurrentPage, Fallback));
}
}
public class ProductPageModel : PublishedContentWrapped
{
public ProductPageModel(Product product, IPublishedContent content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback)
{
this.Product = product;
}
public Product Product { get; set; }
}
Umbraco gives you several ways to map and handle routes.
If part of the route is predictable eg /products/product-name/123
Then your controller can implement IVirtualPageController and this involves implementing a method alongside the route called FindContent which has access to the full request context, and has the responsibility of finding and associating a model which implements IPublishedContent with the request. Now this item 'could' be a content item in umbraco, but it doesn't need to be, as long as your model implements IPublishedContent it doesn't need to be in Umbraco, you can construct it from an external source perhaps using PublishedContentWrapped class as a starting point. Sometimes I'll have a placeholder content item published in Umbraco and I'll grab that and then add external info retrieved with the route parameters... Then in your Controller indexActionResult Current Page becomes what you have constructed...
... But if you don't have a predictable pattern to the Url, and editors can move it around, change its name, there is no fixed prefix to route by, but there is a pattern... then you are in the world of ContentFinders...
The way Umbraco handles a non routed request is to run the request through a series of IContentFinder implementations, each with their own logic for how the Url might match to an Umbraco content item, one after another until, the first one finds a match and takes responsibility for handling the request and setting the associated IPublishedContent item. (again doesn't need to be from Umbraco, it is the same concept as the IVirtualPage Controller, except it is actioned for all routes)
This is the list of core ContentFinders and the order in which they execute...
Therefore if you were to create your own IContentFinder called ProductAndProductIdContentFinder
And add it to the collection of content finders just before the ContentFinderByRedirectUrl
Then, when a request came in for /any-old-product-name/123
That wouldn't make a match for any of the core Umbraco Content Finders because 123 isn't a content item...
So it would fall through to your content finder and in your Try Find Content method you could take off the number, and use the remaining url to find in the Umbraco published cache the content item at /any-old-product-name, use it to construct an IPublishedContent Item, using PublishedContentWrapped and add a new property called ProductId and set it to be the route parameter eg 123, and return 'true'... The request will be handled and because the Document Type of the content item will be Product, your ProductController above will still hijack and handle the request but this time your CurrentPage will include your product Id parameter...
Phew, but appreciate what you mean about it would be good if it just worked like that out of the box, but I guess it's trying to cater for lots of different content management rendering scenarios.
Anyway hope thst gives you a steer on the routing possibilities!
[FromQuery] -> [FromRoute] and then use Route Hijacking like anything else. Currently, I get a query string ([FromQuery]string id) for my custom content. I fetch it from a custom / separate database and return the model, render the content. I can put this anywhere on the site. It is not hard coded to "/fixed/site?id={123}" which is preferable. But [Route] does not work with parameters.
Above approach and any approach I've found seems way to complex for this relative simply task.
If there's no better way than to hard map it via MapControllerRoute then I'll have to do that then. That approach can potentially break links that someone adds with the same page structure, though.
For the flexibility you describe I'd probably go with the IContentFinder approach
Like you say, mapping specific routes is likely to break, when the Urls change when things get updated or moved around by editors...
The IContentFinder gives you a way to match the root and get your data from your external system, but still use route hijacking by doc type to render the view..
I thing that is the better way!
If you stub one out and register it in the Content Finder collection you'll get a good idea of the simplicity and flexibility it provides.
Maybe we can contribute what you build back into the core so this pattern is an option available for others to use in this hybrid routing setup?
Custom Controller - With Route Params Instead of Query Strings
Basically, this:
https://docs.umbraco.com/umbraco-cms/reference/routing/custom-controllers#processing-querystring-values-in-the-controller
But instead of the routes being: https://www.mydomain.com/product?id=123 It should be: https://www.mydomain.com/product/123
This routing seems to work when using UmbracoPageController and:
But with overriding the page like this I can't change it from /product?id=123 to /product-with-longer-name?id=123 or /product-with-longer-name/123 that is used when adding the page in Backoffice.
Also, above setup requires me to implement FindContent (CurrentPage is always null) which is not something I need.
I mean that you should be able to, in the Umbraco Backoffice, to supply any initial routing to this page, then just add a magical route /{id} at the end and it maps to the supplied material. Otherwise, it's not flexible. And query strings are ugly.
Current Setup that works as it should, but only with Query Strings:
Hi Jimmy
Umbraco gives you several ways to map and handle routes.
If part of the route is predictable eg /products/product-name/123
Then your controller can implement IVirtualPageController and this involves implementing a method alongside the route called FindContent which has access to the full request context, and has the responsibility of finding and associating a model which implements IPublishedContent with the request. Now this item 'could' be a content item in umbraco, but it doesn't need to be, as long as your model implements IPublishedContent it doesn't need to be in Umbraco, you can construct it from an external source perhaps using PublishedContentWrapped class as a starting point. Sometimes I'll have a placeholder content item published in Umbraco and I'll grab that and then add external info retrieved with the route parameters... Then in your Controller indexActionResult Current Page becomes what you have constructed...
https://docs.umbraco.com/umbraco-cms/reference/routing/custom-routes/
... But if you don't have a predictable pattern to the Url, and editors can move it around, change its name, there is no fixed prefix to route by, but there is a pattern... then you are in the world of ContentFinders...
The way Umbraco handles a non routed request is to run the request through a series of IContentFinder implementations, each with their own logic for how the Url might match to an Umbraco content item, one after another until, the first one finds a match and takes responsibility for handling the request and setting the associated IPublishedContent item. (again doesn't need to be from Umbraco, it is the same concept as the IVirtualPage Controller, except it is actioned for all routes) This is the list of core ContentFinders and the order in which they execute...
https://github.com/umbraco/Umbraco-CMS/blob/d3b96a163feb031f8b33d63ab68098d65aa6686d/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs#L57
Therefore if you were to create your own IContentFinder called ProductAndProductIdContentFinder
And add it to the collection of content finders just before the ContentFinderByRedirectUrl
Then, when a request came in for /any-old-product-name/123
That wouldn't make a match for any of the core Umbraco Content Finders because 123 isn't a content item...
So it would fall through to your content finder and in your Try Find Content method you could take off the number, and use the remaining url to find in the Umbraco published cache the content item at /any-old-product-name, use it to construct an IPublishedContent Item, using PublishedContentWrapped and add a new property called ProductId and set it to be the route parameter eg 123, and return 'true'... The request will be handled and because the Document Type of the content item will be Product, your ProductController above will still hijack and handle the request but this time your CurrentPage will include your product Id parameter...
Phew, but appreciate what you mean about it would be good if it just worked like that out of the box, but I guess it's trying to cater for lots of different content management rendering scenarios.
Anyway hope thst gives you a steer on the routing possibilities!
Regards
Marc
To simplify:
[FromQuery] -> [FromRoute] and then use Route Hijacking like anything else. Currently, I get a query string ([FromQuery]string id) for my custom content. I fetch it from a custom / separate database and return the model, render the content. I can put this anywhere on the site. It is not hard coded to "/fixed/site?id={123}" which is preferable. But [Route] does not work with parameters.
Routes that preferable should work:
Above approach and any approach I've found seems way to complex for this relative simply task.
If there's no better way than to hard map it via MapControllerRoute then I'll have to do that then. That approach can potentially break links that someone adds with the same page structure, though.
Hi Jimmy
For the flexibility you describe I'd probably go with the IContentFinder approach
Like you say, mapping specific routes is likely to break, when the Urls change when things get updated or moved around by editors...
The IContentFinder gives you a way to match the root and get your data from your external system, but still use route hijacking by doc type to render the view..
I thing that is the better way!
If you stub one out and register it in the Content Finder collection you'll get a good idea of the simplicity and flexibility it provides.
Maybe we can contribute what you build back into the core so this pattern is an option available for others to use in this hybrid routing setup?
Regards
Marc
Regards
Marc
is working on a reply...