I am new to .Net Core (and Blazor) and I just configured a project that uses the latest version of .net core (3.1.xxx) following a tutorial and checking Microsoft Docs.
I have a couple of entities in my project and I'm able to save the entities composed with simple attributes (primitive types) using the API. But I am facing a problem when I try the same with an Entity that has other entities as attributes (so nested objects).
And the error I get is not helping at all, it's complaining about a conversion from Char to String as you can see on the stack trace below:
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] An unhandled exception has occurred while executing the request. System.InvalidCastException: Unable to cast object of type 'System.Char' to type 'System.String'. at System.ComponentModel.DataAnnotations.StringLengthAttribute.IsValid(Object value) at System.ComponentModel.DataAnnotations.ValidationAttribute.IsValid(Object value, ValidationContext validationContext) at System.ComponentModel.DataAnnotations.ValidationAttribute.GetValidationResult(Object value, ValidationContext validationContext) at Microsoft.AspNetCore.Mvc.DataAnnotations.DataAnnotationsModelValidator.Validate(ModelValidationContext validationContext) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.ValidateNode() at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitSimpleType() at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitChildren(IValidationStrategy strategy) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitComplexType(IValidationStrategy defaultStrategy) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitChildren(IValidationStrategy strategy) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitComplexType(IValidationStrategy defaultStrategy) at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model)
This only happens with nested entities. Is this the case where I should write a custom model binding class?
My Code is this:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace src.Data.Entities
{
[Table("Screening")]
public class Screening
{
public Screening()
{
this.Beneficiary = new Beneficiary();
}
public int ScreeningId { get; set; }
public DateTime ScreeningDate { get; set; } = DateTime.Now;
public virtual Beneficiary Beneficiary { get; set; }
public int BeneficiaryId { get; set; }
public virtual SimpleEntity ActionTaken { get; set; }
public int ActionTakenId { get; set; }
public virtual SimpleEntity ReasonForVisit { get; set; }
public int ReasonForVisitId { get; set; }
}
}
Controller class
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using src.Data;
using src.Data.Entities;
using src.Data.DTO;
using src.Helpers;
using System.Linq;
namespace src.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ScreeningController : ControllerBase
{
private readonly ApplicationDbContext context;
public ScreeningController(ApplicationDbContext context)
{
this.context = context;
}
[HttpGet]
public async Task<ActionResult<List<Screening>>> Get([FromQuery] PaginationDTO pagination)
{
var queryable = context.Screenings.AsQueryable().OrderBy(x => x.ScreeningDate);
await HttpContext.InsertPaginationParameterInResponse(queryable, pagination.RecordsPerPage);
return await queryable.Paginate(pagination).ToListAsync();
}
[HttpGet("{id}", Name = "GetScreening")]
public async Task<ActionResult<Screening>> Get(int id)
{
return await context.Screenings.FirstOrDefaultAsync(x => x.ScreeningId == id);
}
[HttpPost]
public async Task<ActionResult> Post(Screening screening)
{
context.Add(screening);
await context.SaveChangesAsync();
return new CreatedAtRouteResult("GetScreening", new { id = screening.ScreeningId }, screening);
}
[HttpPut]
public async Task<ActionResult> Put(Screening screening)
{
context.Entry(screening).State = EntityState.Modified;
await context.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
var screening = new Screening { ScreeningId = id };
context.Remove(screening);
await context.SaveChangesAsync();
return NoContent();
}
}
}
Views
Screening Form Component
@inject HttpClient http
<RadzenCard>
<EditForm Model="@Screening" OnValidSubmit="@OnValidSubmit">
<DataAnnotationsValidator/>
<div class="from-field">
<label>Screening date:</label>
<div>
<RadzenDatePicker @bind-Value="Screening.ScreeningDate"/>
</div>
</div>
<div class="from-field">
<label>Client name:</label>
<div>
<RadzenTextBox Style="margin-bottom: 20px" @bind-Value="@Screening.Beneficiary.FirstName"/>
</div>
</div>
<div class="from-field">
<label>Client surname:</label>
<div>
<RadzenTextBox Style="margin-bottom: 20px" @bind-Value="@Screening.Beneficiary.Surname"/>
</div>
</div>
<div class="from-field">
<label>Reason for visit:</label>
<div>
<RadzenDropDown
AllowClear="true"
TValue="int"
FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
AllowFiltering="true"
Data="@ReasonForVistList"
TextProperty="Description"
ValueProperty="SimpleEntityID"
Style="margin-bottom: 20px" />
</div>
</div>
<div class="from-field">
<label>Action taken:</label>
<div>
<RadzenDropDown
AllowClear="true"
TValue="int"
FilterCaseSensitivity="FilterCaseSensitivity.CaseInsensitive"
AllowFiltering="true"
Data="@ActionTakenList"
TextProperty="Description"
ValueProperty="SimpleEntityID"
Style="margin-bottom: 20px" />
</div>
</div>
<hr/>
<button type="submit" class="btn btn-success">
Create
</button>
</EditForm>
</RadzenCard>
@code {
[Parameter] public Screening Screening{get;set;}
[Parameter] public string ButtonText{get;set;} = "Save";
[Parameter] public EventCallback OnValidSubmit{get;set;}
IEnumerable<SimpleEntity> ReasonForVistList;
IEnumerable<SimpleEntity> ActionTakenList;
protected override async Task OnInitializedAsync()
{
var reasonForVisitType = "reason-for-visit";
var actionTaken = "screening-action-taken";
ReasonForVistList = await http.GetJsonAsync<SimpleEntity[]>($"api/simpleentity/type/?type={reasonForVisitType}");
ActionTakenList = await http.GetJsonAsync<SimpleEntity[]>($"api/simpleentity/type/?type={actionTaken}");
}
}
Create View
@page "/screening/create"
@inject HttpClient http
@inject NavigationManager uriHelper
<h3>New Screening Process</h3>
<ScreeningForm ButtonText="Create" Screening="@screening" OnValidSubmit="@Save"/>
@code{
Screening screening = new Screening();
async Task Save()
{
await http.PostJsonAsync("api/screening", screening);
uriHelper.NavigateTo("screening");
}
}