Lokki
Lokki

Reputation: 382

Prevent Lazy properties initialization when data is returned in API

I've come across Lazy problem. The case is: I have DataLayer Models with Lazy properties. Each model can have lazy property that returns a list of other model, that can have lazy properties. For example:

public class User
{
    private Guid id;
    public Guid Id { 
       get { return id;}
       set{ 
          id=value; 
          lazyCompany=new Lazy<Company>(()=>GetCompanyByUserId(value));
       }
    }
    public string Name {get; set;}
    public Company Company =>lazyCompany?.Value // lazy returns user's company
    private Lazy<Company> lazyComapany;
}

public class Company
{
    public int Id {get; set;}
    public string Name {get; set;}
    public List<User> Users // lazy returns list of users in company
}

If I use it in C# I have no problems. I can get user's company from User object and I can get all users in company from Company object.
BUT the funny things happen when I use the same objects in my API

[HttpGet]
public dynamic User(Guid userId)
{
   return GetUser(userId);
}

I'm getting StackOverflow exception because of endless recursion (User object returns initialized lazy Company and Company returns lazy list of Users and so on)

So I've started using Anonym objects in API

[HttpGet]
public dynamic User(Guid userId)
{
   var res=GetUser(Guid userId);
   return new {res.Id, res.Name};
}

Or

    [HttpGet]
    public dynamic Company(Guid companyId)
    {
       var res=GetCompany(int companyId).Select(s=>new {s.Id, s.Name}).ToList();
       return res;
    }

It solved the problem but I've started looking for other solutions, because I think it's not the best way to handle it.
So I continued to look for alternative solution. I found one more: Encapsulation and Inheritance.

public class User
{
    public Guid Id {get; set;}
    public string Name {get; set;}
    protected Company Company // lazy returns user's company
}

public class Company
{
    public int Id {get; set;}
    public string Name {get; set;}
    protected List<User> Users // lazy returns list of users in company
}

and I have the wrapper for these classes to return all values

public class UserFull : User
{
}

public class CompanyFull  : Company
{
}

and return User/Company instead of UserFull/CompanyFull
NOW the question:

Are there other ways to solve this problem?

I'll appreciate any suggestion.
Thanx :)


public User GetUser(Guid userId)
{
    using (var c = ConnectionToDataBase())
       {
           return new User(c.Users.FirstOrDefault(u=>u.Id==userId));
       }
}

Upvotes: 0

Views: 131

Answers (2)

Vlad274
Vlad274

Reputation: 6844

What is happening:

When you return an object from an API, it has to be serialized to be "sent across the wire". In order to do this serialization, EVERY property of the object is evaluated and translated into JSON (or whatever serialization you use). Because every property is accessed, Navigation properties are evaluated, triggering lazy loading.

As you correctly noted, circular Navigation properties cause StackOverflowExceptions.

How to fix it:

There are several ways to fix this (anonymous objects is one), but as a good developer it'd be nice to have a strongly typed return. Create a ViewModel!

public class UserViewModel
{
    public Guid Id {get; set;}
    public string Name {get; set;}
}

I highly recommend AutoMapper as it makes translating between Entities and ViewModels very easy.

Using AutoMapper:

[HttpGet]
public UserViewModel User(Guid userId)
{
   var res=GetUser(Guid userId);
   return Mapper.Map<UserViewModel>(res);
}

This gets even better. Because AutoMapper automatically maps between properties of the same name, you can leverage those Navigation Properties

public class CompanyViewModel
{
    public int Id {get; set;}
    public string Name {get; set;}
    public List<UserViewModel> Users
}

Now when you return the Company, you don't get a StackOverflowException but you still get the Users!

The downside (if you could call it that) is that you need to be careful with your ViewModels to ensure that there are no circular references. Fortunately, you can just create more ViewModels for situations that your current ViewModels can't represent.

Upvotes: 1

Stephen Brickner
Stephen Brickner

Reputation: 2602

The problem here is that your navigation properties will never execute when returned from the api. They would have to be pre-executed to return the data. Change your return type to UserFull.

//return full data
public UserFull GetUser(Guid userId)
{
    using (var c = ConnectionToDataBase())
    {
        var user = c.Users
              .Where(u => u.Id==userId)
              .Select(u => new UserFull
              {
                  Id = u.Id,
                  Name = u.Name,
                  Company = u.Company //force execution
              }).FirstOrDefault();

        return user;
    }
}

//return partial data
public dynamic GetUser(Guid userId)
{
    using (var c = ConnectionToDataBase())
    {
        var user = c.Users
              .Where(u => u.Id==userId)
              .Select(u => new 
              {
                  Id = u.Id,
                  Name = u.Name,
              }).FirstOrDefault();

        return user;
    }
}

[HttpGet, Route("api/users/{userId:guid}")]
public IHttpActionResult User(Guid userId)
{
   try
   {
       var res = GetUser(Guid userId);
       return Ok(res);
   }
   catch
   {
        return InternalServerError();
   }
}

Upvotes: 1

Related Questions