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.