Access custom attributes of .NET class inside custom json converter - asp.net-web-api

In my project, I have written a custom json converter to trim the white-spaces present in the string property.
Here is an example of the typical class we will use,
public class Candidate
{
public string CandidateName { get; set; }
}
Here is my custom json converter
public class StringSanitizingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue , JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
if (reader.Value != null)
{
string sanitizedString = (reader.Value as string).Trim();
if (StringSanitizeOptions.HasFlag(StringSanitizeOptions.ToLowerCase))
sanitizedString = sanitizedString.ToLowerInvariant();
return sanitizedString;
}
return reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var text = (string)value;
if (text == null)
writer.WriteNull();
else
writer.WriteValue(text.Trim());
}
}
With my custom converter I am now able to format the string by trimming any white-spaces present sent to the action methods using my 'Candidate' as one of its parameter.
public void Post(ComplexType complexTypeParameter){
}
Everything worked well so far. I later wanted to enhance this json converter to format the string properties based on the attributes set to the string property in the Candidate class. for example, assume I have written my candidate class like this,
public class Candidate
{
[StringSanitizingOptions(Option.ToLowerCase)]
public string CandidateName { get; set; }
}
And if I wanted to format the string properties of a class based on the custom attribute configuration inside the json converter , I am not able to access this custom attribute and its configuration inside the ReadJson method of the custom converter.
Here is what I have tried so far but with no luck,
Not present in the CustomAttributes property of the objectType
parameter sent to the ReadJson() method.
Was trying to see if I could extract the parent class of the property inside the ReadJson() method, so that I could apply reflection on the class to extract the custom attributes given to any of its property,but I could not extract that too.

The stack of containing object(s) is not made available to JsonConverter.ReadJson(), thus you cannot do what you want inside ReadJson().
Instead, what you can do is to create a custom contract resolver that applies an appropriately configured instance of StringSanitizingConverter based on the properties of the object for which a contract is being generated.
First, let's say your data model, attribute, and JsonConverter look like the following (where I had to modify a few things to make your code compile and include some additional test cases):
public class Candidate
{
[StringSanitizingOptions(Option.ToLowerCase)]
public string CandidateName { get; set; }
[StringSanitizingOptions(Option.DoNotTrim)]
public string StringLiteral { get; set; }
public string DefaultString { get; set; }
public List<string> DefaultStrings { get; set; }
}
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field | System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class StringSanitizingOptionsAttribute : System.Attribute
{
public Option StringSanitizeOptions { get; set; }
public StringSanitizingOptionsAttribute(Option stringSanitizeOptions)
{
this.StringSanitizeOptions = stringSanitizeOptions;
}
}
[Flags]
public enum Option
{
Default = 0,
ToLowerCase = (1<<0),
DoNotTrim = (1<<1),
}
public static class StringSanitizeOptionsExtensions
{
public static bool HasFlag(this Option options, Option flag)
{
return (options & flag) == flag;
}
}
public class StringSanitizingConverter : JsonConverter
{
readonly Option options;
public StringSanitizingConverter() : this(Option.Default) { }
public StringSanitizingConverter(Option options)
{
this.options = options;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
if (reader.Value != null)
{
var sanitizedString = (reader.Value as string);
if (!options.HasFlag(Option.DoNotTrim))
sanitizedString = sanitizedString.Trim();
if (options.HasFlag(Option.ToLowerCase))
sanitizedString = sanitizedString.ToLowerInvariant();
return sanitizedString;
}
return reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// WriteJson is never called with null
var text = (string)value;
if (!options.HasFlag(Option.DoNotTrim))
text = text.Trim();
writer.WriteValue(text);
}
}
Next, grab ConfigurableContractResolver from How to add metadata to describe which properties are dates in JSON.Net, and define the extension method JsonContractExtensions.AddStringConverters():
public static class JsonContractExtensions
{
public static JsonContract AddStringConverters(this JsonContract contract)
{
if (contract is JsonPrimitiveContract)
{
if (contract.UnderlyingType == typeof(string))
contract.Converter = new StringSanitizingConverter();
}
else if (contract is JsonObjectContract)
{
var objectContract = (JsonObjectContract)contract;
foreach (var property in objectContract.Properties)
{
if (property.PropertyType == typeof(string))
{
var attr = property.AttributeProvider.GetAttributes(typeof(StringSanitizingOptionsAttribute), true)
.Cast<StringSanitizingOptionsAttribute>()
.SingleOrDefault();
if (attr != null)
{
property.Converter = property.MemberConverter = new StringSanitizingConverter(attr.StringSanitizeOptions);
}
}
}
}
return contract;
}
}
public class ConfigurableContractResolver : DefaultContractResolver
{
// This contract resolver taken from the answer to
// https://stackoverflow.com/questions/46047308/how-to-add-metadata-to-describe-which-properties-are-dates-in-json-net
// https://stackoverflow.com/a/46083201/3744182
readonly object contractCreatedPadlock = new object();
event EventHandler<ContractCreatedEventArgs> contractCreated;
int contractCount = 0;
void OnContractCreated(JsonContract contract, Type objectType)
{
EventHandler<ContractCreatedEventArgs> created;
lock (contractCreatedPadlock)
{
contractCount++;
created = contractCreated;
}
if (created != null)
{
created(this, new ContractCreatedEventArgs(contract, objectType));
}
}
public event EventHandler<ContractCreatedEventArgs> ContractCreated
{
add
{
lock (contractCreatedPadlock)
{
if (contractCount > 0)
{
throw new InvalidOperationException("ContractCreated events cannot be added after the first contract is generated.");
}
contractCreated += value;
}
}
remove
{
lock (contractCreatedPadlock)
{
if (contractCount > 0)
{
throw new InvalidOperationException("ContractCreated events cannot be removed after the first contract is generated.");
}
contractCreated -= value;
}
}
}
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
OnContractCreated(contract, objectType);
return contract;
}
}
public class ContractCreatedEventArgs : EventArgs
{
public JsonContract Contract { get; private set; }
public Type ObjectType { get; private set; }
public ContractCreatedEventArgs(JsonContract contract, Type objectType)
{
this.Contract = contract;
this.ObjectType = objectType;
}
}
public static class ConfigurableContractResolverExtensions
{
public static ConfigurableContractResolver Configure(this ConfigurableContractResolver resolver, EventHandler<ContractCreatedEventArgs> handler)
{
if (resolver == null || handler == null)
throw new ArgumentNullException();
resolver.ContractCreated += handler;
return resolver;
}
}
Then, finally you can deserialize and serialize Candidate as follows:
var settings = new JsonSerializerSettings
{
ContractResolver = new ConfigurableContractResolver
{
}.Configure((s, e) => { e.Contract.AddStringConverters(); }),
};
var candidate = JsonConvert.DeserializeObject<Candidate>(json, settings);
var json2 = JsonConvert.SerializeObject(candidate, Formatting.Indented, settings);
Notes:
I don't know why the stack of containing object(s) is not available in ReadJson(). Possibilities include:
Simplicity.
A JSON object is "an unordered set of name/value pairs", so trying to access the containing .Net object while reading a property value isn't guaranteed to work, since the information required might not have been read in yet (and the parent might not even have been constructed).
Because a default instance of StringSanitizingConverter is applied to the contract generated for string itself, it is not necessary to add the converter to JsonSerializer.SettingsConverters. This in turn may lead to a small performance enhancement as CanConvert will no longer get called.
JsonProperty.MemberConverter was recently marked obsolete in Json.NET 11.0.1 but must be set to the same value as JsonProperty.Converter in previous versions of Json.NET. If you are using 11.0.1 or a more recent version you should be able to remove the setting.
You may want to cache the contract resolver for best performance.
To modify JsonSerializerSettings in asp.net-web-api, see JsonSerializerSettings and Asp.Net Core, Web API: Configure JSON serializer settings on action or controller level, How to set custom JsonSerializerSettings for Json.NET in MVC 4 Web API? or ASP.NET Core API JSON serializersettings per request, depending on your requirements and the version of the framework in use.
Sample working .Net fiddle here.

Related

.NET5 Web Api how to allow Guid in any format

I have a Dto that looks like this:
public class CreateObjectDto
{
public Guid SomeGuid { get; set; }
}
problem I'm having is that default Guid converter does not allow values outside "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx" format, so users are required to enter Guid with dashes which is not desirable. I would like an option to parse any regular Guid whether it has dashes or not.
Error if I call api with different Guid format is:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-1714eba1650b1548afd8581204d38a0c-ffc921fac3022540-00",
"errors": {
"$.attachmentList[0].documentId": [
"The JSON value could not be converted to System.Guid. Path: $.attachmentList[0].documentId | LineNumber: 0 | BytePositionInLine: 292."
]
}
}
EDIT with solution
(modified accepted solution a bit):
CustomGuidConverter:
public class CustomGuidConverter : JsonConverter<Guid>
{
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!Guid.TryParse(reader.GetString(), out var parsedGuid))
{
throw new Exception($"Unable to parse {reader.GetString()} to GUID");
}
return parsedGuid;
}
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString("D"));
}
startup.cs:
// add this line to apply conversion globally and not only for one property
services.AddMvc().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.Converters.Add(new CustomGuidConverter());
});
You have to use a custom JsonConverter https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0 for the given type (Guid)
public class CustomGuidJsonConverter : JsonConverter<Guid>
{
private Regex _uwCharsRegex = new Regex("[ \t-]");
private Regex _validityRegex = new Regex("[a-f0-9]{32}");
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string value = _uwCharsRegex.Replace(reader.GetString(), "").ToLower();
// Check validity
if (!_validityRegex.IsMatch(value))
{
return Guid.Empty; // or throw exception
}
return new Guid(value);
}
public override void Write(Utf8JsonWriter writer, Guid guidValue, JsonSerializerOptions options)
=> writer.WriteStringValue(guidValue.ToString());
}
Then you can use it by annoting the wanted Guid property.
public class CreateObjectDto
{
[JsonConverter(typeof(CustomGuidJsonConverter))]
public Guid SomeGuid { get; set; }
}

Xamarin.Android: item previously inserted in ArrayAdapter is not found again

I've inherited this Xamarin.Android app and it has a few issues.
A particular bug involves an ArrayAdapter<ProductListObject>, where ProductListObject is a common POCO that's shared between subprojects (i.e. Android, Windows Phone and iOS); it just has a couple of properties (e.g. an Id) and overrides the (.NET) Equals() method to achieve structural equality:
public class ProductListObject
{
public long Id { get; set; }
public override bool Equals(object obj)
{
if (!(obj is ProductListObject))
{
return false;
}
return Id == (obj as ProductListObject).Id;
}
}
The problem is that whenever I put an instance of this ProductListObject in an ArrayAdapter, I can't find it again, even if they have the same Id:
var p1 = new ProductListObject { Id = 1 };
var p2 = new ProductListObject { Id = 1 };
var areEqual = p1.Equals(p2); // returns True, as expected
var productAdapter = new ArrayAdapter<ProductListObject>(this, 0, new[] { p1 });
var position = productAdapter.GetPosition(p2); // returns -1 >:(
My question is: what do I have to do to make my POCO's work with Xamarin.Android types that rely on the Java equals() method, internally (like ArrayAdapter; which delegates to List.indexOf(Object))?
What I have tried:
verified that the corresponding Java version works as expected (it does)
overrode GetHashCode() (it doesn't matter, as I expected)
googled and checked the Xamarin documentation for information about implementing Equals() (I found nothing particularly relevant)
Thanks,
Jan
I did as Matt R suggested and created a proxy that inherits from Java.Lang.Object and delegates to the actual .NET object:
public class JavaObject<TValue> : Java.Lang.Object
{
public readonly TValue Value;
internal JavaObject(TValue value)
{
Value = value;
}
public override bool Equals(Java.Lang.Object that)
{
if (!(that is JavaObject<TValue>))
{
return false;
}
return Value.Equals((that as JavaObject<TValue>).Value);
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
}
This doesn't tie my platform-agnostic POCO's to the Android implementation, plus it doesn't force me to lock into some rigid inheritance tree, which is always a plus.
Application is straightforward:
var p1 = new JavaObject<ProductListObject>(new ProductListObject { Id = 1 });
var p2 = new JavaObject<ProductListObject>(new ProductListObject { Id = 1 });
var areEqual = p1.Equals(p2); // returns True, as expected
var productAdapter = new ArrayAdapter<JavaObject<ProductListObject>>(this, 0, new[] { p1 });
var position = productAdapter.GetPosition(p2); // returns 0!
It looks like .NET objects get wrapped within a Java.Lang.Object when used inside a Android.Widget.ArrayAdapter. Therefore, the comparison method that's used in the productAdapter.GetPosition(...) call is actually the java Equals(Java.Lang.Object o) method for the wrapping Java.Lang.Object.
To make a ProductListObject resolve to the same index when two objects have the same Id, make ProductListObject derive from Java.Lang.Object, override the Equals(Java.Lang.Object) and forward it to the .NET Equals(System.Object) method:
public class ProductListObject : Java.Lang.Object
{
public long Id { get; set; }
public override bool Equals(object obj) // Inherited from System.Object.
{
if (!(obj is ProductListObject))
{
return false;
}
return Id == (obj as ProductListObject).Id;
}
public override bool Equals (Java.Lang.Object o) // Inherited from Java.Lang.Object.
{
return this.Equals (o as System.Object);
}
}
If you can't inherit ProductListObject from Java.Lang.Object, another option is to implement your own proxy class:
public class ProductListObject
{
public long Id { get; set; }
public override bool Equals(System.Object obj)
{
if (!(obj is ProductListObject))
{
return false;
}
return Id == (obj as ProductListObject).Id;
}
}
public class JavaProxy: Java.Lang.Object
{
public Object Object { get; private set; }
public JavaProxy(System.Object o)
{
Object = o;
}
public override bool Equals (Java.Lang.Object o)
{
var proxy = o as JavaProxy;
if (o != null) {
return Object.Equals (proxy.Object);
}
return base.Equals (o);
}
}
// ...
var productAdapter = new ArrayAdapter<JavaProxy>(this, 0, new[] { new JavaProxy(p1) });
var position = productAdapter.GetPosition(new JavaProxy(p2));
It's not as clean as the first approach but it also works.

Default model example in Swashbuckle (Swagger)

I'm running ASP WebAPI 2 and successfully installed Swashbuckle. I am trying to figure out how one defines what the default schema values are?
For example, on the Swagger live demo site they changed the default value of pet to "doggie". They also defined the allowable values for status. (Live Demo)
I managed to get this working by following what's on this link:
https://github.com/domaindrivendev/Swashbuckle/issues/69#issuecomment-53953785
In short this is what needs to be done:
Create the classes SwaggerDefaultValue and AddDefaultValues as described in the link. Some changes that I did:
public class SwaggerDefaultValue : Attribute
{
public string Name { get; set; }
public string Value { get; set; }
public SwaggerDefaultValue(string value)
{
this.Value = value;
}
public SwaggerDefaultValue(string name, string value) : this(value)
{
this.Name = name;
}
}
public class AddDefaultValues : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
IDictionary<string, object> parameterValuePairs =
GetParameterValuePairs(apiDescription.ActionDescriptor);
foreach (var param in operation.parameters)
{
var parameterValuePair = parameterValuePairs.FirstOrDefault(p => p.Key.IndexOf(param.name, StringComparison.InvariantCultureIgnoreCase) >= 0);
param.#default = parameterValuePair.Value;
}
}
private IDictionary<string, object> GetParameterValuePairs(HttpActionDescriptor actionDescriptor)
{
IDictionary<string, object> parameterValuePairs = new Dictionary<string, object>();
foreach (SwaggerDefaultValue defaultValue in actionDescriptor.GetCustomAttributes<SwaggerDefaultValue>())
{
parameterValuePairs.Add(defaultValue.Name, defaultValue.Value);
}
foreach (var parameter in actionDescriptor.GetParameters())
{
if (!parameter.ParameterType.IsPrimitive)
{
foreach (PropertyInfo property in parameter.ParameterType.GetProperties())
{
var defaultValue = GetDefaultValue(property);
if (defaultValue != null)
{
parameterValuePairs.Add(property.Name, defaultValue);
}
}
}
}
return parameterValuePairs;
}
private static object GetDefaultValue(PropertyInfo property)
{
var customAttribute = property.GetCustomAttributes<SwaggerDefaultValue>().FirstOrDefault();
if (customAttribute != null)
{
return customAttribute.Value;
}
return null;
}
}
Edit your SwaggerConfig and add the AddDefaultValues class to the OperationFilters:
GlobalConfiguration.Configuration
.EnableSwagger(c => {
...
c.OperationFilter<AddDefaultValues>()
...
});
Now for the parameters I want default values I just add the following:
public IHttpActionResult Put([FromBody]Pet pet)
{
...
return Ok();
}
public class Pet {
[SwaggerDefaultValue("doggie")]
public string Name { get; set; }
[SwaggerDefaultValue("available")]
public string Status;
...
}
Well the code of vgaspar.trivix did not work completly for me, the default values did not get set for the schema. Also i got an NullPointerException. I managed to get it working as intended by editing the Apply method and manipulated the schemaRegistry like this:
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
if (operation.parameters == null)
return;
IDictionary<string, object> parameterValuePairs =
GetParameterValuePairs(apiDescription.ActionDescriptor);
foreach (var param in operation.parameters)
{
if (param.schema != null && param.schema.#ref != null)
{
string schemaName = param.schema.#ref.Split('/').LastOrDefault();
if (schemaRegistry.Definitions.ContainsKey(schemaName))
foreach (var props in schemaRegistry.Definitions[schemaName].properties)
{
if (parameterValuePairs.ContainsKey(props.Key))
props.Value.#default = parameterValuePairs[props.Key];
}
}
var parameterValuePair = parameterValuePairs.FirstOrDefault(p => p.Key.IndexOf(param.name, StringComparison.InvariantCultureIgnoreCase) >= 0);
param.#default = parameterValuePair.Value;
}
}
An example Model Schema can be defined by implementing ISchemaFilter and registering it using the following:
httpConfig
.EnableSwagger(c =>
{
c.SchemaFilter<AddSchemaExamples>()
});
An example implementation is provided here:
public class AddSchemaExamples : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
if (type == typeof(Product))
{
schema.example = new Product
{
Id = 123,
Type = ProductType.Book,
Description = "Treasure Island",
UnitPrice = 10.0M
};
}
}
}
Source: https://github.com/domaindrivendev/Swashbuckle/issues/162
I know this thread is quite old, but I wanted to share my solution which creates a custom constructor just for the Swagger example schema.
In my model:
/// <summary>
/// Supply a custom constructor for Swagger where you can apply defaults to control the example schema.
/// The constructor must have one parameter of type System.Reflection.ParameterInfo[].
/// Note: Setting a property to null will prevent it from showing in the Swagger example.
/// </summary>System.Reflection.ParameterInfo[].
/// </summary>
public class SwaggerConstructor : Attribute { }
In SwaggerConfig.cs:
c.SchemaFilter<ApplySchemaVendorExtensions>();
The schema extension:
public class ApplySchemaVendorExtensions : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
ConstructorInfo constructor = type.GetConstructors().FirstOrDefault(c => c.GetCustomAttribute<SwaggerConstructor>() != null);
if (constructor != null)
{
schema.example = constructor.Invoke(new object[] { constructor.GetParameters() });
}
}
}
Usage:
[SwaggerConstructor]
public MyClass(System.Reflection.ParameterInfo[] decoy) : base()
{
MyProperty = false;
}
Stumbled across this just now, you can also set the tag in the XML documentation, in one of my models, I have this defined
/// <summary>
/// Note content
/// </summary>
/// <example>Any text for a note.</example>
public string Note { get; set; }
which ends up looking like this in the swagger documentation when selecting "Try It Now"
Hope that helps someone!
Using .NET 5 with Swashbuckle.AspNetCore 5.6.3, the only way I could get this to work efficiently is this:
public class ExampleDocFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
string ToCamelCase(string name) => char.ToLowerInvariant(name[0]) + name.Substring(1);
if (schema.Properties == null) return;
var setProperties = context.Type.GetProperties().ToList().Where(f => f.GetCustomAttribute<DefaultValueAttribute>() != null).Where(f => schema.Properties.Any(n => n.Key.Equals(ToCamelCase(f.Name)))).ToDictionary(f => f, f => f.GetCustomAttribute<DefaultValueAttribute>());
foreach (var prop in setProperties) schema.Properties[ToCamelCase(prop.Key.Name)].Example = OpenApiAnyFactory.CreateFor(schema.Properties[ToCamelCase(prop.Key.Name)], prop.Value.Value);
}
}
To use this - in your startup.cs:
services.AddSwaggerGen(swagger => {
...
swagger.SchemaFilter<ExampleDocFilter>();
});

MediaTypeFormatter issue in Self-Hosted ASPNET WebAPI

I have implemented a custom MediaTypeFormatter in a SelfHosted AspNet WebAPI. I've used Unity.WebApi for dependency resolution. The controller classes only know about the interfaces implemented by the model classes, whereas, repositories give the concrete models as a result of actions.
The custom MediaTypeFormatter is inherited from BufferedMediaTypeFormatter as discussed here: http://www.asp.net/web-api/overview/formats-and-model-binding/media-formatters.
The problem is this media type formatter is not working. Even when I use to debug the code, the ReadFormStream method is never hit. Does someone know:
What could be the possible issue?
Do I need to tell the unity container for mapping of interfaces to model classes?
How would I get the reference to the dependency resolver inside the custom media type formatter?
Below is the code to add formatter:
var config = new SelfHostConfiguration("https://xxx.xxx.xxx.xxx:xxxx/");
config.Formatters.Add(new EntityTypeFormatter());
Below is the code for the EntityController:
public class EntityController : ApiController
{
private readonly IEntitiesRepository repository = null;
public EntityController(IEntitiesRepository repository)
{
if (repository == null)
{
throw new ArgumentNullException("repository");
}
this.repository = repository;
}
public IEnumerable<IEntity> Get()
{
return (IEnumerable<IEntity>)repository.Get();
}
}
Below is the code for the EntityRepository:
public class EntitiesRepository : IEntitiesRepository
{
public IEnumerable<IEntity> Get()
{
return new Entities[]
{
new Entity
{
Prop1 = "value for property 1",
Prop2 = "value for property 2",
Prop3 = "value for property 3"
},
new Entity
{
Prop1 = "value for property 1",
Prop2 = "value for property 2",
Prop3 = "value for property 3"
}
};
}
public IEntity Get(long id)
{
return new Entity
{
Prop1 = Convert.ToString(id),
Prop2 = "value for property 2",
Prop3 = "value for property 3"
};
}
}
Below is the implementation of the EntityMediaTypeFormatter class:
public class EntityMediaTypeFormatter : BufferedMediaTypeFormatter
{
public EntityMediaTypeFormatter()
: base()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
}
public override bool CanReadType(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
if (type is IEntity)
{
return true;
}
else if (type.IsGenericType)
{
return type.GenericTypeArguments.Single(a => a.GetType().Equals(typeof(IEntity))) != null;
}
return false;
}
public override bool CanWriteType(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
if (type is IEntity)
{
return true;
}
return false;
}
public override object ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
if (type.IsInterface)
{
type = GetConcreteType(type);
}
return base.ReadFromStream(type, readStream, content, formatterLogger);
}
public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
{
//nothing special for now...
base.WriteToStream(type, value, writeStream, content);
}
private Type GetConcreteType(Type type)
{
//TODO: Need to find a way to DependencyResolver to get the concrete type
return typeof(Entity);
}
}
Thanking in advance for any help.
Looks to me like one of the existing formatters is taking priority over yours. Either clear the existing formatter collection before adding yours, or insert yours at position 0.

MVC3 Validation with Lightspeed

My ORM (LightSpeed) generates this for Animals table, with Name and Age. Using MVC3 and Razor
[Serializable]
[System.CodeDom.Compiler.GeneratedCode("LightSpeedModelGenerator", "1.0.0.0")]
[System.ComponentModel.DataObject]
[Table(IdColumnName="AnimalID", IdentityMethod=IdentityMethod.IdentityColumn)]
public partial class Animal : Entity<int>
{
[ValidatePresence]
[ValidateLength(0, 50)]
private string _name;
[ValidateComparison(ComparisonOperator.GreaterThan, 0)]
private int _age;
public const string NameField = "Name";
public const string AgeField = "Age";
[System.Diagnostics.DebuggerNonUserCode]
[Required] // ****I put this in manually to get Name required working
public string Name
{
get { return Get(ref _name, "Name"); }
set { Set(ref _name, value, "Name"); }
}
[System.Diagnostics.DebuggerNonUserCode]
public int Age
{
get { return Get(ref _age, "Age"); }
set { Set(ref _age, value, "Age"); }
}
With [Required] attribute added:
With no [Required] attribute added: (notice LightSpeed strange rendering of validation)
With name filled in:
In images above - the validation at the top is LightSpeed (put into ValidationSummary) and at the side is MVC3 (put into ValidationMessageFor)
Am only using Server Side validation currently.
Question: How do I get LightSpeed validation working well in MVC3?
I think it is something in this area http://www.mindscapehq.com/staff/jeremy/index.php/2009/03/aspnet-mvc-part4/
For the server side validation - you will want to use a custom model binder which emits the errors from LightSpeed validation more precisely rather than the leveraging the DefaultModelBinder behavior. Have a look at either directly using or adapting the EntityModelBinder from the community code library for Mvc
http://www.mindscapehq.com/forums/Thread.aspx?PostID=12051
See link http://www.mindscapehq.com/forums/Thread.aspx?ThreadID=4093
Jeremys answer (Mindscape have great support!)
public class EntityModelBinder2 : DefaultModelBinder
{
public static void Register(Assembly assembly)
{
ModelBinders.Binders.Add(typeof(Entity), new EntityModelBinder2());
foreach (Type type in assembly.GetTypes())
{
if (typeof(Entity).IsAssignableFrom(type))
{
ModelBinders.Binders.Add(type, new EntityModelBinder2());
}
}
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = base.BindModel(controllerContext, bindingContext);
if (typeof(Entity).IsAssignableFrom(bindingContext.ModelType))
{
Entity entity = (Entity)result;
if (!entity.IsValid)
{
foreach (var state in bindingContext.ModelState.Where(s => s.Value.Errors.Count > 0))
{
state.Value.Errors.Clear();
}
foreach (var error in entity.Errors)
{
if (error.ErrorMessage.EndsWith("is invalid")) continue;
bindingContext.ModelState.AddModelError(error.PropertyName ?? "Custom", error.ErrorMessage);
}
}
}
return result;
}
}
and in Global.asax register using:
EntityModelBinder2.Register(typeof(MyEntity).Assembly);
The Register call sets up the model binder to be used for each entity type in your model assembly so modify as required.
You can get client side validation working with Lightspeed nightly builds from 04/04/2011 onwards.
Create a validator provider as follows:
public class LightspeedModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
private string GetDisplayName(string name)
{
return name; // go whatever processing is required, eg decamelise, replace "_" with " " etc
}
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
if(typeof(Entity).IsAssignableFrom(metadata.ContainerType))
{
List<Attribute> newAttributes = new List<Attribute>(attributes);
var attr = DataAnnotationBuilder.GetDataAnnotations(metadata.ContainerType, metadata.PropertyName, GetDisplayName(metadata.PropertyName));
newAttributes.AddRange(attr);
return base.GetValidators(metadata, context, newAttributes);
}
return base.GetValidators(metadata, context, attributes);
}
}
Then in Application_Start() add
ModelValidatorProviders.Providers.Clear();
ModelValidatorProviders.Providers.Add(new LightspeedModelValidatorProvider());

Resources