Today I’ve faced unexpected behavior during querying data and JSON deserialization while playing with Marten library which uses PostgreSQL Database as storage. So let’s imagine we have a very dummy 2 classes, one for saving data (in JSON format) and one class for the projection (as a response of some Web API for instance):
Origin class for data storing:
public class User
{
private User() { }
public User(int id, string name)
{
Id = id;
Name = name;
Status = "Pending";
}
public int Id { get; private set; }
public string Name { get; private set; }
public string Status { get; private set; }
}
Projection class
public class ReadOnlyUser
{
public string Name { get; set; }
public string Status { get; set; }
}
So as you see, the User class has private setters and due to MSDN Deserialization behavior, JSON Serializer ignores read-only properties. This means that this code will always create a User instance with Status Pending
var stringData = "{\"Id\": \"1\", \"Status\": \"Active\",\"Name\": \"John\"}";
var stream = new MemoryStream(Encoding.ASCII.GetBytes(stringData));
var textReader = new JsonTextReader(new StreamReader(stream));
var user = new JsonSerializer().Deserialize<User>(textReader);
Result:
Output object:
User Id: 1
User Name: John
User Status: Pending
So if we follow the example above, making a projection from such class, will produce the ReadOnlyUser which also will have a Status Pending:
var stringArrayData = "[{\"Id\": \"1\", \"Status\": \"Active\",\"Name\": \"John\"}]";
JsonSerializer serializer = new JsonSerializer();
var stream = new MemoryStream(Encoding.ASCII.GetBytes(stringArrayData));
var reader = new JsonTextReader(new StreamReader(stream));
var user = serializer
.Deserialize<IEnumerable<User>>(reader)
.Where(usr => usr.Id == "1")
.Select(usr => new ReadOnlyUser {Name=usr.Name, Status=usr.Status})
.FirstOrDefault();
Console.WriteLine($"\r\nOutput object:\r\nUser Name: {user.Name} \r\nUser Status: {user.Status}");
Result:
Output object:
User Name: John
User Status: Pending
But what if we are not querying from a string variable, but rather from the database? Most probably instead of IEnumerable, IQueryable interface will be used.
In a nutshell, IQueryable brings 1 huge benefit – it knows how to execute expressions on special data sources. (For instance Where statement will be implemented in the database, not on the memory side)
So, coming back to the previous example, this code will also return User with Status Pending, won’t it?
return await _session.Query<User>() //Returns IQueryable<User> from db
.Where(usr => usr.Id == "1")
.Select(usr => new ReadOnlyUser {Name=usr.Name, Status=usr.Status})
.FirstOrDefaultAsync();
Result:
Output object:
User Name: John
User Status: Active
So how did it happen, why Status is Active if the User has a private setter on that field?
In the first example, as we used IEnumerable, all of the operations are processed in our App memory. The order of commands was like that:
- Deserialize the JSON to IEnumerable list of Users (by using the only 1 public constructor which set Status to Pending without possibility to set it to Active because of private setter)
- Use Where statement to filter the Users with Id = “1”
- Use the Select projection for creating a new ReadOnlyUser
In the example with an IQueryable interface, steps 1 and 2 were “outsourced” to the database. So commands look so:
- Find by Object Type a proper table from which data should be fetched and create a query like “Select * from User”
- Add Where the statement “Where Id = ‘1’ ” to the query from step 1
- Execute a query in the database and return the result as a JSON object
- Use JsonSerializer for deserialization from JSON to ReadOnlyUser
So as you see, in the second scenario, many steps executed on the database side, but in that particular case, we gain even more profit because we could omit the limitation of Newtonsoft.Json of ignoring read-only fields during deserialization. In fact, we are not creating a User object but just go directly to the creation of ReadOnlyUser projection.
Please be aware, that every implementation of an IQueryable interface has its own rules of converting command and using expression tree. So before relying on “outsourcing processing”, just spend a few minutes to check what exactly will be executed.