I'm faced with a confusing problem where in my Edit or Create action result methods, EF4 will throw a DbEntityValidationException with the inner message stating:
The field Body must be a string or
array type with a maximum length of
'128'.
The model in question looks like this:
[Table("tblArticles")]
public class Article
{
[Key]
public int ID { get; set; }
[Required(ErrorMessage="Title must be included")]
public string Title { get; set; }
[AllowHtml]
public string Body { get; set; }
[Required(ErrorMessage="Start Date must be specified")]
[Display(Name="Start Date")]
[DisplayFormat(DataFormatString="dd-mm-yyyy")]
public DateTime? StartDate { get; set; }
[Required(ErrorMessage = "End Date must be specified")]
[Display(Name = "End Date")]
public DateTime? EndDate { get; set; }
public int Priority { get; set; }
public bool Archived { get; set; }
public virtual ICollection<ArticleImage> Images { get; set; }
}
The "Body" field in the actual database is of type Text, so there's no obvious limit there. The data that I'm trying to post is this:
<p>
This is an example to confirm that new articles are looking right.</p>
<p>
<img alt="" src="http://www.google.co.nz/logos/2011/houdini11-sr.jpg"
style="width: 160px; height: 56px; float: left;" /></p>
An example of the Edit method looks like this:
[HttpPost]
public ActionResult Edit(Article article)
{
if (ModelState.IsValid)
{
try
{
articleRepository.Update(article);
}
catch (DbEntityValidationException dbevEx)
{
ErrorSignal.FromCurrentContext().Raise(dbevEx);
ModelState.AddModelError("FORM", dbevEx);
return View("Edit", article);
}
// Other exception handling happens...
}
return RedirectToAction("Index");
}
And finally, the method that actually does the grunt work is:
public void Update(T Entity)
{
dbset.Attach(Entity);
db.Entry(Entity).State = System.Data.EntityState.Modified;
db.Commit();
}
I can't see anything in code or in the database that might be causing the problem, so where else should I look?
Default length of string field in code first is 128. If you are using EF validation it will throw exception. You can extend the size by using:
[StringLength(Int32.MaxValue)]
public string Body { get; set; }
This post became somehow popular so I'm adding second approach which also works:
[MaxLength]
public string Body { get; set; }
StringLengthAttribute is from System.ComponentModel.DataAnnotations assembly and MaxLengthAttribute is from EntityFramework assembly (EF 4.1).
If you get this error using a Model First approach, check the EF Model: it may be simply that a property on the Entity you're updating has the Max Length attribute set.
For Entity Framework 4.3.1, 5.0.0 and 6.2.0, you can use IsMaxLength().
Property(m => m.Body ).IsRequired().IsMaxLength();
Configures the column to allow the maximum length supported by the database provider.
https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.modelconfiguration.configuration.stringcolumnconfiguration.ismaxlength?view=entity-framework-6.2.0
May be you have used
Property(m => m.Body ).IsRequired().HasMaxLength(128);
On your dbcontext class in OnModelCreating.
So change as per your length
For me, [MaxLength] didn't work. I have added below statement to the context.
modelBuilder.Entity<YourModel>().Property(e => e.YourColumn).HasMaxLength(4000);
I have not tried exceeding "4000" limit but I think it can be extended further. You don't have to keep it 128 as the error showed by compiler.
Related
I had a class called Document, which I split into two entities, in order to separate an expensive binary field:
[Table("Document")]
public class Document
{
[Key]
public int Id { get; set; }
... other fields ...
[Required]
public virtual DocumentBinary DocumentBinary { get; set; }
}
[Table("Document")]
public class DocumentBinary
{
[Key, ForeignKey("Document")]
public int DocumentId { get; set; }
public Document Document { get; set; }
public byte[] DocumentData { get; set; }
}
So, everything works fine, both entities share the same database table and DocumentData is only loaded when it's needed.
However, when it comes to updating the Document entity, I get an error stating that 'DocumentBinary is required'.
When I remove the [Required] attribute from DocumentBinary virtual property, I get the following error:
The entity types 'Document' and 'DocumentBinary' cannot share table 'Documents' because they are not in the same type hierarchy or do not have a valid one to one foreign key relationship with matching primary keys between them.
I can obviously do something like:
var test = document.DocumentBinary;
before updating the document object:
documentRepository.Update(document);
This will then load the binary data on my request and save the changes without any issues, but the whole point is that I shouldn't need to do that.
This can be achieved using the fluent API. If you remove the data annotations and in your OnModelCreating add this, it should work.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Document>().HasRequired(d => d.DocumentBinary).
WithRequiredDependent(db => db.Document);
}
I managed to resolve it by overriding my Update method in DocumentRepository:
public override void Update(Document document)
{
try
{
DataContext.Entry(document.DocumentBinary).State = EntityState.Modified; // added this line
DataContext.Entry(document).State = EntityState.Modified;
}
catch (System.Exception exception)
{
throw new EntityException("Failed to update document");
}
}
I know it probably does the same thing as me evaluating DocumentBinary by assigning it to 'test' variable, but it looks like a much cleaner solution.
I'm in the midst of upgrading from v1-3 to v4, but I've run into a few problems.
My understanding is that DateTime is unsupported, and I have to always use DateTimeOffset. Fine.
But before I was storing Sql date data type in the DateTime, now it seems I get this error:
Member Mapping specified is not valid. The type 'Edm.DateTimeOffset[Nullable=False,DefaultValue=,Precision=]' of member 'CreatedDate' in type 'MyEntity' is not compatible with 'SqlServer.date[Nullable=False,DefaultValue=,Precision=0]'
What is the work around for this? I need to be able to store specifically just dates in the database (time and locality is not important). Would be great if I could get the Edm.Date aswell as a returned data type, but I didn't have that before.
Thanks.
Edit: Example classes
Before:
public class Ticket
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required, MaxLength(50)]
public string Reference { get; set; }
[Column(TypeName = "date")]
public DateTime LoggedDate { get; set; }
}
After:
public class Ticket
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required, MaxLength(50)]
public string Reference { get; set; }
[Column(TypeName = "date")]
public DateTimeOffset LoggedDate { get; set; }
}
This isn't valid in EF.
One option is to define a new property in the entity. Say Title is mapped to EF:
public partial class Title
{
public int Id { get; set; }
public string Name { get; set; }
public Nullable<System.DateTime> CreatedOn { get; set; }
}
then add a new property of DateTimeOffset:
public partial class Title
{
[NotMapped]
public DateTimeOffset? EdmCreatedOn
{
// Assume the CreateOn property stores UTC time.
get
{
return CreatedOn.HasValue ? new DateTimeOffset(CreatedOn.Value, TimeSpan.FromHours(0)) : (DateTimeOffset?)null;
}
set
{
CreatedOn = value.HasValue ? value.Value.UtcDateTime : (DateTime?)null;
}
}
}
and the code for generate OData Model looks like:
public static IEdmModel GetModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
EntityTypeConfiguration<Title> titleType= builder.EntityType<Title>();
titleType.Ignore(t => t.CreatedOn);
titleType.Property(t => t.EdmCreatedOn).Name = "CreatedOn";
builder.EntitySet<Title>("Titles");
builder.Namespace = typeof(Title).Namespace;
return builder.GetEdmModel();
}
}
The controller looks like:
public class TitlesController : ODataController
{
CustomerManagementSystemEntities entities = new CustomerManagementSystemEntities();
[EnableQuery(PageSize = 10, MaxExpansionDepth = 5)]
public IHttpActionResult Get()
{
IQueryable<Title> titles = entities.Titles;
return Ok(titles);
}
public IHttpActionResult Post(Title title)
{
entities.Titles.Add(title);
return Created(title);
}
}
For anyone coming to this in the future, the OData v4 team have fixed this issue.
[Column(TypeName = "date")]
public DateTime Birthday { get; set; }
This will now auto-resolve to Edm.Date.
If you are like me and are doing date type by convention, you have to manually declare the properties as dates lest they be auto-resolved as DateTimeOffset. OData currently does not allow you to add your own conventions.
customer.Property(c => c.Birthday).AsDate();
http://odata.github.io/WebApi/#12-01-DateAndTimeOfDayWithEF
You can refer to the link below to define your DateTimeAndDateTimeOffsetWrapper to do the translation between two types.
http://www.odata.org/blog/how-to-use-sql-spatial-data-with-wcf-odata-spatial/
Define two properties on your model, one is DateTime which only exists in the Edm model, the other is DateTimeOffset which only exists in the DB.
If the solution above doesn't meet your request, you have to change the data to DateTime before saving it to database and change it back to DateTimeOffset after retrieving it from database in the controller actions.
You can define two almost-same classes to achieve this. The only difference is that one has DateTime property and the other has DateTimeOffset property.
The former one is used for EF and mapping into DB.
The latter one is used for defining OData Edm model and presenting to the users.
As I said above, you have to do the translation between these two classes before saving the data and after retrieving the data.
You can add the AppendDatetimeOffset method to add automatically the methods
using the microsoft T4 engine (i.e. updating the template file *.tt). So that when regenerating the code, you don't have to append classes again. Hope this Helps :)
public string Property(EdmProperty edmProperty)
{
return string.Format(
CultureInfo.InvariantCulture,
(_ef.IsKey(edmProperty) ? "[Key]" : "") +
"{0} {1} {2} {{ {3}get; {4}set; }} {5}",
Accessibility.ForProperty(edmProperty),
_typeMapper.GetTypeName(edmProperty.TypeUsage),
_code.Escape(edmProperty),
_code.SpaceAfter(Accessibility.ForGetter(edmProperty)),
_code.SpaceAfter(Accessibility.ForSetter(edmProperty)),
AppendDateTimeOffset(edmProperty));
}
public string AppendDateTimeOffset(EdmProperty edmProperty){
if(!_typeMapper.GetTypeName(edmProperty.TypeUsage).Contains("DateTime")) return " ";
//proceed only if date time
String paramNull = #"public Nullable<System.DateTimeOffset> edm{0}
{{
get
{{
return {0}.HasValue ? new DateTimeOffset({0}.Value, TimeSpan.FromHours(0)) : (DateTimeOffset?)null;
}}
}}";
String paramNotNull = #"public System.DateTimeOffset edm{0}
{{
get
{{
return new DateTimeOffset({0}, TimeSpan.FromHours(0));
}}
}}";
String s= String.Empty;
if(edmProperty.Nullable){
s = string.Format(paramNull, edmProperty.Name);
}else
{
s = string.Format(paramNotNull, edmProperty.Name);
}
return s;
}
I am using Entity Framwwork and Code First and getting really confused. I have this class:
public class Blocks
{
[Display(Name = "ID"),Required(ErrorMessage = "ID is required")]
[Key,HiddenInput(DisplayValue=false)]
public int BlockId { get;set; }
[Display(Name = "Blocked By"),Required(ErrorMessage = "Blocked By is required")]
public int ProfileId { get;set; }
[Display(Name = "Blocked"),Required(ErrorMessage = "Blocked is required")]
public int ProfileBlockedId { get;set; }
[Display(Name = "Date Blocked"),Required(ErrorMessage = "Date Blocked is required")]
public DateTime BlockDateTime { get;set; }
[Display(Name = "Block Reason")] public string BlockReason { get;set; }
public virtual Profiles Profile { get; set; }
public virtual Profiles ProfileBlocked { get; set; }
}
The profile class is more or less the same and that adds fine and has the correct SQL, but when I run /Blocks I get this error:
MySql.Data.MySqlClient.MySqlException (0x80004005): Unknown column 'Extent1.Profile_ProfileId' in 'field list'
at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int32& insertedId)
at MySql.Data.MySqlClient.Driver.GetResult(Int32 statementId, Int32& affectedRows, Int32& insertedId)
at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId)
at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
at MySql.Data.Entity.EFMySqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
at System.Data.Common.DbCommand.ExecuteReader(CommandBehavior behavior)
at System.Data.EntityClient.EntityCommandDefinition.ExecuteStoreCommands(EntityCommand entityCommand, CommandBehavior behavior)
this is because the sql produced is:
SELECT
`Extent1`.`BlockId`,
`Extent1`.`ProfileId`,
`Extent1`.`ProfileBlockedId`,
`Extent1`.`BlockDateTime`,
`Extent1`.`BlockReason`,
`Extent1`.`Profile_ProfileId`,
`Extent1`.`ProfileBlocked_ProfileId`
FROM `Blocks` AS `Extent1`
Notice the Profile_ and ProfileBlocked_. I have them virtual so I have a dropdown of profiles when adding or editing or have the profile name when shown on a list. The strange thing is the other tables. Everything has worked fine except for this one.
Here is the code that creates the wrong SQL and breaks:
//
// GET: /Blocks/
public ViewResult Index()
{
try {
return View(context.Blocks.Include(blocks => blocks.Profile).Include(blocks => blocks.ProfileBlocked).ToList());
}
catch (Exception ex)
{
ModelState.AddModelError("",ex.Message);
CompileAndSendError(ex);
return View(context.Blocks.ToList());
}
}
I am using:
ASP.net MVC 3
Razor Templates
Entity Framework
MVC Scaffolding [custom T4 ]
Give EF a little hint what are your Foreign Key properties by putting annotations on the properties:
...
[ForeignKey("Profile")]
public int ProfileId { get;set; }
...
[ForeignKey("ProfileBlocked")]
public int ProfileBlockedId { get;set; }
...
I believe that this is always necessary when you have more than one navigation property referencing to the same target class. The conventions don't detect in this case which properties could be the foreign keys - and EF creates their own FK column names (the Profile_ and ProfileBlocked_ things). And because the column names in the DB are different you get the exception.
(I think, the problem has nothing to do with properties being virtual or not.)
Edit
You can also put the ForeignKey attribute on the navigation properties and specify what's the name of the FK properties:
...
[ForeignKey("ProfileId")]
public virtual Profiles Profile { get; set; }
...
[ForeignKey("ProfileBlockedId")]
public virtual Profiles ProfileBlocked { get; set; }
...
This leads to the same mapping and it's only a matter of taste what you prefer, as far as I can tell.
I'm facing the same problem, but I can;t sole it using the suggested ForeignKey attribute.
I have installed the MySQL Connector/NET 6.4.3.0. When I run my project I get almost the same error, but referenced to System.Data.Entity. Shouldn;t that be MySql.Data.Entity?
Can you show me how to possibly modify my Web.config or references to work with MySQL.
EDIT
With some help (other post) and trail and error, I got it working too.
I have found myself with at little problem, and I think a custom model binder is the way to go.
My Domain model looks like this,readly standard
I got a Page and a Template. The Page has the Template as a ref.
So the Default asp.net mvc Binder, does not know how to bind it, therefore I need to make some rules for it. (Custom Model Binder)
public class PageTemplate
{
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual DateTime? Created { get; set; }
public virtual DateTime? Modified { get; set; }
}
public class Page
{
public virtual string Title { get; set; }
public virtual PageTemplate Template { get; set; }
public virtual string Content { get; set; }
public virtual DateTime? Created { get; set; }
public virtual DateTime? Modified { get; set; }
}
So I have Registreted the ModelBinder in globals.asax
ModelBinders.Binders.Add(typeof(Cms.Domain.Entities.Page),
new Cms.UI.Web.App.ModelBinders.PageModelBinder(
new Services.GenericApplicationService<Cms.Domain.Entities.Page>().GetEntityStore()
)
);
My ModelBinder tage a paremeter, witch is my Repository, where I get all my Entities ( Page, Template )
My Controller for a Page looks like this.
I have posted into a Create Controler, it does not matter for now, if it was a Update method.
Since I in this case have a dropdown, that represents the Template, I will get an ID in my form collection.
I then call: TryUpdateModel and I got a hit in my PageModelBinder.
[AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken]
[ValidateInput(false)]
public ActionResult Create(FormCollection form)
{
Page o = new Page();
string[] exclude = new { "Id" }
if (base.TryUpdateModel<Page>(o, string.Empty, null, exclude, form.ToValueProvider()))
{
if (ModelState.IsValid)
{
this.PageService.Add(o);
this.CmsViewData.PageList = this.PageService.List();
this.CmsViewData.Messages.AddMessage("Page is updated.", MessageTypes.Succes);
return View("List", this.CmsViewData);
}
}
return View("New", this.CmsViewData);
}
So I end op with the Model Binder.
I have search the internet dry for information, but im stock.
I need to get the ID from the FormCollection, and parse it to at Model from my IEntityStore.
But how ?
public class PageModelBinder : IModelBinder
{
public readonly IEntityStore RepositoryResolver;
public PageModelBinder(IEntityStore repositoryResolver)
{
this.RepositoryResolver = repositoryResolver;
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
if (modelType == typeof(Cms.Domain.Entities.Page))
{
// Do some magic
// Get the Id from Property and bind it to model, how ??
}
}
}
// Dennis
I hope, my problom is clear.
Did find a work around.
I download the sourcecode for asp.net r2 rtm 2
And did copy all code for the default ModelBinder, and code it need. Did some minor change, small hacks.
the work around is doing a little hack in this method:
[SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)",
Justification = "The target object should make the correct culture determination, not this method.")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
Justification = "We're recording this exception so that we can act on it later.")]
private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType)
{
try
{
object convertedValue = valueProviderResult.ConvertTo(destinationType);
return convertedValue;
}
catch (Exception ex)
{
try
{
// HACK if the binder still fails, try get the entity in db.
Services.GenericApplicationService<Cms.Domain.Entities.PageTemplate> repo;
repo = new Services.GenericApplicationService<Cms.Domain.Entities.PageTemplate>();
int id = Convert.ToInt32(valueProviderResult.AttemptedValue);
object convertedValue = repo.Retrieve(id);
return convertedValue;
}
catch (Exception ex1)
{
modelState.AddModelError(modelStateKey, ex1);
return null;
}
}
}
This question is closed.
I have searched like a fool but does not get much smarter for it..
In my project I use Entity Framework 4 and own PoCo classes and I want to use DataAnnotations for validation. No problem there, is how much any time on the Internet about how I do it. However, I feel that it´s best to have my validation in ViewModels instead and not let my views use my POCO classes to display data.
How should I do this smoothly? Since my repositories returns obejekt from my POCO classes I tried to use AutoMapper to get everything to work but when I try to update or change anything in the ModelState.IsValid is false all the time..
My English is really bad, try to show how I am doing today instead:
My POCO
public partial User {
public int Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
And my ViewModel
public class UserViewModel {
public int Id { get; set; }
[Required(ErrorMessage = "Required")]
public string UserName { get; set; }
[Required(ErrorMessage = "Required")]
public string Password { get; set; }
}
Controller:
public ActionResult Edit(int id) {
User user = _userRepository.GetUser(id);
UserViewModel mappedUser = Mapper.Map<User, UserViewModel>(user);
AstronomiGuidenModelItem<UserViewModel> result = new AstronomiGuidenModelItem<UserViewModel> {
Item = mappedUser
};
return View(result);
}
[HttpPost]
public ActionResult Edit(UserViewModel viewModel) {
User user = _userRepository.GetUser(viewModel.Id);
Mapper.Map<UserViewModel, User>(viewModel, user);
if (ModelState.IsValid) {
_userRepository.EditUser(user);
return Redirect("/");
}
AstronomiGuidenModelItem<UserViewModel> result = new AstronomiGuidenModelItem<UserViewModel> {
Item = viewModel
};
return View(result);
}
I've noticed now that my validation is working fine but my values are null when I try send and update the database. I have one main ViewModel that looks like this:
public class AstronomiGuidenModelItem<T> : AstronomiGuidenModel {
public T Item { get; set; }
}
Why r my "UserViewModel viewModel" null then i try to edit?
If the validation is working, then UserViewModel viewModel shouldn't be null... or is it that the client side validation is working but server side isn't?
If that's the case it could be because of the HTML generated.
For instance, if in your view you have:
<%: Html.TextBoxFor(x => x.Item.UserName) %>
The html that gets rendered could possibly be:
<input name="Item.UserName" id="Item_UserName" />
When this gets to binding on the server, it'll need your action parameter to be named the same as the input's prefix (Item). E.g.
public ActionResult Edit(UserViewModel item) {
To get around this, do as above and change your action parameter to item OR you could encapsulate the form into a separate PartialView which takes the UserViewModel as it's model - that way the Html.TextBoxFor won't be rendered with a prefix.
HTHs,
Charles
Ps. If I'm totally off track, could you please post some code for the view.