ODataRoutePrefix not working for 3rd level - asp.net-web-api2

When I try to use a third level segment with [ODataRoutePrefix] attribute it throws an error like this:
The path template '[TEMPLATE]' on the action 'Get' in controller 'CONTROLLER-NAME' is not a valid OData path template. Found an unresolved path segment '[LAST-SEGMENT]' in the OData path template '[TEMPLATE]'.
Details
ODataRoutePrefix for the parent segment that works:
[ODataRoutePrefix("lawsuits/{parentId}/depositsGuarantees")]
ODataRoutePrefix for the new controller that DOES NOT work:
[ODataRoutePrefix("lawsuits/{parentId}/depositsGuarantees/{subResourceId}/customFields")]
Error message when starting up the API (config.EnsureInitialized()):
The path template 'lawsuits/{parentId}/depositsGuarantees/{subResourceId}/customFields' on the action 'Get' in controller 'LawsuitDepositGuaranteeCustomFields' is not a valid OData path template. Found an unresolved path segment 'customFields' in the OData path template 'lawsuits/{parentId}/depositsGuarantees/{subResourceId}/customFields'.
Additional info
The action methods properly expect the parameters defined in the Route prefix. Example:
public IHttpActionResult Get(int parentId, int subResourceId)
{
// [...]
}
Although it complained that "Found an unresolved path segment 'customFields'", the same segment works for other existing controllers with the prefix like the following: [ODataRoutePrefix("lawsuits/{parentId}/customFields")], which means there's a model registered on OData for the "customFields" segment.
Assemblies affected
OData WebApi lib 6.0.0

I've found what was the problem...
The model for the depositsGuarantees segment was missing a collection property of the model registered for the customFields segment. In practice this property was missing :
public IEnumerable<CustomFieldModel> CustomFields { get; set; }
I still wonder if I should really use lawsuits/{parentId}/depositsGuarantees/{subResourceId}/customFields or just depositsGuarantees/{parentId}/customFields (even though depositsGuarantees is also a sub-resource), but this is more a conceptual discussion.

Related

Blazor #page route url define with variable

I have a question for Blazor Server Side.
I want to #page route url define with variable or property.
I can use now with below default method
#page "/route-url"
<h1>Page Test</h1>
#code {
}
But i want use like as below method
#page MenuItem.Role
<h1>Page Test</h1>
#code {
}
I'm tried above method then throwed exception. Like as below exception.
C:\Projects\TestBlazorProject\Pages\TestPage.razor(1,7): error RZ1016: The 'page' directive expects a string surrounded by double quotes. [C:\Projects\TestBlazorProject\TestBlazorProject.csproj]
How to define #page route url with any different variable or any class property?
Can this be done?
Yes
How?
Page file
#attribute [Route(PageRoute.TaskList)]
<div>PAGE HTML HERE</div>
#code{ ... }
PageRoute.cs:
public static class PageRoute
{
public const string TaskList = "/route-url";
}
Explanation
The page directive gets compiled down to an attribute and has the same restrictions as C# attributes.
You can use the #attribute with the [Route] attribute and use string concatenation instead of string interpolation to define a constant for the route, since that's what the C# compiler supports.
Why would anybody do this?
This is a good coding practice, because you are not hardcoding the page/component name in multiple places, but only in one place.
So one fine day when you manager asks to change page name "Earth" to "Planet3",
you just change it in 1 place, and be 98% sure that your app wont crash because of it.
#page isn't C#, it's Razor talk. Razor files are pre-compiled into c# files during compilation.
As an example, this is the important section of the C# pre-compiled file for Index.razor (Index.razor.g.cs):
[Microsoft.AspNetCore.Components.RouteAttribute("/")]
public partial class Index : Microsoft.AspNetCore.Components.ComponentBase
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
{
__builder.AddMarkupContent(0, "<h1>Hello, world!</h1>\r\n\r\nWelcome to your new app.\r\n\r\n");
__builder.OpenComponent<Blazor.Starter.Shared.SurveyPrompt>(1);
__builder.AddAttribute(2, "Title", "How is Blazor working for you?");
__builder.CloseComponent();
}
#pragma warning restore 1998
}
Note that #page has become a compile time attribute [Microsoft.AspNetCore.Components.RouteAttribute("/")]. It's fixed at compiletime, you can't change it at runtime.
Routes are set this way because the router builds a routemap - essentially a dictionary of route url/component class pairs - when the application loads by trawling the application assembly for any component classes with a Route attribute. On a routing event it reads the new url, finds the component class and loads it into the layout. Any variables - stuff in curly brackets - get passed into the component as Parameters.
You haven't made it clear what the line below is supposed to do:
#page MenuItem.Role
Do you want to capture a variable supplied in the route into MenuItem.Role?
Do you want to set this page's route to the value in MenuItem.Role?
If 1, either the other answers will work for you. If 2, you'll need to consider writing your own router. A subject beyond a simple answer here.
I think you can achieve that by following.
#page "/{Role}"
#code{
[Parameter]
public string Role { get; set; }
}
Building off of the above you can I was able to get this to work with the code isolation approach.
Client/Pages/Foo/
----Index.razor
----Index.cs
namespace Client.Pages.Foo;
[Microsoft.AspNetCore.Components.RouteAttribute(Path)]
public partial class Index
{
public const string Path = "/Foo";
}

Using ApiExplorerSettings GroupName in Route in ASPNET Core

In ASP.NET Core - Web API project
Using [ApiExplorerSettings(GroupName = "<group-name>")] decorated on ApiController
and in [Route] attribute I want to refer above GroupName property value.
Also note I do have [ApiVersion("<some-version>")] on same controller to classify further.
Here are some samples to explain:
Example 1:
Attribute on LeadController: [ApiVersion("1.0"), ApiExplorerSettings(GroupName = "sales"), [Route("api/{groupName}/v{version:apiVersion}/leads"]
Expected translated route format: /api/sales/v1/leads
Attribute on AccountsController: [ApiVersion("2.1"), ApiExplorerSettings(GroupName = "finance"), [Route("api/{groupName}/v{version:apiVersion}/accounts"]
Expected translated route format: /api/finance/v2.1/leads
In above {version:apiVersion} gives me ApiVersion value (I assume because that attribute has ToString set to version value). But when I try {groupName} or {grp:groupName} or {grp:ApiExplorerSettings.GroupName} - none of them works. How can access this group name in route attribute?
Do you have any special settings somewhere else, it works fine on my side.
LeadController:
[ApiVersion("1.0"), ApiExplorerSettings(GroupName = "sales"),Route("api/{groupName}/v{version:apiVersion}/leads")]
[ApiController]
public class LeadController : ControllerBase
{
public string Get()
{
return "sales group";
}
}
AccountsController:
[ApiVersion("2.1"), ApiExplorerSettings(GroupName = "finance"), Route("api/{groupName}/v{version:apiVersion}/accounts")]
[ApiController]
public class AccountsController : ControllerBase
{
public string Get()
{
return "finance group";
}
}
Result:
ApiExplorerSettings.GroupName is only for the logical group name used in API descriptions. It is not used or evaluated in route templates. There is no way to make that work. That is why it never expands. It is possible to use a {groupName} route parameter, but it will be just that - a route parameter.
It's not entirely clear if you are trying to control grouping for the API Explorer or use a route parameter. Grouping is typically used by the API Explorer. API Versioning will assign or override the group name so that APIs are bucketized by their version. It is possible to change this behavior, but you'll need an API Explorer (via IApiDescriptionProvider) or OpenAPI/Swagger document generator extension to do it.

How are Razor Page View Component paths searched? I fail to get them working, .NET Core tries to include irrelevant model

I cannot get ViewComponent to be included in a page.
I have a folder structure:
- Pages
- Components
- ExternalSystems
- Default.cshtml
- Views
- Shared
- Components
- ExternalSystems
- Default.cshtml
Class file
public class Default : ViewComponent
{
private readonly Models.PermissionRegisterContext _context;
public Default(Models.PermissionRegisterContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var externalSystems = await _context.ExternalSystem.ToListAsync();
return View("Default", externalSystems);
}
}
within Pages/Index.cshtml i'v tried to include this component by trial & error (i'm learning this stuff after all):
#await Component.InvokeAsync("ExternalSystems");
#await Component.InvokeAsync("/Pages/Components/ExternalSystems");
#await Component.InvokeAsync("/Pages/Components/ExternalSystems/Default");
#await Component.InvokeAsync("/Pages/Components/ExternalSystems/Default.cshtml");
#await Component.InvokeAsync("/Views/Shared/ExternalSystems");
I expected that exception will show places searched like it does for #Html.Partial:
InvalidOperationException: The partial view 'none' was not found. The following locations were searched: /Pages/none.cshtml /Views/Shared/none.cshtml
However for every single call to #await Component.InvokeAsync it spits this exception:
InvalidOperationException: Could not find an 'Invoke' or 'InvokeAsync' method for the view component '.Pages.Identities.IndexModel'.
First of all, it doesn't show me paths searched. Second, wait what? Why are you telling me something about Pages.Identities? Yes, I have that model, but it is nowhere referenced in neither Pages/Index.cshtml or View Component i'm trying to include.
Could someone please provide me with a guidance to determine: How view component paths are searched? For Razor Pages it is not documented, only in 3rd party site.
Anyway it doesn't work that way for me - what would be the debugging steps? Console debug doesn't show anything useful.
dotnet 2.0.7
Let's say you have created a view component as follows :
public class ExternalSystems : ViewComponent
{
public ExternalSystems()
{
//constructor can have dependencies injected.
}
public IViewComponentResult Invoke()
{
//View is a helper method available in classes inherited
//from ViewComponent that returns an instance of
//ViewViewComponentResult. It has multiple overloads as described
//later.
return View(viewName,viewModel);
}
}
View method has multiple overrides :
View() - use default view for view component without any viewmodel
View(viewModel) - use default view for view component with specified viewmodel
View(viewName) - use specified view for view component without any viewmodel
View(viewName,viewModel) - use specified view for view component with specified viewmodel
When you try to render this view component from a Controller, view will be looked up at following locations :
"/Views/{ControllerName}/Components/ExternalSystems/{ViewName}.cshtml" .
So, if you are using HomeController and have specified viewName as ExternalSystemsView in the View(viewName,viewModel) call , your path becomes
/Views/Home/Components/ExternalSystems/ExternalSystemsView.cshtml . This allows each controller to have its own custom view for the view returned by view component.
If the ExternalSystemsView.cshtml is not located at above path , it will be looked up at /Views/Shared/Components/ExternalSystems/ExternalSystemsView.cshtml
You can override the lookup position by passing the complete path of view - View("Views/Shared/Components/Common/YourView.cshtml") while calling View(viewName,viewModel) from your ViewComponent's Invoke method.
Note : If you don't specify a viewName, it defaults to Default.cshtml which is different from Index.html used for controllers
For your case, #await Component.InvokeAsync("ExternalSystems") is the correct call as it expects the viewcomponent name as parameter. ViewName will be picked up from what you have passed as the viewName parameter value to View(viewName,viewModel) call in your ViewComponent's Invoke method and will default to Default.cshtml if no viewname has been specified.
Alright, my mistake was that I didn't see I have decorated ...Pages.Identities.IndexModel with [ViewComponent]. It was unintentional and was just trying to make ViewComponents work by trial & error and failed to see this mistake.
Basically dotnet discovered type that didn't have either Invoke/InvokeAsync function and thus was stuck on that exception.
I validated that these paths are searched when providing "HelloWorld" as view name:
/Pages/Components/HelloWorld/HelloWorld.cshtml
/Views/Shared/Components/HelloWorld/HelloWorld.cshtml

Using attribute routing on a controller is forcing me to manage all routes

I am just getting to grips with Asp.net Core and I'm trying to set up a basic site.
I want to build an admin panel that is under a subdirectory.
I have a simple controller which was scaffolded by the EF crud feature.
So it seems that from the examples I should just be able to add a [Route()] attribute to the controller and it will prefix everything. Something like this:
[Route("Admin/Subfolder/[controller]")]
public class EventsController : Controller
{
}
But when I do that I just get an error page saying "multiple actions matched" and it lists index, details, create, etc.
I can get it working if I then go through every method and put a [Route()] attribute on it but this doesn't seem to be in line with the documentation.
It feels like I should be able to just add a prefix to the controller route without having to take over management of every route within the controller. Case in point, the POSTS are not working now and I'm not sure what the format of the route attribute should be for them.
What am I doing wrong?
You are doing it correctly. Default route attribute can be applied at the controller level. “Placing a route attribute on the controller makes all actions in the controller use attribute routing.”
Can you post complete code of your controller? There must be something else going on in there. Make sure you use HttpPost/HttpGet attribute for actions with the same name, like so:
[Route("Admin/Subfolder/[controller]")]
public class EventsController : Controller
{
[HttpGet]
public IActionResult NewEvent()
{ }
[HttpPost]
public IActionResult NewEvent()
{ }
}
Good explanation on routing can be found here

ASP.NET Core DisplayAttribute Localization

According to the documentation:
The runtime doesn’t look up localized strings for non-validation attributes. In the code above, “Email” (from [Display(Name = "Email")]) will not be localized.
I'm looking for a way to localize text in DisplayAttribute. Any suggestions to do it in a proper way(s)?
You can set the ResourceType on the DisplayAttribute which can be used to localize your text.
Add a resource .resx file to your project e.g. MyResources.resx, and add a resource for your field:
Then reference the name of the field and the MyResources type in your DisplayAttribute
[Display(Name = "RememberMe", ResourceType = typeof(MyResources))]
public bool RememberMe { get; set; }
The localized resource will be pulled through automatically (see the text box)
Having a Central location for all your localization whether in view or dataannotations is the best approach I can think of, and this how I got to work.
In Startup.cs file after you installed nuget packages for localization add the following code
services.AddMvc().AddViewLocalization().AddDataAnnotationsLocalization(options =>
options.DataAnnotationLocalizerProvider = (type, factory) => new StringLocalizer<Resources>(factory));
services.Configure<RequestLocalizationOptions>(options => {
var cultures = new[]
{
new CultureInfo("en"),
new CultureInfo("ar")
};
options.DefaultRequestCulture = new RequestCulture("en", "en");
options.SupportedCultures = cultures;
options.SupportedUICultures = cultures;
});
This way the DataAnnotationLocalizerProvider will be from the Resources.{culture}.rex -( The Resource file must have an access modifier of No code gen)- assuming that no resources will be needed for the default language, and to be able to access the resource file since no code will be generated and empty class with the same name must be created.
and in _ViewImports.cshtml file inject the following
#inject IHtmlLocalizer<Resources> Localizer
by doing this you now have a global variable Localizer to be used in any of the views for localization purposes.
you can find further information on Globalization and localization in ASP.NET Core
For those who struggle (#lucius, #vladislav) with error:
Cannot retrieve property 'Name' because localization failed. Type 'Xxxx.EmployeeResx' is not public or does not contain a public static string property with the name 'FirstName'.
It is caused by access modifier on .resx files which is by default set to Internal (in my case it was No code generation). Change it to public in Access Modifier dropdown in the resource file toolbar.
After that you should be able to see the properties from the resource type:
Also, consider not using special signs in field names as they are a basis for auto-generated C# property names. The field names are converted into C# friendly names and that is why you can end up with inconsistency between name of resource file field and name of auto-generated property. Best to avoid any hyphens - or dots . Underscores _ are fine. You can always look up how the auto-generated properties look like in resource_file_name.Designer.cs class under the related resource file.
Many thanks to Bala Murugan who wrote a good article concerning this topic on Code Digest.
Actually I found an simple solution for the followers. The display name in most of time is used in the label of an input field. So do this if you like:
<label asp-for="Email">#Localizer["Email"]</label>
of course, you can pass the property name by #Html.DisplayNameFor, but most of time, this one already works well.
I have just created a project which demonstrates localization including localization of Display attribute for class properties as well as enums.
The project can be found here https://github.com/feradz/ASPNetCoreLocalization/wiki
The Display attribute has to be localized using the approach prior to ASP.NET Core 1.0. Have a look at the DataAnnotations.resx file in the project.
The Name property of Display cannot contain empty spaces, and special characters.
[Display(Name = "NoSpacesAndSpecialChanractersHere", ResourceType = typeof(Resources.DataAnnotations))]
public string FirstName { get; set; }
ResourceType should be the fully qualified resource class name (i.e. including the name space).