Reputation: 741
We have separated our business logic layer and business objects into a completely separate project/assembly. Some properties of the models can contain HTML content. In front of the business logic, we have an ASP.NET MVC web application, where users can manage the business objects.
So, how can we indicate to the MVC model binder that we want to allow HTML content on (and only on) some specific properties, without referencing ASP.NET MVC in our business logic layer? Or, how can metadata be injected from another assembly without strong references?
Thank you.
Upvotes: 11
Views: 3816
Reputation: 623
In the event this is still useful to someone: I had similar requirements however my classes were generated by Entity Framework database first and so the project had used the [MetadataType] attribute extensively.
I cobbled together pieces from this question and linked questions into this solution to allow the approach to work with Metadata classes that specify [AllowHtml] (or similar)
In your Entity Framework project, define an attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class SkipRequestValidationAttribute : Attribute
{
}
Then in your metadata classes, assign this attribute to the relevant properties:
[MetadataType(typeof(ActivityLogMetadata))]
public partial class ActivityLog
{
}
public class ActivityLogMetadata
{
[Required]
[SkipRequestValidation]
[Display(Name = "Body")]
public string Body { get; set; }
}
Now, in your MVC project add this custom Model binder to look for these meta attributes.
public class MyModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var containerType = bindingContext.ModelMetadata.ContainerType;
if (containerType != null)
{
/* Do we have a Metadata attribute class specified for the Type we're binding? */
var metaRedirectInfo = containerType
.GetCustomAttributes(typeof(MetadataTypeAttribute), true)
.OfType<MetadataTypeAttribute>().FirstOrDefault();
if (metaRedirectInfo != null)
{
/* If our Meta class has a definition for this property, check it */
var thisProperty = metaRedirectInfo.MetadataClassType.GetProperty(bindingContext.ModelMetadata.PropertyName);
if (thisProperty != null)
{
var hasAttribute = thisProperty
.GetCustomAttributes(false)
.Cast<Attribute>()
.Any(a => a.GetType().IsEquivalentTo(typeof(SkipRequestValidationAttribute)));
/* If we have a SkipRequestValidation attribute, ensure this property isn't validated */
if (hasAttribute)
bindingContext.ModelMetadata.RequestValidationEnabled = false;
}
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Finally in your MVC project startup method (Startup.cs for example), replace the default model binder:
ModelBinders.Binders.DefaultBinder = new MyModelBinder();
Upvotes: 0
Reputation: 598
I had to change BindModel to the following (this builds on Russ Cam's answer) in order to check the attribute on the actual property. I also looked at this answer for help:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var holderType = bindingContext.ModelMetadata.ContainerType;
if (holderType != null)
{
var propertyType = holderType.GetProperty(bindingContext.ModelMetadata.PropertyName);
var attributes = propertyType.GetCustomAttributes(true);
var hasAttribute = attributes
.Cast<Attribute>()
.Any(a => a.GetType().IsEquivalentTo(typeof(MyAllowHtmlAttribute)));
if (hasAttribute)
{
bindingContext.ModelMetadata.RequestValidationEnabled = false;
}
}
return base.BindModel(controllerContext, bindingContext);
}
Upvotes: 8
Reputation: 125488
Implement your own IModelBinder
and AllowHtmlAttribute
- put the attribute in your core project and the IModelBinder
in your MVC application.
public class MyAllowHtmlAttribute : Attribute
{
}
To implement the IModelBinder
, simply inherit from DefaultModelBinder
and add logic to turn off request validation based on the presence of your own AllowHtmlAttribute
public class MyBetterDefaultModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var allowHtmlAttribute = bindingContext.ModelType.GetCustomAttribute<MyAllowHtmlAttribute>();
if (allowHtmlAttribute != null)
{
bindingContext.ModelMetadata.RequestValidationEnabled = false;
}
return base.BindModel(controllerContext, bindingContext);
}
}
Then hook up your own ModelBinder in Application_Start
(or other startup code)
ModelBinders.Binders.DefaultBinder = new MyBetterDefaultModelBinder();
This logic in the custom model binder is what the AllowHtmlAttribute
in MVC does but you wouldn't be able to use that one easily as it is intrinsically tied to ModelMetadata in MVC.
Upvotes: 4
Reputation: 56490
The request validation concept that AllowHtml relies on, and the binding checks are specific to web requests. There's no separation of concerns here, they're intimately linked. So no, you can't use it without taking a reference on System.Web etc.
You rule out the (in my opinion) most correct option - View Models even though validation and binding is really a view model concept.
You can't have portable business objects with web specific binding and validation concepts.
Upvotes: 1