1

Let we have system with following entities:

public class Doctor
{
    public int ID { get; set; }
    public int DepartmentID { get; set; }
    public string Name { get; set; }
    public ICollection<Recipe> Recipes { get; set; }
}

public class Patient
{
    public int ID { get; set; }
    public string Name { get; set; }
    public ICollection<Recipe> Recipes { get; set; }
}

public class Recipe
{
    public int ID { get; set; }
    public int DoctorID { get; set; }
    public int PatientID { get; set; }
    public Doctor Doctor { get; set; }
    public Patient Patient { get; set; }
    public ICollection<RecipeDetails> Details { get; set; }
}

public class RecipeDetails
{
    public int ID { get; set; }
    public Guid SomeGuid { get; set; }
    public double SomeValue { get; set; }
}

Also we have requirements:

  • Doctor should be able to edit his recipes
  • Doctor should be able to see only recipes from doctors within his department
  • Doctor should be able to perform search by available recipes
  • Doctor should be able to generate reports by available recipes details

For now I've implemented following security check:

public void ValidateAccess(Doctor doctor, Recipe aRecipe, EntityAction action)
{
    if (action == EntityAction.Modify && doctor.ID == aRecipe.Doctor.ID)
        return;
    if (action == EntityAction.Read && doctor.DepartmentID == aRecipe.Doctor.DepartmentID)
        return
    throw new SecurityException();
}

That works perfect for simple methods when i have receipe entity, I can easy validate access by caling this method at the begining of my logic method.

But now I have problem, this solution won't work for search and reporting when I don't have exact entity but have some statistics on them.

Lets imagine that I want to generate report for patients with name "aName" that have receipes with component "someGuid", I will have some query with 2 criterias :

var res = RecipeRepository.Query(r => aName.Contains(r.Patient.Name)).SelectMany(r => r.Details).Where(d => d.SomeGuid == someGuid).Sum(d => d.SomeValue);

This query is not correct, it will display statistics for all recipes, including those which should be hidden. To fix that, we should add our access condition to our query:

currentDoctor.DepartmentID == r.Doctor.DepartmentID

So now I have query:

var res = RecipeRepository.Query(r => aName.Contains(r.Patient.Name) && currentDoctor.DepartmentID == r.Doctor.DepartmentID).SelectMany(r => r.Details).Where(d => d.SomeGuid == someGuid).Sum(d => d.SomeValue);

The problem is that I should add this part to each query in the system that makes any calculations on receipes.

UPDATE (2012-11-12):

First example is very simple, and can be solved as StuartLC mentioned in his post. But we have more complex reports in our system. For example - show all patients, that had component someGuid in their recipes. Now our query starts with another repository, so we can't apply private or protected methods from RecipeRepository. Here is sample query:

var res = PatientRepository.Query(p => p.Name.Contains(aName) && p.Recipes.Any(r => r.Details.Any(d => d.SomeGuid == someGuid)));

In this case we still need to add our filter directly into query:

var res = PatientRepository.Query(p => p.Name.Contains(aName) && p.Recipes.Any(r => currentDoctor.DepartmentID == r.Doctor.DepartmentID && r.Details.Any(d => d.SomeGuid == someGuid)));

END UPDATE.

What pattern or practice could be applied to make this solution easier and prevent copy-pasting expression to each query? I'll appreciate your answers and advices.

1 Answers1

1

If your repository pattern Query() method returns a non-materialized IQueryable<T>, then you can refactor the concern of data access restriction into helper methods, one for each 'restrictable' entity, for example:

private IQueryable<Recipe> ApplyAccessFilters(IQueryable<Recipe> query, User user)
{
    IQueryable<Recipe> filteredQuery = query;

    // Custom method to determine if user is restricted to just his / her recipes
    if (!CheckUserPermission(currentUser, Access.MaySeeAllRecipies) ))
    {
        filteredQuery = filteredQuery
                              .Where(r => r.DepartmentId = currentUser.DepartmentId)
    } // Else no restriction, e.g. Admin Users can access all recipes

    // Other access related filters here

    return filteredQuery;
}

Each of your MVC Controller Actions requiring access restriction can then use this method to build up the resultant filter Expression, such as:

var recipes =  RecipeRepository.Query(r => r.SomeFields == someFilters); // NB, do NOT materialize the lambda
var recipesForDoctor =  ApplyAccessFilters(recipes, currentUser) // Access Filter
...
return View(recipesForDoctor); // [AsEnumerable()] - Consider materializing here

You can handle other concerns, such as pagination, in the same way.

And even better, you can make this access filter fluent, in which case the filter is easy on the eyes:

return View(RecipeRepository
            .Query(r => r.SomeFields == someFilters)
            .ApplyAccessFilters(currentUser)
            .Paginate(pagingInfo)
            .AsEnumerable());
Community
  • 1
  • 1
StuartLC
  • 104,537
  • 17
  • 209
  • 285
  • Thank you for your answer. It will work for all queries that start from RecipeRepository, but this is not always true. I need some more general solution that can be applied even when query started from another repository, and Recipe is inside query... I'll think for better illustration in my question. – Serhiy Prysyazhnyy Nov 08 '12 at 10:02
  • I've added one more example to my question, please take a look on this and tell me what do you think about it. – Serhiy Prysyazhnyy Nov 12 '12 at 08:05