3

假设我想发出一个更新房屋状态的发布请求,理想情况下,这些数据应该在某种服务层中,通常这涉及

  1. 验证用户 - 他们仍然活跃还是被管理员踢出?
  2. 检查 houseid - houseid/记录是否有效?
  3. 用户可以看到房子的详细信息吗?
  4. 将状态更新为“打开”或“关闭”

在现实世界/复杂领域中 - 大多数视图都非常复杂,我们必须抛出该区域的房屋数量,房屋评论数量,房屋详细信息等,也许房屋未完成任务的数量...

简而言之-上述所有代码都可能在服务层内,但是可以说抛出异常,用户无法更新房屋的状态-现在要填充视图,您必须首先(再次)获取房屋详细信息,加载将您刚刚在服务层中加载的所有其他内容所有这些都放在控制器中,或者对加载此数据的服务层进行其他创新...

如何通过运行验证和各种方式来确保我的域模型受到保护,而无需多次重写相同的代码......

此代码在操作方法内,它很容易在服务层内......

//注意:_repo 是对 linq to sql 的简单抽象...

    [HttpGet]
    public ActionResult TaskDetail(int houseid, int taskid)
    {
        var loggedonuser = _repo.GetCurrentUser();

        var _house = _repo.Single<House>(x => x.HouseID == houseid && x.Handler == loggedonuser.CompanyID);

        if (_house == null)
            throw new NoAccessException();

        var summary = _house.ToSummaryDTO();

        var companies = _repo.All<Company>();
        var users = _repo.All<User>();

        var task = _repo.Single<HouseTask>
            (x => x.HouseID == _house.HouseID && x.TaskID == taskid && (x.CompanyID == loggedonuser.CompanyID));

        var dto = new TaskDTO
        {
            TaskID = task.TaskID,
            Title = task.Title,
            Description = task.Description,
            DateCreated = task.DateCreated,
            IsClosed = task.IsClosed,
            CompanyID = companies.Where(y => task.CompanyID == y.CompanyID).SingleOrDefault().Identifier,
        };

        if (task.DueDate.HasValue)
            dto.DueDate = task.DueDate.Value;

        var comments = _repo.All<HouseTaskComment>()
            .Where(x => x.TaskID == task.TaskID)
            .OrderByDescending(x => x.Timestamp)
            .Select(x => new TaskCommentDTO
            {
                Comment = x.Comment,
                Timestamp = x.Timestamp,
                CompanyID = companies.Where(y => x.CompanyID == y.CompanyID).SingleOrDefault().Identifier,
                UserID = users.Where(y => x.UserID == y.UserID).SingleOrDefault().Login,
                Type = EnumHelper.Convert<TaskCommentType>(x.Type)
            });

        dto.AllComments = comments;

        return View(new TaskViewModel
        {
            Summary = summary,
            TaskDetail = dto,
            NewComment = new TaskCommentDTO()
        });
    }

简而言之-获取摘要的房屋详细信息,获取任务详细信息(从多个可用任务中)并获取任务评论。恕我直言,这是一个简单的观点,没什么太复杂的。

此时用户可以: 添加评论、关闭/打开任务 - 如果他们有权这样做(为简单起见省略了代码)、设置任务截止日期,甚至清除任务的截止日期。

现在UpdateTaskStatus - 如果不能更新状态必须返回上面的视图,类似于评论,如果你不能评论请返回详细视图——评论可能被关闭。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult TaskDueDate(int houseid, int taskid)
{

    var duedate = Request.Form["duedate"];
    var duetime = Request.Form["duetime"];
    try
    {
        if (ModelState.IsValid)
        {

            var newduedate = DateHelper.GoodDate(duedate, duetime);
            _service.SetTaskDueDate(houseid, taskid, newduedate);

            return RedirectToAction("TaskDetail");
        }
    }
    catch (RulesException ex)
    {
        ex.CopyTo(ModelState);
    }

    var loggedonuser = _repo.GetCurrentUser();

    var _house = _repo.Single<House>(x => x.InstructionID == houseid && x.HandlerID == loggedonuser.CompanyID);

    if (_house == null)
        throw new NoAccessException();

    var summary = _house.ToSummaryDTO();

    var companies = _repo.All<Company>();
    var users = _repo.All<User>();

    var task = _repo.Single<HouseTask>
        (x => x.InstructionID == _house.HouseID && x.CompanyID == loggedonuser.CompanyID && x.TaskID == taskid);

    var dto = new TaskDTO
    {
        TaskID = task.TaskID,
        Title = task.Title,
        Description = task.Description,
        DateCreated = task.DateCreated,
        IsClosed = task.IsClosed,
        CompanyID = companies.Where(y => task.CompanyID == y.CompanyID).SingleOrDefault().Identifier
    };

    if (task.DueDate.HasValue)
        dto.DueDate = task.DueDate.Value;

    var comments = _repo.All<HouseTaskComment>()
        .Where(x => x.TaskID == task.TaskID)
        .OrderByDescending(x => x.Timestamp)
        .Select(x => new TaskCommentDTO
        {
            Comment = x.Comment,
            Timestamp = x.Timestamp,
            CompanyID = companies.Where(y => x.CompanyID == y.CompanyID).SingleOrDefault().Identifier,
            UserID = users.Where(y => x.UserID == y.UserID).SingleOrDefault().Login
        });

    dto.AllComments = comments;

    return View("TaskDetail", new TaskViewModel
    {
        Summary = summary,
        TaskDetail = dto,
        NewComment = new TaskCommentDTO()
    });
}

我知道上面的代码结构很糟糕,但是对于如何纠正它的一些建议将不胜感激。

  1. 我将所有只读代码留在操作中,因为每个视图可能不同,我不希望服务层干扰这里
  2. 我想“保护”我的更新/编辑并将其保存在我的服务层或核心项目(单独的 c# 类库)甚至域层中,我将如何编写代码处理验证,(这是我在服务中所做的调用),执行实际保存?

我听说过 CommandHandler 方法,这是一个好方法吗?理想情况下,我想在我的域中使用一种简单的方法而不是控制器操作来保持我的验证+持久性....

4

2 回答 2

8

也许你有点想多了

据我了解您的流程,这是应该完成的方式

  1. 用户验证(无论它是否仍然是有效用户)在用户登录时完成,而不是稍后进行,因为未注册(或被踢出的用户)应该无法访问您的应用程序。如果您的用户在工作过程中被踢出局,您可以通过创建一个操作过滤器并将其置于以下任一位置来解决此问题:

    • 你所有的控制器类(不是动作)

    • 有一个基本控制器类并在其上放置过滤器

  2. 您的步骤 2,3 和 4 实际上是相关的,因为

    • 首先,您必须获得房屋数据(包括许可)

    • 如果房屋 ID 无效,您将不会从 DB 获得任何回报

    • 如果您确实获得了数据,请检查权限

    • 在允许时相应地设置状态

补充说明

对于复杂的域模型,通常不需要复杂的视图。您可能只是拥有更多它们并且具有更复杂的导航以帮助可用性。与复杂的视图相比,用户也会更乐意(呃)使用简单的视图。

模型验证

我会像 Asp.net MVC 实现的那样保持静态验证——在你的 POCO 对象层上使用数据注释——因为它是内置的,并且会减少你的代码(更少的行数 = 更小的错误表面),它会使它更健壮,因为你不会不小心忘记验证一些东西。

但是您的动态验证(与实体实例的特定用户/公司权限相关)最好保留在您的服务层中。如果发生任何违规行为,我会抛出自定义异常并将它们传播到模型状态(类似于您正在做的事情 - 基于我对代码的理解 - 使用RulesException类)。

为了避免 try/catch 代码块的繁琐(和重复代码),您可以编写一个自定义异常过滤器,将值传播到模型状态并返回显示模型状态错误所需的任何视图。这种异常过滤器的实现完全取决于您。然后只需将它放在您的父控制器上(因为您没有使用具有全局过滤器的 MVC 3)并忘记它。这也将减少重复代码。

如果将动态模型验证放入服务层,则必须将用户所需的详细信息传递给它,因为它依赖于它们。

用户获取

我可以看到您每次都从数据存储中读取用户数据,这是不希望的。大多数时候,您可能只需要用户 ID(显然还需要公司 ID)。这少量数据可以以不同的方式保存(通常保存在安全的 cookie 中,但您可以自行决定策略),这样可以节省一些时间。使此缓存数据无效是相当简单的,因为用户可能只能更改他们自己的数据,因此您可以在该点无效并重新读取。您可以将其与您经常需要的其他数据(可能是用户显示名或登录名)捆绑在一起。但是,它。大多数情况下,您的代码将使用用户 ID。

模型声明/权限验证

您的模型不仅仅是验证模型实体数据是否正确,还需要验证声明(或权限),即特定用户是否具有更改实体权限(取决于他们的公司)。你说你不能把它放在过滤器级别,但我可以看到你的问题。如果您保留您的用户数据(如前所述),您将拥有所需的所有信息。你有用户数据,你有你的模型,并且基于动作,你还知道与该动作相关的静态声明。

[RequirePermission(Permission.UpdateTaskDueDate)]
public ActionResult TaskDueDate(...) { ... }

如果动作过滤器不能做到那么简单,您还可以为其提供一个自定义验证器类,该类实现了某个接口(因此过滤器可以调用任何接口)和应该验证的参数名称。这将可以根据用户/公司数据验证自定义类。它还允许您使用不同的验证器验证多个权限,甚至是相同/不同的权限。

[RequirePermission(Permission.UpdateTaskDueDate, typeof(TaskValidator), "paramName")]
public ActionResult TaskDueDate(...) { ... }

此验证器的Validate方法将获取权限枚举值并验证它的要求。

还有另一种可能。您可以在定义其自定义验证器的实体类上拥有一个自定义属性(或更多属性),因此过滤器仍将仅采用应验证的权限类型。这也将使验证单个操作上的多个对象变得容易。只需提供权限,过滤器就会检查操作参数、它们的类型和声明的自定义验证器。

拥有过滤器几乎不调用的自定义验证器也可以让您的验证保持在服务层而不是呈现中。因此,如何验证那些使 UI 独立于底层代码的方法取决于您的域逻辑。万一发生任何改变,只有您的输入和输出类型应该匹配,但所有 UI 层代码实际上应该保持原样。

读取请求数据

而不是使用Request.Form["duedate"]您读取数据,而应该将它们作为操作参数。它们将由 MVC 为您填充。因为在您的代码中:

try
{
    if (ModelState.IsValid)
    {

        var newduedate = DateHelper.GoodDate(duedate, duetime);
        _service.SetTaskDueDate(houseid, taskid, newduedate);

        return RedirectToAction("TaskDetail");
    }
}
catch (RulesException ex)
{
    ex.CopyTo(ModelState);
}

if声明完全是多余的。您的整数参数在动作主体中将始终有效(它们不会设置为可为空)。无论您DateHelper.GoodDate是否静态检查另一对,它们都可以更好地包含在自定义类中(TaskId也可以),并且您可以在其上放置数据注释。并且模型验证实际上具有无效的能力(因此if声明是有意义的)。

但是,您不必在您的TaskDueDate操作中重复相同的代码,您可以这样做:

[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.UpdateTaskDueDate)]
public ActionResult TaskDueDate(int houseid, TaskDue taskDueData)
{
    if (!this.ModelState.IsValid)
    {
        // don't repeat code and just call another action within this controller
        return this.TaskDetail(houseid, taskDueData.TaskId);
    }
    _service.SetTaskDueDate(houseid, taskid, newduedate);
    return RedirectToAction("TaskDetail");
}

因为你在这两个动作中做同样的事情。没有人说你不能在动作中调用其他动作。ActionResult反正他们都回来了。

我想我们都同意这个动作要简单得多。

动作方法代码

我不会将所有这些_repo调用放在控制器操作中,因为那是服务域代码。您所要做的就是提供参数并调用服务层以提供视图使用的对象。

结语

我希望我至少回答了你的一些问题/问题/问题,因为我很难理解实际问题是什么(或者更好地说明你到底在问什么)。您以一种相当混乱的方式编写了您的问题,因此没有得到您问题的答案。人们不明白你的问题是什么。

反正。我试图重构你的代码并解释如果我必须实现动态验证我会怎么做。

于 2011-08-04T14:09:37.007 回答
0

您可以从标准的 FaultException 继承并将 HouseDetails 属性添加到 HouseDTO 类型的 if 中吗?

于 2011-08-04T13:55:34.660 回答