I have an MVC Controller and a class that creates a menu with items that must only be displayed to users with a specific role for an action of a controller. In the below case I don't want to display the menu item of Details to users with Role2. What I end up doing is to specify the same roles on the menu items, the same roles that I already specified on the controllers. So I have 2 places where I define the roles and they must be the same, so it's error prone.
What I would like to do is to get the roles from the controller somehow but I have no clue how to do it or if it's even possible.
[Authorize(Roles = "Role1,Role2")]
public class MyController
{
public IActionResult Index()
{
return View();
}
[Authorize(Roles = "Role1")]
public IActionResult Details(int? id)
{
...
return View(...);
}
}
public class MenuItem
{
public string Action { get; set; }
public string Controller { get; set; }
public string Roles { get; set; }
}
...
var item = new MenuItem
{
Action = "Index",
Controller = "MyController",
Roles = "Role1,Role2", <---- this is what I do now.
Roles = GetRoles(MyController.Index.AuthorizedRoles) <---- this is what I need.
};
How about this factory method for your MenuItem:
public class MenuItem
{
public string Action { get; private set; }
public string Controller { get; private set; }
public string Roles { get; private set; }
private MenuItem() { }
public static MenuItem For<TMethod>(TMethod method) where TMethod : Delegate
{
var methodInfo = method.GetMethodInfo();
var attributes = methodInfo
.GetCustomAttributes(typeof(AuthorizeAttribute))
.Cast<AuthorizeAttribute>();
// If no attribute is defined on the action method, check the controller itself
if (attributes.Count() == 0)
{
attributes = methodInfo.DeclaringType
.GetCustomAttributes(typeof(AuthorizeAttribute))
.Cast<AuthorizeAttribute>();
}
return new MenuItem
{
Action = methodInfo.Name,
Controller = methodInfo.DeclaringType.Name,
Roles = string.Join(',', attributes.Select(a => a.Roles))
};
}
}
This can be called like:
var menuItem = MenuItem.For<Func<IActionResult>>(MyController.Details);
Related
I am doing Web project with MVC 5 . I need pass to some data to layout page (data as Category_id or Category_Name).
I read some answers that say I need to make View Model , but my project must be in MVC and not in MVVM,
Do you any ideas?
Thanks!
you have to create a base view model that you will have to use for ALL your views
using Microsoft.AspNetCore.Mvc.Rendering;
public interface IBaseViewModel
{
public int CategoryId { get; set; }
public List<SelectListItem> CategoryList { get; set; }
}
public class BaseViewModel : IBaseViewModel
{
public int CategoryId { get; set; }
public List<SelectListItem> CategoryList { get; set; }
}
action
public IActionResult Index()
{
var baseViewModel=new BaseViewModel();
InitBaseViewModel(baseViewModel);
return View(baseViewModel);
}
private void InitBaseViewModel(IBaseViewModel baseViewModel)
{
//this is for test
// in the real code you can use context.Categories.Select ....
var items = new List<SelectListItem> {
new SelectListItem {Text = "Category1", Value = "1"},
new SelectListItem {Text = "Category2", Value = "2"},
new SelectListItem {Text = "Category3", Value = "3"}
};
baseViewModel.CategoryList= items;
}
layout
#model IBaseViewModel // you can omit it but I like to have it explicitly
#if(Model!=null && Model.CategoryList!=null && Model.CategoryList.Count > 0)
{
<select class="form-control" style="width:450px" asp-for="CategoryId" asp-items="CategoryList">
}
for another view you can create this action code
public IActionResult MyAction()
var myActionViewModel= new MyActionViewModel {
..... your init code
}
InitBaseViewModel(myActionViewModel);
return View(myActionViewModel)
}
public class MyActionViewModel : BaseViewModel
//or
public class MyActionViewModel : IBaseViewModel
{
public .... {get; set;}
}
You can pass directly a obj to View if you want in this way:
public virtual async Task<IActionResult> Index()
{
var model = await MethodThatRedurnModel();
return View(model);
}
I have _serviceOne injected into my controller that has a method which returns an int value. I'm trying to pass this value into my custom action filter.
This isn't working and I am getting the error: An object reference is required for the non-static field, method, or property 'NameController._serviceOne' where I try to set the Number = _serviceOne.GetIntNumber.
I am aware that I can access a value if it is inside the controller (eg: controller parameter, ViewBag, ViewData, a variable in the controller), but I want to pass the value to the CustomActionFilter filter's Number property.
The filter and service method work the way I want it to, but it won't let me pass the value from _serviceOne.GetIntNumber into the filter. Why is this not working, and how could I make it work?
NameController.cs:
public class NameController : Controller
{
private readonly ServiceOne _serviceOne;
public NameController(ServiceOne serviceOne)
{
_serviceOne = serviceOne;
}
[CustomActionFilter(Name = "CorrectName", Number = _serviceOne.GetIntNumber)] //does not work
[HttpGet]
public IActionResult Index()
{
return View();
}
}
CustomActionFilter.cs:
public class CustomActionFilter : ActionFilterAttribute
{
public string Name { get; set; }
public int Number { get; set; }
public override void OnActionExecuted(ActionExecutedContext context)
{
if (Name == "CorrectName" && Number == 1) {
RouteValueDictionary routeDictionary = new RouteValueDictionary { { "action", "SomeAction" }, { "controller", "NameController" } };
context.Result = new RedirectToRouteResult(routeDictionary);
}
base.OnActionExecuted(context);
}
}
Attributes are created at compile time so it's not possible to pass them run-time values in the constructor.
Instead, you can access NameController's instance of the service, like this:
public class NameController : Controller
{
private readonly ServiceOne _serviceOne;
public ServiceOne ServiceOne => _serviceOne;
public NameController(ServiceOne serviceOne)
{
_serviceOne = serviceOne;
}
}
public class CustomActionFilter : ActionFilterAttribute
{
public string Name { get; set; }
public int Number { get; set; }
public override void OnActionExecuted(ActionExecutedContext context)
{
var controller = context.Controller as NameController;
var service = controller.ServiceOne;
//Use the service here
}
}
See also Access a controller's property from an action filter
I have a fairly simple ASP.NET MVC app that I am trying to resolve some dependencies in my controller. I have casting problems with List and I am not sure what to do at this moment. I have read about the Resolve() method with Autofac, but again I am not sure if this will resolve my particular issue.
Here is my controller code:
public class NumbersController : Controller
{
private INumbersModel _model;
private INumbersBusinessLayer _numbersBusinessLayer;
private IEnumerable<INumbersModel> _modelList;
public NumbersController(INumbersModel model, IEnumerable<INumbersModel> modelList, INumbersBusinessLayer numbersBusinessLayer)
{
_model = model;
_numbersBusinessLayer = numbersBusinessLayer;
_modelList = new List<INumbersModel>(modelList);
}
public ActionResult Index()
{
_modelList = _numbersBusinessLayer.AllNumbers.ToList();
return View(_modelList);
}
[HttpGet]
public ActionResult Edit(int id)
{
_model = _numbersBusinessLayer.AllNumbers.Single(n => n.ID == id);
return View(_model);
}
}
Here are my two interfaces:
public interface INumbersBusinessLayer
{
IEnumerable<NumbersModel> AllNumbers { get; }
void AddNumbers(NumbersModel model);
void DeleteNumbers(int id);
void UpdateNumbers(NumbersModel model);
}
public interface INumbersModel
{
int ID { get; set; }
bool IsValid { get; set; }
string Numbers { get; set; }
string Order { get; set; }
string Time { get; set; }
}
Here is my container config:
public static void ConfigureDependencyInjection()
{
var builder = new ContainerBuilder();
// ...or you can register individual controlllers manually.
builder.RegisterType<NumbersController>().InstancePerRequest();
// register models
builder.RegisterType<NumbersModel>().As<INumbersModel>();
builder.RegisterType<List<NumbersModel>>().As<List<INumbersModel>>();
builder.RegisterType<NumbersBusinessLayer>().As<INumbersBusinessLayer>();
IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
In the browser I get this error:
The type 'System.Collections.Generic.List1[BusinessLayer.NumbersModel]' is not assignable to service 'System.Collections.Generic.List1[[BusinessLayer.INumbersModel, BusinessLayer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]'.
You should only need to register your INumbersBusinessLayer, since that is the only thing you actually need to inject into your controller.
Furthermore, change INumbersModel to a class, so you can use it.
So your controller then looks like this:
public class NumbersController : Controller
{
private INumbersBusinessLayer _numbersBusinessLayer;
public NumbersController(INumbersBusinessLayer numbersBusinessLayer)
{
_numbersBusinessLayer = numbersBusinessLayer;
}
public ActionResult Index()
{
var modelList = _numbersBusinessLayer.AllNumbers.ToList();
return View(modelList);
}
[HttpGet]
public ActionResult Edit(int id)
{
var model = _numbersBusinessLayer.AllNumbers.Single(n => n.ID == id);
return View(model);
}
}
Then you can simplify your AutoFac config:
public static void ConfigureDependencyInjection()
{
var builder = new ContainerBuilder();
// ...or you can register individual controlllers manually.
builder.RegisterType<NumbersController>().InstancePerRequest();
builder.RegisterType<NumbersBusinessLayer>().As<INumbersBusinessLayer>();
IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
and change your interface to a class:
public class NumbersModel
{
int ID { get; set; }
bool IsValid { get; set; }
string Numbers { get; set; }
string Order { get; set; }
string Time { get; set; }
}
Since your businesslayer interface already expects Numbersmodel, but not INumbersmodel, no changes are needed there.
Only two parameters require to be injected
private INumbersModel _model;
private INumbersBusinessLayer _numbersBusinessLayer;
public NumbersController(INumbersModel model,INumbersBusinessLayer numbersBusinessLayer)
{
_model = model;
_numbersBusinessLayer = numbersBusinessLayer
}
and no need to register a list of NumbersModel, comment this line and try again
builder.RegisterType<List<NumbersModel>>().As<List<INumbersModel>>();
I'm having a trouble with my project (ASP.NET MVC 5/AJAX/BOOTSTRAP).
When click on Save button on Page, .Net calls in POST the proper action, but the Hidden Fields for PSATOKEN does not contain value (see #Html.HiddenFor(m => m.PSAToken) in the View), despite PSAToken contains a GUID value (saw in Debug Mode) in the Controller method.
Let's see some code below.
Many thanks to answerers!
Model
public interface IPSAPageViewModel
{
String PSAToken { get; set; }
int IdPSAAzienda { get; set; }
}
public abstract class BasePSAPageViewModel : IPSAPageViewModel
{
public String PSAToken { get; set; }
public int IdPSAAzienda { get; set; }
}
public class DatiGeneraliViewModel : BasePSAPageViewModel
{
public DatiGeneraliViewModel()
{
this.Item = new InformazioniGenerali();
}
public Crea.PSA.ServiceLayer.BO.InformazioniGenerali Item { get; set; }
public List<SelectListItem> FormeGiuridicheList { set; get; }
public List<SelectListItem> FormeConduzioneList { set; get; }
}
Controller
private ViewResult ViewPSAPage(IPSAPageViewModel vm)
{
base.createViewBagPaginePrecSucc();
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[HttpParamAction]
public ActionResult SalvaDatiGeneraliProsegui(DatiGeneraliViewModel vm)
{
return salvataggioDatiGenerali(vm, true);
}
[HttpPost]
[ValidateAntiForgeryToken]
[HttpParamAction]
public ActionResult SalvaDatiGenerali(DatiGeneraliViewModel vm)
{
//Here vm.PSAToken doesn't contain the value setted
return salvataggioDatiGenerali(vm);
}
private ActionResult salvataggioDatiGenerali(DatiGeneraliViewModel vm, bool proseguiCompilazione = false)
{
if (ModelState.IsValid)
{
var resp = aziendeManager.Save(vm.PSAToken, vm.Item, SessionManager.UserIdConnected, CONTROLLERNAME);
if (resp.Success)
{
var psaAzienda = resp.DataObject;
setVarsInSession(psaAzienda.idToken.ToString(), psaAzienda.idPsaAzienda.ToString(), psaAzienda.Aziende.ragioneSociale);
//Here there is some Value (POST)
vm.PSAToken = psaAzienda.idToken.ToString();
//vm.IdPSAAzienda = psaAzienda.idPsaAzienda.ToString();
if (proseguiCompilazione)
return RedirectToAction("DatiAziendaliRiepilogativi", new { id = psaAzienda.idToken });
}
else
ModelState.AddModelError("", resp.Message);
}
setSuccessMessage();
vm.FormeGiuridicheList = aziendeManager.GetAllFormeGiuridiche().ToSelectItems();
vm.FormeConduzioneList = aziendeManager.GetAllFormeConduzione().ToSelectItems();
return ViewPSAPage(vm);
}
View
to see the view click here
Here you can see the value at debug in VS
But in the generated HTML the Hidden Field of PSATOKEN is empty
I found the solution here:
patrickdesjardins.com/blog/… .
I'm creating a simple Private Message system for my website. Here is the model:
public class PrivateMessage : GlobalViewModel
{
[Key]
public int MessageId { get; set; }
public bool IsRead { get; set; }
public DateTime CreatedDate { get; set; }
[MaxLength(100)]
public string Subject { get; set; }
[MaxLength(2500)]
public string Body { get; set; }
public virtual UserProfile Sender { get; set; }
public virtual UserProfile Receiver { get; set; }
}
I want to check on every page request if you have any new messages, so I can notify the user. Therefore I made a base viewmodel, which contains:
public class GlobalViewModel
{
[NotMapped]
public virtual int NewMessages { get; set; }
}
All other viewmodels inherit from this class. To get the amount of new private messages for the user, I do this:
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
DBContext db = new DBContext();
int userID = (int)Membership.GetUser().ProviderUserKey;
int newMessages = db.PrivateMessages.Where(a => a.Receiver.UserId == userID && a.IsRead == false).Count();
base.OnActionExecuted(filterContext);
}
I came to this and the OnActionExecuting is indeed called on every Action. But my question is:
How can I add the newMessages to the GlobalViewModel?
What I want to eventually do, is call this in the 'master' view
You have #Model.NewMessages new messages
You could override the OnActionExecuted event which runs after your action has finished running and which would allow you to inspect the model being passed to the view and potentially modify it by setting some properties on it:
public class PrivateMessageFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutedContext filterContext)
{
GlobalViewModel model = null;
var viewResult = filterContext.Result as ViewResultBase;
if (viewResult != null)
{
// The action returned a ViewResult or PartialViewResult
// so we could attempt to read the model that was passed
model = viewResult.Model as GlobalViewModel;
}
if (model == null)
{
var jsonResult = filterContext.Result as JsonResult;
if (jsonResult != null)
{
// The action returned a JsonResult
// so we could attempt to read the model that was passed
model = jsonResult.Data as GlobalViewModel;
}
}
if (model != null)
{
// We've managed to read the model
// Now we can set its NewMessages property
model.NewMessages = GetNewMessages();
}
}
private int GetNewMessages()
{
int userId = (int)Membership.GetUser().ProviderUserKey;
int newMessages = db.PrivateMessages.Where(a => a.Receiver.UserId == userId && a.IsRead == false).Count();
}
}
As an alternative to using a base view model you could write a custom HTML helper which will return this information.