I am working on limiting access depending on user roles.
I want to be able to somehow override a belongsToMany relation to return all, if user->isAdmin() returns true.
Currently have as the AccountController index method:
public function index()
{
if(Auth::user()->isAdmin()) // can this go in beforeFilter?
return Account::all();
else
return Auth::user()->accounts;
}
in my User model:
public function accounts()
{
return $this->belongsToMany("Account");
}
Is there a neat way to do this without needing an if statement in the controller functions?
You cannot do this.
The relation method must return an instance of Relation, otherwise it throws an error.
There's nothing stopping you from creating a separate method for this:
AccountController.php:
public function index()
{
return Auth::user()->userAccounts();
}
User.php:
public function accounts()
{
return $this->belongsToMany("Account");
}
public function userAccounts()
{
if ($this->isAdmin()) return Account::all();
return $this->accounts;
}
Related
One of the most popular books on ASP.NET Core is "Pro ASP.NET Core 3" by Adam Freeman.
In chapters 7-11, he builds an example application, SportsStore.
The Index method of the HomeController shows a list of products:
Here's the Index method:
public ViewResult Index(int productPage = 1)
=> View(new ProductsListViewModel {
Products = repository.Products
.OrderBy(p => p.ProductID)
.Skip((productPage - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = productPage,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
}
});
The view file that corresponds to this method is Views\Home\Index.cshtml. This file has the following line at the top:
#model ProductsListViewModel
So the view is expecting an object of type ProductsListViewModel. However, in Visual Studio, IntelliSense shows View as expecting an argument of type object:
I'm surprised that View here isn't shown to expect an object of type ProductsListViewModel. Since it's setup to accept an argument of type object, we can actually pass in some nonsensical value:
public ViewResult Index(string category, int productPage = 1) =>
View(10);
and the project will still compile!
Is there a way to set things up so that View actually only accepts the model type specified in the view file? I.e. in this case, set things up so that View only accepts ProductsListViewModel?
Thanks!
By using generic, I did the following.
//This is the Base class for every model must inherit.
public class EntityModel
{
public int ID { get; set; }
}
//Model / view model must inherit from EntityModel
public class EmployeeModel : EntityModel
{
public string Name { get; set; }
}
Create a new ViewController which accepts only the EntityModel or its derived class.
Mark the View and its overloaded method obsolete so that only the entity Model calls as a parameter can only be used. Also if anyone uses it show restrict them for using it. Therefore throw error.
public class ViewController<T> : Controller where T: EntityModel
{
[Obsolete]
public override ViewResult View()
{
return base.View();
}
[Obsolete]
public override ViewResult View(object model)
{
throw new Exception("Use view method which accepts EntityModel");
}
[Obsolete]
public override ViewResult View(string viewName)
{
throw new Exception("Use view method which accepts EntityModel");
}
[Obsolete]
public override ViewResult View(string viewName, object model)
{
throw new Exception("Use view method which accepts EntityModel");
}
public new ViewResult View(T model)
{
return base.View(model);
}
}
Use the newly created ViewController in your Home Controller.
public class HomeController : ViewController<EmployeeModel>
{
public IActionResult Index()
{
EmployeeModel emp = new EmployeeModel();
emp.ID = 1;
emp.Name = "Satish Pai";
return View(emp);
}
}
I don't think there is a way to catch any wrong view models got passed into views at compile time!
The problem
The view is strongly typed with the view model you declare on the top so it knows what model is coming in, but the controller doesn't know which view you want it to return to...
By default, yes the controller is going to return to a view that has the same name as the method, but you can change that default, and you can even pass the name of the view as string parameter to one of the View() overloads:
public IActionResult Index(string category, int page = 1)
{
...
return View("OutOfStock", vm);
}
Now Visual Studio doesn't know which view model you want the controller to build and pass to the view. In fact, Visual Studio doesn't even know what view I want to return. Even after I put "OutOfStock" as the view name, Visual Studio doesn't know whether the view even exists or not...
Using Generic
#Satish's solution is indeed interesting but it assumes you are only working with 1 single view model for a single controller, which normally isn't the case. Usually you will have different view models for different actions.
If Generic were the way to go, I would suggest to put it on the action, rather on the controller:
public abstract class BaseController : Controller
{
public ViewResult View<T>(T viewModel) where T : new()
{
return View(viewModel);
}
}
Then you can use it like this in the controller:
public class ProductController : BaseController
{
public IActionResult Index(string category, int page = 1)
{
var vm = new ProductListViewModel
{
...
};
return View<ProductListViewModel>(vm);
// This would give you the compile time error!
// return View<ProductListViewModel>(10);
}
}
But why? What's the point of doing this? You, as the developer, have to know ProductListViewModel is the right view model to pass anyway. Putting something like this in place would be only helpful if there is a junior or new hire who's working on your code and doesn't bother to check the view model the returned view is asking for?
Maybe?
Now I know a tool like Resharper might be able to help and catch the mismatch at compile time.
Also writing unit tests on what the methods in the controller return might be helpful?
I have API that based on ASP.NET Core 2.2 and I want to return result not from my public method that handles request but from an inner method.
public class UsersController : MainController
{
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
var value = GetSomeValue();
}
private string GetSomeValue()
{
// and here I want to return result to user not even returning result to calling method `Get`
return "";
}
}
I can set status code to response through HttpContext.Response.StatusCode and I know how to set body to the response but I don't know either could I return response to user from GetSomeValue method or not. May be somehow using HttpContext?
I'm not really if this is what you're asking but here are my two cents:
I want to return result not from my public method that handles request but from an inner method.
Well this is not trivial, what you're asking If I get it right is to manipulate the HttpContext so you start the response on an inner method instead of returning from your controller, which is the right way.
I don't know why you would want to do that but, I guess you can get some advice here
In any case I don't get why don't you just:
return Ok(value);
like:
public class UsersController : MainController
{
[HttpGet("{id:int}")]
public IActionResult Get(int id)
{
var value = GetSomeValue();
return Ok(value);
}
private string GetSomeValue()
{
// and here I want to return result to user not even returning result to calling method `Get`
return "";
}
}
I can do this by using this code :
[HttpPost("SampleRoute1")]
public JsonResult Post([FromBody]SampleModel1 value)
{
.....Functionone.....
return Json("");
}
[HttpPost("SampleRoute2")]
public JsonResult Post([FromBody]SampleModel2 value)
{
.....Functiontwo.....
return Json("");
}
but i cant do this :
[HttpPost("SampleRoute1")]
public JsonResult Post([FromBody]SampleModel1 value)
{
.....Functionone.....
return Json("");
}
[HttpPost("SampleRoute2")]
public JsonResult Post([FromBody]SampleModel1 value)
{
.....Functiontwo.....
return Json("");
}
it gives error "Type 'Controller1' already defines a member called 'Post' with the same parameter types"
so is there any way that i can make two Post in one controller with same paramter but with different route?
like this :
Posting(SampleModel1) => "Controller1\SampleRoute1" => Doing Function1
Posting(SampleModel1) => "Controller1\SampleRoute2" => Doing Function2
Yes, you can do that. Problem is that you're trying to have two methods in a class that have same name & parameters and that's not possible. You should change name of your methods to something different.
Note that the action name & Post request type are already specified in the HttpPost attribute so you don't have to rely on the method name.
[HttpPost("SampleRoute1")]
public JsonResult Aaa([FromBody]SampleModel1 value)
{
.....Functionone.....
return Json("");
}
[HttpPost("SampleRoute2")]
public JsonResult Bbb([FromBody]SampleModel1 value)
{
.....Functiontwo.....
return Json("");
}
You are getting the error because you have 2 methods that are identical. How would you know which one to execute? Are you basing this on the routes that you defined?
If I gave you 2 identical red apples to eat, there is no difference between the 2 apples, and I told you to eat the correct apple, would you know which is the correct apple?
You are going to have to change your method names so that they are unique and identifiable.
[HttpPost("SampleRoute1")]
public ActionResult Function1(SampleModel1 model)
{
return Json("");
}
[HttpPost("SampleRoute2")]
public ActionResult Function2(SampleModel1 model)
{
return Json("");
}
So based on the above, the following will happen:
So now when posting SampleModel1, using route Controller1\SampleRoute1 will execute action method Function1
So now when posting SampleModel2, using route Controller1\SampleRoute2 will execute action method Function2.
I have two models with a one-to-many relationship.
class User extends ConfideUser {
public function shouts()
{
return $this->hasMany('Shout');
}
}
class Shout extends Eloquent {
public function users()
{
return $this->belongsTo('User');
}
}
This seem to work fine.
BUT, How do I get this to return the users object nested in the shout objects?
Right now it only returns all my Shouts, but I have no access in the JSON to the belonging user model.
Route::get('api/shout', function() {
return Shout::with('users')->get();
});
This just returns this JSON, with no user object for every shout:
[{"id":"1","user_id":"1","message":"A little test shout!","location":"K","created_at":"2013-05-23 19:51:44","updated_at":"2013-05-23 19:51:44"},{"id":"2","user_id":"1","message":"And here is an other shout that is a little bit longer...","location":"S","created_at":"2013-05-23 19:51:44","updated_at":"2013-05-23 19:51:44"}]
I was having the same trouble using Laravel 5. Just wanted to add that I got it to work by using the Model::with("relationship")->get() method on the model.
I figured it out.
The method needs to be named user() not users() when working with "belongsTo" relationship.
Makes sense.
And seems to work.
If you are using:
protected $visible = ['user'];
Don't forget to add there relationship, to be visible in JSON
u can use protected $with = ['users']; on Class Shout and use protected $with = ['shouts'];.
and Give Full namespace model name
class Shout extends Eloquent {
protected $with = ['users'];
public function users()
{
return $this->belongsTo('App\User');
}
}
and
class User extends ConfideUser {
protected $with = ['shouts'];
public function shouts()
{
return $this->hasMany('App\Shout');
}
}
Receive It
Route::get('api/shout', function() {
return Shout::all()->toJson;
});
Considering this example:
public ViewResult View1()
{
return View();
}
public ViewResult View2()
{
return View();
}
[HttpPost]
public ActionResult Processor(SomeModel model)
{
if (comeFromView1)
{
}
//implementation
return RedirectToAction("View3");
}
Both View1 and View2 have inside a form that post to Processor.
How to detect inside it where did i come from?
One option would be to check Request.UrlReferrer. However, a user can easily spoof the referrer.
A better way would be an action filter which sets the previous action. Like this:
public class SavePreviousActionAttribute
{
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
filterContext.HttpContext.Session["PreviousAction"] = filterContext.RouteData["action"]
}
}
Add this to all actions by registering it as a global filter (in Global.asax):
GlobalFilters.Filters.Add(new SavePreviousActionAttribute());
And then access it in your action:
if (Session["PreviousAction"].ToString() == "View1")
{
// Came from view1
}