C
C#2y ago
ronkpunk

Unit Testing, AcionResult Metadata Properties and JsonSerializer.Deserialize [Answered]

Hi all, I'm trying to add Unit Test to my WebAPI. I choose to call my endpoints with an httpclient and make my asserts on deserialized response. Now, it'm almost my first Unit Test experience so I don't know if I'm right or not so my first question is. Should it is an approach? After that, when I try to deserialize my reponse, all content contains metadata properties that, in Array cases, change completely the type of property (object with { "$id": "1", "$values": [<the-array>]), so deserialize not work if I specify the response type. Is there a way to avoid/ignore metadata properties and just return my plain model? Now I'm trying to remove them through Regex Replace but (and here it is the third question), Regex found correctly my pattern with groups, I try to return Group[1].Value from my MatchEvaluator but Regex.Replace return exactly the input variable, without changes, any help? Thank you in advance
60 Replies
Pobiega
Pobiega2y ago
If you could show some code, this would be much easier. Can you show the controller, your tests, and perhaps some of your response models?
ronkpunk
ronkpunk2y ago
ok just a minute this is my controller method
[HttpGet]
[Route("")]
public ActionResult Get()
{
try
{
return this.Ok(this.iMapper.Map<UserGet>(this.CurrentUser));
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error getting current user");
return this.Problem(ex.ToString());
}
}
[HttpGet]
[Route("")]
public ActionResult Get()
{
try
{
return this.Ok(this.iMapper.Map<UserGet>(this.CurrentUser));
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error getting current user");
return this.Problem(ex.ToString());
}
}
Pobiega
Pobiega2y ago
$code
MODiX
MODiX2y ago
To post C# code type the following: ```cs // code here ``` Get an example by typing $codegif in chat If your code is too long, post it to: https://paste.mod.gg/
Pobiega
Pobiega2y ago
okay, and what does UserGet look like? any custom mapping going on or just normal stuff?
ronkpunk
ronkpunk2y ago
Unit Test
[Fact(DisplayName = "Recupero lista tenant")/*, TestPriority(2)*/]
public async Task TestGet()
{
var response = await this.httpHelper.Get("", otherParams); //it works
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeWithQualityHeaderValue.Parse("application/json; charset=utf-8"), response.Content.Headers.ContentType);

var myObj = await this.httpHelper.GetContent<List<UserGet>>(response);
Assert.NotNull(tenantResponse);
}
[Fact(DisplayName = "Recupero lista tenant")/*, TestPriority(2)*/]
public async Task TestGet()
{
var response = await this.httpHelper.Get("", otherParams); //it works
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(MediaTypeWithQualityHeaderValue.Parse("application/json; charset=utf-8"), response.Content.Headers.ContentType);

var myObj = await this.httpHelper.GetContent<List<UserGet>>(response);
Assert.NotNull(tenantResponse);
}
Pobiega
Pobiega2y ago
hm, I don't see any WAF or similar here, where do you start the API?
ronkpunk
ronkpunk2y ago
Models
public class UserGet
{
public int UserId { get; set; }
public string NameSurname { get; set; } = null!;
public string Email { get; set; } = null!;

public bool IsActive { get; set; }

public HashSet<UserTenantRoleGet> UserTenantRoles { get; set; }
}

public class UserTenantRoleGet
{
public int UserId { get; set; }
public string UserMail { get; set; }
public string UserName { get; set; }

public int TenantId { get; set; }
public Guid TenantGuid { get; set; }
public string TenantName { get; set; }

public int RoleID { get; set; }
public string RoleName { get; set; }

public List<AccessControlListGet> AccessControlList { get; set; }

}

public class AccessControlListGet
{
public int Id { get; set; }
public int RoleId { get; set; }
public string RoleName { get; set; }
public int EntityScopeId { get; set; }
public string EntityScopeName { get; set; }
public bool CanRead { get; set; }
public bool CanWrite { get; set; }

}
public class UserGet
{
public int UserId { get; set; }
public string NameSurname { get; set; } = null!;
public string Email { get; set; } = null!;

public bool IsActive { get; set; }

public HashSet<UserTenantRoleGet> UserTenantRoles { get; set; }
}

public class UserTenantRoleGet
{
public int UserId { get; set; }
public string UserMail { get; set; }
public string UserName { get; set; }

public int TenantId { get; set; }
public Guid TenantGuid { get; set; }
public string TenantName { get; set; }

public int RoleID { get; set; }
public string RoleName { get; set; }

public List<AccessControlListGet> AccessControlList { get; set; }

}

public class AccessControlListGet
{
public int Id { get; set; }
public int RoleId { get; set; }
public string RoleName { get; set; }
public int EntityScopeId { get; set; }
public string EntityScopeName { get; set; }
public bool CanRead { get; set; }
public bool CanWrite { get; set; }

}
Pobiega
Pobiega2y ago
what is httpHelper?
ronkpunk
ronkpunk2y ago
I've a BaseTest Unit with all theese things, dbContext, playgroundApplication and so on httpHelper is a custom class I made
Pobiega
Pobiega2y ago
well its hard to help you troubleshoot things when you are not showing where the stuff is happening 😛 http helper seems to be making the actual HTTP requests, so we need to see that
ronkpunk
ronkpunk2y ago
ahahah sorry, I'm writing it now
Pobiega
Pobiega2y ago
and perhaps show the raw response you get for the user endpoint?
ronkpunk
ronkpunk2y ago
the raw response it a HttpMessageResponse the content red as string is that {"$id":"1","UserId":2219,"NameSurname":"Namebd2547c4-879c-44b3-927a-5a5d19262187","Email":"Mailc5643a9a-74be-4395-add7-9cce004a68b4","IsActive":true,"UserTenantRoles":{"$id":"2","$values":[{"$id":"3","UserId":2219,"UserMail":"Mailc5643a9a-74be-4395-add7-9cce004a68b4","UserName":"Namebd2547c4-879c-44b3-927a-5a5d19262187","TenantId":2727,"TenantGuid":"92298313-27aa-437f-a454-6539be1699de","TenantName":"Name3c1cb7af-9050-489e-ae60-c224e260adf2","RoleID":1,"RoleName":"Admin","AccessControlList":{"$id":"4","$values":[{"$id":"5","Id":9,"RoleId":1,"RoleName":"Admin","EntityScopeId":1,"EntityScopeName":"Tenants","CanRead":true,"CanWrite":true},{"$id":"6","Id":10,"RoleId":1,"RoleName":"Admin","EntityScopeId":6,"EntityScopeName":"Updates","CanRead":true,"CanWrite":true},{"$id":"7","Id":11,"RoleId":1,"RoleName":"Admin","EntityScopeId":3,"EntityScopeName":"Tvs","CanRead":true,"CanWrite":true},{"$id":"8","Id":12,"RoleId":1,"RoleName":"Admin","EntityScopeId":2,"EntityScopeName":"Users","CanRead":true,"CanWrite":true},{"$id":"9","Id":13,"RoleId":1,"RoleName":"Admin","EntityScopeId":4,"EntityScopeName":"Media","CanRead":true,"CanWrite":true},{"$id":"10","Id":14,"RoleId":1,"RoleName":"Admin","EntityScopeId":5,"EntityScopeName":"Pages","CanRead":true,"CanWrite":true}]}}]}}
Pobiega
Pobiega2y ago
yeah but if you extract the string body yeah
ronkpunk
ronkpunk2y ago
(sorry, I'm a little slow to copy and paste)
Pobiega
Pobiega2y ago
and if you visit this endpoint with a normal browser, do you get the same json? or is the metadata only showing during tests?
ronkpunk
ronkpunk2y ago
yes I've tried with postman
Pobiega
Pobiega2y ago
yes to what 🙂
ronkpunk
ronkpunk2y ago
and I can see metadata properties and also calling API from frontend have theese properties
Pobiega
Pobiega2y ago
ah okay, sounds like there is a problem with the mapping then is that AutoMapper?
ronkpunk
ronkpunk2y ago
yep (i'm looking for its config)
Pobiega
Pobiega2y ago
hm, thats weird
public class UserGet
{
public int UserId { get; set; }
public string NameSurname { get; set; } = null!;
public string Email { get; set; } = null!;

public bool IsActive { get; set; }

public HashSet<UserTenantRoleGet> UserTenantRoles { get; set; }
}
public class UserGet
{
public int UserId { get; set; }
public string NameSurname { get; set; } = null!;
public string Email { get; set; } = null!;

public bool IsActive { get; set; }

public HashSet<UserTenantRoleGet> UserTenantRoles { get; set; }
}
given that model, there shouldn't be any metadata unless something funny is going on with the this.Ok Normally, you would type the controller action ie public ActionResult Get() would be
public ActionResult<UserGet> Get()
{ ...
public ActionResult<UserGet> Get()
{ ...
and just return the value directly, instead of manually wrapping it in an OK exceptions will automatically become a Problem, by the default http pipeline I gotta run, but will be back in ~30-60
ronkpunk
ronkpunk2y ago
ok, thank you so I've to change my Controller like that
[HttpGet]
[Route("")]
public ActionResult<UserGet> Get()
{
try
{
return this.iMapper.Map<UserGet>(this.CurrentUser);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error getting current tenant");
return this.Problem(ex.ToString());
}
}
[HttpGet]
[Route("")]
public ActionResult<UserGet> Get()
{
try
{
return this.iMapper.Map<UserGet>(this.CurrentUser);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error getting current tenant");
return this.Problem(ex.ToString());
}
}
HttpClientHelper.cs
public class HttpClientHelper
{
public async Task<HttpResponseMessage> Get(string route, otherParams)
{
return await this.Send(HttpMethod.Get, route, otherParams);
}
private async Task<HttpResponseMessage> Send(HttpMethod method, string? route, otherParams)
{
var request = new HttpRequestMessage(method,
this.GetRoute(this._routePrefix, route ?? ""));

using var client = this.GetClient(customHeaders);
return await client.SendAsync(request);
}

private HttpClient GetClient(TokenResponseDto accessToken = null, Dictionary<string, string> customHeaders = null)
{
var client = this._application.CreateClient();

client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

if (customHeaders != null && customHeaders.Any()) {
foreach (var customHeader in customHeaders) {
client.DefaultRequestHeaders.Add(customHeader.Key, customHeader.Value);
}
}
return client;
}

public async Task<T> GetContent<T>(HttpResponseMessage response)
{
var body = await response.Content.ReadAsStringAsync();

var regex = new Regex("(?:\\{\\\"\\$id\\\":\\\"[0-9]+\\\",\\\"\\$values\\\":(\\[[^\\]]+\\])\\})");
var matchEval = new MatchEvaluator((match) => {
return match.Groups[1].Value;
});
var newBody = regex.Replace(body, m => m.Groups[1].Value);
var model = JsonSerializer.Deserialize<T>(newBody);
return model;
}
}
public class HttpClientHelper
{
public async Task<HttpResponseMessage> Get(string route, otherParams)
{
return await this.Send(HttpMethod.Get, route, otherParams);
}
private async Task<HttpResponseMessage> Send(HttpMethod method, string? route, otherParams)
{
var request = new HttpRequestMessage(method,
this.GetRoute(this._routePrefix, route ?? ""));

using var client = this.GetClient(customHeaders);
return await client.SendAsync(request);
}

private HttpClient GetClient(TokenResponseDto accessToken = null, Dictionary<string, string> customHeaders = null)
{
var client = this._application.CreateClient();

client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

if (customHeaders != null && customHeaders.Any()) {
foreach (var customHeader in customHeaders) {
client.DefaultRequestHeaders.Add(customHeader.Key, customHeader.Value);
}
}
return client;
}

public async Task<T> GetContent<T>(HttpResponseMessage response)
{
var body = await response.Content.ReadAsStringAsync();

var regex = new Regex("(?:\\{\\\"\\$id\\\":\\\"[0-9]+\\\",\\\"\\$values\\\":(\\[[^\\]]+\\])\\})");
var matchEval = new MatchEvaluator((match) => {
return match.Groups[1].Value;
});
var newBody = regex.Replace(body, m => m.Groups[1].Value);
var model = JsonSerializer.Deserialize<T>(newBody);
return model;
}
}
I've tried to change returned type according with your suggestion but still get the same response
Pobiega
Pobiega2y ago
Back. Okay, so I find it weird that your response contains the$id property regardless of what you do
ronkpunk
ronkpunk2y ago
maybe Automapper config?
Pobiega
Pobiega2y ago
and we'll need to figure that out before moving on to testing Potentially. Do you have any automapper profiles loaded?
ronkpunk
ronkpunk2y ago
yep
Pobiega
Pobiega2y ago
okay, show me? actually, a faster way to rule that out is comment out the mapping, and just do return new UserGet(); maybe give it some values
ronkpunk
ronkpunk2y ago
this.CreateMap<TVC.Backend.Data.Models.User, UserGet>()
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Mail))
.ForMember(dest => dest.NameSurname, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.UserTenantRoles, opt => opt.MapFrom(src => src.UsersTenantsRoles))
.ForMember(dest => dest.IsActive, opt => opt.MapFrom(src => src.MailConfirmed.HasValue && src.MailConfirmed.Value))
.ReverseMap();
this.CreateMap<TVC.Backend.Data.Models.User, UserGet>()
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Mail))
.ForMember(dest => dest.NameSurname, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.UserTenantRoles, opt => opt.MapFrom(src => src.UsersTenantsRoles))
.ForMember(dest => dest.IsActive, opt => opt.MapFrom(src => src.MailConfirmed.HasValue && src.MailConfirmed.Value))
.ReverseMap();
where backend data model user is the EF model
Pobiega
Pobiega2y ago
right
ronkpunk
ronkpunk2y ago
UserGet is the "outer" model
Pobiega
Pobiega2y ago
this is all fine, not the source yeah the DTO
ronkpunk
ronkpunk2y ago
yep
Pobiega
Pobiega2y ago
actually, my current line of thinking is that this is because of a bad-configured json serializer in ASP what .NET/ASP version are you using?
ronkpunk
ronkpunk2y ago
Net 6
Pobiega
Pobiega2y ago
okay do you configure the json serializer at all? should be in your startup.cs file, most likely
ronkpunk
ronkpunk2y ago
i'm looking in my program.cs and so on
Pobiega
Pobiega2y ago
can you show that file? just make sure it doesnt contain any connectionstrings or tokens
ronkpunk
ronkpunk2y ago
builder.Services .AddControllers(options => { options.Filters.Add<AclActionFilter>(); }) .AddJsonOptions(options => { options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; options.JsonSerializerOptions.PropertyNamingPolicy = null; }); this is in my program.cs
Pobiega
Pobiega2y ago
alright, here we go
ronkpunk
ronkpunk2y ago
it there a way to use the shortcut to highlight the code?
Pobiega
Pobiega2y ago
wdym?
ronkpunk
ronkpunk2y ago
modix suggested me before but I've to search it in chat every time ^^
Pobiega
Pobiega2y ago
just type three backticks: ```cs <your code here> ```
ronkpunk
ronkpunk2y ago
I've italian layout and no backticks in my keyboard
Pobiega
Pobiega2y ago
oh. and a newline after the cs Im sure its there somewhere. On mine, its shift + the key left of backspace. (swedish layout) turns out there might not be, lol https://superuser.com/questions/667622/italian-keyboard-entering-tilde-and-backtick-characters-without-changin
Pobiega
Pobiega2y ago
ronkpunk
ronkpunk2y ago
yeah, Alt 96, thank you
Pobiega
Pobiega2y ago
you can edit your keyboard layout btw so you could add it 🙂
ronkpunk
ronkpunk2y ago
I'll try to add next time
Pobiega
Pobiega2y ago
its useful in progrmaming okay options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; its this line that adds the $id field
Pobiega
Pobiega2y ago
Pobiega
Pobiega2y ago
you should avoid having cyclic references in general its really bad practice :p
ronkpunk
ronkpunk2y ago
ok, I'll try to remove it but if I remember right, I added it to resolve other issues I'm trying to remove them but in some cases they return...
Pobiega
Pobiega2y ago
well, since you are using DTOs you should be mapping them away example:
public class Person
{
public Pet Pet { get; set; }
}

public class Pet
{
public Person Owner { get; set; }
}
public class Person
{
public Pet Pet { get; set; }
}

public class Pet
{
public Person Owner { get; set; }
}
this makes sense at a C# level but for json, if you tried serializing this you'd get a forever nested structure, as json doesnt support references (natively) instead, you'd probably rely on Ids, or jsut the structure itself if a pet can only have one owner, we can list them as an array under the person essentially just removing the Owner prop from the pet, when serializing thats a great usecase for a DTO EF navigation properties will often lead to this result which is why its important to map them to DTOs before returning
ronkpunk
ronkpunk2y ago
Yep, but in some case I had nested objects to avoid too much call from frontend and sometime it creates a loop I inherit the backend from old developer and trying to fix many things maybe this is the right time I resolve also this ^^
Pobiega
Pobiega2y ago
absolutely yes 😛
ronkpunk
ronkpunk2y ago
yep, I'll confirm that removing ReferenceHandler resolve this issue thank you for your help and for your patience
Accord
Accord2y ago
✅ This post has been marked as answered!