I have been using Telerik MVC Grid for quite a while now. It is a great control, however, one annoying thing keeps showing up related to using the grid with Ajax Binding to objects created and returned from the Entity Framework. Entity objects have circular references, and when you return an IEnumerable<T> from an Ajax callback, it generates an exception from the JavascriptSerializer if there are circular references. This happens because the MVC Grid uses a JsonResult, which in turn uses JavaScriptSerializer which does not support serializing circular references.
My solution to this problem has been to use LINQ to create view objects that do not have the Related Entities. This works for all cases, but requires the creation of new objects and the copying of data to / from entity objects to these view objects. Not a lot of work, but it is work.
I have finally figured out how to generically make the grid not serialize the circular references (ignore them) and I wanted to share my solution for the general public, as I think it is generic, and plugs into the environment nicely.
The solution has a couple of parts
Swap the default grid serializer with a custom serializer
Install the Json.Net plug-in available from Newtonsoft (this is a great library)
Implement the grid serializer using Json.Net
Modify the Model.tt files to insert [JsonIgnore] attributes in front of the navigation properties
Override the DefaultContractResolver of Json.Net and look for the _entityWrapper attribute name to ensure this is also ignored (injected wrapper by the POCO classes or entity framework)
All of these steps are easy in and of themselves, but without all of them you cannot take advantage of this technique.
Once implemented correctly I can now easily send any entity framework object directly to the client without creating new View objects. I don't recommend this for every object, but sometimes it is the best option. It is also important to note that any related entities are not available on the client side, so don't use them.
Here are the Steps required
Create the following class in your application somewhere. This class is a factory object that the grid uses to obtain JSON results. This will be added to the telerik library in the global.asax file shortly.
public class CustomGridActionResultFactory : IGridActionResultFactory
{
public System.Web.Mvc.ActionResult Create(object model)
{
//return a custom JSON result which will use the Json.Net library
return new CustomJsonResult
{
Data = model
};
}
}
Implement the Custom ActionResult. This code is boilerplate for the most part. The only interesting part is at the bottom where it calls JsonConvert.SerilaizeObject passing in a ContractResolver. The ContactResolver looks for properties called _entityWrapper by name and sets them to be ignored. I am not exactly sure who injects this property, but it is part of the entity wrapper objects and it has circular references.
public class CustomJsonResult : ActionResult
{
const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";
public string ContentType { get; set; }
public System.Text.Encoding ContentEncoding { get; set; }
public object Data { get; set; }
public JsonRequestBehavior JsonRequestBehavior { get; set; }
public int MaxJsonLength { get; set; }
public CustomJsonResult()
{
JsonRequestBehavior = JsonRequestBehavior.DenyGet;
MaxJsonLength = int.MaxValue; // by default limit is set to int.maxValue
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if ((JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(JsonRequest_GetNotAllowed);
}
var response = context.HttpContext.Response;
if (!string.IsNullOrEmpty(ContentType))
{
response.ContentType = ContentType;
}
else
{
response.ContentType = "application/json";
}
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Data != null)
{
response.Write(JsonConvert.SerializeObject(Data, Formatting.None,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new PropertyNameIgnoreContractResolver()
}));
}
}
}
Add the factory object to the telerik grid. I do this in the global.asax Application_Start() method, but realistically it can be done anywhere that makes sense.
DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory());
Create the DefaultContractResolver class that checks for _entityWrapper and ignores that attribute. The resolver is passed into the SerializeObject() call in step 2.
public class PropertyNameIgnoreContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (member.Name == "_entityWrapper")
property.Ignored = true;
return property;
}
}
Modify the Model1.tt file to inject attributes that ignore the related entity properties of the POCO Objects. The attribute that must be injected is [JsonIgnore]. This is the hardest part to add to this post but not hard to do in the Model1.tt (or whatever filename it is in your project). Also if you are using code first then you can manually place the [JsonIgnore] attributes in front of any attribute that creates a circular reference.
Search for the region.Begin("Navigation Properties") in the .tt file. This is where all of the navigation properties are code generated. There are two cases that have to be taken care of the many to XXX and the Singular reference. There is an if statement that checks if the property is
RelationshipMultiplicity.Many
Just after that code block you need to insert the [JSonIgnore] attribute prior to the line
<#=PropertyVirtualModifier(Accessibility.ForReadOnlyProperty(navProperty))#> ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.Escape(navProperty)#>
Which injects the property name into the generated code file.
Now look for this line which handles the Relationship.One and Relationship.ZeroOrOne relationships.
<#=PropertyVirtualModifier(Accessibility.ForProperty(navProperty))#> <#=code.Escape(navProperty.ToEndMember.GetEntityType())#> <#=code.Escape(navProperty)#>
Add the [JsonIgnore] attribute just before this line.
Now the only thing left is to make sure the NewtonSoft.Json library is "Used" at the top of each generated file. Search for the call to WriteHeader() in the Model.tt file. This method takes a string array parameter that adds extra usings (extraUsings). Instead of passing null, construct an array of strings and send in the "Newtonsoft.Json" string as the first element of the array. The call should now look like:
WriteHeader(fileManager, new [] {"Newtonsoft.Json"});
That's all there is to do, and everything starts working, for every object.
Now for the disclaimers
I have never used Json.Net so my implementation of it might not be
optimal.
I have been testing for about two days now and haven't found any cases where this technique fails.
I also have not found any incompatibilities between the JavascriptSerializer and the JSon.Net serializer but that doesn't mean
there aren't any
The only other caveat is that the I am testing for a property called "_entityWrapper" by name to set its ignored property to true. This is obviously not optimal.
I would welcome any feedback on how to improve this solution. I hope it helps someone else.
The first solution works with the grid editing mode, but we have the same problem with the load of the grid that already has rows of objects with circular reference, and to resolve this we need to create a new IClientSideObjectWriterFactory and a new IClientSideObjectWriter.
This is what I do:
1- Create a new IClientSideObjectWriterFactory:
public class JsonClientSideObjectWriterFactory : IClientSideObjectWriterFactory
{
public IClientSideObjectWriter Create(string id, string type, TextWriter textWriter)
{
return new JsonClientSideObjectWriter(id, type, textWriter);
}
}
2- Create a new IClientSideObjectWriter, this time I do not implement the interface, I've inherited the ClientSideObjectWriter and overrided the AppendObject and AppendCollection methods:
public class JsonClientSideObjectWriter : ClientSideObjectWriter
{
public JsonClientSideObjectWriter(string id, string type, TextWriter textWriter)
: base(id, type, textWriter)
{
}
public override IClientSideObjectWriter AppendObject(string name, object value)
{
Guard.IsNotNullOrEmpty(name, "name");
var data = JsonConvert.SerializeObject(value,
Formatting.None,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new PropertyNameIgnoreContractResolver()
});
return Append("{0}:{1}".FormatWith(name, data));
}
public override IClientSideObjectWriter AppendCollection(string name, System.Collections.IEnumerable value)
{
public override IClientSideObjectWriter AppendCollection(string name, System.Collections.IEnumerable value)
{
Guard.IsNotNullOrEmpty(name, "name");
var data = JsonConvert.SerializeObject(value,
Formatting.Indented,
new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new PropertyNameIgnoreContractResolver()
});
data = data.Replace("<", #"\u003c").Replace(">", #"\u003e");
return Append("{0}:{1}".FormatWith((object)name, (object)data));
}
}
NOTE: The replace its because the grid renders html tags for the client template in edit mode and if we don't encode then the browser will render the tags. I didn't find a workarround yet if not using a Replace from string object.
3- On my Application_Start on Global.asax.cs I registered my new factory like this:
DI.Current.Register<IClientSideObjectWriterFactory>(() => new JsonClientSideObjectWriterFactory());
And it worked for all components that Telerik has. The only thing that I do not changed was the PropertyNameIgnoreContractResolver that was the same for the EntityFramework classes.
I put the new call into my Application_Start for implement the CustomGridActionResultFactory but the create method never called...
I have taken a slightly different approach which I believe might be a little easier to implement.
All I do is apply an extended [Grid] attribute to the grid json returning method instead of the normal [GridAction] attribute
public class GridAttribute : GridActionAttribute, IActionFilter
{
/// <summary>
/// Determines the depth that the serializer will traverse
/// </summary>
public int SerializationDepth { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="GridActionAttribute"/> class.
/// </summary>
public GridAttribute()
: base()
{
ActionParameterName = "command";
SerializationDepth = 1;
}
protected override ActionResult CreateActionResult(object model)
{
return new EFJsonResult
{
Data = model,
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
MaxSerializationDepth = SerializationDepth
};
}
}
and
public class EFJsonResult : JsonResult
{
const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet.";
public EFJsonResult()
{
MaxJsonLength = 1024000000;
RecursionLimit = 10;
MaxSerializationDepth = 1;
}
public int MaxJsonLength { get; set; }
public int RecursionLimit { get; set; }
public int MaxSerializationDepth { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(JsonRequest_GetNotAllowed);
}
var response = context.HttpContext.Response;
if (!String.IsNullOrEmpty(ContentType))
{
response.ContentType = ContentType;
}
else
{
response.ContentType = "application/json";
}
if (ContentEncoding != null)
{
response.ContentEncoding = ContentEncoding;
}
if (Data != null)
{
var serializer = new JavaScriptSerializer
{
MaxJsonLength = MaxJsonLength,
RecursionLimit = RecursionLimit
};
serializer.RegisterConverters(new List<JavaScriptConverter> { new EFJsonConverter(MaxSerializationDepth) });
response.Write(serializer.Serialize(Data));
}
}
Combine this with my serializer Serializing Entity Framework problems and you have a simple way of avoiding circular references but also optionally serializing multiple levels (which I need)
Note: Telerik added this virtual CreateActionResult very recently for me so you may have to download the latest version (not sure but I think maybe 1.3+)
Another good pattern is to simply not avoid creating a ViewModel from the Model.
It is a good pattern to include a ViewModel. It gives you the opportunity to make last minute UI related tweaks to the model. For example, you can tweak a bool to have an associated string Y or N to help make the UI look nice, or vice versa.
Sometimes the ViewModel is exactly like the Model and the code to copy the properties seems unnecessary, but the pattern is a good one and sticking to it is the best practice.
Related
I have the following method where I read from a key-value XML file. I pass in a key and am returned a value where I used to display on my view.
public static class TextManager
{
public static string GetValue(string key)
{
string returnVal = null;
XmlSerializer serializer = new XmlSerializer(typeof(Entries));
string path = HttpContext.Current.Server.MapPath("/App_Data/text-key-value.xml");
if (File.Exists(path))
{
Entries entries = (Entries)serializer.Deserialize(File.OpenRead(path));
var entry = entries.Where(u => u.Key == key).FirstOrDefault();
if (entry != null)
{
returnVal = entry.Value;
}
}
return returnVal;
}
}
Basically I want to be able to use this method in my model class as a data-annotation that will pull directly from my site text file and set to the display name property.
For instance I want to replace
[Display(Name = "Reference Code")]
public string ReferenceCode { get; set; }
With this
[DisplaySiteText("ReferenceCodeKey")]
public string ReferenceCode { get; set; }
DisplaySiteText would pass the string reference "ReferenceCodeKey" to the GetValue method, file the reference in the file and then set the standard Display name attribute to whatever was in the file.
How do I create my own custom model annotation to do this, I've written custom validation annotations in the past by creating a class that inherits from ValidationAttribute, but I don't think that will work in this case.
You can inherit DisplayNameAttribute for this purpose
public class DisplaySiteTextAttribute : DisplayNameAttribute
{
private string _key;
public DisplaySiteTextAttribute(string key)
{
_key = key;
}
public override string DisplayName
{
get
{
return TextManager.GetValue(_key);
}
}
}
There are several options to customize model metadata:
Customize the way that framework provides metadata. (Create ModelMedatadaProvider)
Create new Metadata attributes. (Implement IMetadataAware)
Modify existing attributes. (Derive existing attributes.)
The 3rd option has been discussed in the other answer. Here in this post, I'll share first and second options.
Option 1 - Customize the way that framework provides metadata
You can change the logic of getting display text without changing the attribute.
In fact it's responsibility of ModelMetaDataProvider to get mete data for model, including display text for properties. So as an option, you can keep the Display attribute intact and instead, create a new model metadata provider and return property metadata from a different source.
To do so, you can create a new metadata provider by deriving from DataAnnotationsModelMetadataProvider. Then override GetMetadataForProperty and call base, to get metadata. Then change DisplayName based on your logic by reading from your text manager.
You also need to register the new metadata provider as ModelMetadataProviders.Current in App_Start.
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
public class MyCustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor,
Type containerType,
PropertyDescriptor propertyDescriptor)
{
var metadata = base.GetMetadataForProperty(modelAccessor,
containerType, propertyDescriptor);
var display = propertyDescriptor.Attributes
.OfType<DisplayAttribute>().FirstOrDefault();
if (display != null)
{
metadata.DisplayName = TextManager.GetValue(display.Name);
}
return metadata;
}
}
And then register it in Application_Start():
ModelMetadataProviders.Current = new MyCustomModelMetadataProvider();
For more information take a look at DataAnnotationsModelMetadataProvider.cs source code in ASP.NET MVC sources.
This approach is useful when you want to change the way that you provide metadata for model. For example when you want to load display name and description from an external file rather than resources, without changing existing attributes.
Option 2 - Create new Metadata attributes
Another standard solution for creating metadata-aware attributes is creating an attribute and implementing IMetadataAware interface. Then in implementation of OnMetadataCreated you can easily set properties of metadata.
This approach doesn't need to register a new meta data provider. This approach is supported by the default metadata provider and is useful for creating new metadata-aware attributes:
using System;
using System.Web.Mvc;
public class CustomMetadataAttribure : Attribute, IMetadataAware
{
public string Key { get; set; }
public CustomMetadataAttribure(string key) => this.Key = key;
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.DisplayName = TextManager.GetValue(this.Key);
}
}
This approach is useful when you want to extend metadata attributes and add a few more attributes. For example when you want to add some attributes to control rendering. You can set ModelMetadata properties or add some new values to its AdditionalValues dictionary.
Maybe your DisplaySiteText attribute could inherit from the Display attribute and set the name using your helper class. Something like this:
public class DisplaySiteTextAttribute : DisplayAttribute
{
public DisplaySiteTextAttribute(string key)
{
Name = TextManager.GetValue(key);
}
}
The get request is from an SMS API delivery report to get the information about the SMS.
One of variable that will be posted to my api is this: ?err-code=0. Is it possible to do it in a .Net Web API solution or should I use another language?
Web API Get Method:
public HttpResponseMessage Get([FromUri]TestModel testingDetials)
{
return Request.CreateResponse(System.Net.HttpStatusCode.OK);
}
Model
public class TestModel
{
public string foo { get; set; }
public string err_code { get;set; }
}
I tried various solution found on this website none of them work like adding [JsonProperty] and [DataMember] to the err_code property.
You can use [JsonProperty(PropertyName = "err-code")] provided the request is being received as JSON. This is because JsonProperty is part of the Newtonsoft JSON serializer library which is what Web API uses to deserialize JSON. If the request is not JSON, the library is not used in the pipeline.
As you mentioned you can use HttpContext. If I remember correctly the model binding in MVC converts '-' to '_' but I could be wrong. Regardless to continue using strongly typed models, which I recommend, is to use model binding. This is basically writing a custom mapping between the http context and the model. You could even expand the usual one and map something like "err-code" to a property called ErrCode automatically by writing a convention based one. Here is an example, scroll a bit: http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
Happy Coding!
(Through I would provide a complete answer for the sake of...well... having a complete answer)
For my case I created a model binder to convert the var "_" to "-" and setting the value by using reflection. This answer is just for a reference.
Here is the code: (This solution is used for Web API not MVC)
public class SmsReceiptModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(SmsReceiptModel))
{
return false;
}
Type t = typeof(SmsReceiptModel);
var smsDetails = new SmsReceiptModel();
foreach (var prop in t.GetProperties())
{
string propName = prop.Name.Replace('_', '-');
var currVal = bindingContext.ValueProvider.GetValue(
propName);
if (currVal != null)
prop.SetValue(smsDetails, Convert.ChangeType(currVal.RawValue, prop.PropertyType), null);
}
bindingContext.Model = smsDetails;
return true;
}
}
I want to use custom serialization for my Nancy Modules, which uses the requested URL as part of its inputs.
I've seen that you need to register dependencies that use the context in an override of the ConfigureRequestContainer (as answered in C# IOC and Request State in NancyFX)
public class NancyBootstrapper : DefaultNancyBootstrapper
{
protected override void ConfigureRequestContainer(
TinyIoCContainer container,
NancyContext context)
{
container.Register<JsonSerializer>(new CustomJsonSerializer(context));
}
}
And I've seen that you can specify a different serialiser for Nancy using the IRegistrations interface (as answered in Configuring JsonNetSerializer and JsonNetBodyDeserializer using Nancy TinyIoC)
public class JsonRegistration : IRegistrations
{
public IEnumerable<TypeRegistration> TypeRegistrations
{
get
{
yield return new TypeRegistration(typeof(JsonSerializer), typeof(CustomJsonSerializer));
}
}
public IEnumerable<CollectionTypeRegistration> CollectionTypeRegistrations { get; protected set; }
public IEnumerable<InstanceRegistration> InstanceRegistrations { get; protected set; }
}
If I register my serializer the first way, I can't get Nancy to use it for JSON serialization.
If I register it the second way, I can't inject a copy of the current NancyContext and get access to the request - it tries to create an instance of the serialiser before the ConfigureRequestContainer method is even called.
What am I missing here?
Assuming that you are interested in the "serialization" part - i.e. returning your model as json, did you consider to extend your model to carry the needed information trough the pipeline, and then the serializer to strip it?
The following code makes the assumption that the custom serializer is smart enough to not serialize null properties.
Get["/somedata"] = _ => new MyModelEx
{
WhateverRealProperty = "some data",
RequestUri = this.Context.Request.Uri
};
public class MyModelEx : MyModel, IModelWithRequestUri
{
public string RequestUri {get; set;}
}
And in your serializer you can test for this "extra" data element, grab whatever you need from it, strip it off (set it to null??) and serialize the rest.
Or variations of the above, where you can have some "container" generic model like
public class ExtendedModel<T>
{
public T Model {get; set;}
public string RequestUri {get;set;} // or even pass the whole context if u need to
}
Some variations of the above would be the simplest implementation.
I would prefer a different approach, where I would create some form of a per-request "bag", in which to hold the current context, and inject that bag into the serializer (of course, this will require the serializer to be registered per-request as well).
Check this answer for ideas.
I have a ViewModel that I can decorate with the [Required] attribute (see below). I've come to the point where I need to let the client control which fields are required or not. They can configure this trough XML and all this info is stored in the Model when it's first created. Now I have fields that are not decorated with [Required] but still need to get validated (as per "user settings") before submitting (for example the Phone field).
public class MyBusinessObjectViewModel
{
[Required]
public string Email { get; set; } //compulsory
public string Phone { get; set; } //not (yet) compulsory, but might become
}
If the user will not enter the Phone number, the data will still get posted. Wanting not to mess with custom validators, I just add the "data-val" and "data-val-required" attributes to the Html, like this:
Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add("data-val", "true");
dict.Add("data-val-required", "This field is required.");
#Html.TextBoxFor(x => x, dict);
This forces the client side validation for all the properties that are dynamically set as required. Is this good practice? What kind of side effects can I expect?
You should look into extending the meta model framework with your own metadata provider to do the actual binding between your site's configuration and the model metadata. You can actually set the required property flag to true on the property model metadata during the metadata creation process. I can't remember for sure whether this causes the built in editor templates to generate the attribute, but I think it does. Worst case scenario you can actually create and attach a new RequiredAttribute to the property, which is a tad bit kluggy, but works pretty well in certain scenarios.
You could also do this with IMetadataAware attributes, especially if Required is the only metadata aspect your users can customize, but the implementation really depends on what you're trying to do.
One major advantage of using a custom ModelMetadataProvider in your specific case is that you can use dependency injection (via ModelMetadataProviders) to get your customer settings persistence mechanism into scope, whereas with the data attribute you only get to write an isolated method that runs immediately after the metadata model is created.
Here is a sample implementation of a custom model metadata provider, you'd just have to change the client settings to whatever you wanted to use.
UPDATED but not tested at all
public class ClientSettingsProvider
{
public ClientSettingsProvider(/* db info */) { /* init */ }
public bool IsPropertyRequired(string propertyIdentifier)
{
// check the property identifier here and return status
}
}
public ClientRequiredAttribute : Attribute
{
string _identifier;
public string Identifier { get { return _identifer; } }
public ClientRequiredAttribute(string identifier)
{ _identifier = identifier; }
}
public class RequiredModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
ClientSettings _clientSettings;
public RequiredModelMetadataProvider(ClientSettings clientSettings)
{
_clientSettings = clientSettings;
}
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
// alternatively here is where you could 'inject' a RequiredAttribute into the attributes list
var clientRequiredAttribute = attributes.OfType<ClientRequiredAttribute>().SingleOrDefault();
if(clientRequiredAttribute != null && _clientSettings.IsPropertyRequired(clientRequiredAttribute.Identifier))
{
// By injecting the Required attribute here it will seem to
// the base provider we are extending as if the property was
// marked with [Required]. Your data validation attributes should
// be added, provide you are using the default editor templates in
// you view.
attributes = attributes.Union(new [] { new RequiredAttribute() });
}
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
// REMOVED, this is another way but I'm not 100% sure it will add your attributes
// Use whatever attributes you need here as parameters...
//if (_clientSettings.IsPropertyRequired(containerType, propertyName))
//{
// metadata.IsRequired = true;
//}
return metadata;
}
}
USAGE
public class MyModel
{
[ClientRequired("CompanyName")]
public string Company { get; set; }
}
public class MyOtherModel
{
[ClientRequired("CompanyName")]
public string Name { get; set; }
public string Address { get; set; }
}
Both of these models would validate the string "CompanyName" against your client settings provider.
Not wanting to mess with custom validators, so you messed in the View obfuscating the logic of your validation by removing it from the place where it is expected to be found.
Really, don't be afraid of creating a custom attribute validator. What you are doing right now is getting a technical debt.
I need advice on how to return a limited set of data from an MVC controller.
Lets say I have a class that is constructed like so:
public interface ICustomerExpose
{
string Name {get; set;}
string State {get; set;}
}
public interface ICustomer: ICustomerExpose
{
int Id {get; set;}
string SSN {get; set;}
}
public class Customer: ICustomer
{
...
}
In my MVC project I have a controller action that returns customer data. The project is actually more like a web service as there is no View associated with the data... we use the XmlResult (provided by the MVCContrib project). The controller action looks like this:
// GET: /Customer/Show/5
public ActionResult Show(int id)
{
Customer customer = Customer.Load(id);
... // some validation work
return new XmlResult((ICustomerExpose)customer);
}
The above controller code does not work like I want it to. What I want to happen is that only the Name and State properties are serialized and returned in the XmlResult. In practice the whole customer object is serialized including the data I definitely don't want exposed.
I know the reason this doesn't work: you can't serialize an interface.
One idea floated around the office was to simply mark the properties Name and State as [XmlIgnore]. However, this doesn't seem like a good solution to me. There might be other instances where I want to serialize those properties and marking the properties on the class this way prohibits me.
What is the best way to achieve my goal of only serializing the properties in the ICustomerExpose interface?
Addendum:
For those interested in what XmlResult does here are the relevant parts of it:
public class XmlResult : ActionResult
{
private object _objectToSerialize;
public XmlResult(object objectToSerialize)
{
_objectToSerialize = objectToSerialize;
}
/// <summary>
/// Serialises the object that was passed into the constructor
/// to XML and writes the corresponding XML to the result stream.
/// </summary>
public override void ExecuteResult(ControllerContext context)
{
if (_objectToSerialize != null)
{
var xs = new XmlSerializer(_objectToSerialize.GetType());
context.HttpContext.Response.ContentType = "text/xml";
xs.Serialize(context.HttpContext.Response.Output, _objectToSerialize);
}
}
}
You can try this, however I am not sure if it works with xml serializers:
return new XmlResult(new { customer.Name, customer.State });
See this related question which recommends using an anonymous type.
// GET: /Customer/Show/5
public ActionResult Show(int id)
{
Customer customer = Customer.Load(id);
... // some validation work
var result = from c in cusomter
select new
{
Name = c.Name,
State = c.State,
};
// or just
var result = new
{
Name = customer.Name,
State = customer.State,
};
return new XmlResult(result);
}
Consider using, just for this one problem, XML literals in VB9 rather than serialization. Seriously. Just give it 20 minutes of your time. There's many options.
http://www.hanselman.com/blog/TheWeeklySourceCode30VBNETWithXMLLiteralsAsAViewEngineForASPNETMVC.aspx
http://www.hanselman.com/blog/XLINQToXMLSupportInVB9.aspx
http://blogs.msdn.com/dmitryr/archive/2008/12/29/asp-net-mvc-view-engine-using-vb-net-xml-literals.aspx
http://haacked.com/archive/2008/12/29/interesting-use-of-xml-literals-as-a-view-engine.aspx
http://www.infoq.com/news/2009/02/MVC-VB
For what you're doing, returning XML as a poor-man's Web Service, this is tailor-made.
I ended up just doing the XmlIgnore as co-workers suggested, even though this left me with some undesirable (or so I thought) behaviors.
To get around the fact that XmlIgnore would continue hiding properties that I might want serialized later I asked another question trying to find a way to around that issue. Cheeso came up with a great answer making the XmlIgnore the best route (in my opinion) to take.