I am writing an MVC 5 app with a relatively complicated data model.
I have Listings and Listings have photo Albums associated with them.
To get things started, I just made sure that when a user is trying to call the Edit function of a controller, that the user was the owner of the object. Like so:
// Listing Controller
public bool VerifyOwnership(int? id)
{
if (id == null) return false;
Listing listingModel = db.Listings.Find(id);
if (listingModel == null)
{
return false;
}
else
{
return User.Identity.GetUserId() == listingModel.SellerID;
}
}
However, this check is now propagating itself throughout my code base. Since Albums are owned by Listings, this code didn't seem that terrible to me:
// AlbumController
public ActionResult Edit(int? id, int listingId)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Album a = db.Albums.Find(id);
if (a == null)
{
return HttpNotFound();
}
var l = new ListingController();
if (!l.VerifyOwnership(listingId))
return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
ViewBag.ListingID = listingId;
return View(a);
}
I think I'm doing it wrong.
It seems that ideally the Album controller would not be instantiating a ListingController just to check ownership. I could copy the ownership logic out of the ListingController and paste it into the AlbumController, but now I'm copy pasting code. Yuck.
I read this article about making a custom Authorize attribute - ASP.NET MVC Attribute to only let user edit his/her own content, which seems ok except that I wasn't sure how to instantiate an ApplicationDbContext object inside the AuthorizeCore override so that I could lookup the owner of the listing and do my checks. Is it ok to just create ApplicationDbContext objects willy-nilly? Do they correlate to persistent database connections or are they an abstraction?
This is where you're going wrong....
Album a = db.Albums.Find(id);
I could have typed in any ID and your app would go and fetch it and then perform a ownership verification which is unneeded...
Tackle the problem from the other end, what if, we were to scope our results by what the users has access to first, and then performing a search for an album within the scope of what the user has access to. Here's a few examples to give you an idea of what I mean.
db.Users.Where(user => user.Id == this.CallerId)
.SelectMany(user => user.Albums)
.Where(album => album.Id == "foo");
db.Albums.Where(album => album.OwnerId == this.CallerId)
.Where(album => album.Id == "bar");
It's all going to come down to the layout of your db and the fashion in which you've mapped your entity models, but the concept is the same.
Related
I'm teaching myself C# and MVC but have a background in SQL. When updating an existing master-detail set of records in a single action (let's say for instance a customer order and order details), updating the master record is no problem. Regarding the detail records, I'm seeing examples that simply delete all existing details and then add them back in rather than add, delete or update only what's changed. That seems easy and effective but involves unnecessary changes to database records and might be an issue in complex relationships.
I've tried writing code that checks the existing values against posted values to determine the right EntityState (Added, Deleted, Modified, Unchanged) for each detail. Accomplishing this using LINQ Except and Intersect works but seems to cause an unexpected performance hit.
(Instead, I could load the original values in an "oldValue" hidden field in the original GET request to compare to the POST values except that would be unreliable in a multi-user environment and seems like a bad idea.)
I'll be happy to provide code examples, but my question is more about best practices. Is there a preferred method for updating existing master-detail sets of records?
EDIT: I've added the code below in response to questions. In this example, our application allows additional attributes to be attached to a product, kept in a separate table ProductAttributes. The view allows the user to edit both the product and the attributes on the same webpage and save at the same time. The code works fine but seems slow and lags at SaveChanges.
public ActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
db.Entry(product).State = EntityState.Modified;
// Establish entity states for product attributes.
List<ProductAttribute> existingAttributes = new List<ProductAttribute>();
existingAttributes = db.ProductAttributes.AsNoTracking()
.Where(x => x.Sku == product.Sku).ToList();
// Review each attribute that DID NOT previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Except(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value didn't exist and still doesn't.
db.Entry(pa).State = EntityState.Unchanged;
}
else
{
// New value exists that didn't before.
db.Entry(pa).State = EntityState.Added;
}
}
// Review each attribute that DID previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Intersect(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value existed and has been cleared.
db.Entry(pa).State = EntityState.Deleted;
}
else
{
if (pa.Value != existingAttributes
.FirstOrDefault(x => x.Attribute == pa.Attribute).Value)
{
// Value has been altered.
db.Entry(pa).State = EntityState.Modified;
}
else
{
// Value has not been altered.
db.Entry(pa).State = EntityState.Unchanged;
}
}
}
db.SaveChanges();
return RedirectToAction("Details", new { id = product.ProductId });
}
return View(product);
}
internal class ProductAttributeComparer : IEqualityComparer<ProductAttribute>
{
public bool Equals(ProductAttribute x, ProductAttribute y)
{
if (string.Equals(x.Attribute, y.Attribute,
StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public int GetHashCode(ProductAttribute obj)
{
return obj.Attribute.GetHashCode();
}
}
}
Im using ASP.Net Core 3 with membership. Currently I have the index method returning all entries in the Assets table. I want to change it so that it returns Assets based on the logged in user while allowing the Administrator role to view all of them.
In the Assets table I have a column where the User ID gets stored for each user who creates a new Asset. I'm not sure which way to go with this with what I already have.
Controller Code
public ViewResult Index() {
// retrieve all the assets
var model = _assetRepository.GetAllAssets();
// Pass the list of assets to the view
return View(model);
}
Respository Code
public IEnumerable<Asset> GetAllAssets() {
return context.Asset;
}
Interface Code
public interface IAssetRepository {
Asset GetAsset(int Id);
IEnumerable<Asset> GetAllAssets();
Asset Add(Asset asset);
Asset Update(Asset assetChanges);
Asset Delete(int Id);
}
A good place to start is following one of the tutorials around working with authentication and authorization in ASP.NET Core. Here's one that seems to address your case.
First at all, I recommend you to add a new column that keeps the "role". If you don't want to use the framework that already does that. Then if you add a column with the role you can do on the repository something like this:
public IEnumerable<Asset> GetAllAssets(Guid userId) {
var role = context.Asset.Where(x => x.userId = userId).Select(x => x.role);
if (role = "admin")
return context.Asset;
return context.Asset.Where(x => x.userId = userId);
}
For this case I hardcode "admin" you can replace with your implementation. But I think you want to arrive to a solution like this...
I hope to help you. And I recommend you to add a new layer "service", between the controller and the repository, you can add there the "business logic".
I am building a mail system where at every page that you land you will get a notification that you have unread mail.
As this needs to be on every page, I thought that I should probably then just add functionality to Base Controller and have the function called that way as every controller I have will be extended of my Base Controller.
As such in my base controller I have the following function which will get me the number of unread invitations this user has:
public void GetUnreadInvitationCount(string userId)
{
var count = Db.Request.Where(r => r.ReceiverId == userId && r.DateLastRead == null).Count();
if (count > 0) ViewBag.UnreadInvitations = count;
}
Then in my constructor I tried the following:
public class BaseController : Controller
public BaseController()
{
if (User != null && User.Identity != null && User.Identity.IsAuthenticated)
{
GetUnreadInvitationCount(User.Identity.GetUserId());
}
}
}
The problem is that the User is null as it has not been instantiated.
How do I get around this? How do I make a common functionality such as this be on every page and not have to repeat my code on every controller specifically?
I have thought of few solutions myself, but none of these seem to be the right way to go.
Option 1: Create a BaseViewModel which will be called in every page that has this value, this would mean I have to instantiate the method in every action on the website, but at least the code is common for it if I ever need to update it.
Option 2: Do not do this on the server side but setup an ajax script to be called after the page has loaded. This would have an initial delay but it would work.
Does anyone has a different solution?
EDIT - For JohnH:
I have tried solution suggested by john, here is the code:
_Layout.cshtml
#{ Html.RenderAction("GetUnreadInvitationCount", "Base");}
BaseController.cs
public ActionResult GetUnreadInvitationCount()
{
string userId = User.Identity.GetUserId();
var count = Db.Request.Where(r => r.ReceiverId == userId && r.DateLastRead == null).OrderByDescending(r => r.Id).Count();
BaseViewModel model = new BaseViewModel {RequestCount = count};
return View("UnreadInvitations", model);
}
UnreadInvitations.cshtml
#model Azularis.System.Events.Models.ViewModels.BaseViewModel
#if (#Model.RequestCount > 0)
{
<li>
#Html.ActionLink("Mail", "Index", "Teams", null, new { #class = "mail-image" })
#Html.ActionLink(#Model.RequestCount.ToString(), "Index", "Teams", null, new { #class = "mail-number" })
</li>
}
However this forces me into a loop where _Layout.cshtml is constantly repeating until the page crashes with
The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.
Does anyone knows why it constantly loops?
As discussed in the comments above, the real issue here is not that the code should be shared amongst various controllers, it's that you want a common point in which to run your particular piece of code. In that sense, it lends itself to being abstracted out into a separate controller, which centralises all invitation logic in one place, leading to better separation of concerns. You can then invoke those actions either in your _Layout.cshtml view, or in any other views if need be.
Using the code in your answer as an example (thanks for that):
InvitationController:
public ActionResult GetUnreadInvitationCount()
{
string userId = User.Identity.GetUserId();
var count = Db.Request.Where(r => r.ReceiverId == userId && r.DateLastRead == null).OrderByDescending(r => r.Id).Count();
BaseViewModel model = new BaseViewModel {RequestCount = count};
return View("UnreadInvitations", model);
}
InvitationController\UnreadInvitations.cshtml:
#if (Model.RequestCount > 0)
{
// Render whatever you need to display the notification
}
Then finally, in your _Layout.cshtml, somewhere, you would invoke this action by calling:
#{ Html.RenderAction("GetUnreadInvitationCount", "Invitation"); }
It's important to note that you may need to use #{ Layout = null; } in the child view being rendered, otherwise it will default to rendering _Layout.cshtml again... which in turn renders the action again... then calls the child view again... and so on. :) Setting the layout to null will prevent that from happening.
Edit: Actually, the reason the _Layout.cshtml file is being called again is because we're returning a ViewResult from the action. Change that to a PartialViewResult and you no longer need the #{ Layout = null; }. Thus:
return View("UnreadInvitations", model);
becomes:
return PartialView("UnreadInvitations", model);
User property is null because it is set after constructor is invoked. However, you do not have to do your logic in the constructor. The following should be placed in your BaseController.
protected int? GetUserId()
{
return (User != null && User.Identity != null && User.Identity.IsAuthenticated) ? User.Identity.GetUserId() : null;
}
protected void GetUnreadInvitationCount()
{
int? userId = GetUserId();
if (userId == null)
throw new SecurityException("Not authenticated");
var count = Db.Request.Where(r => r.ReceiverId == userId.value && r.DateLastRead == null).Count();
if (count > 0) ViewBag.UnreadInvitations = count;
}
GetUnreadInvitationCount is called after User is initialized (I guess when some controller action is gets called) and can use GetUserId from the BaseController.
I have following method in my mvc controller:
[HttpGet]
public ActionResult UserProfile(String username)
{
var user = db.Users.Find(username);
return View(user);
}
This function returns View with user profile. But result of this is the same, regardless of changes in database.
When I debug it seems like db is not changing at all, while in other controllers everything works just fine.
EDIT:
Place when I make changes
public ActionResult ExecuteRetreive(String username, String ISBN)
{
if (IsValid(username))
{
var resBook = db.Books.Find(ISBN);
var resUser = db.Users.Find(username);
var resRentedBooks = (from rb in db.RentedBooks
join b in db.Books on rb.ISBN equals b.ISBN
where b.ISBN == ISBN
where rb.Login == username
where rb.Returned == null
select rb).FirstOrDefault();
if (resRentedBooks == null)
{
return RedirectToAction("Fail", "FailSuccess",
new { error = "" });
}
resRentedBooks.Returned = DateTime.Now;
resBook.IsRented = false;
resUser.RentedBooks--;
db.SaveChanges();
return RedirectToAction("Success", "FailSuccess");
}
else
{
return RedirectToAction("Fail", "FailSuccess",
new { error = "Niepoprawna nazwa użytkownika" });
}
}
Im new to this so dont laugh at my code :P When I display resUser.RentedBooks--; it is the same every time.
As a follow up to what #JeroenVannevel said in the comments, another problem that you might be having because you're using a static context (and one that I've had to deal with in the past) is that once a specific DbContext has loaded an entity (or a set of entities, in my case) it won't tend to refresh just because some outside changes were made in the database. It loads those entities into Local and just refers to those automatically if you query for it.
The solution, then, is to always put your DbContext calls wrapped up in a using block, since DbContext implements IDisposable.
One word of caution with this approach, since you're using MVC: If you are using lazy loading, and you know that your View will need some information from a child object (or to list the names of a collection of child objects), you will absolutely need to hydrate those child entities before you get out of the using block, or you will find yourself getting exceptions saying that your context has been disposed.
I would appreciate some pointers regarding data access/control in a MVC based multi tenant site:
Is there a better/more secure/elegant way to make sure that in a multi tenant site the user can handle only its own data.
There are number of tenants using same app: firstTenant.myapp.com, secondTenant.myapp.com...
//
// GET: /Customer/
// show this tenant's customer info only
public ViewResult Index()
{
//get TenantID from on server cache
int TenantID = Convert.ToInt16( new AppSettings()["TenantID"]);
return View(context.Customers.ToList().Where(c => c.TenantID == TenantID));
}
If a user logs in for the first time and there is no server side cache for this tenant/user- AppSettings checks in db and stores TenantID in the cache.
Each table in database contains the field TenantID and is used to limit access to data only to appropriate Tenant.
So, to come to the point, instead of checking in each action in each controller if data belong to current tenant, can I do something more 'productive'?
Example:
When firstTenant admin tries editing some info for user 4, url has:
http://firstTenant.myapp.com/User/Edit/4
Let's say that user with ID 2 belongs to secondTenant. Admin from firstTenant puts
http://firstTenant.myapp.com/User/Edit/2 in url, and tries getting info which is not owned by his company.
In order to prevent this in the controller I check if the info being edited is actually owned by current tenant.
//
// GET: /User/Edit/
public ActionResult Edit(int id)
{
//set tennant ID
int TenanatID = Convert.ToInt32(new AppSettings()["TenantID"]);
//check if asked info is actually owned by this tennant
User user = context.Userss.Where(u => u.TenantID == TenantID).SingleOrDefault(u => u.UserID == id);
//in case this tenant doesn't have this user ID, ie.e returned User == null
//something is wrong, so handle bad request
//
return View(user);
}
Basically this sort of setneeds to be placed in every controller where there is an access to any data. Is there (and how) a better way to handle this? (Filters, attributes...)
I choose to use action filters to do this. It may not be the most elegant solution, but it is the cleanest of the solutions we've tried so far.
I keep the tenant (in our case, it's a team) in the URL like this: https://myapp.com/{team}/tasks/details/1234
I use custom bindings to map {team} into an actual Team object so my action methods look like this:
[AjaxAuthorize, TeamMember, TeamTask("id")]
public ActionResult Details(Team team, Task id)
The TeamMember attribute verifies that the currently logged in user actually belongs to the team. It also verifies that the team actually exists:
public class TeamMemberAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var httpContext = filterContext.RequestContext.HttpContext;
Team team = filterContext.ActionParameters["team"] as Team;
long userId = long.Parse(httpContext.User.Identity.Name);
if (team == null || team.Members.Where(m => m.Id == userId).Count() == 0)
{
httpContext.Response.StatusCode = 403;
ViewResult insufficientPermssions = new ViewResult();
insufficientPermssions.ViewName = "InsufficientPermissions";
filterContext.Result = insufficientPermssions;
}
}
}
Similarly, the TeamTask attribute ensures that the task in question actually belongs to the team.
Since my app is using subdomains (sub1.app.com, sub2.app.com.....) I basically choose to:
a) use something like the following code to cache info about tenants and
b) to call an action filter on each controller as suggested by Ragesh & Doc:
(Following code is from the blog on : http://www.developer.com/design/article.php/10925_3801931_2/Introduction-to-Multi-Tenant-Architecture.htm )
// <summary>
// This class is used to manage the Cached AppSettings
// from the Database
// </summary>
public class AppSettings
{
// <summary>
// This indexer is used to retrieve AppSettings from Memory
// </summary>
public string this[string Name]
{
get
{
//See if we have an AppSettings Cache Item
if (HttpContext.Current.Cache["AppSettings"] == null)
{
int? TenantID = 0;
//Look up the URL and get the Tenant Info
using (ApplContext dc =
new ApplContext())
{
Site result =
dc.Sites
.Where(a => a.Host ==
HttpContext.Current.Request.Url.
Host.ToLower())
.FirstOrDefault();
if (result != null)
{
TenantID = result.SiteID;
}
}
AppSettings.LoadAppSettings(TenantID);
}
Hashtable ht =
(Hashtable)HttpContext.Current.Cache["AppSettings"];
if (ht.ContainsKey(Name))
{
return ht[Name].ToString();
}
else
{
return string.Empty;
}
}
}
// <summary>
// This Method is used to load the app settings from the
// database into memory
// </summary>
public static void LoadAppSettings(int? TenantID)
{
Hashtable ht = new Hashtable();
//Now Load the AppSettings
using (ShoelaceContext dc =
new ShoelaceContext())
{
//settings are turned off
// no specific settings per user needed currently
//var results = dc.AppSettings.Where(a =>
// a.in_Tenant_Id == TenantID);
//foreach (var appSetting in results)
//{
// ht.Add(appSetting.vc_Name, appSetting.vc_Value);
//}
ht.Add("TenantID", TenantID);
}
//Add it into Cache (Have the Cache Expire after 1 Hour)
HttpContext.Current.Cache.Add("AppSettings",
ht, null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(1, 0, 0),
System.Web.Caching.CacheItemPriority.NotRemovable, null);
}
}
If you want to execute common code like this on every Action in the Controller, you can do this:
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
// do your magic here, you can check the session and/or call the database
}
We have developed a multi tenant application using ASP.NET MVC as well and including the tenant ID in every query is a completely acceptable and really necessary thing to do. I'm not sure where you are hosting your application but if you can use SQL Azure they have a new product called Federations that allows you to easily manage multi tenant data. One nice feature is that when you open the connection you can specify the tenant ID and all queries executed thereafter will only effect that tenants data. It is essentially just including their tenant ID in every request for you so you don't have to do it manually. (Note that federating data is not a new concept, Microsoft just released their own implementation of it recently)