• Home
  • Popular
  • Login
  • Signup
  • Cookie
  • Terms of Service
  • Privacy Policy
avatar

Posted by User Bot


26 Apr, 2025

Updated at 18 May, 2025

Angular 19 returning partially correct DTOs from server eventhough all server data is correct

return in controller shows [mostly] correct data but when returning it on client side is null.

i use Angular 19, dotnet and EFCore. mapping seems to be selective..

i create an Article which can have Trip property. Trip can have some nested properties.

when i create it - the server returns correct mapping [apart of Ids] - while on client side part of them is null.

ArticleDto:

public class  ArticleDTO
{
    public int? Id { get; set; }
    public string Title { get; set;}
    public string Url { get; set; }
    public string BackgroundImageUrl { get; set; }
    public string Content { get; set; }
    
    [JsonConverter(typeof(StringEnumConverter))]
    public Country Country { get; set; }
    
    [JsonConverter(typeof(StringEnumConverter))]
    public ArticleCategory ArticleCategory { get; set; }

    public int? TripId { get; set; }
    public TripDTO? TripDto { get; set; }
}

Article Model:

public class Article
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string Title { get; set;}
    public string Url { get; set; }
    public string Content { get; set; }
    public string BackgroundImageUrl { get; set; }
    
    [JsonConverter(typeof(StringEnumConverter))]
    public Country Country { get; set; }
    
    [JsonConverter(typeof(StringEnumConverter))]
    public ArticleCategory ArticleCategory { get; set; }
    
    [ForeignKey("Trip")]
    public int? TripId { get; set; }
    public Trip? Trip { get; set; }
}

TripDto:

public class TripDTO
{
    public int? Id { get; set; }
    public string Name { get; set; }
    public TripType Type { get; set; }
    public ICollection TripTermIds { get; set; }
    public ICollection TripTermDtos { get; set; }
    public int? SurveyId { get; set; }
    public SurveyDTO? SurveyDto { get; set; }
    public int? ArticleId { get; set; }
    public ArticleDTO? ArticleDto { get; set; }
    public ICollection TripApplicationIds { get; set; }
    public ICollection TripApplicationDtos { get; set; }
}

Trip model:

public class Trip
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string Name { get; set; }
    public TripType Type { get; set; }
    public ICollection TripTerms { get; set; }
    public Survey? Survey { get; set; }
    public int? ArticleId { get; set; }
    public Article? Article { get; set; }
    public ICollection TripApplications = new List();
}

this is createArticle Controller:

[HttpPost("createArticle")]
public async Task> CreateArticle([FromBody] ArticleDTO articleDto)
{
    if (articleDto.Id.HasValue)
    {
        ArticleDTO existingArticle = await articleService.GetArticleDetails(articleDto.Id.Value);
        if (existingArticle != null)
        {
            throw new ApplicationException("ArticleDTO with given ID already exists");
        }
    }

    var article = await articleService.CreateArticle(articleDto);
    return Ok(article);
}

the article at the end has correct DTOs inside:

article = ArticleDTO
 ArticleCategory = {ArticleCategory} Wyprawy
 BackgroundImageUrl = {string} "http://localhost:5000/uploads/7c1ebd63-6b78-4027-af44-d49e6d0febd0.jpg"
 Content = {string} "

test

" Country = {Country} Polska Id = {int} 63 Title = {string} "test" TripDto = TripDTO ArticleDto = ArticleDTO ArticleId = {int} 63 Id = {int} 58 Name = {string} "test" SurveyDto = {SurveyDTO} null SurveyId = {int?} null TripApplicationDtos = {List} Count = 0 TripApplicationIds = {ICollection} null TripTermDtos = {List} Count = 1 [0] = TripTermDTO DateFrom = {DateTime} 27.04.2025 12:54:03 DateTo = {DateTime} 30.04.2025 12:54:05 Id = {int} 56 Name = {string} "test" ParticipantsCurrent = {int} 0 ParticipantsTotal = {int} 0 Price = {Decimal} 0 TripDto = TripDTO TripId = {int} 58 TripTermIds = {ICollection} null Type = {TripType} Weekend TripId = {int} 58 Url = {string} "test"

but on client side i get:

{
    "Id": 63,
    "Title": "test",
    "Url": "test",
    "BackgroundImageUrl": "http://localhost:5000/uploads/7c1ebd63-6b78-4027-af44-d49e6d0febd0.jpg",
    "Content": "

test

", "Country": "Polska", "ArticleCategory": "Wyprawy", "TripId": 58, "TripDto": { "Id": 58, "Name": "test", "Type": "Weekend", "TripTermIds": null, "TripTermDtos": [ { "Id": 56, "Price": 0, "Name": "test", "DateFrom": "2025-04-27T12:54:03.4Z", "DateTo": "2025-04-30T12:54:05.019Z", "ParticipantsCurrent": 0, "ParticipantsTotal": 0, "TripId": 58, "TripDto": null } ], "SurveyId": null, "SurveyDto": null, "ArticleId": 63, "ArticleDto": null, "TripApplicationIds": null, "TripApplicationDtos": [] } }

-> TripTermDtos in TripDto are assigned correctly but the ArticleDto is null

-> TripDto in TripTermDtos in null in client but correct on server side

-> TripTermIds is null

here are mappings:

public class ArticleMappingProfile : Profile
{
    public ArticleMappingProfile()
    {
        CreateMap()
            .ForMember(
                dest => dest.TripDto,
                opt => opt.MapFrom(
                    src => src.Trip))
            .ReverseMap();

        CreateMap()
            .ForMember(
                dest => dest.Trip,
                opt =>
                    opt.MapFrom(src => src.TripDto))
            .ReverseMap();

    }
}
public class TripMappingProfile : Profile
{
    public TripMappingProfile()
    {
        CreateMap()
            .ForMember(dest => dest.TripApplicationDtos,
                opt => opt.MapFrom(
                    src => src.TripApplications))
            .ForMember(dest => dest.TripApplicationIds,
                opt => opt.MapFrom(
                    src => src.TripApplications.Select(ta => ta.Id)))
            .ForMember(dest => dest.TripTermIds,
                            opt => opt.MapFrom(
                                src => src.TripTerms.Select(tt => tt.Id)))
            .ForMember(dest => dest.TripTermDtos,
                opt => opt.MapFrom(
                    src => src.TripTerms))
            .ForMember(dest => dest.ArticleId,
                opt => opt.MapFrom(src => src.ArticleId))
            .ForMember(dest => dest.ArticleDto,
                opt => opt.MapFrom(
                    src => src.Article))
            .ReverseMap();

        CreateMap()
            .ForMember(dest => dest.TripApplications,
                opt => opt.MapFrom(
                    src => src.TripApplicationDtos))
            .ForMember(dest => dest.TripTerms,
                opt => opt.MapFrom(
                    src => src.TripTermDtos))
            .ForMember(dest => dest.Article,
                opt => opt.MapFrom(
                    src => src.ArticleDto))
            .ReverseMap();
    }
}

how Article and Trip is created [service]:

public async Task CreateArticle(ArticleDTO articleDto)
{
    if (articleDto == null)
    {
        throw new ArgumentNullException(nameof(articleDto), "ArticleDTO cannot be null");
    }
    var articleEntity = mapper.Map
(articleDto); if (articleDto.TripDto != null) { var t = articleDto.TripDto; t.ArticleDto = articleDto; mapper.Map(t); } var newArticle = await articleRepository.CreateArticle(articleEntity); if (articleDto.ArticleCategory == ArticleCategory.Wyprawy) { if (articleDto.TripDto == null) { throw new ApplicationException("Trip given in the article is null but should not be"); } if (newArticle.Trip == null) { throw new ApplicationException("Trip in article is null but should not be"); } newArticle.TripId = newArticle.Trip.Id; newArticle.Trip.ArticleId = newArticle.Id; await articleRepository.UpdateArticle(newArticle); } var articleWithIncludes = await articleRepository.GetArticleDetails(newArticle.Id); var article = mapper.Map(articleWithIncludes); return article; }

[repository]:

public async Task
CreateArticle(Article article) { await sfDbContext.Articles.AddAsync(article); await sfDbContext.SaveChangesAsync(); return article; }

[update used in creation[:

public async Task
UpdateArticle(Article article) { sfDbContext.Articles.Update(article); await sfDbContext.SaveChangesAsync(); return article; }

[Program.cs]

class Program
{
    static async Task Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // services config
        ConfigureServices(builder);

        // build app
        var app = builder.Build();

        // middleware config
        ConfigureMiddleware(app);

        // register default user
        await RegisterDefaultUser(app);

        // ensures app is running
        app.Run();
    }

    private static async Task RegisterDefaultUser(WebApplication app)
    {
        var scope = app.Services.CreateScope();
        var userService = scope.ServiceProvider.GetRequiredService();
        /.../
       
    }

    private static void ConfigureServices(WebApplicationBuilder builder)
    {
        
        builder.Services.Configure(options =>
        {
            options.MultipartBodyLengthLimit = 209715200;
        });
        
        builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
                options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
            });

        builder.Services.AddAutoMapper(typeof(Program)); 
        builder.Services.Configure(builder.Configuration.GetSection("JwtSettings"));

        builder.Services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                var keyBytes = Convert.FromBase64String(builder.Configuration["JwtSettings:Key"]);
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
                    ValidAudience = builder.Configuration["JwtSettings:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(keyBytes)
                };
            });

        builder.Services.AddDbContext(options =>
            options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
        
        // add services
        builder.Services.AddScoped();
        
        builder.Services.AddScoped();
        builder.Services.AddScoped();

        builder.Services.AddScoped();
        builder.Services.AddScoped();

        builder.Services.AddScoped();
        builder.Services.AddScoped();

        builder.Services.AddScoped();
        builder.Services.AddScoped();

        builder.Services.AddScoped();

        builder.Services.AddCors(options =>
        {
            options.AddPolicy("AllowAngularApp", policy =>
            {
                policy.WithOrigins("http://localhost:4200")
                    .AllowAnyHeader()
                    .AllowAnyMethod();
            });
        });

        builder.Services.AddAuthorization();
        builder.Services.AddControllers();
        
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();
    }

    private static void ConfigureMiddleware(WebApplication app)
    {
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseCors("AllowAngularApp");
        app.UseStaticFiles();
        app.UseRouting();
        
        app.UseAuthentication(); 
        app.UseAuthorization();

        app.MapControllers();
    }
}

on client side i have data-types.ts where i have all DTOs. i am using signal store:

type SfArticleState = {
  articles: ArticleDTO[];
  article: ArticleDTO | undefined;
  loading: boolean;
  categoryFilter: ArticleCategory | undefined;
  error: any;
};

const initialState: SfArticleState = {
  articles: [],
  article: undefined,
  loading: false,
  categoryFilter: undefined,
  error: null,
};

const selectId: SelectEntityId = (article) => article.Id ?? -1;

export const ArticleStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withEntities(),
  withProps(() => ({
    articleService: inject(ArticleService),
  })),
  withComputed((store) => ({
    trips: computed(() =>
      store
        .articles()
        .filter((a) => !!a.TripDto)
        .map((a) => a.TripDto!),
    ),
  })),
  withMethods((store) => ({
    async createArticle(article: ArticleDTO) {
      patchState(store, { loading: true });
      const createArticleApiCall$ = store.articleService.createArticle(article);
      const createdArticle = await firstValueFrom(createArticleApiCall$);
      patchState(store, addEntity(createdArticle, { selectId }));
      patchState(store, { loading: false });
    },

    /.../

  })),

the

createdArticle 

from above returns wrong data.

service method used in store:

public createArticle(articleDto: ArticleDTO) {
  return this.http.post(
    this.apiUrl + '/createArticle',
    articleDto,
  );
}

no idea what i am doing wrong.. what is missing? i suppose in angular there is something wrong ;<

i tried changing mapping mainly and experimenting with it. did migrations and updated db.

i want to know why mapping is selective - TripTermDtos are on client side - but the rest not.