Reputation: 321
So I have bought into the "thin controller, fat model" guideline. That to me means that as little code as possible is in the controller, and most/all of the actual business logic is in the model (or in separate repository/services code).
In fact, I like the idea of the controller being a VERY simple conduit between the view and the model, and mostly concerned with calling an appropriate method in the model to do something, catch exceptions that then are added as ModelErrors, and deciding what view to go to next. Keep it as simple as possible.
At least I bought all this until I tried to write some code in my model that dealt with anything related to identity and roles.
It seems all the info required is in the Controller base class. So the only way I can think of to really access that in a Model method is by passing it as parameters? That gets really ugly really fast.
How can I access the info (IPrincipal, Session info etc) from within the Models?
Upvotes: 1
Views: 966
Reputation: 6873
I've solved this by creating a base Controller (and inheriting every controller from that) and a base Model (forcing each Model in my apps to inherit from that).
In the 2 base objects I've added 2 properties
public string UserId { get; private set; }
public Object UserInfo { get; private set; }
containing the readonly user informations (userId is also session stored, but for UserInfo I've used MemoryCache).
in this way I've got everything accessible from both my controllers and models ( even in Views obvioulsy).
Informations are setted just in the base controller (by overriding the OnAuthentication or using a custom AuthorizeAttribute).
I don't know if this is politically correct, but since I'm not bound to ASP.NET authoring (got to use SSO with Kerberos style), I've choose to implement in this way.
EDIT: How I do this:
I have an HomeController
which simply stores userId in session then forwards to the real starting point of the webapp (a controller which extends my BaseController
)
This pattern is secured by having the Authorize attribute on the base controller, each derived controller will have it applyed for each action, thus the OnAuthorization method of the base controller (or the one in your derived controller if you need to override for special cases) will be fired (e.g. No way to access without passing at least once from the HomeController/Index())
mock of the base controller:
[Authorize]
[HandleError]
public abstract class BaseController : Controller
{
public string UserId
{
get
{
return (string)HttpContext.Session[MyConstants.UserId];
}
}
public Object UserInfo
{
get { /* Access a MemoryCache with the UserId and SessionId */ }
}
[NonAction]
protected override void OnAuthorization(AuthorizationContext filterContext)
{
String Reason = "Everything's good :D";
bool Ok = true;
bool auth = true;
// Base authorization (NTLM)
base.OnAuthorization(filterContext);
// Checks retriving UserId from session and base authentication
Ok = ((UserId != null) && // Exits in session
(filterContext.HttpContext.User.Identity.IsAuthenticated) // is legit
);
if (!Ok) {
Reason = "Meaningfull message (session expired or not authenticated)!";
} else {
// My Authorization Tests Start here
try
{
MyUserInfo u = (MyUserInfo) UserInfo;
if (u != null)
{
// Found, check if has rights to access (heavy business logic in the IsAuthorized omitted)
Ok = u.IsAuthorized();
}
if (!Ok)
{
Reason = "Your credentials don't allow you to do this!";
}
}
catch (Exception e)
{
Reason = "doooh, exception checking auth: " + e.Message ;
Ok = false;
}
}
if (!Ok) {
if (Request.IsAjaxRequest())
{
// If it was an ajax, I return a status 418, via global Ajax Error function I handle this
// client side with an alert.
filterContext.Result = new HttpStatusCodeResultWithJson(418, Reason);
}
else
{
// Redirect to a specific Controller
TempData["Reason"] = Reason;
// A class which uses the Url helper in order to build up an url
string denied = UrlFactory.GetUrl("Denied", "Errors", null);
// Redirect
filterContext.Result = new RedirectResult(denied);
}
}
return;
}
}
Of course you need an HomeController not inherithing from the base (the entry point of your webapp)
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index(string StringaRandom, string HashCalcolato)
{
string Motivo = "";
string UserId = null;
try {
bool UtenteCollegato = MySignOn(StringaRandom, HashCalcolato, ref UserId, ref Motivo);
// ok, valid user, SignOn stores UserInfos in a Cache (SLQ Server or MemoryCache)
if (UtenteCollegato)
{
if (HttpContext.Session != null)
{
// Salvo in sessione
HttpContext.Session.Add(MyConstants.UserId, UserId);
}
// Redirect to the start controller (which inherits from BaseController)
return RedirectToAction("Index", "Start");
}
}
catch (Exception e)
{
Log.Error(e);
Motivo = "Errore interno: " + e.Message;
}
HttpContext.Session.Remove(MyConstants.UserId);
string denied = UrlFactory.GetUrl("Denied", "Errors", null);
TempData["Reason"] = Motivo;
return new RedirectResult(denied);
}
}
The StartController inherits from BaseController, thus each action needs OnAuthorize invocation before, since the StartController doesn't override it, the BaseController one is called.
More or less this is everything, as a bonus I add the HttpStatusCodeWithJon class.
public class HttpStatusCodeResultWithJson : JsonResult
{
private int _statusCode;
private string _description;
public HttpStatusCodeResultWithJson(int statusCode, string description = null)
{
_statusCode = statusCode;
_description = description;
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = context.HttpContext;
var response = httpContext.Response;
response.StatusCode = _statusCode;
response.StatusDescription = _description;
base.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
base.ExecuteResult(context);
}
}
this class is usefull to trigger STATUS errors for Ajax callbacks. If you use jQuery you can then use a global ajax error function to manage this.
This is it, maybe it's not elegant, maybe is not politically correct, but does almost everything I need, it's somewhat centralized, and for my current project works.
For models, simply add a public get, private set property for userid and userInfo and have them set in your model constructor (since each model is created in a controller you should have no problems at all on invoking the base model constructor via : base(params)
Warning: the code is a mock of the real thing (so can have typos or missing something), I've avoided to paste my business logic and reworked some parts, I guess it can be taken as a good hand drawn roadmap.
Let me know if this helps or if you need other infos.
PS:I was almost forgetting. Ff you work with ASP.NET profiles, Identities, I suggest you to look at the AuthorizeAttribute class, you can extend it and create your own Authorize Attribute, in that case, you don't need to write the OnAuthorization on the base controller or having inheritance (I still suggest you to have a base model and a base controller), but you'll provide that method in your Attribute. It's cleaner. I've not done it because of some legacy constraint with my Single Sign On solution, but will migrate to that.
Automatically Injection in the model can be done extending the ModelBinder (or registering a custom one). Never looked deep in that, I prefer another approach for data filtering (it's authorization not authentication, for me is app based and cannot rely on ASP.NET profiling)
The approach I would probably use is having a business object taking care of DataFiltering
Assume you've got an action like
ActionResult Something(SomeModel TheModel) {
// perform anything
TheModel.DoSomething();
return View(TheModel);
}
you can change it to something like
ActionResult Something(SomeModel TheModel)
{
MyBusiness bsn = new MyNusiness(UserId, TheModel); // Give UserId or UserInfo directly to business
TheModel = bsn.SomethingInABusinessWay();
return View(TheModel);
}
or if you want to keep everything in your model, just add the UserId parameter to the DoSomething method. Yeah we're working with object, but there're cases in which an object can also rely on external data (not only on data members or properties).
This is a pretty neat and fast solution, major downside is adding a param to each vm business method, but it's better than scanning every single action in order to inject it (at least the compiler gives an error on each calls)
Will look further in injecting a sproperty in a model extending the default modelbinder as soon as I'll be free from some javascript namespaces nightmare I'm actually in. But if I recall correctly I've seen something like that on the net (Phil Haak or ScottGu's blogs or even here on SO), just search for Injecting data in a Model at runtime.
Upvotes: 2