I can not insert properly into many-to-many table - c#

I have Articles and Tags. Both articles and tags can exists without depending each other and articles have tags and tags have articles as well.
public class Article : AuditableBaseEntity
{
[Required]
[MaxLength(50)]
public string Title { get; set; } = string.Empty;
[Required]
[MaxLength(500)]
public string Content { get; set; } = string.Empty;
public ICollection<Tag> Tags { get; set; } = new Collection<Tag>();
}
public class Tag : AuditableBaseEntity
{
[Required]
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
public ICollection<Article> Articles { get; set; } = new Collection<Article>();
}
I did not set up anything on fluent api:
public class ApplicationDbContext : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<Tag> Tags { get; set; }
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
I want to add tags after both tags and article are independently created.
public class AddTagToArticleCommand : ICommand<ArticleQueryDto>
{
public int ArticleId { get; set; }
public int TagId { get; set; }
}
public class AddTagToArticleCommandHandler : ICommandHandler<AddTagToArticleCommand, ArticleQueryDto>
{
private readonly IArticleRepository _articleRepository;
private readonly ITagRepository _tagRepository;
public AddTagToArticleCommandHandler(IArticleRepository articleRepository, ITagRepository tagRepository) => (_articleRepository, _tagRepository) = (articleRepository, tagRepository);
public async Task<ArticleQueryDto> Handle(AddTagToArticleCommand request, CancellationToken cancellationToken)
{
var article = await _articleRepository.GetByIdIncludeTags(request.ArticleId);
var tag = await _tagRepository.GetByIdIncludeArticles(request.TagId);
article.Tags.Add(tag);
await _articleRepository.UpdateAsync(article);
return article.Adapt<ArticleQueryDto>();
}
}
But I getting:
System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles. Path: $.Tags.Articles.Tags.Articles.Tags.Articles.Tags.Articles.Tags.Articles.Tags.Articles.Tags.Articles.Tags.Articles.
at System.Text.Json.ThrowHelper.ThrowJsonException_SerializerCycleDetected(Int32 maxDepth)
What is my mistake?
By the way:
public async Task<Article> GetByIdIncludeTags(int id)
{
var article = await _context.Articles.Include(a => a.Tags).FirstOrDefaultAsync(a => a.Id == id);
if (article == null)
throw new EntityNotFoundException<Article>(id);
return article;
}
public async Task<Tag> GetByIdIncludeArticles(int id)
{
var tag = await _context.Tags.Include(t => t.Articles).FirstOrDefaultAsync();
if (tag == null)
throw new EntityNotFoundException<Tag>(id);
return tag;
}
---------------------- UPDATES -------------------------
I removed the cycle by removing Articles from Tags Dto. And it solved the error.
Then updated the code for:
public async Task<Article> GetByIdIncludeTags(int id)
{
var article = await _context.Articles.Include(a => a.Tags).ThenInclude(t => t.Articles).FirstOrDefaultAsync(t => t.Id == id);
if (article == null)
throw new EntityNotFoundException<Article>(id);
return article;
}
public async Task<Tag> GetByIdIncludeArticles(int id)
{
var tag = await _context.Tags.Include(t => t.Articles).ThenInclude(a => a.Tags).FirstOrDefaultAsync(t => t.Id == id);
if (tag == null)
throw new EntityNotFoundException<Tag>(id);
return tag;
}
And updated the AddTagToArticleCommand:
public async Task<ArticleQueryDto> Handle(AddTagToArticleCommand request, CancellationToken cancellationToken)
{
var article = await _articleRepository.GetByIdIncludeTags(request.ArticleId);
var tag = await _tagRepository.GetByIdIncludeArticles(request.TagId);
article.Tags.Add(tag);
tag.Articles.Add(article);
await _tagRepository.UpdateAsync(tag);
article = await _articleRepository.UpdateAsync(article);
return article.Adapt<ArticleQueryDto>();
}
This is the output after executing AddTagToArticle:
{
"title": "article1",
"content": "content1",
"tags": [
{
"id": 1,
"name": "tag1"
}
],
"createdAt": "2023-02-08T13:22:15.908037+03:00",
"updatedAt": "2023-02-08T13:22:15.9080612+03:00",
"id": 1
}
But after that, I call await _await _articleRepository.GetByIdIncludeTags(id); into another request called GetArticleById, but I get empty tags.
{
"title": "article1",
"content": "content1",
"tags": [],
"createdAt": "2023-02-08T13:22:15.908037+03:00",
"updatedAt": "2023-02-08T13:22:15.9080612+03:00",
"id": 1
}

Related

How to use Expression in Expression for single element

How can I use an Expression in another Expression. For a set I can use blog.Posts.Select(postMapper.ProjectPost). But How can I use it for a single object? I don't want to call compile, I need to use that in EF sql translator. I try some hacks like new List<Blog>{post.Blog}.Select(blogMapper.ProjectBlog).First() but it's not working.
public class BloggingContext : DbContext
{
private readonly ILoggerFactory loggerFactory;
public BloggingContext(ILoggerFactory loggerFactory)
{
this.loggerFactory = loggerFactory;
}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseLoggerFactory(loggerFactory).UseSqlServer(#"Server=(LocalDB)\MSSqlLocalDb;Database=EFExpressionMapper;Trusted_Connection=True");
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
class Program
{
static async Task Main(string[] args)
{
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddLogging(builder => builder
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)
);
var loggerFactory = serviceCollection.BuildServiceProvider().GetService<ILoggerFactory>();
await using var dbContext = new BloggingContext(loggerFactory);
dbContext.Add(new Blog
{
Url = "http://blogs.msdn.com/sample-blog",
Posts =
{
new Post {Title = "Post 1", Content = "Post 1 content"},
new Post {Title = "Post 2", Content = "Post 2 content"},
new Post {Title = "Post 3", Content = "Post 3 content"},
}
});
await dbContext.SaveChangesAsync();
var postMapper = new PostMapper(new BlogMapper());
var posts = await dbContext.Posts.Select(postMapper.ProjectPost).ToArrayAsync();
foreach (var post in posts)
{
Console.WriteLine($"{post.Title} {post.Blog.Url}");
}
}
}
public class PostMapper
{
public Expression<Func<Post, PostDto>> ProjectPost { get; }
public PostMapper(BlogMapper blogMapper)
{
//TODO USE blogMapper.ProjectBlogList WITHOUT COMPILE
ProjectPost = post => new PostDto(post.PostId, post.Title, post.Content, blogMapper.ProjectBlogList.Compile()(post.Blog));
}
}
public class BlogMapper
{
public Expression<Func<Blog, BlogListDto>> ProjectBlogList { get; } = blog => new BlogListDto(blog.BlogId, blog.Url);
}
public class BlogListDto
{
public int BlogId { get; }
public string Url { get; }
public BlogListDto(int blogId, string url)
{
BlogId = blogId;
Url = url;
}
}
public class PostDto
{
public int PostId { get; }
public string Title { get; }
public string Content { get; }
public BlogListDto Blog { get; }
public PostDto(int postId, string title, string content, BlogListDto blog)
{
PostId = postId;
Title = title;
Content = content;
Blog = blog;
}
}
Look into PostMapper constructor. I'm used a Compile method there. But it's not good for EF
Actually LINQKit may correct your query and make EF happy when using Compile. Just add AsExpandable() to your query.
But I suggest to do not create mapping class for each DTO but collect them in logical one:
public static class DtoMapper
{
[Expandable(nameof(AsDtoPost))]
public static PostDto AsDto(this Post post)
=> throw new NotImplementedException();
[Expandable(nameof(AsDtoBlogList))]
public static BlogListDto AsDto(this Blog blog)
=> throw new NotImplementedException();
static Expression<Func<Post, PostDto>> AsDtoPost()
=> post => new PostDto(post.PostId, post.Title, post.Content, post.Blog.AsDto()));
static Expression<Func<Blog, BlogListDto>> AsDtoBlogList()
=> blog => new BlogListDto(blog.BlogId, blog.Url);
}
So your sample can be rewritten
var posts = await dbContext.Posts
.AsExpandable()
.Select(p => p.AsDto()).ToArrayAsync();
Similar but answer already created before, which covers other libraries which do the same lambda expression expanding. https://stackoverflow.com/a/66386142/10646316
This answer is focused on LINQKit realisation.

.Net Core 3.1 - The instance of entity type 'City' cannot be tracked - when using custom Date Tracker

I know similar question have been asked many times but unfortunately after days of research still haven't been able to find a solution for my problem.
I try to explain in details what the issue is.
The following is a simple project which can reproduce the issue!
The problem:
I am using a custom Date Tracker so that I can not only add Created Date or Updated date at the time of saving entities but also soft delete my records.
However when I use this I get the following error
System.InvalidOperationException: 'The instance of entity type 'City' cannot be tracked because another instance with the key value '{Id: a6606535-76a5-4a23-b204-08d882481a95}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'
The strange thing is, it only happens sometimes, I'd say when I run the project for the first time it happens 90% of the time but then if I bypass the exception and hit the endpoint then it will happen only 10% of the time.
The code:
Models:
public class CitySync : ICreatedDateTracking
{
public long Id { get; set; }
public Guid CityId { get; set; }
public virtual City City { get; set; }
public DateTimeOffset CreatedDate { get; set; }
public int Added { get; set; }
}
public partial class School : IDateTracking
{
public string Id { get; set; }
public string Name { get; set; }
public DateTimeOffset CreatedDate { get; set; }
public virtual City City { get; set; }
public Guid CityId { get; set; }
public DateTimeOffset? DeletedDate { get; set; }
public DateTimeOffset UpdatedDate { get; set; }
}
public partial class City : IDateTracking
{
public Guid Id { get; set; }
public DateTimeOffset CreatedDate { get; set; }
public string OwnerName { get; set; }
public virtual List<School> Schools { get; set; }
public virtual ICollection<CitySync> CitySync { get; set; }
public DateTimeOffset? DeletedDate { get; set; }
public DateTimeOffset UpdatedDate { get; set; }
}
DbContext:
public class ApplicationDbContext : DbContext
{
private readonly IChangeTracker _changeTracker;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IChangeTracker changeTracker)
: base(options)
{
_changeTracker = changeTracker;
}
public void DetachAllEntities()
{
var changedEntriesCopy = this.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added ||
e.State == EntityState.Modified ||
e.State == EntityState.Deleted)
.ToList();
foreach (var entry in changedEntriesCopy)
entry.State = EntityState.Detached;
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
Task.Run(async () => await _changeTracker.BeforeSaveChanges(this));
var saveChanges = base.SaveChanges(acceptAllChangesOnSuccess);
Task.Run(async () => await _changeTracker.AfterSaveChanges(this));
return saveChanges;
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
{
await _changeTracker.BeforeSaveChanges(this);
var saveChangesAsync = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
await _changeTracker.AfterSaveChanges(this);
return saveChangesAsync;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<School>(entity =>
{
entity.Property(e => e.Id).HasMaxLength(50);
entity.Property(e => e.Name).HasMaxLength(255);
entity.HasOne(x => x.City).WithMany(x => x.Schools);
entity.HasKey(x => new
{
x.Id,
x.CitytId
});
});
modelBuilder.Entity<City>(entity =>
{
entity.Property(e => e.OwnerName).HasMaxLength(255);
entity.HasMany(x => x.Schools).WithOne(x => x.City);
});
modelBuilder.Entity<CitySync>(entity =>
{
entity.HasOne(x => x.City).WithMany(x => x.CitySync);
});
foreach (var entityType in modelBuilder.Model.GetEntityTypes().Where(e => typeof(IDateTracking).IsAssignableFrom(e.ClrType)))
{
if (entityType.BaseType == null)
{
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(ConvertFilterExpression<IDateTracking>(e => e.DeletedDate == null, entityType.ClrType));
}
}
}
private static LambdaExpression ConvertFilterExpression<TInterface>(Expression<Func<TInterface, bool>> filterExpression, Type entityType)
{
var newParam = Expression.Parameter(entityType);
var newBody = ReplacingExpressionVisitor.Replace(filterExpression.Parameters.Single(), newParam, filterExpression.Body);
return Expression.Lambda(newBody, newParam);
}
public virtual DbSet<School> Schools { get; set; }
public virtual DbSet<City> Cities { get; set; }
public virtual DbSet<CitySync> CitySync { get; set; }
}
DateChangeTracker:
public class DateChangeTracker : IChangeTracker
{
public static EntityState[] AuditedEntityStates = { EntityState.Added, EntityState.Modified, EntityState.Deleted };
public DateChangeTracker()
{
}
public virtual Task BeforeSaveChanges(DbContext dbContext)
{
var now = DateTimeOffset.UtcNow;
foreach (var entry in dbContext.ChangeTracker.Entries().Where(e => AuditedEntityStates.Contains(e.State)).ToList())
{
if (entry.Entity is ICreatedDateTracking createdDateTracking)
{
if (entry.State == EntityState.Added)
{
createdDateTracking.CreatedDate = now;
}
}
if (entry.Entity is IUpdatedCreatedDateTracking updatedCreatedDateTracking)
{
updatedCreatedDateTracking.UpdatedDate = now;
}
if (entry.Entity is IDateTracking dateTracking)
{
if (entry.State == EntityState.Added)
{
dateTracking.CreatedDate = now;
}
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
dateTracking.DeletedDate = now;
}
}
}
return Task.CompletedTask;
}
public virtual Task AfterSaveChanges(DbContext dbContext)
{
return Task.CompletedTask;
}
}
DbContext Factory:
public class ApplicationDbContextDesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection");
builder.UseSqlServer(connectionString);
return new ApplicationDbContext(builder.Options, new DateChangeTracker());
}
}
IDate interfaces, implement created, updated, deleted fileds
SchoolService:
public class SchoolService : ISchoolService
{
private readonly ApplicationDbContext applicationDbContext;
public SchoolService(ApplicationDbContext applicationDbContext)
{
this.applicationDbContext = applicationDbContext;
}
public Guid PopulateCity()
{
var existingCity = applicationDbContext.Cities.FirstOrDefault();
if (existingCity == null)
{
var city = new Models.City { CreatedDate = DateTimeOffset.Now, OwnerName = "test" };
applicationDbContext.Cities.Add(city);
applicationDbContext.SaveChanges();
return city.Id;
}
return existingCity.Id;
}
public void ProcessSchools(Guid cityId)
{
var city = applicationDbContext.Cities.FirstOrDefault(x => x.Id == cityId);
if (city == null)
throw new Exception("City doesnt exist");
var citySync = new CitySync
{
CityId = city.Id,
CreatedDate = DateTimeOffset.Now,
};
var existingSchools = applicationDbContext.Schools.Where(x => x.CitytId == cityId).ToList();
// update schools if the exists
// add new ones if they dont
var schools = new List<School>();
if (!existingSchools.Any())
{
schools.Add(new School { CitytId = cityId, CreatedDate = DateTimeOffset.Now, Id = "1", Name = "school1" });
schools.Add(new School { CitytId = cityId, CreatedDate = DateTimeOffset.Now, Id = "2", Name = "school2" });
schools.Add(new School { CitytId = cityId, CreatedDate = DateTimeOffset.Now, Id = "3", Name = "school3" });
}
else
{
foreach (var item in existingSchools)
{
item.UpdatedDate = DateTimeOffset.Now;
}
applicationDbContext.SaveChanges();
}
var additions = schools.Except(existingSchools, new SchoolComparer()).ToList();
foreach (var school in additions)
{
school.CitytId = city.Id;
}
applicationDbContext.Schools.AddRange(additions);
city.UpdatedDate = DateTimeOffset.Now;
city.OwnerName = "Updated Name";
citySync.Added = additions.Count;
applicationDbContext.CitySync.Add(citySync);
applicationDbContext.SaveChanges();
}
}
public class SchoolComparer : IEqualityComparer<School>
{
public bool Equals(School x, School y)
{
return x?.Id == y?.Id;
}
public int GetHashCode(School obj)
{
return 0;
}
}
The error happens right on the following line
*applicationDbContext.CitySync.Add(citySync);*
Startup:
public class Startup
{
public Startup(IConfiguration configuration, IHostEnvironment environment)
{
Configuration = configuration;
Environment = environment;
}
public IHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(IChangeTracker), typeof(DateChangeTracker));
services.AddTransient<ISchoolService, SchoolService>();
var connectionString = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(connectionString); options.EnableSensitiveDataLogging(); }, ServiceLifetime.Scoped);
services.AddControllers();
services.AddHttpContextAccessor();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>().Database.Migrate();
}
}
}
Please note that this only happens if I use the date tracker, If I don't user the overridden saveChanges methods then all works fine.
Please try to use this way
services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlServer(connectionString,x => x.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)),ServiceLifetime.Transient);
services.AddTransient<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());

Entity Framework Core deletes entities during update

I have a problem concerning entities in ASP.NET Core.
I use Entity Framework Core as data access library.
The issue I've come across happens when I'm trying to update an entity. After I modify the properties and call SaveChanges, the entity gets deleted and I don't understand why.
Here's the entity:
public class Contract
{
public int Id { get; set; }
[Required]
public DateTime ExpiryDate { get; set; }
[Required]
[Range(0, float.MaxValue)]
public float MonthlyFee { get; set; }
[Required]
public string UserId { get; set; }
[Required]
public int CarId { get; set; }
public User User { get; set; }
public Car Car { get; set; }
}
Here's the related entities for reference:
public class User : IdentityUser
{
[Required]
[PersonalData]
public string Name { get; set; }
[Required]
[PersonalData]
public string Surname { get; set; }
[Required]
[PersonalData]
public string TaxCode { get; set; }
[Required]
[PersonalData]
[DataType(DataType.Date)]
public DateTime DateOfBirth { get; set; }
public string ProfilePictureUrl { get; set; }
public Contract Contract { get; set; }
public ICollection<CarAccident> CarAccidents { get; set; }
}
public class Car
{
public int Id { get; set; }
[Required]
[RegularExpression("[A-Z][A-Z][0-9][0-9][0-9][A-Z][A-Z]")]
public string LicensePlate { get; set; }
public int CarModelId { get; set; }
public string FittingDescription { get; set; }
public Contract Contract { get; set; }
public ICollection<CarAccident> CarAccidents { get; set; }
public CarModel CarModel { get; set; }
}
Here's my update method in repository:
public async Task<Contract> Update(Contract entity)
{
var dbContract = await GetById(entity.Id);
if (dbContract == null)
return null;
var dbUser = await _userRepository.GetById(entity.UserId);
if (dbUser == null)
return null;
var dbCar = await _carRepository.GetById(entity.CarId);
if (dbCar == null)
return null;
dbContract.ExpiryDate = entity.ExpiryDate;
dbContract.User = entity.User;
dbContract.Car = dbCar;
dbContract.User = dbUser;
//_context.Contracts.FromSqlInterpolated($"UPDATE dbo.Contracts SET ExpiryDate={entity.ExpiryDate}, MonthlyFee={entity.MonthlyFee} WHERE Id={entity.Id}");
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
return null;
}
return await GetById(entity.Id);
}
Has anyone got any idea how to solve this?
UPDATE:
This is the new Update method:
public async Task<Contract> Update(Contract entity)
{
var dbContract = await GetById(entity.Id);
if (dbContract == null)
return null;
var dbUser = await _userRepository.GetById(entity.UserId);
if (dbUser == null)
return null;
var dbCar = await _carRepository.GetById(entity.CarId);
if (dbCar == null)
return null;
dbContract.ExpiryDate = entity.ExpiryDate;
dbContract.Car = dbCar;
dbContract.User = dbUser;
//_context.Contracts.FromSqlInterpolated($"UPDATE dbo.Contracts SET ExpiryDate={entity.ExpiryDate}, MonthlyFee={entity.MonthlyFee} WHERE Id={entity.Id}");
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
return null;
}
return await GetById(entity.Id);
}
Here's the Fluent API configuration:
private void _configureUsers(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOne(u => u.Contract)
.WithOne(c => c.User)
.HasForeignKey<Contract>(c => c.UserId);
}
private void _configureCars(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasAlternateKey(c => c.LicensePlate);
modelBuilder.Entity<Car>()
.HasOne(c => c.Contract)
.WithOne(c => c.Car)
.HasForeignKey<Contract>(c => c.CarId);
}
Both this methods get called in the OnModelCreating method of the context.
I've finally managed to solve my issue.
I was already tracking the entity in my api controller like that:
[HttpPut("{id}")]
[Authorize(Roles = "Backoffice")]
public async Task<ActionResult<ContractDTO>> PutContract(int id, [FromBody] PutContractViewModel viewModel)
{
if (viewModel == null || !ModelState.IsValid)
return BadRequest(new { message = "Your model is wrong" });
var contract = await _contractService.GetContractDTO(id);
if (contract == null)
return NotFound();
var modifiedContract = await _contractService.UpdateContract(viewModel);
if (modifiedContract == null)
return BadRequest(new { message = "User or car may be busy in another contract" });
return Ok(modifiedContract);
}
This type of approach works in one to many relationships, but evidently when you have one to one relationship and you have to objects that rapresent the same entity the ChangeTracker cannot track the changes correctly.
I post my new controller and repository code if someone will burst into my same problem.
Controller:
[HttpPut("{id}")]
[Authorize(Roles = "Backoffice")]
public async Task<ActionResult<ContractDTO>> PutContract(int id, [FromBody] PutContractViewModel viewModel)
{
if (viewModel == null || !ModelState.IsValid)
return BadRequest(new { message = "Your model is wrong" });
ContractDTO modifiedContract;
try
{
modifiedContract = await _contractService.UpdateContract(viewModel);
}
catch (EntityNotFoundException)
{
return NotFound();
}
if (modifiedContract == null)
return BadRequest(new { message = "User or car may be busy in another contract" });
return Ok(modifiedContract);
Service:
public async Task<ContractDTO> UpdateContract(PutContractViewModel viewModel)
{
try
{
return await ParseContractToContractDTO(await _contractRepository.Update(ParsePutContractViewModelToContract(viewModel)));
}
catch(EntityNotFoundException)
{
throw;
}
}
Repository:
public async Task<Contract> Update(Contract entity)
{
var dbContract = await _context.Contracts.Include(c => c.User).Include(c => c.Car).FirstOrDefaultAsync(c => c.Id == entity.Id);
if (dbContract == null)
{
throw new EntityNotFoundException();
}
var dbUser = await _context.Users.Include(u => u.Contract).FirstOrDefaultAsync(u => u.Id == entity.UserId);
if (dbUser == null)
return null;
var dbCar = await _context.Cars.Include(c => c.Contract).FirstOrDefaultAsync(c => c.Id == entity.CarId);
if (dbCar == null)
return null;
dbContract.ExpiryDate = entity.ExpiryDate;
dbContract.MonthlyFee = entity.MonthlyFee;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
return null;
}
return await GetById(entity.Id);
}
I want to thank you all, you've been very helpful and patient with me.

C# - Model is always getting its default values

I have an ASP.Net Core 2.1 application.
Below is my DTO
public class Movie
{
public int Id { get; set;}
public bool IsSpecial {get; set;}
public IEnumerable<Ticket> Tickets { get; set; }
public Movie()
{
if(IsSpecial)
{
this.Tickets = new List<TicketSpecial>();
}
else
{
this.Tickets = new List<Ticket>();
}
}}}
Tickets (Base Class)
public class Ticket
{
public int Id { get; set;}
public string Name { get; set;}
public decimal price { get; set;}
}
TicketsSpecial (Child/Derived Class)
public class TicketsSpecial : Ticket
{
public string SpecialProp1 { get; set;}
public string SpecialProp2 { get; set;}
}
WebAPI Controller
public class MovieController : ControllerBase
{
public IActionResult Post([FromBody]Movie movie)
{
}
}
Postman (HTTPPost Req payload Content-Type = application/json)
{
"IsSpecial": true,
"SpecialProp1": "Mumbai Test",
}
When I call the above API via Postman & debug at Movie ctor, it always catches the value of IsSpecial = false & all fields default value (ex. for string type null)
Thanks!
There are two issues in your current implementation. First, your request json is invalid for nested properties and json deserializer would not deserialize for you with TicketsSpecial.
Follow steps below for a workaround:
Movie
public class Movie
{
public int Id { get; set; }
public bool IsSpecial { get; set; }
public IEnumerable<Ticket> Tickets { get; set; }
}
MyJsonInputFormatter
public class MyJsonInputFormatter : JsonInputFormatter
{
public MyJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var result = await base.ReadRequestBodyAsync(context);
if (result.Model is Movie movie && movie.IsSpecial)
{
context.HttpContext.Request.Body.Position = 0;
string request = await new StreamReader(context.HttpContext.Request.Body).ReadToEndAsync();
var tickets = JObject.Parse(request)["Tickets"].ToObject<List<TicketSpecial>>();
movie.Tickets = tickets;
}
return result;
}
}
Register MyJsonInputFormatter
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new MyJsonInputFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<MyJsonInputFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
})
Request
{
"IsSpecial": true,
"Tickets": [
{"SpecialProp1": "Mumbai Test"}
]
}
Change "Isspecial" to "isSpecial", same with the other property.
Another problem is that you're checking "IsSpecial" in the constructor and at this time it should be false anyway.

ASP.NET Core WebAPI : custom InputFormatter validate Model State

I have used custom InputFormatters for creating a subset of request from the generic request that request body receives in API request.
var reqModel = new XmlSerializer(CurrentType).Deserialize(xmlDoc.CreateReader());
SubSetRequest model = ConvertToSubSetRequestObject(reqModel as BigRequest);
return InputFormatterResult.Success(model);
Now in controller ModelState.IsValid is not pointing to SubSetRequest but to BigRequest, which I have received request body
public ActionResult<object> Calculate(SubSetRequest request)
{
if (!ModelState.IsValid){ }
// other codes..
}
Any idea how can we validate ModelState against SubSetRequest type.
Important Classes :
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(options =>
{
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
options.InputFormatters.Insert(0, new XMLDocumentInputFormatter());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
}
BigRequest.cs
[System.SerializableAttribute()]
public class BigRequest
{
public string Name { get; set; }
public string Email { get; set; }
public string Address { get; set; }
public string Company { get; set; }
public string Designation { get; set; }
public string CompanyAddress { get; set; }
}
SubSetRequest.cs
[System.SerializableAttribute()]
public class SubSetRequest
{
public string Name { get; set; }
[Required] //This should tiger **Validation** error
public string Email { get; set; }
public string Address { get; set; }
}
XMLDocumentInputFormatter.cs
internal class XMLDocumentInputFormatter : InputFormatter
{
private Type CurrentType { get; set; }
public XMLDocumentInputFormatter()
{
SupportedMediaTypes.Add("application/xml");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
using (var streamReader = new StreamReader(context.HttpContext.Request.Body))
{
CurrentType = typeof(BigRequest);
var xmlDoc = await XDocument.LoadAsync(streamReader, LoadOptions.None, CancellationToken.None);
var reqModel = new XmlSerializer(CurrentType).Deserialize(xmlDoc.CreateReader());
var model = ConvertToSubSetRequestObject(reqModel as BigRequest);
return InputFormatterResult.Success(model);
}
}
public SubSetRequest ConvertToSubSetRequestObject(BigRequest request)
{
var retObject = new SubSetRequest
{
Name = request.Name,
Address = request.Address
};
return retObject;
}
}
ValueController.cs
[HttpPost]
[Route("/api/Value/Calculate")]
public virtual ActionResult<object> Calculate(SubSetRequest request)
{
TryValidateModel(request);
if (ModelState.IsValid) // is giving as TRUE, even if EMAIL is NULL
{
var context = new ValidationContext(request, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
// this is working properly
var isValid = Validator.TryValidateObject(request, context, results);
}
return new ActionResult<object>(request.ToString());
}

Categories

Resources