I really need a second set of eyes on this so I'm hoping some of you can give me some feedback, I think I've been staring at it too long.
I'm trying to setup a website using ASP.NET MVC3, and in this site I need the flexibility of creating dynamic objects. But that I mean in my database there are a series of tables setup to store information about the structure and data contained in these dynamic objects. I'm working with a pre-existing database so I'm limited (to a certain extent) as to what I can modify. When I query the database for an dynamic object (not .NET 4.0's dynamic object) I pass in my Id and what I get back is a simple object with maybe a handful of properties, which are meant for internal use only, and a property that is a collection containing all of the properties for my dynamic object. So if my dynamic object was for a person with an Name, DoB and Sex, my collection would have three objects, one for each property. This allows the administrator of the site to add new fields at runtime and the website will automatically render them, allow updating etc. Now I have the model binding working currently for both display and postback for this data structure, For each object in the collection I render two pieces of data, the unique ID of the property (which is currently a hidden field and the Id is a Guid) and the value of the property. My problem is the security aspect.
If I were dealing with strongly typed objects I could create custom ViewModels and be done with it, or add Bind() attributes to the action's signature but because these objects' properties are flexible collections I'm not sure how to approach it. Action level security is simple enough, I can create a custom Authorize attribute and query the database for permissions, but I need to be able to restrict what the collections behavior for displaying and accepting information based off of user permissions. For example, if I were to add a Social Security Number property to the person object I do not want it rendered to the screen for certain people. But because the property is something that can change at runtime, so can the permissions.
Here is where I'm at so far as far as my thoughts go...
So I need a way to determine which objects in the collection of properties can be rendered to the screen or bound to on post back, depending on the user permissions. For Displaying the object I don't think I have much choice but to include the permissions in a ViewModel object somehow and query for these permissions in a DisplayTemplate intended for the object type that is used in the collection of properties. Or I could do write some sort of custom ModelBinder since it's used for calls to Html.Display() and Html.Editor() and look into filtering the list inside of the ModelBinder.
I have a similar problem for postbacks though. When it's posted back I have a collection of data being passed back with only a Guid and a value. But I need to ensure that the user has not injected their own fields into the form, and I also need to make sure that for the properties that are being passed back to the action, the user has adequate permissions. Ideally I'd like to integrate this check into the Model Binding and reuse some of the information populated from MetaData if I can, thing like , so that it simple ignores data passed in that the user doesn't have the rights to alter, or failing that, check that the user has access to all of the attributes they is trying to set in the IsValid check done at the beginning of an action handling the postback.
Then there's the dynamic building of the MetaData for use in the call to Html.Display() and Html.Editor() for each property based off of information in the database since I don't have physical properties is a class I can decorate with Data Annotations.
Problem is, I'm not familiar with the innards of MVC when it comes to overriding default implementations of things like ModelBinders, or ModelMetaDataProviders or ModelValidationProviders.
Can you provide some suggestions as to the best way you can think of to achieve what I'm describing or if you know of other articles covering this example I'd very much like to see them, I haven't had much luck with Google on this specific subject so far.
EDIT: See my answer below for the full details on what I did
EDIT: I got the metadata provider working. Just needed to implement my own class and inherit from ModelMetadataProvider.
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName)
{
ModelMetadata metadata;
if (containerType == typeof(PseudoObjectAttributeViewModel))
{
switch (propertyName)
{
case "StringValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(string), propertyName);
break;
case "DateValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(DateTime?), propertyName);
break;
case "DoubleValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(double?), propertyName);
break;
case "LongValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(long?), propertyName);
break;
case "BooleanValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(bool?), propertyName);
break;
case "GuidValue":
metadata = new ModelMetadata(this, typeof(PseudoObjectAttribute), modelAccessor, typeof(Guid?), propertyName);
break;
default:
return defaultMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
break;
}
DataAnnotationsModelMetadata daMetadata = (DataAnnotationsModelMetadata)metadata;
System.Reflection.FieldInfo container = modelAccessor.Target.GetType().GetField("vdi");
AddSupplimentalMetadata(daMetadata, (PseudoObjectAttributeViewModel)((System.Web.Mvc.ViewDataInfo)container.GetValue(modelAccessor.Target)).Container);
}
else
metadata = defaultMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
return metadata;
}
The first part is pretty self explanatory, started by populating the metadata with GetMetadataForType() by passing in the .NET type that most closely matches the column name the data is being pulled from. (My editor teplate helps with this by dynamically selecting which column the data is in as defined in the datastructure that defines this data)
Html.Editor(Model.PseudoObjectStructure.PseudoObjectControl.DataType)
It's a a little odd to work with, but like I said, it's a preexisting data structure.
After the switch statement is where it got odd. As I understand it, in MVC2 the GetMetadataForProperty()
method no longer took the Model itself as a parameter and find the property using propertyName
, instead it passes in an expression of type Func<object>
that points to the property that MVC wants metadata for. This presented a problem because I needed the root Model to use a different property to determine the structure details. I found another solution on here that said you can use reflection to get the Model but it requires Reflection. Not what I was hoping for but it works. After I have the Model, I pass in the Metadata so far and the Model to a method created called AddSupplimentalMetadata()
and I'll set the rest of the properties on the DataAnnotationsModelMetadata
object that MVC uses from there.
Now I just need to figure out a way to dynamically choose to render or not render certain properties depending on user permissions. I think what I may have to do is filter the list of properties before passing the model to the view, by using LINQ or something of that nautre. I don't like the idea of putting business logic in the Display/EditorTemplate. For saving changes I still need to take a look at the Validation system and see if I can hook into that for validating which properties the user is trying to pass information for.
Posting an answer to accept since I figured out the solution I needed myself, though I still have one portion remaining, I'll post it as a separate question if I need additional assistance rather than leaving this question unanswered. See my original post for details.
Down-votes served as a great reminder to get back to this, so here you are :-)
Ok, so Here's where I ended up going with my implementation. Hopefully this will help some of you out with your own situations. I must disclose that I'm giving this a Works on my Machine seal of approval and you should test it yourself to see if it meets your needs. Certain decisions were made to comply with existing data/practices. If you encounter any problems with this code that you manage to solve, feel free to contribute back to this post so others can benefit. I'll try to shorten the code for brevity, and because some of the specifics belong to my employer. This is the gist of it as it pertains to MVC.
Note: I am currently using MVC4, but this may work for MVC3 as well. The virtual modifiers are for nHibernate
POCOs
public class PseudoObject
{
// Other properties and such...
public virtual IList<PseudoObjectAttribute> Attributes { get; set; }
// Other methods, etc...
}
public class PseudoObjectAttribute
{
// Other properties and such...
public virtual string Value { get; set; }
//This holds all of the info I need for determine metadata & validation
public virtual PseudoObjectStructure Structure { get; set; }
// Other methods, etc...
}
public class PseudoObjectStructure
{
public virtual bool IsRequired { get; set; }
public virtual string RegularExpression { get; set; }
public virtual string RegularExpressionErrorMessage { get; set; }
}
MetadataProvider
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
//We only care about providing custom model metadata to PseudoObjectAttribute objects
if ((containerType != typeof(PseudoObjectAttribute) && modelType != typeof(PseudoObjectAttribute)) || modelAccessor == null)
return base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
ModelMetadata metadata = null;
PseudoObjectAttribute attributeViewModel = null;
System.Reflection.FieldInfo container = null;
//The contents of this if statement allows me to get the PseudoObjectAttribute instance I need to work with.
//This happens when we want metadata for the PseudoObjectAttribute type as a whole, not a specific attribute
if (modelType == typeof(PseudoObjectAttribute) && containerType == null)
{
//
if (modelAccessor.Target is ViewDataDictionary)
attributeViewModel = (PseudoObjectAttribute)((ViewDataDictionary)modelAccessor.Target).Model;
else
{
container = modelAccessor.Target.GetType().GetField("item");
if (container != null)
{
attributeViewModel = (PseudoObjectAttribute)container.GetValue(modelAccessor.Target);
}
container = modelAccessor.Target.GetType().GetField("model");
if (container != null)
attributeViewModel = (PseudoObjectAttribute)container.GetValue(modelAccessor.Target);
}
}
else if(!string.IsNullOrEmpty(propertyName))
{
if (modelAccessor.Method.Name.Contains("FromStringExpression"))
{
//This happens when we want metadata for a specific property on the PseudoObjectAttribute
container = modelAccessor.Target.GetType().GetField("vdi");
attributeViewModel = (PseudoObjectAttribute)((System.Web.Mvc.ViewDataInfo)container.GetValue(modelAccessor.Target)).Container;
}
//GetPropertyValueAccessor is used when you bind the posted back form
else if (modelAccessor.Method.Name.Contains("FromLambdaExpression") || modelAccessor.Method.Name.Contains("GetPropertyValueAccessor"))
{
//Accessed property via lambda
container = modelAccessor.Target.GetType().GetField("container");
var accessor = container.GetValue(modelAccessor.Target);
//Sometimes the property is access straight from the parent object for display purposes in the view ex. someRegistration["ProductId"].GuidValue
//In these situations the access is the Registration object and is not something we can use to derive the attribute.
if (accessor is PseudoObjectAttribute)
attributeViewModel = (PseudoObjectAttribute)accessor;
}
}
// At this point I have an instance of the actual PseudoObjectAttribute object I'm trying to derive Metadata for and can build my Metadata easily using it.
// I'm using typeof (String) as a starting point to build my custom metadata from but it could be any value type if you wanted to befit from the defaults
metadata = new ModelMetadata(this, typeof (PseudoObjectAttribute), modelAccessor, typeof (String), propertyName);
// Be sure to store any of the information you've obtained here that is needed to derive validation rules in the AdditionalValues
metadata.AdditionalValues.Add("Structure", attributeViewModel.Structure);
//TODO: Populate the rest of the Metadata here....
return metadata;
}
ValidatorProvider
public class PseudoObjectAttributeValidatorProvider : ModelValidatorProvider
{
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context)
{
if (metadata.ContainerType == typeof(PseudoObjectAttribute) && metadata.PropertyName == "StringValue")
{
PseudoObjectStructure structure = null;
try
{
if (metadata.AdditionalValues.Any())
structure = (PseudoObjectStructure)metadata.AdditionalValues["Structure"];
}
catch (KeyNotFoundException) { }
if (structure != null)
{
if (structure.IsRequired)
yield return new RequiredAttributeAdapter(metadata, context, new RequiredAttribute());
if (structure.RegularExpression != null)
yield return new RegularExpressionAttributeAdapter(metadata, context, new RegularExpressionAttribute(structure.RegularExpression) { ErrorMessage = structure.RegularExpressionErrorMessage });
}
}
else
yield break;
}
}
I THINK that's everything. if I missed something let me know.