在API中返回数据时防止Lazy属性初始化

本文关键字:Lazy 属性 初始化 API 返回 数据 | 更新日期: 2023-09-27 18:20:54

我遇到了Lazy问题。情况是:我有具有Lazy属性的DataLayer模型。每个模型都可以具有惰性属性,该属性返回其他模型的列表,这些模型可以具有惰性特性。例如:

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
}

如果我在C#中使用它,我没有问题。我可以从user对象中获取用户的公司,也可以从company对象中获取公司中的所有用户
但是当我在我的API 中使用相同的对象时,有趣的事情发生了

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

由于无休止的递归,我得到了StackOverflow异常(User对象返回初始化的惰性公司,Company返回用户的惰性列表,依此类推)

所以我开始在API 中使用Anonym对象

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

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

它解决了这个问题,但我已经开始寻找其他解决方案,因为我认为这不是处理它的最佳方法。
所以我继续寻找替代方案。我又找到了一个:封装和继承。

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
}

我有这些类的包装器来返回所有值

public class UserFull : User
{
}
public class CompanyFull  : Company
{
}

并返回User/Company而不是UserFull/CompanyFull
现在的问题是:

有其他方法可以解决这个问题吗

如果有任何建议,我将不胜感激。
Thanx:)


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

在API中返回数据时防止Lazy属性初始化

这里的问题是,当从api返回时,您的导航属性将永远不会执行。它们必须预先执行才能返回数据。将您的返回类型更改为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();
   }
}

发生了什么:

当您从API返回一个对象时,必须将其序列化为"跨线发送"。为了进行这种序列化,对象的EVERY属性将被评估并转换为JSON(或您使用的任何序列化)。因为每个属性都被访问,所以会评估导航属性,从而触发延迟加载。

正如您正确指出的,循环导航属性会导致StackOverflowException。

如何修复:

有几种方法可以解决这个问题(匿名对象就是其中之一),但作为一个优秀的开发人员,最好有一个强类型的返回。创建ViewModel!

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

我强烈推荐AutoMapper,因为它使实体和ViewModels之间的转换非常容易。

使用AutoMapper:

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

这会变得更好。因为AutoMapper会自动映射相同名称的属性,所以您可以利用这些导航属性

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

现在,当你返回公司时,你不会得到StackOverflowException,但你仍然会得到Users!

缺点(如果你可以这么称呼的话)是,你需要小心使用ViewModels,以确保没有循环引用。幸运的是,您可以为当前ViewModel无法表示的情况创建更多的ViewModel。