ASP.net MVC控制器 - 构造函数用法

37

我正在开发一个ASP.net MVC应用程序,对于我的控制器使用构造函数有疑问。

我正在使用Entity Framework和linq to Entities进行所有的数据事务。几乎所有的控制器操作都需要访问我的Entity模型。当我开始编写应用程序时,我在每个Action方法的开头创建实体对象,执行必要的工作,然后返回结果。

我意识到我为每个操作方法创建了相同的对象,因此为每个控制器创建了一个私有成员变量的实体对象,并在其构造函数中实例化它。现在,每个方法只引用该私有成员变量来执行其工作。

我仍然对哪种方式是正确的有疑问。我想知道:A)哪种方法最合适?B)在构造函数方法中,这些对象存在多长时间?C)构造函数方法是否存在性能/完整性问题?


你只需要这个对象用于一个控制器吗? - Andrei Andrushkevich
也许你应该使用单例模式,私有静态实例和延迟加载获取实例的属性。 - Andrei Andrushkevich
2个回答

33

你正在问一些正确的问题。

A. 在每个操作方法中创建这些依赖项肯定不合适。MVC 的一个主要特性是能够分离关注点。通过将这些依赖关系加载到控制器中,您使控制器变得臃肿。它们应该注入到控制器中。有各种各样的依赖注入(DI)选项。通常,这些类型的对象可以注入到构造函数或属性中。我更喜欢构造函数注入。

B. 这些对象的生命周期将由垃圾收集器确定。GC 不是确定性的。因此,如果您有连接到资源受限服务(数据库连接)的对象,则可能需要确保自己关闭这些连接(而不是依赖于 dispose)。许多时候,“生命周期”关注点会被分离到一个反转控制(IOC)容器中。有很多选择。我的首选是 Ninject。

C. 实例化成本可能是最小的。数据库事务成本可能是您想要关注的重点。有一个称为“工作单位”的概念,您可能需要了解一下。从本质上讲,一个数据库可以处理超过只保存/更新操作的事务。增加事务大小可以带来更好的数据库性能。

希望这能让您开始。


1
尝试使用NINJECT进行依赖注入! - Omkar
1
“确保自己关闭这些连接(而不是依赖于dispose)”- 我强烈反对。IDisposable控制器由ControllerFactory处理,而不是由GC处理,它们的处理是确定性的。有关详细信息,请参见此问题:https://dev59.com/8XM_5IYBdhLWcg3wcCnc。 - Alex

31
RCravens有些很棒的见解。我想展示一下如何实现他的建议。
最好从定义一个数据访问类接口开始实现:
public interface IPostRepository 
{
    IEnumerable<Post> GetMostRecentPosts(int blogId);
}

然后实现一个数据类。Entity Framework上下文的构建成本很低,但是当您不处理它们时,可能会出现不一致的行为,因此我通常发现最好将所需的数据提取到内存中,然后处理上下文。

public class PostRepository : IPostRepository
{
    public IEnumerable<Post> GetMostRecentPosts(int blogId)
    {
        // A using statement makes sure the context is disposed quickly.
        using(var context = new BlogContext())
        {
            return context.Posts
                .Where(p => p.UserId == userId)
                .OrderByDescending(p => p.TimeStamp)
                .Take(10)
                // ToList ensures the values are in memory before disposing the context
                .ToList(); 
        }
    }
}

现在您的控制器可以将这些存储库之一作为构造函数参数接受:
public class BlogController : Controller
{
    private IPostRepository _postRepository;
    public BlogController(IPostRepository postRepository)
    {
        _postRepository = postRepository;
    }

    public ActionResult Index(int blogId)
    {
        var posts = _postRepository.GetMostRecentPosts(blogId);
        var model = new PostsModel { Posts = posts };
        if(!posts.Any()) {model.Message = "This blog doesn't have any posts yet";}
        return View("Posts", model);
    }

}

MVC允许您使用自己的控制器工厂代替默认工厂,这样您就可以指定像Ninject这样的IoC框架来决定如何创建控制器。您可以设置注入框架,使其知道当您请求一个IPostRepository时,它应该创建一个PostRepository对象。
这种方法的一个重要优点是它使得您的控制器可单元测试。例如,如果您想确保在没有帖子时模型能够收到消息,您可以使用Moq这样的模拟框架来设置一个场景,其中您的存储库返回零个帖子。
var repositoryMock = new Mock<IPostRepository>();
repositoryMock.Setup(r => r.GetMostRecentPosts(1))
    .Returns(Enumerable.Empty<Post>());
var controller = new BlogController(repositoryMock.Object);
var result = (ViewResult)controller.Index(1);
Assert.IsFalse(string.IsNullOrEmpty(result.Model.Message));

这使得测试你控制器操作中期望的特定行为变得容易,无需设置数据库或任何特殊的内容。像这样的单元测试易于编写、确定性强(其通过/失败状态基于代码,而非数据库内容),且速度快(通常可以在一秒钟内运行数千个这样的测试)。


3
你如何将参数传递给构造函数?我使用相同的方式,只是在构造函数中我只有 myRepository = new MyRepository() - Cody
@DoctorOreo:正如我在帖子中所说,MVC允许您指定控制器工厂。您可以创建一个控制器工厂,该工厂知道在创建控制器时传递存储库。或者,您可以使用现有的依赖注入框架,设置存储库的绑定,并要求MVC请求DI框架创建控制器。 - StriplingWarrior
@StriplingWarrior:你的例子只有在有IoC容器的情况下才能工作吗?还是当我们请求IPostRepository时,MVC框架会自动找到PostRepository的实例呢? - user203687
@user203687:您需要使用某种自定义工厂来设置ASP.NET MVC,该工厂知道如何创建您的控制器 - 默认情况下,它只会调用默认构造函数。您可以编写自己的控制器工厂来定义如何构造每个控制器。但是,大多数DI框架都有插件,让您轻松地将它们与ASP.NET连接起来,因此一般情况下,只有当他们连接了IoC容器时,您才会看到人们使用这种模式。 - StriplingWarrior
实际上,我在一个项目中尝试使用这种方法,但它不允许我执行控制器。显然,在MVC中,控制器中的构造函数必须是无参数的。这是版本问题吗?为什么会出现这个消息? - Victor_Tlepshev
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接