This question already has answers here:
How do you create a dropdownlist from an enum in ASP.NET MVC?
(36 answers)
Closed 8 years ago.
I've been finding all over the place that the common way to bind Enums to DropDowns is through helper methods, which seems a bit overbearing for such a seemingly simple task.
What is the best way to bind Enums to DropDownLists in ASP.Net MVC 4?
You can to this:
#Html.DropDownListFor(model => model.Type, Enum.GetNames(typeof(Rewards.Models.PropertyType)).Select(e => new SelectListItem { Text = e }))
I think it is about the only (clean) way, which is a pity, but at least there are a few options out there. I'd recommend having a look at this blog: http://paulthecyclist.com/2013/05/24/enum-dropdown/
Sorry, it's too long to copy here, but the gist is that he created a new HTML helper method for this.
All the source code is available on GitHub.
Enums are supported by the framework since MVC 5.1:
#Html.EnumDropDownListFor(m => m.Palette)
Displayed text can be customized:
public enum Palette
{
[Display(Name = "Black & White")]
BlackAndWhite,
Colour
}
MSDN link: http://www.asp.net/mvc/overview/releases/mvc51-release-notes#Enum
In my Controller:
var feedTypeList = new Dictionary<short, string>();
foreach (var item in Enum.GetValues(typeof(FeedType)))
{
feedTypeList.Add((short)item, Enum.GetName(typeof(FeedType), item));
}
ViewBag.FeedTypeList = new SelectList(feedTypeList, "Key", "Value", feed.FeedType);
In my View:
#Html.DropDownList("FeedType", (SelectList)ViewBag.FeedTypeList)
The solution from PaulTheCyclist is spot on. But I wouldn't use RESX (I'd have to add a new .resx file for each new enum??)
Here is my HtmlHelper Expression:
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TEnum>> expression, object attributes = null)
{
//Get metadata from enum
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var enumType = GetNonNullableModelType(metadata);
var values = Enum.GetValues(enumType).Cast<TEnum>();
//Convert enumeration items into SelectListItems
var items =
from value in values
select new SelectListItem
{
Text = value.ToDescription(),
Value = value.ToString(),
Selected = value.Equals(metadata.Model)
};
//Check for nullable value types
if (metadata.IsNullableValueType)
{
var emptyItem = new List<SelectListItem>
{
new SelectListItem {Text = string.Empty, Value = string.Empty}
};
items = emptyItem.Concat(items);
}
//Return the regular DropDownlist helper
return htmlHelper.DropDownListFor(expression, items, attributes);
}
Here is how I declare my enums:
[Flags]
public enum LoanApplicationType
{
[Description("Undefined")]
Undefined = 0,
[Description("Personal Loan")]
PersonalLoan = 1,
[Description("Mortgage Loan")]
MortgageLoan = 2,
[Description("Vehicle Loan")]
VehicleLoan = 4,
[Description("Small Business")]
SmallBusiness = 8,
}
And here is the call from a Razor View:
<div class="control-group span2">
<div class="controls">
#Html.EnumDropDownListFor(m => m.LoanType, new { #class = "span2" })
</div>
</div>
Where #Model.LoanType is an model property of the LoanApplicationType type
UPDATE: Sorry, forgot to include code for the helper function ToDescription()
/// <summary>
/// Returns Description Attribute information for an Enum value
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string ToDescription(this Enum value)
{
if (value == null)
{
return string.Empty;
}
var attributes = (DescriptionAttribute[]) value.GetType().GetField(
Convert.ToString(value)).GetCustomAttributes(typeof (DescriptionAttribute), false);
return attributes.Length > 0 ? attributes[0].Description : Convert.ToString(value);
}
Technically, you don't need a helper method, since Html.DropdownListFor only requires a SelectList or Ienumerable<SelectListItem>. You can just turn your enums into such an output and feed it in that way.
I use a static library method to convert enums into List<SelectListItem> with a few params/options:
public static List<SelectListItem> GetEnumsByType<T>(bool useFriendlyName = false, List<T> exclude = null,
List<T> eachSelected = null, bool useIntValue = true) where T : struct, IConvertible
{
var enumList = from enumItem in EnumUtil.GetEnumValuesFor<T>()
where (exclude == null || !exclude.Contains(enumItem))
select enumItem;
var list = new List<SelectListItem>();
foreach (var item in enumList)
{
var selItem = new SelectListItem();
selItem.Text = (useFriendlyName) ? item.ToFriendlyString() : item.ToString();
selItem.Value = (useIntValue) ? item.To<int>().ToString() : item.ToString();
if (eachSelected != null && eachSelected.Contains(item))
selItem.Selected = true;
list.Add(selItem);
}
return list;
}
public static class EnumUtil
{
public static IEnumerable<T> GetEnumValuesFor<T>()
{
return Enum.GetValues(typeof(T)).Cast<T>();
}
// other stuff in here too...
}
/// <summary>
/// Turns Camelcase or underscore separated phrases into properly spaces phrases
/// "DogWithMustard".ToFriendlyString() == "Dog With Mustard"
/// </summary>
public static string ToFriendlyString(this object o)
{
var s = o.ToString();
s = s.Replace("__", " / ").Replace("_", " ");
char[] origArray = s.ToCharArray();
List<char> newCharList = new List<char>();
for (int i = 0; i < origArray.Count(); i++)
{
if (origArray[i].ToString() == origArray[i].ToString().ToUpper())
{
newCharList.Add(' ');
}
newCharList.Add(origArray[i]);
}
s = new string(newCharList.ToArray()).TrimStart();
return s;
}
Your ViewModel can pass in the options you want. Here's a fairly complex one:
public IEnumerable<SelectListItem> PaymentMethodChoices
{
get
{
var exclusions = new List<Membership.Payment.PaymentMethod> { Membership.Payment.PaymentMethod.Unknown, Membership.Payment.PaymentMethod.Reversal };
var selected = new List<Membership.Payment.PaymentMethod> { this.SelectedPaymentMethod };
return GetEnumsByType<Membership.Payment.PaymentMethod>(useFriendlyName: true, exclude: exclusions, eachSelected: selected);
}
}
So you wire your View's DropDownList against that IEnumerable<SelectListItem> property.
Extending the html helper to do it works well, but if you'd like to be able to change the text of the drop down values based on DisplayAttribute mappings, then you would need to modify it similar to this,
(Do this pre MVC 5.1, it's included in 5.1+)
public static IHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> html, Expression<Func<TModel, TEnum>> expression)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var enumType = Nullable.GetUnderlyingType(metadata.ModelType) ?? metadata.ModelType;
var enumValues = Enum.GetValues(enumType).Cast<object>();
var items = enumValues.Select(item =>
{
var type = item.GetType();
var member = type.GetMember(item.ToString());
var attribute = member[0].GetCustomAttribute<DisplayAttribute>();
string text = attribute != null ? ((DisplayAttribute)attribute).Name : item.ToString();
string value = ((int)item).ToString();
bool selected = item.Equals(metadata.Model);
return new SelectListItem
{
Text = text,
Value = value,
Selected = selected
};
});
return html.DropDownListFor(expression, items, string.Empty, null);
}
Related
I'm developing a project that will require me to include credentials for things like an SMTP server. I'd like to store this information along with the complete details of the endpoint in an embedded JSON file, but I would like to have that information encrypted and then let my application decrypt it when it needs to establish a connection and log in. The JSON structure looks something like this:
{
"Endpoints" : [
{
"Endpoint" : {
"Host": "smtp.mydomain.tld",
"Port": 587,
"Username": "user#mydomain.tld",
"Password": "mYp#s$w0?d"
}
}
]
}
While what I'd really like to have actually stored in the file would look something like this:
{
"Endpoints" : [
{
"Endpoint" : "<BASE64_ENCODED_STRING>"
}
]
}
Using Newtonsoft's Json.NET, I've built the class object/properties to desriealize this structure:
<JsonProperty("Endpoints")>
Public Property Endpoints As List(Of EndpointContainer) = Nothing
Public Class EndpointContainer
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
Private Const EncryptedPrefix As String = "myappcipher:"
<EditorBrowsable(EditorBrowsableState.Never)> <DebuggerBrowsable(DebuggerBrowsableState.Never)>
<JsonProperty("Endpoint")> <JsonConverter(GetType(EndpointProtector))>
Public Property Endpoint As Endpoint = Nothing
End Class
And I've built the inherited JsonConverter class ("EndpointProtector") like this:
Public Class EndpointProtector
Inherits JsonConverter
Public Sub New()
Using SHAEncryption = New SHA256Managed()
_EncryptionKey = SHAEncryption.ComputeHash(Encoding.UTF8.GetBytes(TestEncryptionKey))
End Using
End Sub
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim clearText As String = JsonConvert.SerializeObject(value)
If clearText Is Nothing Then
Throw New ArgumentNullException(NameOf(clearText))
End If
writer.WriteValue(EncryptEndpoint(clearText))
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim DecryptString As String = TryCast(reader.Value, String)
If String.IsNullOrEmpty(DecryptString) Then
Return reader.Value
ElseIf Not DecryptString.StartsWith(EncryptedPrefix, StringComparison.OrdinalIgnoreCase) Then
Return DecryptString
Else
Return DecryptEndpoint(DecryptString)
End If
End Function
Public Overrides Function CanConvert(objectType As Type) As Boolean
Throw New NotImplementedException()
End Function
End Class
Currently I have the JSON file itself with the full object definition (as in the first code block). When my application reads that JSON, it correctly moves to the overridden ReadJson() method I have, but the reader.Value is null (Nothing), so it never actually gets to the DecryptEndpoint() method. Of course, that means there's nothing to encrypt, so the application won't even step into the WriteJson() method.
I've tried a couple of variations, including making the Endpoint property into a private variable with a generic Object type, and then having a separate public property with the <JsonIgnore> decoration to "read" from that, but nothing seems to get me where I need to be. I'm sure I'm overlooking something here, but I can't seem to figure out why it's not getting anything at all.
I looked at a few other SO questions like Encrypt and JSON Serialize an object, but I've still not yet been able to figure out quite where I've gone wrong here.
NOTE: I intentionally didn't include the code for the EncryptEndpoint() or DecryptEndpoint() methods here simply because the code is never making it that far in the process. If you feel it's needed to fully answer the question, please let me know.
this is a linqpad example of working encrypt/decrypt base on JsonAttribute
void Main()
{
string str = "";
var t = new Test() { encName = "some long text some long text some long text", Name = "test" };
JsonSerializerSettings theJsonSerializerSettings = new JsonSerializerSettings();
theJsonSerializerSettings.TypeNameHandling = TypeNameHandling.None;
str = JsonConvert.SerializeObject(t, theJsonSerializerSettings).Dump();
JsonConvert.DeserializeObject<Test>(str, theJsonSerializerSettings).Dump();
}
public class Test
{
[JsonConverter(typeof(EncryptingJsonConverter))]
public string encName { get; set; }
public string Name { get; set; }
}
/// <summary>[JsonConverter(typeof(EncryptingJsonConverter), string 32byte array)]</summary>
public class EncryptingJsonConverter : JsonConverter
{
private readonly byte[] _encryptionKeyBytes;
private readonly string _encryptionKeyString;
///<summary>Key must be 32char length</summary>
public EncryptingJsonConverter()
{
string encryptionKey = "E546C8DF278CD5931069B522E695D4F2"; //get from config
if (string.IsNullOrEmpty(encryptionKey))
throw new ArgumentNullException(nameof(encryptionKey));
_encryptionKeyString = encryptionKey;
_encryptionKeyBytes = Convert.FromBase64String(encryptionKey);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var stringValue = (string)value;
if (string.IsNullOrEmpty(stringValue))
{
writer.WriteNull();
return;
}
//string enc = stringValue.Encrypt(_encryptionKeyString);
string enc = Crypto.Encrypt(stringValue, _encryptionKeyBytes);
writer.WriteValue(enc);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = reader.Value as string;
if (string.IsNullOrEmpty(value))
return reader.Value;
try
{
//return value.Decrypt(_encryptionKeyString);
return Crypto.Decrypt(value, _encryptionKeyBytes);
}
catch
{
return string.Empty;
}
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
}
public static class Crypto
{
public static string Encrypt(this string text, string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key must have valid value.", nameof(key));
if (string.IsNullOrEmpty(text))
throw new ArgumentException("The text must have valid value.", nameof(text));
var buffer = Encoding.UTF8.GetBytes(text);
var hash = SHA512.Create();
var aesKey = new byte[24];
Buffer.BlockCopy(hash.ComputeHash(Encoding.UTF8.GetBytes(key)), 0, aesKey, 0, 24);
using (var aes = Aes.Create())
{
if (aes == null)
throw new ArgumentException("Parameter must not be null.", nameof(aes));
aes.Key = aesKey;
using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
using (var resultStream = new MemoryStream())
{
using (var aesStream = new CryptoStream(resultStream, encryptor, CryptoStreamMode.Write))
using (var plainStream = new MemoryStream(buffer))
{
plainStream.CopyTo(aesStream);
}
var result = resultStream.ToArray();
var combined = new byte[aes.IV.Length + result.Length];
Array.ConstrainedCopy(aes.IV, 0, combined, 0, aes.IV.Length);
Array.ConstrainedCopy(result, 0, combined, aes.IV.Length, result.Length);
return Convert.ToBase64String(combined);
}
}
}
public static string Decrypt(this string encryptedText, string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key must have valid value.", nameof(key));
if (string.IsNullOrEmpty(encryptedText))
throw new ArgumentException("The encrypted text must have valid value.", nameof(encryptedText));
var combined = Convert.FromBase64String(encryptedText);
var buffer = new byte[combined.Length];
var hash = new SHA512CryptoServiceProvider();
var aesKey = new byte[24];
Buffer.BlockCopy(hash.ComputeHash(Encoding.UTF8.GetBytes(key)), 0, aesKey, 0, 24);
using (var aes = Aes.Create())
{
if (aes == null)
throw new ArgumentException("Parameter must not be null.", nameof(aes));
aes.Key = aesKey;
var iv = new byte[aes.IV.Length];
var ciphertext = new byte[buffer.Length - iv.Length];
Array.ConstrainedCopy(combined, 0, iv, 0, iv.Length);
Array.ConstrainedCopy(combined, iv.Length, ciphertext, 0, ciphertext.Length);
aes.IV = iv;
using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
using (var resultStream = new MemoryStream())
{
using (var aesStream = new CryptoStream(resultStream, decryptor, CryptoStreamMode.Write))
using (var plainStream = new MemoryStream(ciphertext))
{
plainStream.CopyTo(aesStream);
}
return Encoding.UTF8.GetString(resultStream.ToArray());
}
}
}
public static string Encrypt(string text, byte[] key)
{
//string keyString = "encrypt123456789";
//var key = Encoding.UTF8.GetBytes(keyString);//16 bit or 32 bit key string
using (var aesAlg = Aes.Create())
{
using (var encryptor = aesAlg.CreateEncryptor(key, aesAlg.IV))
{
using (var msEncrypt = new MemoryStream())
{
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(text);
}
var iv = aesAlg.IV;
var decryptedContent = msEncrypt.ToArray();
var result = new byte[iv.Length + decryptedContent.Length];
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
Buffer.BlockCopy(decryptedContent, 0, result, iv.Length, decryptedContent.Length);
return Convert.ToBase64String(result);
}
}
}
}
public static string Decrypt(string cipherText, byte[] key)
{
var fullCipher = Convert.FromBase64String(cipherText);
var iv = new byte[16];
var cipher = new byte[fullCipher.Length - iv.Length];//new byte[16];
Buffer.BlockCopy(fullCipher, 0, iv, 0, iv.Length);
Buffer.BlockCopy(fullCipher, iv.Length, cipher, 0, cipher.Length);
//var key = Encoding.UTF8.GetBytes(keyString);//same key string
using (var aesAlg = Aes.Create())
{
using (var decryptor = aesAlg.CreateDecryptor(key, iv))
{
string result;
using (var msDecrypt = new MemoryStream(cipher))
{
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (var srDecrypt = new StreamReader(csDecrypt))
{
result = srDecrypt.ReadToEnd();
}
}
}
return result;
}
}
}
}
to activate encryption just add a tag
[JsonConverter(typeof(EncryptingJsonConverter))]
I have a situation in which I have a very large C# object, however, I only need to return a handful of properties (which can be on nested objects), allow for client-side JavaScript to modify those properties and then send the resulting object back to the server in order to perform in-place partial de-serialization.
The idea is to re-use some very large existing business objects, but be intelligent about only serializing and sending only those properties back to the client application for modification (to keep the amount of data transferred at a minimum).
I basically have an XML file where I pre-define all of the bindings using a "path syntax" which would indicate only those properties I need to serialize. So, I could use something like "WorkOrder.UserField1" or "WorkOrder.Client.Name".
I have tried using a custom contract resolver to determine whether or not a property should be serialized; however, it doesn't seem that I have information as to the "path" (in other words, other properties in the object model up the chain) in order to determine if the property should or should not be serialized.
I have also tried using a custom JsonTextWriter, but it doesn't seem that I can override the methods necessary to keep track of the path, even though there is a Path property available. Is there something perhaps simple that I am overlooking in order to be able to view the path hierarchy of a property being serialized and determine if it should be serialized by looking up the path in a table and making the decision?
The basic difficulty here is that Json.NET is a contract-based serializer which creates a contract for each type to be serialized, then (de)serializes according to the contract. If a type appears in multiple locations in the object hierarchy, the same contract applies. But you want to selectively include properties for a given type depending on its location in the hierarchy, which conflicts with the basic "one type one contract" design.
One quick way to work around this is to serialize to a JObject, then use JToken.SelectTokens() to select only the JSON data you want to return, removing everything else. Since SelectTokens has full support for JSONPath query syntax, you can selectively include using array and property wildcards or other filters, for instance:
"$.FirstLevel[*].Bar"
includes all properties named "Bar" in all array members of a property named "FirstLevel" of the root object.
This should reduce your network usage as desired, but won't save any processing time on the server.
Removal can be accomplished with the following extension methods:
public static partial class JsonExtensions
{
public static TJToken RemoveAllExcept<TJToken>(this TJToken obj, IEnumerable<string> paths) where TJToken : JToken
{
if (obj == null || paths == null)
throw new NullReferenceException();
var keepers = new HashSet<JToken>(paths.SelectMany(path => obj.SelectTokens(path)), ObjectReferenceEqualityComparer<JToken>.Default);
var keepersAndParents = new HashSet<JToken>(keepers.SelectMany(t => t.AncestorsAndSelf()), ObjectReferenceEqualityComparer<JToken>.Default);
// Keep any token that is a keeper, or a child of a keeper, or a parent of a keeper
// I.e. if you have a path ""$.A.B" and it turns out that B is an object, then everything
// under B should be kept.
foreach (var token in obj.DescendantsAndSelfReversed().Where(t => !keepersAndParents.Contains(t) && !t.AncestorsAndSelf().Any(p => keepers.Contains(p))))
token.RemoveFromLowestPossibleParent();
// Return the object itself for fluent style programming.
return obj;
}
public static string SerializeAndSelectTokens<T>(T root, string[] paths, Formatting formatting = Formatting.None, JsonSerializerSettings settings = null)
{
var obj = JObject.FromObject(root, JsonSerializer.CreateDefault(settings));
obj.RemoveAllExcept(paths);
var json = obj.ToString(formatting);
return json;
}
public static TJToken RemoveFromLowestPossibleParent<TJToken>(this TJToken node) where TJToken : JToken
{
if (node == null)
return null;
JToken toRemove;
var property = node.Parent as JProperty;
if (property != null)
{
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
toRemove = property;
property.Value = null;
}
else
{
toRemove = node;
}
if (toRemove.Parent != null)
toRemove.Remove();
return node;
}
public static IEnumerable<JToken> DescendantsAndSelfReversed(this JToken node)
{
if (node == null)
throw new ArgumentNullException();
return RecursiveEnumerableExtensions.Traverse(node, t => ListReversed(t as JContainer));
}
// Iterate backwards through a list without throwing an exception if the list is modified.
static IEnumerable<T> ListReversed<T>(this IList<T> list)
{
if (list == null)
yield break;
for (int i = list.Count - 1; i >= 0; i--)
yield return list[i];
}
}
public static partial class RecursiveEnumerableExtensions
{
// Rewritten from the answer by Eric Lippert https://stackoverflow.com/users/88656/eric-lippert
// to "Efficient graph traversal with LINQ - eliminating recursion" http://stackoverflow.com/questions/10253161/efficient-graph-traversal-with-linq-eliminating-recursion
// to ensure items are returned in the order they are encountered.
public static IEnumerable<T> Traverse<T>(
T root,
Func<T, IEnumerable<T>> children)
{
yield return root;
var stack = new Stack<IEnumerator<T>>();
try
{
stack.Push((children(root) ?? Enumerable.Empty<T>()).GetEnumerator());
while (stack.Count != 0)
{
var enumerator = stack.Peek();
if (!enumerator.MoveNext())
{
stack.Pop();
enumerator.Dispose();
}
else
{
yield return enumerator.Current;
stack.Push((children(enumerator.Current) ?? Enumerable.Empty<T>()).GetEnumerator());
}
}
}
finally
{
foreach (var enumerator in stack)
enumerator.Dispose();
}
}
}
/// <summary>
/// A generic object comparerer that would only use object's reference,
/// ignoring any <see cref="IEquatable{T}"/> or <see cref="object.Equals(object)"/> overrides.
/// </summary>
public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
// Adapted from this answer https://stackoverflow.com/a/1890230
// to https://stackoverflow.com/questions/1890058/iequalitycomparert-that-uses-referenceequals
// By https://stackoverflow.com/users/177275/yurik
private static readonly IEqualityComparer<T> _defaultComparer;
static ObjectReferenceEqualityComparer() { _defaultComparer = new ObjectReferenceEqualityComparer<T>(); }
public static IEqualityComparer<T> Default { get { return _defaultComparer; } }
#region IEqualityComparer<T> Members
public bool Equals(T x, T y)
{
return ReferenceEquals(x, y);
}
public int GetHashCode(T obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
#endregion
}
And then use them like:
public class TestClass
{
public static void Test()
{
var root = new RootObject
{
FirstLevel1 = new FirstLevel
{
SecondLevel1 = new List<SecondLevel> { new SecondLevel { A = "a11", B = "b11", Third1 = new ThirdLevel { Foo = "Foos11", Bar = "Bars11" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList11", Bar = "BarList11" } } } },
SecondLevel2 = new List<SecondLevel> { new SecondLevel { A = "a12", B = "b12", Third1 = new ThirdLevel { Foo = "Foos12", Bar = "Bars12" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList12", Bar = "BarList12" } } } },
},
FirstLevel2 = new FirstLevel
{
SecondLevel1 = new List<SecondLevel> { new SecondLevel { A = "a21", B = "b21", Third1 = new ThirdLevel { Foo = "Foos21", Bar = "Bars21" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList21", Bar = "BarList21" } } } },
SecondLevel2 = new List<SecondLevel> { new SecondLevel { A = "a22", B = "b22", Third1 = new ThirdLevel { Foo = "Foos22", Bar = "Bars22" }, Third2 = new List<ThirdLevel> { new ThirdLevel { Foo = "FooList22", Bar = "BarList22" } } } },
}
};
Assert.IsTrue(JObject.FromObject(root).DescendantsAndSelf().OfType<JValue>().Count() == 24); // No assert
var paths1 = new string[]
{
"$.FirstLevel2.SecondLevel1[*].A",
"$.FirstLevel1.SecondLevel2[*].Third2[*].Bar",
};
Test(root, paths1, 2);
var paths3 = new string[]
{
"$.FirstLevel1.SecondLevel2[*].Third2[*].Bar",
};
Test(root, paths3, 1);
var paths4 = new string[]
{
"$.*.SecondLevel2[*].Third2[*].Bar",
};
Test(root, paths4, 2);
}
static void Test<T>(T root, string [] paths, int expectedCount)
{
var json = JsonExtensions.SerializeAndSelectTokens(root, paths, Formatting.Indented);
Console.WriteLine("Result using paths: {0}", JsonConvert.SerializeObject(paths));
Console.WriteLine(json);
Assert.IsTrue(JObject.Parse(json).DescendantsAndSelf().OfType<JValue>().Count() == expectedCount); // No assert
}
}
public class ThirdLevel
{
public string Foo { get; set; }
public string Bar { get; set; }
}
public class SecondLevel
{
public ThirdLevel Third1 { get; set; }
public List<ThirdLevel> Third2 { get; set; }
public string A { get; set; }
public string B { get; set; }
}
public class FirstLevel
{
public List<SecondLevel> SecondLevel1 { get; set; }
public List<SecondLevel> SecondLevel2 { get; set; }
}
public class RootObject
{
public FirstLevel FirstLevel1 { get; set; }
public FirstLevel FirstLevel2 { get; set; }
}
Note that there is an enhancement request Feature request: ADD JsonProperty.ShouldSerialize(object target, string path) #1857 that would enable this sort of functionality more easily.
Demo fiddles here and here.
The much easier implementation (comparing to the accepted answer) is presented here:
public static class JsonExtensions
{
public static TJToken RemoveAllExcept<TJToken>(this TJToken token, IEnumerable<string> paths) where TJToken : JContainer
{
HashSet<JToken> nodesToRemove = new(ReferenceEqualityComparer.Instance);
HashSet<JToken> nodesToKeep = new(ReferenceEqualityComparer.Instance);
foreach (var whitelistedToken in paths.SelectMany(token.SelectTokens))
TraverseTokenPath(whitelistedToken, nodesToRemove, nodesToKeep);
//In that case neither path from paths has returned any token
if (nodesToKeep.Count == 0)
{
token.RemoveAll();
return token;
}
nodesToRemove.ExceptWith(nodesToKeep);
foreach (var notWhitelistedNode in nodesToRemove)
notWhitelistedNode.Remove();
return token;
}
private static void TraverseTokenPath(JToken value, ISet<JToken> nodesToRemove, ISet<JToken> nodesToKeep)
{
JToken? immediateValue = value;
do
{
nodesToKeep.Add(immediateValue);
if (immediateValue.Parent is JObject or JArray)
{
foreach (var child in immediateValue.Parent.Children())
if (!ReferenceEqualityComparer.Instance.Equals(child, value))
nodesToRemove.Add(child);
}
immediateValue = immediateValue.Parent;
} while (immediateValue != null);
}
}
For most cases this can be achieved by a simple single line extension method
public static string ToJson<T>(this T self, string path) => $#"{{""{path}"":{JObject.FromObject(self)[path]?.ToString(Formatting.None)}}}";
This is only valid for extracting an object nested under the root object but is easily adapted with a separate parameter to specify the output path if needed
Thanks to #dbc answer as a good solution, but like he said, it doesn't affect the performance. Sometimes the data loaded from database has numerous references and only ignoring ReferenceLoopHandling is not enough for serialization; hence the serialized data becomes very large and takes a lot of ram in server, and this is caused by repetition of serializing a single object. In this situation, it's better to make a limited jobject from data straightly, rather than making a jobject and then exclude the unwanted paths from it. This can be done with a little customization of database pure data and a ContractResolver. Let's assume all the database entities inherit from a class or interface like DbModel (this is necessary in this solution). Then by a special ContractResolver, serialization of objects can be limited. A sample is like below:
class TypeName
{
public Type Type { get; set; }
public string Name { get; set; }
}
class MyContractResolver : DefaultContractResolver
{
private List<List<TypeName>> allTypeNames = new List<List<TypeName>>();
public MyContractResolver(Type parentType, string[] includePaths)
{
foreach (var includePath in includePaths)
{
List<TypeName> typeNames = new List<TypeName>() { new TypeName() { Type = parentType } };
var pathChilderen = includePath.Split('.');
for(int i = 0; i < pathChilderen.Length; i++)
{
var propType = typeNames[i].Type.GetProperties().FirstOrDefault(c => c.Name == pathChilderen[i]).PropertyType;
if (propType.GetInterface(nameof(IEnumerable)) != null && propType != typeof(String))
{
propType = propType.GetGenericArguments().Single();
}
typeNames.Add(new TypeName() { Name = pathChilderen[i], Type = propType });
}
allTypeNames.Add(typeNames);
}
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);
// only serializer properties that are in include paths
List<JsonProperty> excludeProperties = new List<JsonProperty>();
foreach (var property in properties)
{
if (typeof(DbModel).IsAssignableFrom(property.PropertyType) || (property.PropertyType.GetInterface(nameof(IEnumerable)) != null && property.PropertyType != typeof(String)))
{
Console.WriteLine(property.PropertyType.ToString());
var exclude = true;
foreach (var typeNames in allTypeNames)
{
var index = typeNames.FindIndex(c => c.Name == property.PropertyName && c.Type == property.PropertyType);
if (index > 0)
{
if (typeNames[index - 1].Type == type)
{
exclude = false;
goto EndSearch;
}
}
}
EndSearch:
if (exclude)
excludeProperties.Add(property);
}
}
properties = properties.Where(c => excludeProperties.All(d => d.PropertyName != c.PropertyName)).ToList();
return properties;
}
}
This class can be used like this:
// return Ok(data);
var jObject = JObject.FromObject(data,
JsonSerializer.CreateDefault(new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
Converters = new List<JsonConverter>()
{
new ValidationProblemDetailsConverter(),
new ProblemDetailsConverter(),
new StringEnumConverter()
},
ContractResolver = new MyContractResolver(typeof(Foo), new[] { "bar", "baz.qux" })
}));
return Ok(jObject);
In this example Foo is the class of main object to return, and bar and baz are properties that are going to be serialized (they are loaded from database too). In addition qux is one of the baz properties that is loaded from database and has to be serialized. In this example all the other properties of each model that are not entities of database (so are not inherited from DbModel) are serialized and all the entities of database that exist in original data but not in the including paths, are ignored to be serialized.
I am trying to make a simple multi-language website. Overriding FindView to get the file based on the language is easy, my problem comes in the routing part.
I would like to do is: (have a website with 2 languages - pt-br and en-us, let abbreviate to onlu pt and en - the default would be PT)
User types www.mysite.com -> would find the languages in the user header request and if the user doesn´t have any of them, he would be redirected to the default PT, so the end result would be www.mysite.com in Portuguese. If he has en, he would be redirected to www.mysite.com/en/ , the result would be the view in English.
If the user types www.mysite.com/pt/ -> I would check that he wants the default and would redirect to www.mysite.com in Portuguese.
I did a custom engine to get the correct view based on the language. But it´s not working 100% because the routing problem.
IN my case I am trying to route with
routes.MapRoute(
"Localization", // Route name
"{lang}/{controller}/{action}", // URL with parameters
new { lang = "pt", controller = "Home", action = "Index" } //defaults
);
But with this, when someone types /pt/ will not redirect to the root of the site.
Another problem with the route is that I don´t want to anyone typing the controller name, I just want the action, my website just have one Home controller with few actions.
Another problem is the that I want a different action name, like, CONTACT in English and CONTATO in Portuguese, they should appear on the address bar like www.mysite.com/Contato or www.mysite.com/en/Contact
Mu Engine I did base on this solution http://www.counity.at/blog/2012/asp-net-mvc3-localization-using-culture-dependent-views/ by Guido Breitenhuber
Here is the code with some tweaks (it´s not working 100% yet)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Globalization;
namespace Ala.MultiLanguage
{
public class LocalizedViewLocation : RazorViewEngine
{
private static readonly string[] _emptyLocations = new string[0];
public string[] LocalizedViewLocationFormats { get; set; }
public string[] LocalizedMasterLocationFormats { get; set; }
protected string[] LocalizedPartialViewLocationFormats { get; set; }
public LocalizedViewLocation()
{
// Define the localized view locations
// 0: Language
// 1: View name
// 2: Controller name
LocalizedViewLocationFormats = new[] {
"~/Views/{0}/{2}/{1}.cshtml",
"~/Views/Shared/{0}/{1}.cshtml"};
MasterLocationFormats = new[] {
"~/Views/{0}/{2}/{1}.cshtml",
"~/Views/Shared/{0}/{1}.cshtml"};
LocalizedPartialViewLocationFormats = new[] {
"~/Views/{0}/{2}/{1}.cshtml",
"~/Views/Shared/{0}/{1}.cshtml"};
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
if (String.IsNullOrEmpty(partialViewName))
throw new ArgumentException("Parameter partialViewName is null or empty.", "partialViewName");
string[] searched;
var controllerName = controllerContext.RouteData.GetRequiredString("controller");
var partialPath = GetPath(controllerContext, LocalizedPartialViewLocationFormats, partialViewName, controllerName, out searched);
if (String.IsNullOrEmpty(partialPath))
{
var baseRes = base.FindPartialView(controllerContext, partialViewName, useCache);
if (baseRes.View != null)
return baseRes;
return new ViewEngineResult(searched.Union(baseRes.SearchedLocations));
}
return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
if (String.IsNullOrEmpty(viewName))
throw new ArgumentException("Parameter viewName is null or empty.", "viewName");
string[] viewLocationsSearched;
string[] masterLocationsSearched;
var controllerName = controllerContext.RouteData.GetRequiredString("controller");
var viewPath = GetPath(controllerContext, LocalizedViewLocationFormats, viewName, controllerName, out viewLocationsSearched);
var masterPath = GetPath(controllerContext, LocalizedMasterLocationFormats, masterName, controllerName, out masterLocationsSearched);
if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
{
var baseRes = base.FindView(controllerContext, viewName, masterName, useCache);
if (baseRes.View != null)
return baseRes;
return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched).Union(baseRes.SearchedLocations));
}
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}
private string GetPath(ControllerContext controllerContext, string[] locations, string name, string controllerName, out string[] searchedLocations)
{
searchedLocations = _emptyLocations;
if (String.IsNullOrEmpty(name))
return String.Empty;
if (IsSpecificPath(name))
return String.Empty;
return GetPathFromGeneralName(controllerContext, locations, name, controllerName, ref searchedLocations);
}
private static bool IsSpecificPath(string name)
{
char c = name[0];
return (c == '~' || c == '/');
}
private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, ref string[] searchedLocations)
{
var result = String.Empty;
searchedLocations = new string[locations.Length];
for (int i = 0; i < locations.Length; i++)
{
var location = locations[i];
var virtualPath = string.Format(CultureInfo.InvariantCulture, location, CultureInfo.CurrentUICulture.TwoLetterISOLanguageName, name, controllerName);
if (FileExists(controllerContext, virtualPath))
{
searchedLocations = _emptyLocations;
result = virtualPath;
break;
}
searchedLocations[i] = virtualPath;
}
return result;
}
}
}
Well, after some timing trying to do in the best way, I ´ve found one solution much easier and fast to implement.
Just mapped the routes that I want one-by-one in the global.asax. This will work and will be fast to implement if you have only few pages and languages.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("Default-BR", "", new { controller = "Home", action = "Index" });
routes.MapRoute("Default-EN", "en", new { controller = "Home", action = "IndexEN" });
routes.MapRoute("HotelBR", "Hotel", new { controller = "Home", action = "Index" });
routes.MapRoute("HotelEN", "en/Hotel", new { controller = "Home", action = "IndexEN" });
routes.MapRoute("Apartamento", "Apartamento", new { controller = "Home", action = "Apartamentos" });
routes.MapRoute("Apartamentos", "Apartamentos", new { controller = "Home", action = "Apartamentos" });
routes.MapRoute("Apartments", "en/Apartments", new { controller = "Home", action = "ApartamentosEN" });
routes.MapRoute("Localizacao", "Localizacao", new { controller = "Home", action = "Localizacao" });
routes.MapRoute("Location", "en/Location", new { controller = "Home", action = "LocalizacaoEN" });
routes.MapRoute("Tarifa", "Tarifa", new { controller = "Home", action = "Tarifas" });
routes.MapRoute("Tarifas", "Tarifas", new { controller = "Home", action = "Tarifas" });
routes.MapRoute("Rates", "en/Rates", new { controller = "Home", action = "TarifasEN" });
routes.MapRoute("Reserva", "Reserva", new { controller = "Home", action = "Reservas" });
routes.MapRoute("Reservas", "Reservas", new { controller = "Home", action = "Reservas" });
routes.MapRoute("Booking", "en/Booking", new { controller = "Home", action = "ReservasEN" });
routes.MapRoute("Contato", "Contato", new { controller = "Home", action = "Contato" });
routes.MapRoute("Contact", "en/Contact", new { controller = "Home", action = "ContatoEN" });
I have an ASP.NET MVC 3 Controller Action that accepts an object as follows:
//// model.ReferenceNumber equals "-1"
public ActionResult EditApplication(EditApplicationViewModel model)
{
if (!ModelState.IsValid)
{
...
}
// Existing Applicant
model.ReferenceNumber = "";
var msg = this.RenderPartialViewToString("_ExistingApplication", model);
}
When the request is received, the "model.ReferenceNumber" equals "-1". Even after setting the property on the model to empty string "", still the results of "RenderPartialViewToString" shows the value of "-1" in the "Value" property of the "input" HTML field.
Any idea why this is happening?
This is the code used to render the partial view:
http://craftycodeblog.com/2010/05/15/asp-net-mvc-render-partial-view-to-string/
public static string RenderPartialViewToString(this Controller controller, string viewName, object model)
{
if (string.IsNullOrEmpty(viewName))
viewName = controller.ControllerContext.RouteData.GetRequiredString("action");
controller.ViewData.Model = model;
using (var sw = new StringWriter())
{
var viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName);
var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw);
viewResult.View.Render(viewContext, sw);
return sw.GetStringBuilder().ToString();
}
}
Thanks
When you modify the value of some model property in your controller qction make sure you remove it from the modelstate as well:
ModelState.Remove("ReferenceNumber");
model.ReferenceNumber = "new value";
var msg = this.RenderPartialViewToString("_ExistingApplication", model);
I'm attempting to perform dynamic sorting of data that I'm putting into grids into our MVC UI. Since MVC is abstracted from everything else via WCF, I've created a couple utility classes and extensions to help with this. The two most important things (slightly simplified) are as follows:
public static IQueryable<TModel> ApplySortOptions<TModel, TProperty>(this IQueryable<TModel> collection, IEnumerable<ISortOption<TModel, TProperty>> sortOptions) where TModel : class
{
var sortedSortOptions = (from o in sortOptions
orderby o.Priority ascending
select o).ToList();
var results = collection;
foreach (var option in sortedSortOptions)
{
var currentOption = option;
var propertyName = currentOption.Property.MemberWithoutInstance();
var isAscending = currentOption.IsAscending;
if (isAscending)
{
results = from r in results
orderby propertyName ascending
select r;
}
else
{
results = from r in results
orderby propertyName descending
select r;
}
}
return results;
}
public interface ISortOption<TModel, TProperty> where TModel : class
{
Expression<Func<TModel, TProperty>> Property { get; set; }
bool IsAscending { get; set; }
int Priority { get; set; }
}
I've not given you the implementation for MemberWithoutInstance() but just trust me in that it returns the name of the property as a string. :-)
Following is an example of how I would consume this (using a non-interesting, basic implementation of ISortOption<TModel, TProperty>):
var query = from b in CurrentContext.Businesses
select b;
var sortOptions = new List<ISortOption<Business, object>>
{
new SortOption<Business, object>
{
Property = (x => x.Name),
IsAscending = true,
Priority = 0
}
};
var results = query.ApplySortOptions(sortOptions);
As I discovered with this question, the problem is specific to my orderby propertyName ascending and orderby propertyName descending lines (everything else works great as far as I can tell). How can I do this in a dynamic/generic way that works properly?
You should really look at using Dynamic LINQ for this. In fact, you may opt to simply list the properties by name instead of using an expression, making it somewhat easier to construct.
public static IQueryable<T> ApplySortOptions<T, TModel, TProperty>(this IQueryable<T> collection, IEnumerable<ISortOption<TModel, TProperty>> sortOptions) where TModel : class
{
var results = collection;
foreach (var option in sortOptions.OrderBy( o => o.Priority ))
{
var currentOption = option;
var propertyName = currentOption.Property.MemberWithoutInstance();
var isAscending = currentOption.IsAscending;
results = results.OrderBy( string.Format( "{0}{1}", propertyName, !isAscending ? " desc" : null ) );
}
return results;
}
While I think #tvanfosson's solution will function perfectly, I'm also looking into this possibility:
/// <summary>
/// This extension method is used to help us apply ISortOptions to an IQueryable.
/// </summary>
/// <param name="collection">This is the IQueryable you wish to apply the ISortOptions to.</param>
/// <param name="sortOptions">These are the ISortOptions you wish to have applied. You must specify at least one ISortOption (otherwise, don't call this method).</param>
/// <returns>This returns an IQueryable object.</returns>
/// <remarks>This extension method should honor deferred execution on the IQueryable that is passed in.</remarks>
public static IOrderedQueryable<TModel> ApplySortOptions<TModel, TProperty>(this IQueryable<TModel> collection, IEnumerable<ISortOption<TModel, TProperty>> sortOptions) where TModel : class
{
Debug.Assert(sortOptions != null, "ApplySortOptions cannot accept a null sortOptions input.");
Debug.Assert(sortOptions.Count() > 0, "At least one sort order must be specified to ApplySortOptions' sortOptions input.");
var firstSortOption = sortOptions.OrderBy(o => o.Priority).First();
var propertyName = firstSortOption.Property.MemberWithoutInstance();
var isAscending = firstSortOption.IsAscending;
// Perform the first sort action
var results = isAscending ? collection.OrderBy(propertyName) : collection.OrderByDescending(propertyName);
// Loop through all of the rest ISortOptions
foreach (var sortOption in sortOptions.OrderBy(o => o.Priority).Skip(1))
{
// Make a copy of this or our deferred execution will bite us later.
var currentOption = sortOption;
propertyName = currentOption.Property.MemberWithoutInstance();
isAscending = currentOption.IsAscending;
// Perform the additional orderings.
results = isAscending ? results.ThenBy(propertyName) : results.ThenByDescending(propertyName);
}
return results;
}
using the code from this question's answer:
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
{
return ApplyOrder(source, property, "OrderBy");
}
public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
{
return ApplyOrder(source, property, "OrderByDescending");
}
public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder(source, property, "ThenBy");
}
public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder(source, property, "ThenByDescending");
}
private static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
{
var props = property.Split('.');
var type = typeof (T);
var arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (var prop in props)
{
// use reflection (not ComponentModel) to mirror LINQ
var pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
var delegateType = typeof (Func<,>).MakeGenericType(typeof (T), type);
var lambda = Expression.Lambda(delegateType, expr, arg);
var result = typeof (Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof (T), type)
.Invoke(null, new object[] {source, lambda});
return (IOrderedQueryable<T>) result;
}