Get another property's value from ModelMetaData - asp.net-mvc

I'm trying to get to another property's value from within the GetClientValidationRules method of a custom validation attribute.
Here is my attempt (based on Darin's response on another question):
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata, ControllerContext context)
{
var parentType = metadata.ContainerType;
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForType(null, parentType);
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, parentType);
var otherProperty = parentMetaData.FirstOrDefault(p =>
p.PropertyName == "SomeProperty");
var otherValue = otherProperty.Model;
var rule = new ModelClientValidationRule
{
ValidationType = "customvalidatorattribute",
ErrorMessage = this.FormatErrorMessage(metadata.GetDisplayName()),
};
yield return rule;
}
However, when trying to set otherValue, I get:
System.Reflection.TargetException: Object does not match target type.

The problem is that you are not passing in the bound model. Change the following two lines:
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, parentType);
var otherValue = (string)parentMetaData.FirstOrDefault(p =>
p.PropertyName == "SomeProperty").Model;
This will get the full metadata (including the bound values) from the current model.

#JeradRose, the problem with your TargetException is because of this line:
var parentMetaData = ModelMetadataProviders.Current
.GetMetadataForProperties(context.Controller.ViewData.Model, parentType);
parentType needs to be context.Controller.ViewData.Model.GetType().
Probably you already fixed it, but I just got it today.

Related

How do I get the type of the executing Controller in MVC .Net Core?

In my own DI I am trying to fork code based on whether the current request is executing in the context of an anoymous request or not. I guessed the easiest way would be to infer the type of the current controller and see if it was a subtype of our own anonymous api controller:
public bool InAnonymousContext() {
var anonymousContext = false;
if (_actionContextAccessor?.ActionContext != null)
{
var controllerContext = new ControllerContext(_actionContextAccessor.ActionContext);
var type = controllerContext.ActionDescriptor.ControllerTypeInfo?.GetType();
anonymousContext = type.IsSubclassOf(typeof(AbstractAnonymousApiController)) ?? false;
}
return anonymousContext;
}
but I've made a wrong assumption that IActionContextAccessor would be available to me from DI.
Any ideas how to go about this?
Found meta data on the HttpContext via DI & IHttpContextAccessor
var ctx = sp.GetRequiredService<IHttpContextAccessor>();
var action = ctx.HttpContext?.GetEndpoint()?.Metadata.GetMetadata<ControllerActionDescriptor>();
var type = action?.ControllerTypeInfo.AsType();
if (type != null && type.IsSubclassOf(typeof(AbstractAnonymousApiController)))
return null;

db.Set<T>() how to use correctly in EF6

I observed that we can write custom linq queries if we use
dbContext.set<MyEntity>()
But can not on
dbContext.set(SomeType).
I have a context class EGEntities and I have an Entity say "Employee".
How can I assign Employee type Entity to MyEntity? So that I can create a queryable instance of Employee?
Another Failure Idea
var instance = Activator.CreateInstance(thisType);
GetEntityTemplate(instance, thisType);
// Just to Test
public static List<Object> GetEntityTemplate<T>(T instance,Type targetType) where T :class
{
var Context = new EGEntities();
var set = Context.Set<T>();
if (set == null)
{
throw new Exception();
}
List<object> l = null;
return l;
}
But don't know why 'set' is only looking for 'object' and exception is "Model is not current context" though instance is correctly carrying the class instance in the parameter.
The DbContext.Set() method is having another (non-generic) overload that is expecting a System.Type:
var employeesSet = dbContext.Set(typeof(Employee));
And if you're already having a reference to an existing instance of Employee:
var employeeType = myEmployee.GetType();
var employeesSet = dbContext.Set(employeeType);
See MSDN

How do I access the value to compare when overriding GetClientValidationRules?

I am trying to write a client side validator for angularjs using fluent validation. I used the methods outlined by Darin Dimitrov here. Everything works fine except I can't figure out how to access the greater than value I set up in my validation rule. I need this so I can have my angular directive validate this value for me.
Any ideas? Thanks.
Here is my Rule:
RuleFor(m => m.dropDownListId).GreaterThan(0).WithMessage("Required");
Here is my override code:
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (!ShouldGenerateClientSideRules()) yield break;
var formatter = new MessageFormatter().AppendPropertyName(Rule.PropertyName);
var message = formatter.BuildMessage(Validator.ErrorMessageSource.GetString());
var rule = new ModelClientValidationRule
{
ValidationType = VALIDATIONTYPE,
ErrorMessage = message
};
//CompareAttribute is deprecated and I can't figure out the new syntax
//also 'MemberToCompare' is always null
rule.ValidationParameters["greaterthan"] = CompareAttribute.FormatPropertyForClientValidation(validator.MemberToCompare.Name);
//what I am trying to do is
rule.ValidationParameters.Add("greaterthan", "the value I setup in my rule");
yield return rule;
}
I hate to answer my own questions, especially when I have missed the obvious but this may help someone.
Because 'GreaterThan' validates a number you need to use Validator.ValueToCompare. Duh.
Here is the correct way.
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
if (!ShouldGenerateClientSideRules()) yield break;
var validator = Validator as GreaterThanValidator;
if(validator == null)
throw new ArgumentException("greaterThanValidator");
var valueToCompare = validator.ValueToCompare;
var formatter = new MessageFormatter().AppendPropertyName(Rule.PropertyName);
var message = formatter.BuildMessage(Validator.ErrorMessageSource.GetString());
var rule = new ModelClientValidationRule
{
ValidationType = VALIDATIONTYPE,
ErrorMessage = message
};
rule.ValidationParameters.Add("min", valueToCompare);
yield return rule;
}

MongoDB custom serializer implementation

I am new to MongoDB, and am trying to get the C# driver to work serializing F# classes. I have it working with the class automapper using mutable F# fields & a parameterless constructor, but really I need to retain immutability, so I started looking at implementing an IBsonSerializer to perform custom serialization. I haven't found any documentation for writing one of these so have just tried to infer from the driver source code.
I have run into a problem whereby when the Deserialize method is called on the serializer, the CurrentBsonType is set to EndOfDocument rather than the start as I am expecting. I wrote the equivalent in C# just to make sure it wasn't some F# weirdness, but the problem persists. The serialization part seems to work fine and is queryable from the shell. Here is the sample code:
class Calendar {
public string Id { get; private set; }
public DateTime[] Holidays { get; private set; }
public Calendar(string id, DateTime[] holidays) {
Id = id;
Holidays = holidays;
}
}
class CalendarSerializer : BsonBaseSerializer {
public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options) {
var calendar = (Calendar) value;
bsonWriter.WriteStartDocument();
bsonWriter.WriteString("_id", calendar.Id);
bsonWriter.WriteName("holidays");
var ser = new ArraySerializer<DateTime>();
ser.Serialize(bsonWriter, typeof(DateTime[]), calendar.Holidays, null);
bsonWriter.WriteEndDocument();
}
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options) {
if (nominalType != typeof(Calendar) || actualType != typeof(Calendar))
throw new BsonSerializationException();
if (bsonReader.CurrentBsonType != BsonType.Document)
throw new FileFormatException();
bsonReader.ReadStartDocument();
var id = bsonReader.ReadString("_id");
var ser = new ArraySerializer<DateTime>();
var holidays = (DateTime[])ser.Deserialize(bsonReader, typeof(DateTime[]), null);
bsonReader.ReadEndDocument();
return new Calendar(id, holidays);
}
public override bool GetDocumentId(object document, out object id, out Type idNominalType, out IIdGenerator idGenerator) {
var calendar = (Calendar) document;
id = calendar.Id;
idNominalType = typeof (string);
idGenerator = new StringObjectIdGenerator();
return true;
}
public override void SetDocumentId(object document, object id) {
throw new NotImplementedException("SetDocumentId is not implemented");
}
}
This blows up with FileFormatException in Deserialize when the CurrentBsonType is not Document. I am using the latest version 1.4 of the driver source.
I figured this out in the end. I should have used bsonReader.GetCurrentBsonType() instead of bsonReader.CurrentBsonType. This reads the BsonType in from the buffer rather than just looking at the last thing there. I also fixed a subsequent bug derserializing. The updated method looks like this:
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options) {
if (nominalType != typeof(Calendar) || actualType != typeof(Calendar))
throw new BsonSerializationException();
if (bsonReader.GetCurrentBsonType() != BsonType.Document)
throw new FileFormatException();
bsonReader.ReadStartDocument();
var id = bsonReader.ReadString("_id");
bsonReader.ReadName();
var ser = new ArraySerializer<DateTime>();
var holidays = (DateTime[])ser.Deserialize(bsonReader, typeof(DateTime[]), null);
bsonReader.ReadEndDocument();
return new Calendar(id, holidays);
}

Url Form Action Without ViewContext

Is it possible to get a URL from an action without knowing ViewContext (e.g., in a controller)? Something like this:
LinkBuilder.BuildUrlFromExpression(ViewContext context, Expression<Action<T>> action)
...but using Controller.RouteData instead of ViewContext. I seem to have metal block on this.
Here's how I do it in a unit test:
private string RouteValueDictionaryToUrl(RouteValueDictionary rvd)
{
var context = MvcMockHelpers.FakeHttpContext("~/");
// _routes is a RouteCollection
var vpd = _routes.GetVirtualPath(
new RequestContext(context, _
routes.GetRouteData(context)), rvd);
return vpd.VirtualPath;
}
Per comments, I'll adapt to a controller:
string path = RouteTable.Routes.GetVirtualPath(
new RequestContext(HttpContext,
RouteTable.Routes.GetRouteData(HttpContext)),
new RouteValueDictionary(
new { controller = "Foo",
action = "Bar" })).VirtualPath;
Replace "Foo" and "Bar" with real names. This is off the top of my head, so I can't guarantee that it's the most efficient solution possible, but it should get you on the right track.
Craig, Thanks for the correct answer. It works great, and it also go me thinking. So in my drive to eliminate those refactor-resistent "magic strings" I have developed a variation on your solution:
public static string GetUrlFor<T>(this HttpContextBase c, Expression<Func<T, object>> action)
where T : Controller
{
return RouteTable.Routes.GetVirtualPath(
new RequestContext(c, RouteTable.Routes.GetRouteData(c)),
GetRouteValuesFor(action)).VirtualPath;
}
public static RouteValueDictionary GetRouteValuesFor<T>(Expression<Func<T, object>> action)
where T : Controller
{
var methodCallExpresion = ((MethodCallExpression) action.Body);
var controllerTypeName = methodCallExpresion.Object.Type.Name;
var routeValues = new RouteValueDictionary(new
{
controller = controllerTypeName.Remove(controllerTypeName.LastIndexOf("Controller")),
action = methodCallExpresion.Method.Name
});
var methodParameters = methodCallExpresion.Method.GetParameters();
for (var i = 0; i < methodParameters.Length; i++)
{
var value = Expression.Lambda(methodCallExpresion.Arguments[i]).Compile().DynamicInvoke();
var name = methodParameters[i].Name;
routeValues.Add(name, value);
}
return routeValues;
}
I know what some will say...dreaded reflection! In my particular application, I think the benefit of maintainability outweighs performance conerns. I welcome any feedback on this idea and the code.

Resources