Angular 19 возвращает частично правильные DTO с сервера, хотя все данные сервера верныC#

Место общения программистов C#
Ответить
Anonymous
 Angular 19 возвращает частично правильные DTO с сервера, хотя все данные сервера верны

Сообщение Anonymous »

Возврат в контроллер показывает «в основном» правильные данные, но при возврате на стороне клиента он равен нулю.
Я использую Angular 19, .NET Core и EF Core. Сопоставление кажется выборочным.
Я создаю статью, которая может иметь свойство Trip. Поездка может иметь некоторые вложенные свойства.
Когда я ее создаю, в конце создания статьи сервер возвращает правильное сопоставление, кроме идентификаторов, в то время как на стороне клиента часть из них равна нулю.

Код: Выделить всё

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; }
}
класс модели:

Код: Выделить всё

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; }
}
класс модели:

Код: Выделить всё

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();
}
Это метод контроллера CreateArticle:

Код: Выделить всё

[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);
}
возврат Ok(статья) во время отладки:

Код: Выделить всё

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"
из кода выше видно, что ArticleDto в TripDto назначен (не null), TripTermDtos в TripDto назначены (и TripDto в нем тоже) - все вроде должно быть.
при этом HTTP-ответ со стороны клиента возвращает нулевые значения:

Код: Выделить всё

{
"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
    в TripDto назначены правильно, но ArticleDto имеет значение null
  • в TripTermDtos имеет значение null на клиенте, но правильное на стороне сервера
  • Код: Выделить всё

    TripTermIds
    имеет значение null
Вот сопоставления:

Код: Выделить всё

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();
}
}
Вот как статьи и Trip создаются в классе обслуживания:

Код: Выделить всё

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;
}
Репозиторий:

Код: Выделить всё

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

return article;
}
Обновление, использованное при создании:

Код: Выделить всё

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();
}
}
Я понятия не имею, что я делаю не так... чего не хватает?
В основном я пробовал менять маппинг и экспериментировать с ним. Выполнил миграцию и обновил базу данных.
Я хочу знать, почему сопоставление является выборочным — TripTermDtos находится на стороне клиента, а остальные — нет.

Подробнее здесь: https://stackoverflow.com/questions/795 ... -server-da
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C#»