10

I have a custom authenticantion, when user logs in, I keep the necessary information on Session/Cache...

So, I have some Views with DropDowns that must show data filtered by User id... I´d like to known what the best way to filter that result...

1 - Direct on Controller?

...   
Model.MyList = repository.GetAll().Where(x => x.User.Id == userId);
return View(Model);

2 - Creating an action filter (How can I do that without querying unnecessary data from DB)

3 - Other way?

The problem with 1 is that I have several views that have the same dropdown, so I will have to repeat the same code.

Paul
  • 12,359
  • 20
  • 64
  • 101
  • If you do not have a privacy issue with retreiving all users data at once then maybe you can use knockout or similar and databinding on the client side, when working it will be faster than controllers. – sprocket12 Aug 05 '13 at 14:22
  • I need to filter on server... Thanks – Paul Aug 05 '13 at 14:24
  • Okay then do it on the controller and return JSON, use some sort of templating to display that in your UI using JS. – sprocket12 Aug 05 '13 at 14:25
  • Doing on controller is easier, but I´ll have to do the same filter in every controller... – Paul Aug 05 '13 at 14:28
  • @Paul you could create a global filter for your context. If interested, I posted a similar answer here http://stackoverflow.com/questions/17102563/mvc-repository-pattern-how-to-security-trim/17109614#17109614 – SOfanatic Aug 05 '13 at 15:35
  • What precisely is your condition for when you need to filter data and when you don't? If you could tell us the conditions under which this data is needed then we might be able to help better... – Marko Aug 11 '13 at 12:40
  • Also do you have to fetch data from repository on every request or is that in your cache/session? – Marko Aug 11 '13 at 12:48

7 Answers7

10

Approach - 1

Function

private void userInfo(ResultExecutingContext filtercontext)
{                                        
    if (filtercontext.Controller.TempData[userId.ToString()] == null)
        filtercontext.Controller.ViewBag.userId =
            filtercontext.Controller.TempData[userId.ToString()] = 
            repository.GetAll().Where(x => x.Id == userId);

    else      //This will load the data from TempData. So, no need to 
              //hit DataBase.
        filtercontext.Controller.ViewBag.userId =
            filtercontext.Controller.TempData[userId.ToString()];

    TempData.Keep();  // This will save your Database hit.
}

Filter Method

public class MyActionFilter : ActionFilterAttribute
{
    public override void OnResultExecuting(ResultExecutingContext filtercontext)
    {
        //Call the Action Method before executing the View and after 
        //executing the Action.
        userInfo(filtercontext);
        base.OnResultExecuting(filtercontext);
    }
}

Controller Action Method

[MyActionFilter] 
//Whenever Action Method will execute. We will check TempData contains 
//Data or not.
public ActionResult Index()
{
    return View();
}

Key point about TempData and TempData.Keep()

  1. Items in TempData will only tagged for deletion after they have read.
  2. Items in TempData can be untagged by calling TempData.Keep(key).
  3. RedirectResult and RedirectToRouteResult always calls TempData.Keep() to retain items in TempData.

You could use Session Variable also, Only major problem is that Session Variable are very heavy comparing with TempData. Finally you are able to keep the data across Controllers/Area also.

TempData works in new Tabs/Windows also, like Session variable does.

Approach - 2

You can Cache the Data in some variable and can be reused again In the same manner done for TempData.

Imad Alazani
  • 6,688
  • 7
  • 36
  • 58
1

I would just create an action filter that puts the values you need inside a ViewBag and send it over to the view. This way you don't have to rewrite the same code over and over again and you can just concentrate on the view to display the data as necessary. Please see below for sample code:

using System.Web.Mvc;
namespace CustomActionFilter.CustomActionFilters
{
    public class MyResultFilterAttribute : FilterAttribute, IResultFilter
    {
        public void OnResultExecuting(ResultExecutingContext filterContext)
        {
            //The action filter logic - before 
            filterContext.Controller.ViewBag.userInfo = GetNeccUserInfo(filterContext.HttpContext.User.Identity.Name);
        }

        public void OnResultExecuted(ResultExecutedContext filterContext)
        {
            //The action filter logic - after
        }
    }
    private UserInfo GetNeccUserInfo(string userName)
    {
        using (var repo = new UserRepository(new UniteOfWorkUsers()))
        {
            var userInfo = repo.GetUserInfo(userName);
            return userInfo;
        }
    }
}

Hope this helps out :)

hjavaher
  • 2,589
  • 3
  • 30
  • 52
1

This is a pretty default scenario that you want a user to see only data relevant to him.

Personally, I never did DB-Calls within the controller, I always had an additional DataLayer which I wired with an IoC-Container.

This DataLayer should only know the DataBase and how the data is stored, and filter this data correctly. You can argue if the DataLayer can use the HttpContext to automatically retrieve the user-ID or should get it as an argument.

So you not to have write always that expression you could also create a function, which will give you the correct Where-Lambda-Expression and you can simply use it:

public Expression<TModel> GetUserFilter<TModel>()
{
    var userId = GetUserId();
    var itemParameter = Expression.Parameter(typeof(TModel), "item");
    var whereExpression = Expression.Lambda<Func<TModel, bool>>
        (
        Expression.Equal(
            Expression.Property(
                itemParameter,
                "Id"
                ),
            Expression.Constant(userId)
            ),
        new[] { itemParameter }
        );
    return whereExpression;
}

And now you can call this function in you Controller or DataLayer:

Model.MyList = repository.GetAll().Where(GetUserFilter<Repository>());

You can of course change the names and make it shorter so that it's actually less to write :)

peter
  • 14,348
  • 9
  • 62
  • 96
1

Disclaimer: I am author of Entity REST SDK.

I took different approach and created Security Context which contains all necessary lambda expressions that should be applied before querying anything.

    public class DefaultSecurityContext : BaseSecurityContext {

      public static DefaultSecurityContext Instance = new DefaultSecurityContext();

      // UserID for currently logged in User
      public static long UserID{
           get{
                 return long.Parse( HttpContext.Current.User.Identity.Name );
           }
      }

      public DefaultSecurityContext(){
      }

      protected override void OnCreate(){

            // User can access his own Account only
            var acc = CreateRules<Account>();

            acc.SetRead( y => x=> x.AccountID == UserID ) ;
            acc.SetWrite( y => x=> x.AccountID == UserID );

            // User can only modify AccountName and EmailAddress fields
            acc.SetProperties( SecurityRules.ReadWrite, 
                  x => x.AccountName,
                  x => x.EmailAddress);

            // User can read AccountType field
            acc.SetProperties<Account>( SecurityRules.Read, 
                  x => x.AccountType);

            // User can access his own Orders only
            var order = CreateRules<Order>();
            order.SetRead( y => x => x.CustomerID == UserID );

            // User can modify Order only if OrderStatus is not complete
            order.SetWrite( y => x => x.CustomerID == UserID && x.OrderStatus != "Complete" );

            // User can only modify OrderNotes and OrderStatus
            order.SetProperties( SecurityRules.ReadWrite, 
                  x => x.OrderNotes,
                  x => x.OrderStatus );

            // User can not delete orders
            order.SetDelete(order.NotSupportedRule);
      }
}

As you can see, we can filter down access to properties as well.

In the case most of your duplicate code gets replaced with Security Context, you can create different Security Context as per different User role & still keep same controller.

public class OrdersController : WebAtomsController <MyEntities> {
        protected override BaseSecurityContext CreateSecurityContext(){
             return DefaultSecurityContext.Instance;
        }

        public ActionResult SearchOrders( 
               string productName, 
               string orderBy = "OrderID DESC", 
               int start = 0, 
               int size = 10)
        {
               // Where method automatically applies
               // filter based on current SecurityContext

               var aq = Where<Order>();
               if(!string.IsNullOrEmpty(productName)){
                      aq = aq.Where( 
                                  x=> x.OrderItems.Any( 
                                     y=> y.Product.ProductName.StartsWith(productName)));
               }

               // OrderBy accepts string as a parameter
               aq = aq.OrderBy(orderBy);
               return aq.Page(start,size).Select( 
                     y=> new {
                            y.OrderID,
                            y.OrderDate,
                            y.OrderStatus,
                     });
        }
}

for more details, please visit https://entityrestsdk.codeplex.com

Akash Kava
  • 39,066
  • 20
  • 121
  • 167
0

For example, you may create MembershipLogic class and define there all methods you use now or may be use in a future. Class will return you any data by userID

So in your controller it will look like:

var db = new DbEntities();
List<a> newList = MembershipLogic.UserList(db, userid);

And in MembershipLogic you than need following method:

public static List<a> UserList(DbEntities db, int UserID)
{
var list = db.GetAll().Where(x => x.Id == userId);
return list;
}

I use such logic in my projects. Accumulate methods and use them everythere I need.

Andrey Gubal
  • 3,481
  • 2
  • 18
  • 21
0

best way: get a cached list of all users. +: database efficient. -: uses lots of memory if big table. -: result not up to date (adjust cache time).

In OData there is a database request filter which does this filter, but it is not intended to be used the way you want. It's here to protect against errors in stored procs and queries which returns rows that are not authorized for this user. This is a 2nd level of protection against data "leaks".

var model = new Model(userId)

elsewhere:

Model(Guid userID)
{
    MyList = CacheStore.Get("allUsers", () => repository.GetAll())
                .Where(x => x.Id == userId).ToList();
}
Softlion
  • 12,281
  • 11
  • 58
  • 88
0

Why not create a method with a userId parameter, or create a User's Extension Method use it like this"User.GetList();" ? I'm not sure if it's reasonable for the Repository pattern. :)

shimron
  • 596
  • 6
  • 19