Q. Why JPA Projection can't convert Mysql bit(1) to Java Boolean?
Spring Jpa Projection occur error Projection type must be an interface! when the Mysql bit(1) type maps to the Java Boolean type.
Jpa converts a Boolean column in Entity class to bit(1) column in Mysql Table.
If I change getIsBasic's type in PlanInfoProjection interface Integer to Boolean, It doesn't work. Why does it occur error?
JPA Repository
#Query(nativeQuery=true, value="select true as isBasic from dual")
ProductOrderDto.PlanInfoProjection findPlanInfoById(Long id);
Projection interface
public class ProductOrderDto {
#Getter
public static class PlanInfo {
private Boolean isBasic;
public PlanInfo(PlanInfoProjection projection) {
// this.isBasic = projection.getIsBasic(); //<-- I want to use like this.
if (projection.getIsBasic() == null) {
this.isBasic = null;
} else {
this.isBasic = projection.getIsBasic() == 0 ? false : true; // <-- I have to convert
}
}
}
public interface PlanInfoProjection {
Integer getIsBasic(); // It works, but I have to convert Integer to Boolean to use.
//Boolean getIsBasic(); // doesn't work, but why???
//Boolean isBasic(); // also doesn't work
//boolean isBasic(); // also doesn't work
}
}
It seems like this doesn't work out of the box. What works for me (although I'm using DB2 so my datatype is different but this shouldn't be a problem) is to annotate it and use SpEL like this:
#Value("#{target.isBasic == 1}")
boolean getIsBasic();
This just takes your int value (0 for false, 1 for true) and creturns a boolean value. Should also work with Boolean but I didn't test it.
Another option is to use #Value("#{T(Boolean).valueOf(target.isBasic)}") but this only works for String values, so you would have to store 'true' or 'false' in your database. With T() you can import Static classes into Spring Expression Language, and then just call the valueOf method which returns a boolean (either Boolean or boolean)
If I have a view model like this:
public class MyModel{
public DateTime? StartDate {get;set;}
}
And on a view an input tag is used with an asp-for tag helper like so:
<input asp-for="StartDate" />
The default html that is generated by this is
<input type="datetime" id="StartDate" name="StartDate" value="" />
But what I want it to generate is html that looks like this:
<input type="datetime" id="startDate" name="startDate" value="" />
How can I make the asp-for input tag helper generate camel case names like above without having to make my model properties camelCase?
After studying the code that #Bebben posted and the link provided with it, I continued to dig more into the Asp.Net Core source code. And I found that the designers of the Asp.Net Core provided some extensibility points that could be leveraged to achieve lower camelCase id and name values.
To do it, we need to implement our own IHtmlGenerator which we can do by creating a custom class that inherits from DefaultHtmlGenerator. Then on that class we need to override the GenerateTextBox method to fix the casing. Or alternatively we can override the GenerateInput method to fix the casing of name and id attribute values for all input fields (not just input text fields) which is what I chose to do. As a bonus I also override the GenerateLabel method so the label's for attribute also specifies a value using the custom casing.
Here's the class:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Text.Encodings.Web;
namespace App.Web {
public class CustomHtmlGenerator : DefaultHtmlGenerator {
public CustomHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache) : base
(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory,
htmlEncoder, clientValidatorCache) {
//Nothing to do
}
public CustomHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache,
ValidationHtmlAttributeProvider validationAttributeProvider) : base
(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder,
clientValidatorCache, validationAttributeProvider) {
//Nothing to do
}
protected override TagBuilder GenerateInput(
ViewContext viewContext,
InputType inputType,
ModelExplorer modelExplorer,
string expression,
object value,
bool useViewData,
bool isChecked,
bool setId,
bool isExplicitValue,
string format,
IDictionary<string, object> htmlAttributes) {
expression = GetLowerCamelCase(expression);
return base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData,
isChecked, setId, isExplicitValue, format, htmlAttributes);
}
public override TagBuilder GenerateLabel(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
string labelText,
object htmlAttributes) {
expression = GetLowerCamelCase(expression);
return base.GenerateLabel(viewContext, modelExplorer, expression, labelText, htmlAttributes);
}
private string GetLowerCamelCase(string text) {
if (!string.IsNullOrEmpty(text)) {
if (char.IsUpper(text[0])) {
return char.ToLower(text[0]) + text.Substring(1);
}
}
return text;
}
}
}
Now that we have our CustomHtmlGenerator class we need to register it in the IoC container in place of the DefaultHtmlGenerator. We can do that in the ConfigureServices method of the Startup.cs via the following two lines:
//Replace DefaultHtmlGenerator with CustomHtmlGenerator
services.Remove<IHtmlGenerator, DefaultHtmlGenerator>();
services.AddTransient<IHtmlGenerator, CustomHtmlGenerator>();
Pretty cool. And not only have we solved the id and name casing issue on the input fields but by implementing our own custom IHtmlGenerator, and getting it registered, we have opened the door on all kinds of html customization that can be done.
I'm starting to really appreciate the power of a system built around an IoC, and default classes with virtual methods. The level of customization available with little effort under such an approach is really pretty amazing.
Update
#Gup3rSuR4c pointed out that my services.Remove call must be an extension method that's not included in the framework. I checked, and yep that true. So, here is the code for that extension method:
public static class IServiceCollectionExtensions {
public static void Remove<TServiceType, TImplementationType>(this IServiceCollection services) {
var serviceDescriptor = services.First(s => s.ServiceType == typeof(TServiceType) &&
s.ImplementationType == typeof(TImplementationType));
services.Remove(serviceDescriptor);
}
}
The simplest way to do this is to just write
<input asp-for="StartDate" name="startDate" />
Or do you want to have it generated completely automatically in camel case, for the whole application?
To do that, it seems like you have to implement your own InputTagHelpers in Microsoft.AspNetCore.Mvc.TagHelpers.
Here is the method where the name is generated:
private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputTypeHint, string inputType)
{
var format = Format;
if (string.IsNullOrEmpty(format))
{
format = GetFormat(modelExplorer, inputTypeHint, inputType);
}
var htmlAttributes = new Dictionary<string, object>
{
{ "type", inputType }
};
if (string.Equals(inputType, "file") && string.Equals(inputTypeHint, TemplateRenderer.IEnumerableOfIFormFileName))
{
htmlAttributes["multiple"] = "multiple";
}
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
value: modelExplorer.Model,
format: format,
htmlAttributes: htmlAttributes);
}
(The above code is from https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs, Apache License, Version 2.0, Copyright .NET Foundation)
The line is "For.Name". The name is sent into some other methods, and the one that in the end gives the final name is in a static class (Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.NameAndIdProvider), so nothing we can really plug into easily.
As far as I can tell, there is no literal suffix for defining a compile-time constant of type System::Decimal (compare to the M suffix in C# -- i.e. Decimal d = 100.5M). Furthermore, the following code is rejected by the compiler:
literal System::Decimal myDecimal = 100.5;
If I can't define a decimal literal, how then can I initialize an attribute that requires a decimal as one of its fields? To illustrate, consider the following code:
using namespace System;
public ref class MyConfigElement : ConfigurationElement
{
public:
[ConfigurationProperty("Money", DefaultValue = 500.0, IsRequired = false)]
property Decimal Money { Decimal get(); void set(Decimal value); }
};
The current value of 500.0 is invalid as it compiles to type double, which is then rejected by the runtime as it doesn't match the type of the property (Decimal).
Is it possible to correctly initialize this attribute's default value?
Try DefaultValue = static_cast<Decimal>(500.0).
Use of static_cast is allowed in constant expressions, as long as it does not require a call to a user-defined conversion function.
Failing that, maybe use a global variable which is a constant expression?
const Decimal MyConfigElementMoneyDefault = 500.0;
public ref class MyConfigElement : ConfigurationElement
{
public:
[ConfigurationProperty("Money", DefaultValue = MyConfigElementMoneyDefault, IsRequired = false)]
property Decimal Money { Decimal get(); void set(Decimal value); }
};
I have classes generated via xsd.exe (xsd.exe someschema.xsd /classes). One of the nodes is declared as an element:
<xs:element name="containsxmlelementsbeneath"/>
As the (ficticious) name implies, it looks like this:
<containsxmlelementsbeneath>
<somemore>
...
</somemore>
</containsxmlelementsbeneath>
When it is deserialized, I see in the debugger that it is of type
System.Xml.XmlNode[]
I can force it in the Immediate Window
?((System.Xml.XmlNode[])elem.containsxmlelementsbeneath)[0].InnerXml
I got no IntelliSense, which made sense when I tried the snippet in my code - the class seems to have been removed from the WinRT profile - which is nice if all you need is Windows.Data.Xml.Dom.IXmlNode - but not in this case.
How can I go and get the string representation of that element? Is there a way to "fix" the xsd.exe-generated output so it uses Windows.Data.Xml.Dom for Serialization? (doesn't look like it to me)
Did I hit a fringe case they didn't think about?
Update - tried the following (I know, an abuse of dynamic):
dynamic x = elem.containsxmlelementsbeneath;
string s = x[0].InnerXml;
This yields "The API 'System.Xml.XmlNode[]' cannot be used on the current platform."
I had a (longer) chat with another dev - after some trial and error we came up with a solution:
<xs:element ref="containsxmlelementsbeneath"/>
<xs:element name="containsxmlelementsbeneath">
</xs:element>
This creates an empty class for us (via xsd.exe)
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.17929")]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class containsxmlelementsbeneath
{
}
This has to be modified like this:
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class containsxmlelementsbeneath : IXmlSerializable
{
[XmlIgnore]
public string Text { get; set; }
public System.Xml.Schema.XmlSchema GetSchema()
{
throw new System.NotImplementedException();
}
public void ReadXml(System.Xml.XmlReader reader)
{
Text = reader.ReadInnerXml();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
throw new System.NotImplementedException();
}
}
Note that all attributes except XmlRoot have to be removed, otherwise you get Reflection Exceptions (Only XmlRoot attribute may be specified for the type containsxmlelementsbeneath. Please use XmlSchemaProviderAttribute to specify schema type.)
End Result: this node with all its subnodes as a plain old string. No non-accesible XmlNode or XmlElement any more...
i have this problem: i use the json to send data to server.
All works fine but the problem is a situation like:
public enum SexType
{
Male : 0,
Female : 1
}
class People{
public SexType Sex {get;set;}
}
That create me the json:
{"Sex" : 0}
When i send back to server, this fill the ModelStateError with this issue:
The parameter conversion from type 'System.Int32' to type 'SexType' failed because no type converter can convert between these types.
But if i wrap the value with ' all work well:
{"Sex" : '0'}
Anyone have the same problem?
Tnx for all!
Yes, I got the same problem. The weird problem is that if you sent back:
{"Sex" : 'Male'}
it would deserialize no problem.
To solve the problem, I implemented a custom model binder for enums, leveraging the example found here (slightly modified as there were some errors):
http://eliasbland.wordpress.com/2009/08/08/enumeration-model-binder-for-asp-net-mvc/
namespace yournamespace
{
/// <summary>
/// Generic Custom Model Binder used to properly interpret int representation of enum types from JSON deserialization, including default values
/// </summary>
/// <typeparam name="T">The enum type to apply this Custom Model Binder to</typeparam>
public class EnumBinder<T> : IModelBinder
{
private T DefaultValue { get; set; }
public EnumBinder(T defaultValue)
{
DefaultValue = defaultValue;
}
#region IModelBinder Members
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
return bindingContext.ValueProvider.GetValue(bindingContext.ModelName) == null ? DefaultValue : GetEnumValue(DefaultValue, bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue);
}
#endregion
public static T GetEnumValue<T>(T defaultValue, string value)
{
T enumType = defaultValue;
if ((!String.IsNullOrEmpty(value)) && (Contains(typeof(T), value)))
enumType = (T)Enum.Parse(typeof(T), value, true);
return enumType;
}
public static bool Contains(Type enumType, string value)
{
return Enum.GetNames(enumType).Contains(value, StringComparer.OrdinalIgnoreCase);
}
}
}
and then registering the model binder in global.asax.cs.
In your case it would be something like:
ModelBinders.Binders.Add(typeof(SexType), new EnumBinder<SexType>(SexType.Male));
I am not sure if there is a quicker way, but this works great.
The Model binding uses the Enum.Parse() method, which is fairly smart about interpreting strings but does NOT explicitly cast or convert other types into strings, even if system-level facilities exist to do so and even if they're the internal storage type used within the Enum.
Is this the right behavior? Arguably so, since if you don't know enough to convert your Enum values to strings you might not be aware that the right-hand side of the Enum values are not necessarily unique within the Enum, either.
As a matter of personal taste (and this is probably also because I do way too much statistical analysis programming) for sex I generally prefer to define it as a clear boolean value, i.e. instead of differentiating between arbitrary values for 'Male' and 'Female' I use a variable called e.g. IsFemale and set it to true or false. This plays more nicely with json, since it relies on primitive types common to both languages, and requires less typing when you want to use it.