We have ASP.NET MVC 5 project, that we have plans to migrate to ASP.NET Core 3. Currently I gather dependency list and their equivalent replacements on the new new platform.
We are using MvcCodeRouting package to separate different workflows between different C# namespaces as was described in https://maxtoroq.github.io/2013/02/aspnet-mvc-workflow-per-controller.html.
Now with new platform in place, we need something similar. As last resort we could just specify all our namespaces in routing table, but I'd rather not to do so.
Any suggestions on how accomplish similar behavior?
EDIT:
I think with example it would be more understandable what I'm trying to accomplish.
We have following structure of controllers:
- Namespace1
-- Workflow1Controller.Index/Edit/Action
-- Workflow2Controller.Index/Edit/Action
-- Workflow3Controller.Index/Edit/Action
- Namespace2
-- Workflow4Controller.Index/Edit/Action
Workflow1Controller code:
namespace RootProjectNamespace.Controllers.Namespace1
{
class Workflow1Controller : Controller
{
public ActionResult Index() {}
// and so on
}
}
Appropriate Views are placed in similar manner.
And using MvcCodeRouting we able to create Action urls by following:
Url.Action("Namespace1.Workflow1Controller", "Index") // Creates -> ~/Namespace1/Workflow1Controller/Index
Url.Action("Namespace2.Workflow4Controller", "Action") // Creates -> ~/Namespace2/Workflow4Controller/Index
Is there possibility to achieve similar in .net core without explicit hardcoding routes in route table?
As an start point for your solution, you can create a custom IApplicationModelConvention and change the routing to use namespace in the routing.
There's an example in docs showing how you can do this. To learn more you can take a look at this great docs article: Work with the application model in ASP.NET Core.
To do so, first create the following NamespaceRoutingConvention class:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;
public class NamespaceRoutingConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
var hasAttributeRouteModels = controller.Selectors
.Any(selector => selector.AttributeRouteModel != null);
if (!hasAttributeRouteModels)
{
controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
{
Template = controller.ControllerType.Namespace.Replace('.', '/')
+ "/[controller]/[action]/{id?}"
};
}
}
}
}
Then in startup, register the convention like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => {
options.Conventions.Add(new NamespaceRoutingConvention());
});
}
Then you can browse:
http://localhost:xxxxx/SampleProject/Controllers/Home/Index
I have recently come across this issue.
An MVC .NET Framework project I had been working on needed migrating to .NET Core, however it was made up from lots of smaller assemblies (DLLs), each containing different parts of the overall codebase.
MvcCodeRouting was used to implement this, and provided a way for the assemblies to be utilised, routed, and also provided the ability for the central (core) project to utilise the relevant views, stored within the assemblies.
I have found that the .NET Core "Application Parts" feature seems to fulfill most of these points, with it having the ability for projects to be seperated between different assemblies, imported into a main project, and utilised.
The only real functional difference in using this method is that each "Application Part" generates two assemblies instead of one, with one containing the controllers and module logic, whilst the other contains the (Razor) views (if the specific module has any).
I'm still experimenting with this, however I thought I'd add this here for the sake of completeness.
Below are some useful links to documentation regarding this feature:
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/app-parts?view=aspnetcore-1.0#sample-generic-controller-feature
This is the main documentation for the feature within the ASP.NET Core documentation.
https://learn.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-1.0&tabs=visual-studio
This tutorial demonstrated creating an assembly containing only Razor Views, if some of the modules require no logic.
Related
Why does adding versioning to a webApi project removes number from the controller path name?
Replication steps :
Create a fresh .net6 project. And rename WeatherForecastController to WeatherForecast2Controller.
Run app and call https://localhost:x/WeatherForecast2 (where x is your port)
Observe valid/expected results
Add the below code in program.cs/startup
services.AddApiVersioning(config =>
{
config.AssumeDefaultVersionWhenUnspecified = true;
config.DefaultApiVersion = new ApiVersion(1, 0);
config.ReportApiVersions = true;
config.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("version"),
new HeaderApiVersionReader("x-version"));
config.UseApiBehavior = false;
});
Run app and call https://localhost:x/WeatherForecast2 (where x is your port).
Step 5 will return a 404 not found error.
However if you call https://localhost:x/WeatherForecast. It will work.
So why does adding versioning, change the url path?
It's not entirely clear if you are using <= 5.0 or 6.0+.
History
The reason this behavior happens is because the only logical way to group controllers together is by their names. A controller name, therefore, becomes very important. This is problematic in code because two or more controllers in the same namespace cannot have the same type name. The assumption and long-defined convention has been to allow ASP.NET to remove the Controller suffix and then remove any remaining numbers. This allows ValuesController, Values2Controller and Values3Controller to all map to the logical controller name Values by default. In most cases, that's probably want someone wants. If API Versioning doesn't do this, then there is no way to collate all API versions together for an API.
Contrary to popular belief, route templates are not considered for grouping controllers (e.g. APIs). There is too much ambiguity as to how a template can map to code. Take the simplest example of two different versions of the same API with different route templates: V1 = values/{id}, V2 = values/{id:int}. These are semantically equivalent, but not the same. API Versioning does not try to understand what the route template means nor compare their equivalence. It can easily get a lot more complicated; especially, for overlapping route templates. For example, should order/{oid}/customer/{cid} be part of the Orders API or the Customer API? Only the service author knows for sure.
Regression
In the 5.0 release, a regression was accidentally introduced due to an over-optimization. The controller name is used in two places: the actual name of the controller and the name used to group controllers. It seems reasonable they'd be the same and why normalize (e.g. trim suffixes) more than necessary? It seemed like a good idea, but it caused unexpected behavior - such as this one. There are also legitimate reasons to have a number in the name of a controller; for example, S3Controller.
Fix
In library versions <= 5.0, developers had no control over the behavior of how names were normalized. In 5.1 and 6.0+, this is now exposed via the IControllerNameConvention service, which has two methods: one for normalizing the controller name and one for normalizing the group name. The following implementations are provided out of the box as properties on ControllerNameConvention:
Default: The default, out-of-the-box conventions
Original: The original names without any normalization (could result in the wrong behavior)
Grouped: The group name is normalized, but the controller name is unmodified
If none of those work for you, then you can create your own custom convention. In 5.1 this is wired up via ApiVersioningOptions.ControllerNameConvention, while in 6.0+ IControllerNameConvention is a transient service in the DI container.
Workaround
There are two ways you can workaround the problem using the current version you are leveraging:
Explicit Route Template
If you omit using the [controller] token, the routing problem will be resolved; for example, api/weatherforecast. You appear to have already discovered this.
Explicit Controller Name
The controller name is derived from a convention, even without API Versioning. It was understood this behavior could be a problem so API Versioning provides a way to explicit set it with the ControllerNameAttribute.
[ControllerName("WeatherForecast")]
[Route("api/[controller]")] // ← expands to 'api/WeatherForecast'
public class WeatherForecast2Controller : ControllerBase { }
Edge Case
This will solve the routing issues, but it will not fix the controller name issue. That should only matter if you are planning on documenting your API with OpenAPI (formerly Swagger). For example, S3Controller will simply show up as S, even though the route might be api/s3.
I think that your controller may have issues, as I used your AddApiVersioning code and it works for me. To avoid the conflicting action names, you can two different controllers.
ApiController
namespace WebApiVersioningApp.Controllers;
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/Version2")]
public class Version2Controller : ControllerBase
{
[MapToApiVersion("1.0")]
[HttpGet(Name = "GetWeatherForecastV1")]
public string GetV1()
{
return "Version 1";
}
[MapToApiVersion("2.0")]
[HttpGet(Name = "GetWeatherForecastV2")]
public string GetV2()
{
return "Version 2";
}
}
Program:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First())
);
// Versioning setup
builder.Services.AddApiVersioning(o =>
{
o.ReportApiVersions = true;
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
o.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("version"),
new HeaderApiVersionReader("x-version"));
o.UseApiBehavior = false;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Hope this works for you.
Other than changing startup.cs whenever a new version is built,
services.AddApiVersioning(config =>
{
config.DefaultApiVersion = new ApiVersion(1, 0);
});
is there a way of specifying that the "default version" is the latest version, without specifying what that version is? e.g. some way of saying
config.DefaultApiVersion = new ApiVersionLatest();
. . . or does this fly in the face of all that is considered holy by the RESTApi gods?
Thanks
The correct answer depends a little bit about what you expect to achieve. The DefaultApiVersion only comes into play when no other API version is available. API versioning does not have a concept of "no API version". API version-neutral means that an API accepts any and all API versions, including none at all. The default API version can also be thought of as the initial API version.
Here's a few scenarios where the DefaultAPiVersion comes into play:
A controller has no attribution or conventions applied
Selection of possible API versions yields no results
It sounds like you are interested in configuring the most current API version in a single place. You can use the DefaultApiVersion to do this, but only if the controller has no other attribution or conventions. If an API doesn't carry forward, you will have to explicitly decorate the controller with an attribute or convention that indicates the legacy API version to exclude it from the latest version. While this is possible, it's hard to follow IMO.
A better approach would probably to use an extension that that describes the behavior you want. For example:
[AttributeUsage( AttributeTargets.Class, AllowMultiple = false )]
public sealed class LatestApiVersionAttribute : ApiVersionAttribute, IApiVersionProvider
{
public LatestApiVersionAttribute() : base( new ApiVersion( 2, 0 ) ) { }
}
Now you can apply this to all your controllers a la:
[LatestApiVersion]
public class MyController : ControllerBase
{
// ommitted
}
This gives you the chance to manage the latest API version is single place. You might also consider using conventions so you don't even need the custom attribute. Attributes and conventions can be combined.
#spender does mention using a custom IApiVersionSelector; however, selection currently only comes into play when no API version has been specified. To enable this type of configuration, set things up as:
services.AddApiVersioning( options =>
{
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
options.AssumeDefaultVersionWhenUnspecified = true;
}
This will enable clients that do not specify an API version to always forward to the most recent version of the requested API. A client can still explicitly ask for a specific version, including the latest.
I hope that helps.
I have a ViewComponent stored in an area named "Dashboard" but now I want to use this ViewComponent in another area called "Appplications". Yes I could add it to the root views/shared folder but I'm striving to make a very modular application through the strong contained use of areas.
ASP.NET 5 RC1 MVC 6 doesn't seem to support cross area references to other components.
How do I add additional view locations? I need to add:
/Areas/Dashboard/Views/Shared/Components/DashboardMenu/Default.cshtml
as a search location to the view renderer
InvalidOperationException: The view 'Components/DashboardMenu/Default' was not found. The following locations were searched:
/Areas/Applications/Views/Application/Components/DashboardMenu/Default.cshtml
/Areas/Applications/Views/Shared/Components/DashboardMenu/Default.cshtml
/Views/Shared/Components/DashboardMenu/Default.cshtml.
Worked it out...
Startup.cs
// Add additional razor view engine configuration to facilitate:
// 1. Cross area view path searches
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new RazorViewLocationExpander());
});
Then create a class called RazorViewLocationExpander.cs
using Microsoft.AspNet.Mvc.Razor;
using System.Collections.Generic;
using System.Linq;
public class RazorViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context) { }
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
List<string> locations = viewLocations.ToList();
locations.Add("/Areas/dashboard/Views/Shared/{0}.cshtml");
return locations;
}
}
I would not recommend this generally. I am using this solution as a special case because I am using an area to isolate templating and core code for my other (members-only) areas to consume - so they need to know where to find this shared code. I am attempting to separate the public code from the admin code and this is the cleanest, most modular solution I can think of. The dashboard area will be present for all website that require members-only administration area. It is bending the rules of MVC ever so slightly.
Ok basically I am building a nuget package, and one of the things i would like the class to do is get the current applications controllers
So at the moment in the main project I do the following
using SolutionName.Website.MVC5.Controllers;
What I want to be able to do is something like
using this.Application.Controllers;
So it dynamicaly fills in the namespace for whatever solution the package is installed to.
I have sat here for an hour going through the possible permutations and have also googled a fair bit but not sure exactly what to google for
This is including API Controllers and I am Using MVC 5
Cheers
Martyn
The following little piece of reflection will give you all the namespaces for files that end in "Controller".
var namespaces = this.GetType().Assembly.GetTypes()
.Where(t => t.Name.EndsWith("Controller"))
.Select(x => x.Namespace).Distinct().ToList();
You would need to call this from within your code, preferably in the Global.asax.
Keep in mind that your controllers may be scattered across a number of namespaces, so additional logic will have to account for that. The number of namespaces is solely determined by how rigidly you structure your applications.
Alternatively, you can also pull types that inherit directly from 'System.Web.Mvc.Controller', as pointed out by Andrew Whitaker.
var namespaces = this.GetType().Assembly.GetTypes()
.Where(t => t => t.IsSubclassOf(typeof(Controller)))
.Select(x => x.Namespace).Distinct().ToList();
After using the information that #DavidL and #AndrewWhitaker gave me I thought I would post a little code snippet of how this can work in an MVC application as it maybe useful to go hand in hand with the question
I created the class below
public class GetControllerNameSpace
{
public static string NamespaceTag;
public void GetControllerNameSpaceMethod()
{
var NamespaceTagList = this.GetType().Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Controller))).Select(x => x.Namespace).Distinct().ToList();
NamespaceTag = NamespaceTagList.FirstOrDefault();
}
}
and then in the RouteConfig.cs
using Controllers = GetControllerNameSpace;
This then lets me pass the information to my method here
private static IEnumerable<Type> GetTypesWithFixedControllerRouteAttribute(RouteCollection routes)
{
//This is where i am passing the variables
foreach (Type type in Assembly.GetAssembly(typeof(Controllers.HomeController)).GetTypes())
{
// Register any fixed actions
RegisterFixedActionRoutes(type, routes);
if (type.GetCustomAttributes(typeof(FixedControllerRouteAttribute), true).Any())
{
yield return type;
}
}
}
Please note that this works in my use case and may require more work when using multiple controller namespaces as mentioned in #DavidL's post
We are developing large MVC project and we have an intension to use HttpSessionStateWrapper (as well as HttpRequestWrapper, HttpResponseWrapper, etc) to add extended functionalities do this objects. It would be adding session messages, additional collections, html metadata with response - stuff like that, managable from controllers and accessible in the views when needed.
I have done it in a smaller project and it gennerally worked well, except some casting issues here and there, but it can be worked around by not using wrappers outside controllers or eventually views. Every controller would be a custom controller with a code like that:
public class Controller : System.Web.Mvc.Controller
{
public new CustomHttpResponse Response
{
get
{
return (CustomHttpResponse)HttpContext.Response;
}
}
public new CustomHttpRequestRequest
{
get
{
return (CustomHttpRequestRequest)HttpContext.Request;
}
}
//etc...
}
ContextWrapper would be created in a custom MvcHandler. Response, request and session wrappers would be created and taken from ContextWrapper .
Is this a good policy to use wrappers to extend functionalities, or they where intended only for creating testing mocks?