Using a controller with WEB API Routing - asp.net-mvc

In my project (Asp.net MVC), I want to use DevExtreme GridView to display my data. I've used code first to create databases and tables. In the project, I have a model with the name of Member. I did right click on the Controller folder and select Add->Controller->DevExtreme Web API Controller with actions, using Entity Framework. In the wizard, I selected my database context and model and determine my controller name (MembersController) and then clicked Add. So in the Views folder, I created a folder with name Members and inside it, I added a view with name Index. (I don't know what exactly name must be for view, you suppose Index). In the index view, I used the wizard to add a DevExtreme GridView (Right-click on the view context and click on Insert A DevExtreme Control Here. In the wizard, I selected GridView as control and DatabaseContext, Member model and Members controller. You can see all of my codes in the below:
Member Mode:
Model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WebApplication2.Models
{
public class Member
{
#region Ctor
public Member()
{
}
#endregion
#region Properties
[Key]
public int MemberID { get; set; }
[Required(ErrorMessage ="*")]
public string FirstName { get; set; }
[Required(ErrorMessage = "*")]
public string LastName { get; set; }
public string Phone { get; set; }
public string Mobile { get; set; }
[Required(ErrorMessage = "*")]
public string NID { get; set; }
[Required(ErrorMessage = "*")]
public string MID { get; set; }
[Required(ErrorMessage = "*")]
public string SalaryID { get; set; }
#endregion
}
}
Controller:
[Route("api/Members/{action}", Name = "MembersApi")]
public class MembersController : ApiController
{
private ApplicationDbContext _context = new ApplicationDbContext();
[HttpGet]
public HttpResponseMessage Get(DataSourceLoadOptions loadOptions) {
var members = _context.Members.Select(i => new {
i.MemberID,
i.FirstName,
i.LastName,
i.Phone,
i.Mobile,
i.NID,
i.MID,
i.SalaryID
});
return Request.CreateResponse(DataSourceLoader.Load(members, loadOptions));
}
[HttpPost]
public HttpResponseMessage Post(FormDataCollection form) {
var model = new Member();
var values = JsonConvert.DeserializeObject<IDictionary>(form.Get("values"));
PopulateModel(model, values);
Validate(model);
if (!ModelState.IsValid)
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, GetFullErrorMessage(ModelState));
var result = _context.Members.Add(model);
_context.SaveChanges();
return Request.CreateResponse(HttpStatusCode.Created, result.MemberID);
}
[HttpPut]
public HttpResponseMessage Put(FormDataCollection form) {
var key = Convert.ToInt32(form.Get("key"));
var model = _context.Members.FirstOrDefault(item => item.MemberID == key);
if(model == null)
return Request.CreateResponse(HttpStatusCode.Conflict, "Member not found");
var values = JsonConvert.DeserializeObject<IDictionary>(form.Get("values"));
PopulateModel(model, values);
Validate(model);
if (!ModelState.IsValid)
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, GetFullErrorMessage(ModelState));
_context.SaveChanges();
return Request.CreateResponse(HttpStatusCode.OK);
}
[HttpDelete]
public void Delete(FormDataCollection form) {
var key = Convert.ToInt32(form.Get("key"));
var model = _context.Members.FirstOrDefault(item => item.MemberID == key);
_context.Members.Remove(model);
_context.SaveChanges();
}
private void PopulateModel(Member model, IDictionary values) {
string MEMBER_ID = nameof(Member.MemberID);
string FIRST_NAME = nameof(Member.FirstName);
string LAST_NAME = nameof(Member.LastName);
string PHONE = nameof(Member.Phone);
string MOBILE = nameof(Member.Mobile);
string NID = nameof(Member.NID);
string MID = nameof(Member.MID);
string SALARY_ID = nameof(Member.SalaryID);
if(values.Contains(MEMBER_ID)) {
model.MemberID = Convert.ToInt32(values[MEMBER_ID]);
}
if(values.Contains(FIRST_NAME)) {
model.FirstName = Convert.ToString(values[FIRST_NAME]);
}
if(values.Contains(LAST_NAME)) {
model.LastName = Convert.ToString(values[LAST_NAME]);
}
if(values.Contains(PHONE)) {
model.Phone = Convert.ToString(values[PHONE]);
}
if(values.Contains(MOBILE)) {
model.Mobile = Convert.ToString(values[MOBILE]);
}
if(values.Contains(NID)) {
model.NID = Convert.ToString(values[NID]);
}
if(values.Contains(MID)) {
model.MID = Convert.ToString(values[MID]);
}
if(values.Contains(SALARY_ID)) {
model.SalaryID = Convert.ToString(values[SALARY_ID]);
}
}
private string GetFullErrorMessage(ModelStateDictionary modelState) {
var messages = new List<string>();
foreach(var entry in modelState) {
foreach(var error in entry.Value.Errors)
messages.Add(error.ErrorMessage);
}
return String.Join(" ", messages);
}
protected override void Dispose(bool disposing) {
if (disposing) {
_context.Dispose();
}
base.Dispose(disposing);
}
}
View:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#(Html.DevExtreme().DataGrid<WebApplication2.Models.Member>()
.DataSource(ds => ds.WebApi()
.RouteName("MembersApi")
.LoadAction("Get")
.InsertAction("Post")
.UpdateAction("Put")
.DeleteAction("Delete")
.Key("MemberID")
)
.RemoteOperations(true)
.Columns(columns => {
columns.AddFor(m => m.MemberID);
columns.AddFor(m => m.FirstName);
columns.AddFor(m => m.LastName);
columns.AddFor(m => m.Phone);
columns.AddFor(m => m.Mobile);
columns.AddFor(m => m.NID);
columns.AddFor(m => m.MID);
columns.AddFor(m => m.SalaryID);
})
.Editing(e => e
.AllowAdding(true)
.AllowUpdating(true)
.AllowDeleting(true)
)
)
WebApiConfig.cs file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
namespace WebApplication2
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// WebAPI when dealing with JSON & JavaScript!
// Setup json serialization to serialize classes to camel (std. Json format)
var formatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
formatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
}
}
}
Global.asax.cs file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace WebApplication2
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
}
In addition I've installed all requirements for this project according this link.
But when I try to show View with https://localhost:44328/Members/index RUL, I get this error:
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /Members/index
I'v tried a lot way to correct my wrong but I couldn't find solution. I almost read all of documents about routing (mvc and web api), but after about 5 days I still couldn't to solve it.
Thanks a lot for answer me.

The thing is as far as I can tell, one of the reasons you are receiving a 404 is because you don't seem to be adding your parameter anywhere. Aside from that your 'DataSourceLoadOptions loadOptions' shouldn't be used as a parameter because it is probably too complex. Shouldn't you create a service which retrieves your loadOptions instead of you giving it along?
If you want all members without giving information then you should do exactly that. Not give the request some metadata it doesn't know about along for the ride.
I suggest you do the following:
Create an API which does not need metadata like how to get a datasource. Things such as Members.LastName are acceptable
Make sure you create a service which is responsible for getting your data in the first place. This means also removing all that extra code in your controller and placing it in a more suitable location.
Keep your classes clean and simple. Your controller now has too many responsibilities.
Hopefully this'll help. If you try your API GET Method as is without the 'DataSourceLoadOptions loadOptions' parameter, then your API will not return 404.

Since you didn't put in your ajax call url, I'm going to have to work with this
Requested URL: /Members/index
This is a problem, your webApi default route requires your URL to be prepended with /api/
So something like this should work /api/Members, so you can remove the Index part of that URL as the request type will handle which Action is executed ie HTTPGet/HTTPPost
EDIT: Use this as your route
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { controller = "Members" id = RouteParameter.Optional }
);

Related

Is there get method which not found while run web api service

I'm creating a web api controller and implement ([HttpGet])parameterized get method with datatype class but when I run this its show 404 Not Found.
When i am implementing normal datatype like string or int its show me the answer or datatype like List still it's giving me answer but when I directly declare datatype as class like Student its show 404 not found error. I don't know why it's so. I am trying to learn web api with mvc please help me.
I am creating one simple class Student and one studentapicontroller.In my api controller, I create get method with datatype class and for testing purpose i make other get method with different datatype
Student.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace epay.Models
{
public class student
{
public int id { get; set; }
public string name { get; set; }
}
}
studentapiController :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using epay.Models;
namespace epay.Controllers
{
public class studentapiController : ApiController
{
// GET api/studentapi
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/studentapi/5
{
return "value";
}
// POST api/studentapi
public void Post([FromBody]string value)
{
}
// PUT api/studentapi/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/studentapi/5
public void Delete(int id)
{
}
//POST api/studentapi/
[HttpGet]
public student getdetails(int id, string na)
{
student st = new student();
st.id = id;
st.name = na;
return st;
}
//GET api/studentapi/getstud
[HttpGet]
public List<student> getstud()
{
List<student> lst = new List<student>();
student st = new student();
st.name = "manlai";
st.id = 5;
lst.Add(st);
return lst;
}
}
}
I just want getdetails result or how to do if I want my method datatype as a class and I am passing parameter with my get method how to do this
Make getdetails route like below because it is conflicting with get
[HttpGet("getdetails")]
public student getdetails(int id, string na)
{
student st = new student();
st.id = id;
st.name = na;
return st;
}
you can call route something like this with querystring /studentapi/getdetails?id=1&na=test

Binding parameter to complex type

I have a navigation bar, with several links, like this:
MenuItem1
This request would hit my action method:
public ActionResult Browse(int departmentId)
{
var complexVM = MyCache.GetComplexVM(departmentId);
return View(complexVM);
}
This is my ComplexVM:
public class ComplexVM
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
MyCache, is a static list of departments, which I am keeping in memory, so when user passes in DepartmentId, I wouldn't need to get the corresponding DepartmentName from DB.
This is working fine... but it would be nice if I could somehow initialize ComplexVM in custom model binder, instead of initializing it in the Controller... so I still want to use a link (menu item), but this time, a CustomModelBinder binds my parameter, 2, to ComplexVM: it needs to look up the name of department with id = 2 from MyCache and initialize ComplexVM, then ComplexVM would be passed to this action method:
public ActionResult Browse(ComplexVM complexVM)
{
return View(complexVM);
}
I want to hit the above controller without doing a post-back, as I have a lot of menu item links in my navigation bar... not sure if this is possible? Or if this is even a good idea?
I have seen this link, which sort of describes what I want... but I am not sure how the routing would work... i.e. routing id:2 => ComplexVM
Alternatively would it be possible to do this in RouteConfig, something like this:
routes.MapRoute(
name: "Browse",
url: "{controller}/Browse/{departmentId}",
// this does not compile, just want to explain what I want...
defaults: new { action = "Browse", new ComplexVM(departmentId) });
I can achieve this with little change and with one trick
MenuItem1
Controller action
public ActionResult Browse(ComplexVM complexVM)
{
return View(complexVM);
}
View model
public class ComplexVM
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
public ComplexVM()
{
this.DepartmentId = System.Convert.ToInt32(HttpContext.Current.Request("id").ToString);
this.DepartmentName = "Your name from cache"; // Get name from your cache
}
}
This is without using model binder. Trick may help.
That is possible. It is also a good idea :) Off-loading parts of the shared responsibility to models / action filters is great. The only problem is because they are using some special classes to inherit from, testing them sometimes might be slightly harder then just testing the controller. Once you get the hang of it - it's better.
Your complex model should look like
// Your model class
[ModelBinder(typeof(ComplexVMModelBinder)]
public class ComplexVMModel
{
[Required]
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
// Your binder class
public class ComplexVMModelBinder : IModelBinder
{
// Returns false if you can't bind.
public bool BindModel(HttpActionContext actionContext, ModelBindingContext modelContext)
{
if (modelContext.ModelType != typeof(ComplexVMModel))
{
return false;
}
// Somehow get the depid from the request - this might not work.
int depId = HttpContext.Current.Request.Params["DepID"];
// Create and assign the model.
bindingContext.Model = new ComplexVMModel() { DepartmentName = CacheLookup(), DepId = depId };
return true;
}
}
Then at the beginning of your action method, you check the ModelState to see if it's valid or not. There are a few things which can make the model state non-valid (like not having a [Required] parameter.)
public ActionResult Browse(ComplexVM complexVM)
{
if (!ModelState.IsValid)
{
//If not valid - return some error view.
}
}
Now you just need to register this Model Binder.
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(ComplexVMModel), new ComplexVMModelBinder());
}
Your should be able to use the route config that you've provided.

Is there a way to have a RoutePrefix that starts with an optional parameter?

I want to reach the Bikes controller with these URL's:
/bikes // (default path for US)
/ca/bikes // (path for Canada)
One way of achieving that is using multiple Route Attributes per Action:
[Route("bikes")]
[Route("{country}/bikes")]
public ActionResult Index()
To keep it DRY I'd prefer to use a RoutePrefix, but multiple Route Prefixes are not allowed:
[RoutePrefix("bikes")]
[RoutePrefix("{country}/bikes")] // <-- Error: Duplicate 'RoutePrefix' attribute
public class BikesController : BaseController
[Route("")]
public ActionResult Index()
I've tried using just this Route Prefix:
[RoutePrefix("{country}/bikes")]
public class BikesController : BaseController
Result: /ca/bikes works, /bikes 404s.
I've tried making country optional:
[RoutePrefix("{country?}/bikes")]
public class BikesController : BaseController
Same result: /ca/bikes works, /bikes 404s.
I've tried giving country a default value:
[RoutePrefix("{country=us}/bikes")]
public class BikesController : BaseController
Same result: /ca/bikes works, /bikes 404s.
Is there another way to achieve my objective using Attribute Routing?
(And yes, I know I can do this stuff by registering routes in RouteConfig.cs, but that's what not I'm looking for here).
I'm using Microsoft.AspNet.Mvc 5.2.2.
FYI: these are simplified examples - the actual code has an IRouteConstraint for the {country} values, like:
[Route("{country:countrycode}/bikes")]
I am a bit late to the party, but i have a working solution for this problem. Please find my detailed blog post on this issue here
I am writing down summary below
You need to create 2 files as given below
_3bTechTalkMultiplePrefixDirectRouteProvider.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Web.Http.Controllers;
using System.Web.Http.Routing;
namespace _3bTechTalk.MultipleRoutePrefixAttributes {
public class _3bTechTalkMultiplePrefixDirectRouteProvider: DefaultDirectRouteProvider {
protected override IReadOnlyList GetActionDirectRoutes(HttpActionDescriptor actionDescriptor, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) {
return CreateRouteEntries(GetRoutePrefixes(actionDescriptor.ControllerDescriptor), factories, new [] {
actionDescriptor
}, constraintResolver, true);
}
protected override IReadOnlyList GetControllerDirectRoutes(HttpControllerDescriptor controllerDescriptor, IReadOnlyList actionDescriptors, IReadOnlyList factories, IInlineConstraintResolver constraintResolver) {
return CreateRouteEntries(GetRoutePrefixes(controllerDescriptor), factories, actionDescriptors, constraintResolver, false);
}
private IEnumerable GetRoutePrefixes(HttpControllerDescriptor controllerDescriptor) {
Collection attributes = controllerDescriptor.GetCustomAttributes (false);
if (attributes == null)
return new string[] {
null
};
var prefixes = new List ();
foreach(var attribute in attributes) {
if (attribute == null)
continue;
string prefix = attribute.Prefix;
if (prefix == null)
throw new InvalidOperationException("Prefix can not be null. Controller: " + controllerDescriptor.ControllerType.FullName);
if (prefix.EndsWith("/", StringComparison.Ordinal))
throw new InvalidOperationException("Invalid prefix" + prefix + " in " + controllerDescriptor.ControllerName);
prefixes.Add(prefix);
}
if (prefixes.Count == 0)
prefixes.Add(null);
return prefixes;
}
private IReadOnlyList CreateRouteEntries(IEnumerable prefixes, IReadOnlyCollection factories, IReadOnlyCollection actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
var entries = new List ();
foreach(var prefix in prefixes) {
foreach(IDirectRouteFactory factory in factories) {
RouteEntry entry = CreateRouteEntry(prefix, factory, actions, constraintResolver, targetIsAction);
entries.Add(entry);
}
}
return entries;
}
private static RouteEntry CreateRouteEntry(string prefix, IDirectRouteFactory factory, IReadOnlyCollection actions, IInlineConstraintResolver constraintResolver, bool targetIsAction) {
DirectRouteFactoryContext context = new DirectRouteFactoryContext(prefix, actions, constraintResolver, targetIsAction);
RouteEntry entry = factory.CreateRoute(context);
ValidateRouteEntry(entry);
return entry;
}
private static void ValidateRouteEntry(RouteEntry routeEntry) {
if (routeEntry == null)
throw new ArgumentNullException("routeEntry");
var route = routeEntry.Route;
if (route.Handler != null)
throw new InvalidOperationException("Direct route handler is not supported");
}
}
}
3bTechTalkRoutePrefix.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
namespace _3bTechTalk.MultipleRoutePrefixAttributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class _3bTechTalkRoutePrefix : RoutePrefixAttribute
{
public int Order { get; set; }
public _3bTechTalkRoutePrefix(string prefix) : this(prefix, 0) { }
public _3bTechTalkRoutePrefix(string prefix, int order) : base(prefix)
{
Order = order;
}
}
}
Once done, open WebApiConfig.cs and add this below given line
config.MapHttpAttributeRoutes(new _3bTechTalkMultiplePrefixDirectRouteProvider());
That's it, now you can add multiple route prefix in your controller. Example below
[_3bTechTalkRoutePrefix("api/Car", Order = 1)]
[_3bTechTalkRoutePrefix("{CountryCode}/api/Car", Order = 2)]
public class CarController: ApiController {
[Route("Get")]
public IHttpActionResult Get() {
return Ok(new {
Id = 1, Name = "Honda Accord"
});
}
}
I have uploaded a working solution here
Happy Coding :)
You're correct that you can't have multiple route prefixes, which means solving this particular use case is not going to be straight forward. About the best way I can think of to achieve what you want with the minimal amount of modifications to your project is to subclass your controller. For example:
[RoutePrefix("bikes")]
public class BikeController : Controller
{
...
}
[RoutePrefix("{country}/bikes")]
public class CountryBikeController : BikeController
{
}
You subclassed controller will inherit all the actions from BikeController, so you don't need to redefine anything, per se. However, when it comes to generating URLs and getting them to go to the right place, you'll either need to be explicit with the controller name:
#Url.Action("Index", "CountryBike", new { country = "us" }
Or, if you're using named routes, you'll have to override your actions in your subclassed controller so you can apply new route names:
[Route("", Name = "CountryBikeIndex")]
public override ActionResult Index()
{
base.Index();
}
Also, bear in mind, that when using parameters in route prefixes, all of your actions in that controller should take the parameter:
public ActionResult Index(string country = "us")
{
...
You could use attribute routes with two ordered options.
public partial class GlossaryController : Controller {
[Route("~/glossary", Order = 2)]
[Route("~/{countryCode}/glossary", Order = 1)]
public virtual ActionResult Index()
{
return View();
}
}
If you're planning to have region specific routes for all your pages you could add a route to the route config above the default. This will work only for views/controllers without attribute routes.
routes.MapRoute(
name: "Region",
url: "{countryCode}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { countryCode = #"\w{2}" }
);
The best solution I've come across is detailed by NightOwl888 in response to the following question: ASP.NET MVC 5 culture in route and url. The code below is my trimmed down version of his post. It's working for me in MVC5.
Decorate each controller with a single RoutePrefix, without a culture segment. When the application starts up, the custom MapLocalizedMvcAttributeRoutes method adds a localized route entry for each controller action.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
// Omitted for brevity
MapLocalizedMvcAttributeRoutes(routes, "{culture}/", new { culture = "[a-z]{2}-[A-Z]{2}" });
}
static void MapLocalizedMvcAttributeRoutes(RouteCollection routes, string urlPrefix, object constraints)
{
var routeCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc");
var subRouteCollectionType = Type.GetType("System.Web.Mvc.Routing.SubRouteCollection, System.Web.Mvc");
var linkGenerationRouteType = Type.GetType("System.Web.Mvc.Routing.LinkGenerationRoute, System.Web.Mvc");
FieldInfo subRoutesInfo = routeCollectionRouteType.GetField("_subRoutes", BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo entriesInfo = subRouteCollectionType.GetProperty("Entries");
MethodInfo addMethodInfo = subRouteCollectionType.GetMethod("Add");
var localizedRouteTable = new RouteCollection();
var subRoutes = Activator.CreateInstance(subRouteCollectionType);
Func<Route, RouteBase> createLinkGenerationRoute = (Route route) => (RouteBase)Activator.CreateInstance(linkGenerationRouteType, route);
localizedRouteTable.MapMvcAttributeRoutes();
foreach (var routeCollectionRoute in localizedRouteTable.Where(rb => rb.GetType().Equals(routeCollectionRouteType)))
{
// routeCollectionRoute._subRoutes.Entries
foreach (RouteEntry routeEntry in (IEnumerable)entriesInfo.GetValue(subRoutesInfo.GetValue(routeCollectionRoute)))
{
var localizedRoute = CreateLocalizedRoute(routeEntry.Route, urlPrefix, constraints);
var localizedRouteEntry = new RouteEntry(string.IsNullOrEmpty(routeEntry.Name) ? null : $"{routeEntry.Name}_Localized", localizedRoute);
// Add localized and default routes and subroute entries
addMethodInfo.Invoke(subRoutes, new[] { localizedRouteEntry });
addMethodInfo.Invoke(subRoutes, new[] { routeEntry });
routes.Add(createLinkGenerationRoute(localizedRoute));
routes.Add(createLinkGenerationRoute(routeEntry.Route));
}
}
var routeEntries = Activator.CreateInstance(routeCollectionRouteType, subRoutes);
routes.Add((RouteBase)routeEntries);
}
static Route CreateLocalizedRoute(Route route, string urlPrefix, object constraints)
{
var routeUrl = urlPrefix + route.Url;
var routeConstraints = new RouteValueDictionary(constraints);
// combine with any existing constraints
foreach (var constraint in route.Constraints)
{
routeConstraints.Add(constraint.Key, constraint.Value);
}
return new Route(routeUrl, route.Defaults, routeConstraints, route.DataTokens, route.RouteHandler);
}
}

Minimal way to handle static content routes/controllers/views from data driven menus?

I have a ListItem class that is used to represent menu items in my application:
public class ListItem : Entity
{
public virtual List List { get; set; }
public virtual ListItem ParentItem { get; set; }
public virtual ICollection<ListItem> ChildItems { get; set; }
public int SortOrder { get; set; }
public string Text { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string Area { get; set; }
public string Url { get; set; }
}
I use this data to construct the routes for the application, but I was wondering if there was a clean way to handle controller/views for static content? Basically any page that doesn't use any data but just views. Right now I have one controller called StaticContentController, which contains a unique action for each static page that returns the appropriate view like so:
public class StaticContentController : Controller
{
public ActionResult Books()
{
return View("~/Views/Books/Index.cshtml");
}
public ActionResult BookCategories()
{
return View("~/Views/Books/Categories.cshtml");
}
public ActionResult BookCategoriesSearch()
{
return View("~/Views/Books/Categories/Search.cshtml");
}
}
Is there some way I could minimize this so I don't have to have so many controllers/actions for static content? It seems like when creating my ListItem data I could set the Controller to a specific controller that handles static content, like I have done, but is there anyway to use one function to calculate what View to return? It seems like I still need separate actions otherwise I won't know what page the user was trying to get to.
The ListItem.Url contains the full URL path from the application root used in creating the route. The location of the View in the project would correspond to the URL location to keep the organization structure parallel.
Any suggestions? Thanks.
Edit: My Route registration looks like so:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Shared/{*pathInfo}");
routes.MapRoute("Access Denied", "AccessDenied", new { controller = "Shared", action = "AccessDenied", area = "" });
List<ListItem> listItems = EntityServiceFactory.GetService<ListItemService>().GetAllListItmes();
foreach (ListItem item in listItems.Where(item => item.Text != null && item.Url != null && item.Controller != null).OrderBy(x => x.Url))
{
RouteTable.Routes.MapRoute(item.Text + listItems.FindIndex(x => x == item), item.Url.StartsWith("/") ? item.Url.Remove(0, 1) : item.Url, new { controller = item.Controller, action = item.Action ?? "index" });
}
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
You can use a single Action with one parameter (the View name) which will return all the static pages
public class StaticContentController : Controller
{
public ActionResult Page(string viewName)
{
return View(viewName);
}
}
You will also need to create a custom route for serving these views, for example:
routes.MapRoute(
"StaticContent", // Route name
"page/{viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" } // Parameter defaults
);
I see in your example that you specify different folders for your views. This solution will force you to put all static views in the Views folder of the StaticContentController.
If you must have custom folder structure, then you can change the route to accept / by adding * to the {viewName} like this {*viewname}. Now you can use this route: /page/Books/Categories. In the viewName input parameter you will receive "Books/Categories" which you can then return it as you like: return View(string.Format("~/Views/{0}.cshtml", viewName));
UPDATE (Avoiding the page/ prefix)
The idea is to have a custom constraint to check whether or not a file exists. Every file that exists for a given URL will be treated as static page.
public class StaticPageConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string viewPath = httpContext.Server.MapPath(string.Format("~/Views/{0}.cshtml", values[parameterName]));
return File.Exists(viewPath);
}
}
Update the route:
routes.MapRoute(
"StaticContent", // Route name
"{*viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" }, // Parameter defaults
new { viewName = new StaticPageConstraint() } // Custom route constraint
);
Update the action:
public ActionResult Page(string viewName)
{
return View(string.Format("~/Views/{0}.cshtml", viewName));
}

What approach to take for testing AutoMapper configuration in an ASP.NET MVC application?

We are using AutoMapper extensively in our ASP.NET MVC web applications with the AutoMapViewResult approach set out in this question. So we have actions that look like this:
public ActionResult Edit(User item)
{
return AutoMapView<UserEditModel>(View(item));
}
This creates hidden failure points in the application if the requested mapping has not been configured - in that this is not a compile time fail.
I'm looking at putting something in place to test these mappings. As this needs to test the actual AutoMapper configuration I presume this should be done as part of integration testing? Should these tests be structured per controller or per entity? What about the possibility of automatically parsing all calls to AutoMapView?
Note that we are already testing that the AutoMapper configuration is valid using AssertConfigurationIsValid, it is missing mappings that I want to deal with.
If your controller action looked like this:
public AutoMapView<UserEditModel> Edit(User item)
{
return AutoMapView<UserEditModel>(View(item));
}
Then you can pretty easily, using reflection, look for all controller actions in your project. You then examine the action parameter types and the generic type parameter of your AutoMapView action result. Finally, you ask AutoMapper if it has a type map for those input/output models. AutoMapper doesn't have a "CanMap" method, but you can use the IConfigurationProvider methods of FindTypeMapFor:
((IConfigurationProvider) Mapper.Configuration).FindTypeMapFor(null, typeof(User), typeof(UserEditModel);
Just make sure that's not null.
[Test]
public void MapperConfiguration()
{
var mapper = Web.Dto.Mapper.Instance;
AutoMapper.Mapper.AssertConfigurationIsValid();
}
You can use the AssertConfigurationIsValid method. Details are on the automapper codeplex site (http://automapper.codeplex.com/wikipage?title=Configuration%20Validation)
Strictly speaking you should be writing a test to validate the mapping before you write a controller action that depends on the mapping configuration being present.
Either way, you can use the Test Helpers in the MvcContrib project to check the action method returns the expected ViewResult and Model.
Here's an example:
pageController.Page("test-page")
.AssertViewRendered()
.WithViewData<PortfolioViewData>()
.Page
.ShouldNotBeNull()
.Title.ShouldEqual("Test Page");
I do something like this.
using System.Linq;
using System.Reflection;
using AutoMapper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class SampleDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Sample
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string LoginId { get; set; }
}
public class AutomapperConfig
{
public static void Configure()
{
Mapper.Initialize(cfg => cfg.AddProfile<ViewModelProfile>());
}
}
public class ViewModelProfile : Profile
{
protected override void Configure()
{
CreateMap<SampleDto, Sample>();
}
}
[TestClass]
public class AutoMapperTestsSample
{
public AutoMapperTestsSample()
{
AutomapperConfig.Configure();
}
[TestMethod]
public void TestSampleDtoFirstName()
{
#region Arrange
var source = new SampleDto();
source.FirstName = "Jim";
//source.LastName = "Bob";
var dest = new Sample();
dest.FirstName = "FirstName";
dest.LastName = "LastName";
dest.LoginId = "LoginId";
#endregion Arrange
#region Act
AutoMapper.Mapper.Map(source, dest);
#endregion Act
#region Assert
Assert.AreEqual("Jim", dest.FirstName);
Assert.AreEqual(null, dest.LastName);
Assert.AreEqual("LoginId", dest.LoginId);
#endregion Assert
}
[TestMethod]
public void TestSampleDtoLastName()
{
#region Arrange
var source = new SampleDto();
//source.FirstName = "Jim";
source.LastName = "Bob";
var dest = new Sample();
dest.FirstName = "FirstName";
dest.LastName = "LastName";
dest.LoginId = "LoginId";
#endregion Arrange
#region Act
AutoMapper.Mapper.Map(source, dest);
#endregion Act
#region Assert
Assert.AreEqual(null, dest.FirstName);
Assert.AreEqual("Bob", dest.LastName);
Assert.AreEqual("LoginId", dest.LoginId);
#endregion Assert
}
/// <summary>
/// This lets me know if something changed in the Dto object so I know to adjust my tests
/// </summary>
[TestMethod]
public void TestSampleDtoReflection()
{
#region Arrange
var xxx = typeof(SampleDto);
#endregion Arrange
#region Act
#endregion Act
#region Assert
Assert.AreEqual(2, xxx.GetRuntimeFields().Count());
Assert.AreEqual("System.String", xxx.GetRuntimeFields().Single(a => a.Name.Contains("FirstName")).FieldType.ToString());
Assert.AreEqual("System.String", xxx.GetRuntimeFields().Single(a => a.Name.Contains("LastName")).FieldType.ToString());
#endregion Assert
}
}

Resources