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

Discussão em 'Angular' iniciado por wicked.cosmos, Abril 26, 2025.

  1. Return in controller shows "mostly" correct data but when returning it on client side is null.

    I use Angular 19, .NET Core and EF Core. The mapping seems to be selective.

    I create an Article which can have a Trip property. The 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 class:

    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<int> TripTermIds { get; set; }
    public ICollection<TripTermDTO> 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<int> TripApplicationIds { get; set; }
    public ICollection<TripApplicationDTO> TripApplicationDtos { get; set; }
    }


    Trip model class:

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


    This is the CreateArticle controller method:

    [HttpPost("createArticle")]
    public async Task<ActionResult<ArticleDTO>> 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} "<p>test</p>"
    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<TripApplicationDTO>} Count = 0
    TripApplicationIds = {ICollection<int>} null
    TripTermDtos = {List<TripTermDTO>} 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<int>} null
    Type = {TripType} Weekend
    TripId = {int} 58
    Url = {string} "test"


    but on the client side, I get:

    {
    "Id": 63,
    "Title": "test",
    "Url": "test",
    "BackgroundImageUrl": "http://localhost:5000/uploads/7c1ebd63-6b78-4027-af44-d49e6d0febd0.jpg",
    "Content": "<p>test</p>",
    "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 the mappings:

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

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

    }
    }

    public class TripMappingProfile : Profile
    {
    public TripMappingProfile()
    {
    CreateMap<Trip, TripDTO>()
    .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<TripDTO, Trip>()
    .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();
    }
    }


    This is how Article and Trip are created in a service class:

    public async Task<ArticleDTO> CreateArticle(ArticleDTO articleDto)
    {
    if (articleDto == null)
    {
    throw new ArgumentNullException(nameof(articleDto), "ArticleDTO cannot be null");
    }

    var articleEntity = mapper.Map<Article>(articleDto);

    if (articleDto.TripDto != null)
    {
    var t = articleDto.TripDto;
    t.ArticleDto = articleDto;
    mapper.Map<Trip>(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<ArticleDTO>(articleWithIncludes);

    return article;
    }


    Repository:

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

    return article;
    }


    Update used in creation:

    public async Task<Article> 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<IUserService>();
    /.../
    }

    private static void ConfigureServices(WebApplicationBuilder builder)
    {
    builder.Services.Configure<FormOptions>(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<JwtSettings>(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<SfDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

    // add services
    builder.Services.AddScoped<IAuthService, AuthService>();

    builder.Services.AddScoped<ITripRepository, TripRepository>();
    builder.Services.AddScoped<ITripService, TripService>();

    builder.Services.AddScoped<IUserService, UserService>();
    builder.Services.AddScoped<IUserRepository, UserRepository>();

    builder.Services.AddScoped<IArticleService, ArticleService>();
    builder.Services.AddScoped<IArticleRepository, ArticleRepository>();

    builder.Services.AddScoped<ISurveyService, SurveyService>();
    builder.Services.AddScoped<ISurveyRepository, SurveyRepository>();

    builder.Services.AddScoped<ITripApplicationRepository, TripApplicationRepository>();

    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 the client side, I have data-types.ts where I defined 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<ArticleDTO> = (article) => article.Id ?? -1;

    export const ArticleStore = signalStore(
    { providedIn: 'root' },
    withState(initialState),
    withEntities<ArticleDTO>(),
    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<ArticleDTO>(
    this.apiUrl + '/createArticle',
    articleDto,
    );
    }


    I have 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 the database.

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

    Continue reading...

Compartilhe esta Página