I'm trying to build an authorization system not only based in user -> permissions -> roles -> groups but also in entities -> properties.
So i want to limit the model binding in post and put requests so I can verify which properties the user has permission to update or create, and then let him/her update or create the entity ... else reject the request.
Maybe its a idea too complex but I wanted to have the same functionality as some CMS online.
I was reading and maybe this can be solved with a Custom Model Binder, I'm learning a lot about it but, I wanna know if this is "the right path" or maybe there is a faster or better way to do it
Thank you so much, I'll keep updating my question with code so, maybe can help someone in future with the same idea.
I'm working on exactly the same thing, and I do believe it's entirely possible to do this with custom [Attributes]. Here's a small implementation of how I've done it in dynamic select-statements:
Firstly, I have a custom attribute that takes an enum UserRoles as input:
[AttributeUsage(AttributeTargets.Property)]
public class RestrictUserRoles : Attribute
{
public RestrictUserRoles(UserRoles roles)
{
Roles = roles;
}
public UserRoles Roles { get; }
}
The UserRoles-enum can be implemented as such:
[Flags]
public enum UserRoles
{
[Description("Administrator")]
Admin = 1,
[Description("Employee")]
Employee = 2,
[Description("Head of a division")]
DivisionHead = 4,
[Description("Fired")]
Fired = 8
}
I use an enum because some employees can be admin, divisionheads (and even fired).
I then have an IQueryable extension where it gets all the properties that a user is authorized to see and intersects those properties with those selected. To do this I use dynamic Linq and reflection.
public static class QueryableExtensions
{
public static IQueryable SelectProperties<T>(this IQueryable<T> source, UserRoles roles, string criteria)
{
// get all the properties that a user is authorized to see
var authenticatedProperties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(prop => prop.CustomAttributes.Any(attr =>
attr.AttributeType == typeof(RestrictUserRoles) &&
(((RestrictUserRoles) Attribute.GetCustomAttribute(prop, typeof(RestrictUserRoles))).Roles & roles) !=
0))
.Select(prop => prop.Name)
.ToList();
// if there aren't any, then the user is not
// authorized to view any properties
// DISCLAIMER: or someone has forgotten to mark any properties
// with the RestrictUserRoles-attribute
if (!authenticatedProperties.Any()) throw new UnauthorizedAccessException();
// we get all the properties that the user wants to
// select from the string that was passed to the function in
// the form Prop1, Prop2, Prop3
var selectProperties = criteria
.Split(',')
.Select(property => property.Trim());
// Get the intersection between these properties, IE we
// select only those properties that the user has selected
// AND is authorized to view
var properties = authenticatedProperties
.Intersect(selectProperties)
.ToList();
// if there are none that intersect, return all those
// properties that a user is authorized to view
if (!properties.Any()) properties = authenticatedProperties;
// run the query using dynamic linq
return source.Select("new(" + properties.JoinToString(",") + ")");
}
}
This isn't field-tested, but it should do the trick and is easily extendable to mutations.
EDIT: forgot that I use an extension-function to join all properties that I define below:
public static string JoinToString(this IEnumerable<string> list, string delimiter = "")
{
return string.Join(delimiter, list);
}
Related
In my web app I have class with many properties. User is able to modify these properties with bunch of selectboxes, each responsible for one property. Everytime when given property is changed, the change event is triggered and ajax sends the new value of this property to Update method in the Controller.
So far I have one UpdateDto which consists of nullable fields. In my Update controller I check each DTO's field if it is null or not. If it's not null it means that user want to change this property and it is updated and saved in database.
Unfortunately the Update method's code looks a little bit ugly for me. It checks each property and it's quite long. Have a look:
Update Method in the controller:
public IHttpActionResult UpdateScrumTask(int id, ScrumTaskDetailsDto scrumTaskDto)
{
var scrumTaskFromDb = _context.ScrumTasks
.SingleOrDefault(s => s.Id == id);
if (scrumTaskFromDb == null)
return NotFound();
if (!ModelState.IsValid)
return BadRequest();
if (!string.IsNullOrWhiteSpace(scrumTaskDto.UserId))
{
var user = _context.Users
.SingleOrDefault(u => u.Id.Equals(scrumTaskDto.UserId));
scrumTaskFromDb.UserId = user?.Id;
}
else if (scrumTaskDto.EstimationId != null)
{
var estimation = _context.Estimations
.SingleOrDefault(u => u.Id == scrumTaskDto.EstimationId.Value);
scrumTaskFromDb.EstimationId = estimation?.Id;
}
else if (scrumTaskDto.Priority != null)
{
if (scrumTaskDto.Priority.Value == 0)
scrumTaskDto.Priority = null;
scrumTaskFromDb.Priority = scrumTaskDto.Priority;
}
else if (scrumTaskDto.TaskType != null)
{
scrumTaskFromDb.TaskType = scrumTaskDto.TaskType.Value;
}
_context.SaveChanges();
return Ok();
}
UpdateDTO:
public class ScrumTaskDetailsDto
{
public int? EstimationId { get; set; }
[Range(0, 5)]
public byte? Priority { get; set; }
[Range(0, 2)]
public TaskType? TaskType { get; set; }
public string UserId { get; set; }
}
Note that these properties are also nullable in the database. That's why if for example UserId is not found the property in database is set to null.
I wonder how it should look like. What is better solution?
Keep one Update method and use one UpdateDto with many nullable fields OR
Devide Update method into many methods, each responsible for one property and create separate DTOs. It brings that there will be a lot of DTOs connected with one model with single property (is it good?)
Another question is:
Should I use validtion (DataAnnotation) inside DTO? If not, what is alternative solution?
I'd suggest couple of improvements:
Separate your query logic from your core business logic, using repository pattern or query and command object using a library like MediatR, also controller only should call other methods to do something for it and return a response, it shouldn't have any query or any other logic
The DTO looks ok to me, as long as it's centered around one specific task
I would definitely separate the update from controller, as for it being one method or more, it's 35... lines, which is not ideal, maybe you could separate it into two methods, one responsible for validation and one responsible for the actual update, also you can use dependency injection to decouple the update and validation method from your controller
I want to know if it's possible to pre-filter OData results in a WebAPI for items in the expand clause. I only want this to filter based on a predefined interface with a Deleted flag.
public interface IDbDeletedDateTime
{
DateTime? DeletedDateTime { get; set; }
}
public static class IDbDeletedDateTimeExtensions
{
public static IQueryable<T> FilterDeleted<T>(this IQueryable<T> self)
where T : IDbDeletedDateTime
{
return self.Where(s => s.DeletedDateTime == null);
}
}
public class Person : IDbDeletedDateTime
{
[Key]
public int PersonId { get; set }
public DateTime? DeletedDateTime { get; set; }
public virtual ICollection<Pet> Pets { get; set; }
}
public class Pet : IDbDeletedDateTime
{
[Key]
public int PetId { get; set }
public int PersonId { get; set }
public DateTime? DeletedDateTime { get; set; }
}
public class PersonController : ApiController
{
private PersonEntities db = new PersonEntities();
[EnableQuery]
// GET: api/Persons
public IQueryable<Person> GetPersons()
{
return db.Persons.FilterDeleted();
}
}
You can see that I'm very easily filtering deleted people. The problem comes when someone gets deleted Pets from a query like /api/Persons?$expand=Pets
Is there a way to check if this expansion of "Pets" is an IDbDeletedDateTime and filter them accordingly? Maybe there is a better way to approach this?
EDIT:
I tried to solve this based on what was picked up in this answer. I don't think it can be done, at least not in all scenarios. The only part of a ExpandedNavigationSelectItem that even looks like it is related to the filters is the FilterClause. This can be null when it has no filter, and it is only a getter property, meaning we can't set it with a new filter if we wanted to. Weather or not it is possible to modify a current filter is only covering a small use case that I'm not particularly interested in if I can't add a filter freshly.
I have an extension method that will recurse through all the expand clauses and you can at least see what the FilterOption is for each expansion. If anyone can get this 90% code fully realized, that would be amazing, but I'm not holding my breath on it.
public static void FilterDeletables(this ODataQueryOptions queryOptions)
{
//Define a recursive function here.
//I chose to do it this way as I didn't want a utility method for this functionality. Break it out at your discretion.
Action<SelectExpandClause> filterDeletablesRecursive = null;
filterDeletablesRecursive = (selectExpandClause) =>
{
//No clause? Skip.
if (selectExpandClause == null)
{
return;
}
foreach (var selectedItem in selectExpandClause.SelectedItems)
{
//We're only looking for the expanded navigation items.
var expandItem = (selectedItem as ExpandedNavigationSelectItem);
if (expandItem != null)
{
//https://msdn.microsoft.com/en-us/library/microsoft.data.odata.query.semanticast.expandednavigationselectitem.pathtonavigationproperty(v=vs.113).aspx
//The documentation states: "Gets the Path for this expand level. This path includes zero or more type segments followed by exactly one Navigation Property."
//Assuming the documentation is correct, we can assume there will always be one NavigationPropertySegment at the end that we can use.
var edmType = expandItem.PathToNavigationProperty.OfType<NavigationPropertySegment>().Last().EdmType;
string stringType = null;
IEdmCollectionType edmCollectionType = edmType as IEdmCollectionType;
if (edmCollectionType != null)
{
stringType = edmCollectionType.ElementType.Definition.FullTypeName();
}
else
{
IEdmEntityType edmEntityType = edmType as IEdmEntityType;
if (edmEntityType != null)
{
stringType = edmEntityType.FullTypeName();
}
}
if (!String.IsNullOrEmpty(stringType))
{
Type actualType = typeof(PetStoreEntities).Assembly.GetType(stringType);
if (actualType != null && typeof (IDbDeletable).IsAssignableFrom(actualType))
{
var filter = expandItem.FilterOption;
//expandItem.FilterOption = new FilterClause(new BinaryOperatorNode(BinaryOperatorKind.Equal, new , ));
}
}
filterDeletablesRecursive(expandItem.SelectAndExpand);
}
}
};
filterDeletablesRecursive(queryOptions.SelectExpand?.SelectExpandClause);
}
Correct me if I understood wrong: you want to always filter the entities if they implement the interface IDbDeletedDateTime, so when the user wants to expand a navigation property you also want to filter if that navigation property implements the interface, right?
In your current code you enabled OData query options, with the [EnableQuery] attribute, so OData will handle the expand query option for you, and the Pets will not be filtered the way you want.
You have the option of implementing your own [MyEnableQuery] attribute, and override the ApplyQuery method: check there if the user has set the $expand query option and if so, check if the requested entity implements IDbDeletedDateTime and filter accordingly.
You can check here the code of the [EnableQuery] attribute and see that in the ApplyQuery method you have access to the object ODataQueryOptions that will contain all the query options set by the user (WebApi populates this object from the URI query string).
This would be a generic solution that you could use in all your controller methods if you are going to have several entities with that interface with your custom filtering. If you only want this for a single controller method, you can also remove the [EnableQuery] attribute, and invoke the query options directly in the controller method: add the ODataQueryOptions parameter to your method and handle the query options manually.
That would be something like:
// GET: api/Persons
public IQueryable<Person> GetPersons(ODataQueryOptions queryOptions)
{
// Inspect queryOptions and apply the query options as you want
// ...
return db.Persons.FilterDeleted();
}
See the section Invoking Query Options directly to understand more how to play around with that object. If you read the entire article, be aware that the [Queryable] attribute is your [EnableQuery] attribute, since the article is from a lower version of OData.
Hope it points you in the right direction to achieve what you want ;).
EDIT: some information regarding nested filtering in $expand clause:
OData V4 supports filtering in expanded content. This means you can nest a filer inside an expand clause, something like:
GET api/user()?$expand=followers($top=2;$select=gender).
In this scenario, again you have the option to let OData handle it, or handle it yourself exploring the ODataQueryOptions parameter:
Inside your controller you can check expand options and if they have nested filters with this code:
if (queryOptions.SelectExpand != null) {
foreach (SelectItem item in queryOptions.SelectExpand.SelectExpandClause.SelectedItems) {
if (item.GetType() == typeof(ExpandedNavigationSelectItem)) {
ExpandedNavigationSelectItem navigationProperty = (ExpandedNavigationSelectItem)item;
// Get the name of the property expanded (this way you can control which navigation property you are about to expand)
var propertyName = (navigationProperty.PathToNavigationProperty.FirstSegment as NavigationPropertySegment).NavigationProperty.Name.ToLowerInvariant();
// Get skip and top nested filters:
var skip = navigationProperty.SkipOption;
var top = navigationProperty.TopOption;
/* Here you should retrieve from your DB the entities that you
will return as a result of the requested expand clause with nested filters
... */
}
}
}
Zachary, I had a similar requirement and I was able to solve it by writing an algorithm that adds additional filtering to the request ODataUri based on the properties of my model. It examines any properties at the root level entity and the properties of any expanded entities as well to determine what additional filter expressions to add to the OData query.
OData v4 supports filtering in $expand clauses but the filterOption in the expanded entities is read only so you cannot modify the filter expressions for the expanded entities. You can only examine the filterOption contents at the expanded entities.
My solution was to examine all entities (root and expanded) for their properties and then add any additional $filter options I needed at the root filter of the request ODataUri.
Here is an example OData request Url:
/RootEntity?$expand=OtherEntity($expand=SomeOtherEntity)
This is the same OData request Url after I had updated it:
/RootEntity?$filter=OtherEntity/SomeOtherEntity/Id eq 3&$expand=OtherEntity($expand=SomeOtherEntity)
Steps to accomplish this:
Use ODataUriParser to parse the incoming Url into a Uri object
See below:
var parser = new ODataUriParser(model, new Uri(serviceRootPath), requestUri);
var odataUri = parser.ParseUri();
Create a method that will traverse down from the root to all expanded entities and pass the ODataUri by ref (so that you can update it as needed as you examine each entity)
The first method will examine the root entity and add any additional filters based on the properties of the root entity.
AddCustomFilters(ref ODataUri odataUri);
The AddCustomFilters method will the traverse the expanded entities and call the AddCustomFiltersToExpandedEntity which will continue to traverse down all expanded entities to add any necessary filters.
foreach (var item in odatauri.SelectAndExpand.SelectedItems)
{
AddCustomFiltersToExpandedEntity(ref ODataUri odataUri, ExpandedNavigationSelectItem expandedNavigationSelectItem, string parentNavigationNameProperty)
}
The method AddCustomFiltersToExpandedEntity should call itself as it loops over the expanded entities at each level.
To update the root filter as you examine each entity
Create a new filter clause with your additional filter requirements and overwrite the existing filter clause at the root level. The $filter at the root level of the ODataUri has a setter so it can be overriden.
odataUri.Filter = new FilterClause(newFilterExpression, newFilterRange);
Note: I suggest creating a new filter clause using a BinaryOperatorKind.And so that your additional filter expressions are simply appended to any existing filter expressions already in the ODataUri
var combinedFilterExpression = new BinaryOperatorNode(BinaryOperatorKind.And, odataUri.Filter.Expression, newFilterExpression);
odataUri.Filter = new FilterClause(combinedFilterExpression, newFilterRange);
Use ODataUriBuilder to create a new Url based on the updated Uri
See below:
var updatedODataUri = new Microsoft.OData.Core.UriBuilder.ODataUriBuilder(ODataUrlConventions.Default, odataUri).BuildUri();
Replace the request Uri with the updated Uri.
This allows the OData controller to complete processing the request using the updated OData Url which includes the additional filter options you just added to the root level filer.
ActionContext.Request.RequestUri = updatedODataUri;
This should provide you with the capability to add any filtering options you need and be 100% sure that you have not altered the OData Url structure incorrectly.
I hope this helps you achieve your goals.
I had a similar problem and I managed to solve it using Entity Framework Dynamic Filters.
In your case, you would create a filter that filters out all deleted records, like that:
Your DbContext OnModelCreating method
modelBuilder.Filter("NotDeleted", (Pet p) => p.Deleted, false);
This filter will be then applied every time you query your Pets collection, either directly or trough OData's $expand. You have of course full control over the filter, and you can disable it manually or conditionally - it is covered in the Dynamic Filters documentation.
I asked the OData team about this issue, and I may have an answer that can be used. I haven't been able to test it out fully and get it used, but it looks like it will solve my problems when I am able to get around to them. I want to post this answer just in case this will help someone else.
That being said... It looks there is a framework on top of OData that seems to be in its relative infancy called RESTier being developed by Microsoft. It seems to offer a layer of abstraction on top of OData that allows for these kinds of filters, as the examples would suggest.
This looks like it would be an example above with a filter in the Domain object that would be added:
private IQueryable<Pet> OnFilterPets(IQueryable<Pet> pets)
{
return pets.Where(c => c.DeletedDateTime == null);
}
If I get around to implementing this logic, I'll return to this answer to confirm or deny the use of this framework.
I was never able to implement this solution to know if it's worthwhile. There were too many challenges to justify the worth in my particular use case. It may very well be a great solution for new projects or folks the really need these features, but my particular use case was challenging to implement the framework into existing logic.
Your mileage may vary, and this may still be a useful framework to check out.
I got one question related to my model you can see in the picture below.
As you can see I got 3 entities and 1:n and m:n relations between them.
I want that I can edit these models through a web interface. Therefore I scaffold (add controller with entity framework) these three models and got edit/delete/create/ views and of course one controller for each entity.
But there is no input/fields created for the relations automatically by VS. So I thought to implement them manually. Before I want to do that is there an simpler way to implement/scaffold this model, so I can even edit the relations(Checkboxes or (multi)select would be the best)?
Thanks in advance!
For one-many you can use a DropDownList for Tip in the Partner View (see Scott Allen's solution. Many-many can be handled by ViewModels and JavaScript frameworks like Knockout.
No, the scaffolds are intentionally unopinionated here, as there's many different ways you could handle this. Perhaps you just want to choose from a select list? Maybe you want checkboxes, instead? Or, maybe you want to actually add/edit related items inline? And with that last one, would you like to post all at once or use AJAX?
So, instead of picking for you, the framework rightly leaves the decision up to you, since only you know how your application should be built. Regardless, relying on the scaffolds is going to bite you more often than not. They only work in the most basic and ideal scenarios, and when have application requirements ever been either basic or ideal? I don't even bother with them at this point, preferring to just create my controllers/views manually. It ends up being quicker than dealing with the scaffold and undoing all the things that aren't applicable.
So, since you're looking for select boxes (either single-select or multi-select), first, I'd recommend creating view models for your entities. For example, with Tip:
public class TipViewModel
{
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Description { get; set; }
[Required]
public int? SelectedPartnerId { get; set; }
public IEnumerable<SelectListItem> PartnerChoices { get; set;}
[Required]
public int? SelectedBookId { get; set; }
public IEnumerable<SelectListItem> BookChoices { get; set; }
}
Here, I've added nullable int (using a nullable allows them to be initially unselected, instead of just set to the first option) properties to track the id of the selected Book/Partner because it doesn't appear you have explicit properties on your entities for the foreign keys. That's fine, but it doesn't make it slightly more complicated to save the relationship, as you'll see in a bit. If you did have explicit foreign key properties, then you should mirror those in your view models instead.
Now in the GET version of your action, you'll need to do something like the following:
public ActionResult Create()
{
var model = new TipViewModel();
PopulateChoices(model);
return View(model);
}
...
protected void PopulateChoices(TipViewModel model)
{
model.PartnerChoices = db.Partners.Select(m => new SelectListItem
{
Value = m.Id.ToString(),
Text = m.Name
});
model.BookChoices = db.Books.Select(m => new SelectListItem
{
Value = m.Id.ToString(),
Text = string.Format("{0} by {1}", m.Name, m.Author)
});
}
I've abstracted out the code for populating these select lists because the code will be used multiple times throughout your controller. Also, I used string.Format on the Text value for the books just to show that you can do whatever you want with the text for the select list item. Also, the code above would be for a create action, obviously. Doing an edit would be similar but slightly different:
public ActionResult Edit(int id)
{
var tip = db.Tips.Find(id);
if (tip == null)
{
return new HttpNotFoundResult();
}
var model = new TipViewModel
{
Name = tip.Name,
Description = tip.Description,
SelectedPartnerId = tip.Partner != null ? tip.Partner.Id : new int?(),
SelectedBookId = tip.Book != null ? tip.Book.Id : new int?()
}
PopulateChoices(model);
return View(model);
}
The main difference is that you're obviously dealing with an existing instance so you need to pull it from the database. Then, you just need to map the data from your entity onto your view model. Since, again, you don't have explicit foreign key properties, you have to do a little extra leg work to get the currently chosen Partner/Book values, otherwise you could just copy the values for the foreign key properties over directly. Also, here, I'm just doing a manual mapping, but there's third-party libraries to make this task easier (see: AutoMapper).
With that, you can implement your views. Everything will work the same as it did when you were using the entity directly, you just need to make a couple of modifications. First, you'll need to change your view's model declaration:
#model Namespace.To.TipViewModel
Then, add the select lists for your two related properties:
#Html.DropDownListFor(m => m.SelectedPartnerId, Model.PartnerChoices)
...
#Html.DropDownListFor(m => m.SelectedBookId, Model.BookChoices)
The fun happens in the POST version of your actions. Most of the code will stay the same from the GET version, but now you'll have an if (ModelState.IsValid) block:
[HttpPost]
public ActionResult Create(TipViewModel model)
{
if (ModelState.IsValid)
{
// map the data from model to your entity
var tip = new Tip
{
Name = model.Name,
Description = model.Description,
Partner = db.Partners.Find(model.SelectedPartnerId),
Book = db.Books.Find(model.SelectedBookId)
}
db.Tips.Add(tip);
db.SaveChanges();
return RedirectToAction("Index");
}
// Form has errors, repopulate choices and redisplay form
PopulateChoices(model);
return View(model);
}
The edit version, again, is similar, except you're going to map onto you existing instance, for example:
tip.Name = model.Name;
tip.Description = model.Description;
tip.Partner = db.Partners.Find(model.SelectedPartnerId);
tip.Book = db.Books.Find(model.SelectedBookId);
That's all there is to it for reference properties. You don't actually have any thing that's M2M or even one-to-many on your entities in your question. Everything is one-to-one, but if you did have a collection property, you'd need to handle it slightly differently. You still need a property on your view model to hold the selected values and the available choices:
public List<int> SelectedFooIds { get; set; }
public IEnumerable<SelectListItem> FooChoices { get; set; }
Populating the choices would also be the same. The options are the options; it doesn't matter if you're select just one or many as far as that is concerned.
Mapping onto your entity in your create action would be different though, as you'd need to select all of the chosen items from the database and set your collection property on your entity to that:
var tip = new Tip
{
...
Foos = db.Foos.Where(m => model.SelectedFooIds.Contains(m.Id)),
}
And, you'd need to make changes to both the GET and POST versions of your edit action. For the GET, you need to condense your collection property down to a list of ids:
var model = new TipViewModel
{
...
SelectedFooIds = tip.Foos.Select(m => m.Id).ToList(),
}
And in the edit version, you set new selected items:
tip.Foos = db.Foos.Where(m => model.SelectedFooIds.Contains(m.Id);
Finally, in your views, you'd use ListBoxFor instead of DropDownListFor to enable the multiselect:
#Html.ListBoxFor(m => m.SelectedFooIds, Model.FooChoices)
I'm working with EF and code first. I created a User object which contains a list of the object Role. Each Role object contains a object of type Form. I want to get all roles of a specific user including the "Form" object.
For better understanding: user 1<->n role 1<-> 1 Form
I tried something like that:
DBContext.Users.Include(u => u.Roles.AsQueryable().Include(i=>i.Form)).ToList().Where(r => r.Username.Equals(id)).FirstOrDefault<User>();
This results in:
The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties. Parameter name: path
User user = DBContext.Users.Include(u => u.Roles.AsQueryable().Include(i=>i.Form)).ToList().Where(r => r.Username.Equals(id)).FirstOrDefault<User>();
List<Role> roles = user.Roles.AsQueryable().Include(r=>r.Form).ToList();
This doesn't throw any Exception but the Form object is not included in the Role object.
How can I use include on a included object? Is there a way to include "objects" after a linq expression was completed?
Thanks greetings
[Edit] For a better understanding how my model looks
public class User
{
//some properties
public virtual List<Role> Roles { get; set; }
}
public class Role
{
//some properties
public virtual Form Form { get; set; }
}
public class Form
{
//some properties
}
You cannot do an include in another include. Correct way to include a subpath with linq expression is to use select
DBContext.Users.Include(u =>u.Roles.Select(i=>i.Form)).ToList()
I have a standard DbContext with code like the following:
public DbSet<Interest> Interests { get; set; }
public DbSet<User> Users { get; set; }
I've recently implemented multi-tenancy by creating a TenantContext that contains the following:
private readonly DbContext _dbContext;
private readonly Tenant _tenant;
public TenantContext(Tenant tenant)
: base("name=DefaultConnection") {
this._tenant = tenant;
this._dbContext = new DbContext();
}
public IQueryable<User> Users { get { return FilterTenant(_dbContext.Users); } }
public IQueryable<Interest> Interests { get { return FilterTenant(_dbContext.Interests); } }
private IQueryable<T> FilterTenant<T>(IQueryable<T> values) where T : class, ITenantData
{
return values.Where(x => x.TenantId == _tenant.TenantId);
}
So far, this has been working great. Whenever any of my services creates a new TenantContext, all queries directly off of that context are filtered through this FilterTenant method that guarantees I'm only returning tenant-relevant entities.
The problem that I'm encountering is my usage of navigation properties that do not take this into account:
using (var db = CreateContext()) // new TenantContext
{
return db.Users.
Include(u => u.Interests).FirstOrDefault(s => s.UserId == userId);
}
This query pulls up the tenant-specific Users, but then the Include() statement pulls in Interests for that user only - but across all tenants. So if a user has Interests across multiple Tenants, I get all of the user's Interests with the above query.
My User model has the following:
public int UserId { get; set; }
public int TenantId { get; set; }
public virtual ICollection<Interest> Interests { get; set; }
Is there any way that I can somehow modify these navigation properties to perform tenant-specific queries? Or should I go and tear out all navigation properties in favor of handwritten code?
The second option scares me because a lot of queries have nested Includes. Any input here would be fantastic.
As far as I know, there's no other way than to either use reflection or query the properties by hand.
So in your IQueryable<T> FilterTenant<T>(IQueryable<T> values) method, you'll have to inspect your type T for properties that implement your ITenantData interface.
Then you're still not there, as the properties of your root entity (User in this case) may be entities themselves, or lists of entities (think Invoice.InvoiceLines[].Item.Categories[]).
For each of the properties you found by doing this, you'll have to write a Where() clause that filters those properties.
Or you can hand-code it per property.
These checks should at least happen when creating and editing entities. You'll want to check that navigation properties referenced by an ID property (e.g. ContactModel.AddressID) that get posted to your repository (for example from an MVC site) are accessible for the currently logged on tenant. This is your mass assignment protection, which ensures a malicious user can't craft a request that would otherwise link an entity to which he has permissions (a Contact he is creating or editing) to one Address of another tenant, simply by posting a random or known AddressID.
If you trust this system, you only have to check the TenantID of the root entity when reading, because given the checks when creating and updating, all child entities are accessible for the tenant if the root entity is accessible.
Because of your description you do need to filter child entities. An example for hand-coding your example, using the technique explained found here:
public class UserRepository
{
// ctor injects _dbContext and _tenantId
public IQueryable<User> GetUsers()
{
var user = _dbContext.Users.Where(u => u.TenantId == _tenantId)
.Select(u => new User
{
Interests = u.Interests.Where(u =>
u.TenantId == _tenantId),
Other = u.Other,
};
}
}
}
But as you see, you'll have to map every property of User like that.
Just wanted to offer an alternative approach to implementing multi-tenancy, which is working really well in a current project, using EF5 and SQL 2012. Basic design is (bear with me here...):
Every table in the database has a column (ClientSid binary, default constraint = SUSER_SID()) and is never queried directly, only ever via a dedicated view
Each view is a direct select over the table with WHERE (ClientSid = SUSER_SID()) but doesn't select the ClientSid (effectively exposing the interface of the table)
EF5 model is mapped to the VIEW, not the TABLE
The connection string is varied based on the context of the tenant (user / client whatever multi-tenant partition requirement may be)
That's pretty much it - though it might be useful to share. I know it's not a direct answer to your question, but this has resulted in basically zero custom code in the C# area.