I work with ASP.NET Core for first time and I am trying to create an API. I have a problem when I try to return city of a user using lazy loading proxies package. Can anyone help me please? And thank you for any help.
This is my dbcontext
configuration
services.AddDbContext<ApplicationDbContext>(option =>
{
option.UseLazyLoadingProxies()
.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
option.LogTo(Console.WriteLine,LogLevel.Information);
option.EnableSensitiveDataLogging();
});
This is my User
entity:
using ecommerce.Domain.Abstract;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using tables.Base.Entity;
namespace ecommerce.Domain.Entities
{
public class User : BaseEntity
{
public Guid Id { get; set; }
public Guid CityId { get; set; }
public virtual City City { get; set; }
public string Name { get; set; }
[Range(0,Double.MaxValue)]
public int Point { get; set; }
public Guid AccountId { get; set; }
public virtual Account Account { get; set; }
public bool IsBlocked { get; set; }
}
}
This is my configuration for the City
entity:
using ecommerce.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ecommerce.infrutructure.Configuration
{
internal class CityConfigration : IEntityTypeConfiguration<City>
{
public void Configure(EntityTypeBuilder<City> builder)
{
builder.Property(c => c.Delivery_Price).HasDefaultValue(0);
builder.Property(c=>c.status).HasDefaultValue(false);
builder.HasMany(c => c.Users)
.WithOne(u => u.City);
}
}
}
Here user.city
is null in the return
public async Task<OperationResultBase<UserWithToken>> Handle(AddUserCommand request, CancellationToken cancellationToken)
{
var Account = mapper.Map<Account>(request);
var Accountresult = await this.userManager.CreateAsync(Account, request.Password);
this.DbContext.SaveChanges();
if (!Accountresult.Succeeded)
{
throw new CanotCreateAccountException(Accountresult.Errors);
}
var User = mapper.Map<User>(request, opts => opts.AfterMap((src, desc) => desc.AccountId = Account.Id));
this.DbContext.Users.Add(User);
this.DbContext.SaveChanges();
TokenDto TokenInfo = jwtRepository.GetTokens(Account);
UserWithToken result = new UserWithToken()
{
Id = User.Id,
Name = User.Name,
UserName = User.Account.UserName,
City = User.City,
Email = User.Account.Email,
IsBlocked = User.IsBlocked,
Point = User.Point,
TokenInfo = TokenInfo
};
return Created<UserWithToken>(result, "The user was created successfully");
}
When you use code like this:
var User = mapper.Map<User>(request, opts => opts.AfterMap((src, desc) => desc.AccountId = Account.Id));
Mappers will essentially run:
var user = new User();
... which constructs a concrete User class. Lazy loading relies on proxies to know when related entities need to be loaded vs. just happen to be #null. If your mapping is just setting User.CityId then no City reference is set, and your following code using "User" isn't a proxy ready to lazy load anything that isn't already associated. DbSet<T>.Add()
returns the proxy: var addedUser = DbContext.Users.Add(User);
Edit: Wow, that was a brain fog moment, that is the change tracking wrapper that is returned, not the entity. The problem is still the same, when you just use a concrete class and set a FK, EF does not have a proxy that would be normally used to enable lazy loading. When using Microsoft.EntityFrameworkCore.Proxies you can use the DbSet<T>.CreateProxy()
method to instantiate your new entity The corrected code example would be:
var user = DbContext.Users.CreateProxy();
mapper.Map(request, user, opts => opts.AfterMap((src, desc) => desc.AccountId = Account.Id));
This uses Automapper's Map(src, dest)
variant which is used to copy values across to an existing destination instance which we construct as a lazy loading proxy. I was testing whether the Mapper could be told to construct the instance using the CreateProxy but didn't find anything conclusive.
Looking at your code though, since you are creating the account, provided the new account entity comes back in your result you should just be able to set that reference:
mapper.Map(request, user, opts => opts.AfterMap((src, desc) =>
{
desc.AccountId = Account.Id;
desc.Account = Account
}));
Provided both services are using the same injected DbContext instance. Don't do this if they are not as this would result in Account being treated as a new, untracked entity leading to duplicate data being inserted or a constraint violation on the Account PK.
One thing worth mentioning is that when returning DTOs/ViewModels you should avoid mixing them with entities. For instance returning User.City which is a City Entity where you should consider returning just the fields from the City entity that are needed or project the City to a DTO. Mixing references to entities can cause issues with serializers and the like "touching" navigation properties resulting in performance issues and cyclic references.
So the code populating your result DTO:
City = User.City,
should be something like:
City = User.City.Name,
or projected to a CityDTO or the like rather than passing a City entity.