65.9K
CodeProject 正在变化。 阅读更多。
Home

VITA – .NET 应用程序的强大灵活 ORM 和其他构建块

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (29投票s)

2015 年 2 月 24 日

CPOL

40分钟阅读

viewsIcon

91606

downloadIcon

2006

介绍 VITA 开源 ORM 和 .NET 应用程序框架的关键功能,并提供使用 MVC/AngularJS/WebApi/VITA 技术栈的示例 SPA。

背景

VITA 是由 Roman Ivantsov 开发的开源 ORM 和 .NET 应用程序框架。有些朋友可能知道 Roman 是强大的 Irony 解析器和 .NET 语言实现工具包的开发者。我曾在几个项目中使用 Irony 取得了很好的效果。

可以说,我曾经是 Entity Framework 的拥趸。我在不少项目中使用过 Entity Framework,但多数情况并非我的选择。尽管我能够利用 EF 完成工作,但在开发更高效、更具可伸缩性的应用程序时,我一直对 EF 有很多不满。我一直在寻找一个更易用的框架,曾尝试过 NHibernate 等 ORM 和 Dapper 等微型 ORM。

去年年末,我听说 VITA,并发现该框架对我所需的功能最具潜力,甚至激励我写下了这篇文章!

为什么选择 VITA?

为什么您应该考虑 VITA 并继续阅读本文并深入研究?市面上有很多其他的 ORM。对我而言,在我深入了解的过程中,有两个原因引起了我的兴趣:

  • ORM - 总体而言,我认为 VITA 在与 Entity Framework 和 NHibernate 等主流全功能 ORM 相比时,表现出色。本文将仅介绍 VITA 的基本功能。我不会将 VITA 与其他 ORM 进行比较,但可能会在后续文章中进行。
  • 构建块 - 创建实际的、与数据连接的 .NET 应用程序,需要的不仅仅是一个 ORM。VITA 提供了许多不同的构建块,您可以轻松选择添加通常在这些应用程序中需要的功能。本文将介绍这些构建块的基本功能。

对我而言,一个有效的 ORM 和非常有用的构建块的结合(以及它们从一开始的构思和设计方式)使得 VITA 独具特色。

VITA 简介

我将简要概述本文将要介绍或提及的关键 VITA 功能。请参阅 VITA github 站点 上的文档和示例,以获取更全面的 VITA 功能和能力信息。

全功能 ORM

VITA 的核心是一个全功能 ORM(与 Dapper 等轻量级 ORM 不同),它包含您期望从 ORM 中获得的绝大多数功能。一些关键功能包括:

  • 数据模型管理 – 用于定义数据模型和管理相应的数据库架构。
    • 实体(数据映射对象) – 您可以使用 .NET 接口轻松地在代码中定义数据模型,无需可视化设计器或复杂的映射。您的架构将以“代码优先”的方式自动创建(也支持“数据库优先”)。
    • 实体和属性特性 – 您可以利用各种实体或属性级别的特性,来精确地定制您的数据模型和相关的架构(表、列、关系、索引等)。
    • 自动架构更新 – 在“代码优先”方法中,架构更新会根据您当前的数据模型(实体接口)自动为您管理(保留数据)。请参阅下面的“继续使用 VITA”部分,以利用现有数据库信息实现“数据库优先”方法。
    • 数据库类型 – 您可以选择使用多种支持的数据库类型来实现您的数据模型,包括 SQL Server、MySQL、PostgreSQL、SQLite。
    • 计算属性和视图 – 除了定义架构的实体和属性,您还可以定义任意数量的计算属性和视图,以根据需要转换数据以供应用程序使用。
  • 数据管理和访问 – 用于以各种可想象的方式管理和访问数据。
    • 存储过程 – CRUD 操作的存储过程会自动为您生成和更新。如果您愿意,也可以使用 SQL(也自动生成)或在数据库不支持存储过程时(SQL CE、SQLite)使用。
    • 自动和延迟加载关联实体和关联列表 – 完全支持一对多和多对多关系。
    • LINQ - 完全支持 LINQ 来查询您的数据。翻译的 LINQ 查询会被缓存以提高效率。
    • 自跟踪数据 – 创建和更新数据非常方便,因为您的数据会自动注册更新,并组织成事务。在编辑过程中,会自动为您维护新/现有键以及原始/修改的属性。
    • 数据缓存 – 直接支持数据缓存,包括全表缓存,以及针对内存表中自动 LINQ 查询的执行。
  • 打包组件 – 您所有的数据管理功能都整齐地打包到一个实体模块中,该模块可以单独测试并在多个应用程序中使用。

Web API 支持

除了是全功能 ORM 外,VITA 还与 Web API 完全集成,使您能够轻松创建有效的 RESTful 数据服务。一些关键功能包括:

  • Web 调用上下文 – 轻松访问与 Web 相关的参数和响应信息。
  • 会话 – 完全支持公共和安全的 Web 会话。
  • 客户端故障 – 扩展功能,可根据验证或其他问题轻松提供对 HTTP 友好的响应代码和消息。
  • 基类 API 控制器 – 一个易于使用的构建块,供您构建 VITA 增强的 RESTful 数据服务。此构建块提供了上述功能以及与其他服务(如错误日志记录)的集成。

Slim API

VITA 现在提供了一种更简单、更 streamlined 的方式来为您的应用程序提供 RESTful Web 服务。您可以在核心库中轻松添加 slim API 控制器(无需 ASP.NET Web API 包依赖),并在中心位置整合业务逻辑。然后,在 UI 应用程序中利用 slim API 服务将变得非常方便。

标准实体模块

除了使用核心数据模型构建应用程序外,您还可以向应用程序添加任意数量的内置模块,这些模块会自动提供一些强大的功能。其中包括:

  • 日志记录模块 – 此模块提供了一种无缝的方式来为应用程序添加自动日志记录功能。您可以选择特定类型的日志记录,如错误、SQL 操作和事务、Web API 调用以及一般事件。包括自动添加事务日志记录的跟踪列的功能。持久化会话现在已包含在日志模块中。
  • 登录 – 此模块提供高级的注册和登录功能。您可以轻松地将登录与您自己的核心用户或登录表集成,以增强注册、跟踪等功能。

授权框架

这个强大的框架提供了定义和实现所有关键授权规则的手段,这些规则决定了谁可以访问什么数据。授权规则可以通过实体资源、数据过滤器、权限、活动和用户角色轻松配置。规则可以使用各种访问类型进行配置,甚至可以定义到属性级别。

通过示例学习 VITA

我将通过可下载和试用的工作示例,引导您了解 VITA 的各种功能。每个示例都是一个 MVC/AngularJS SPA,它进行 Web API 调用,而 Web API 调用又利用 VITA 框架。示例使用 MS SQL 数据库,但您可以根据需要轻松切换到其他支持的数据库类型。

下面是一个 VITA 基础部分,随后是两个深入探讨部分。每个部分都有一个单独的示例解决方案和下载。这样组织起来,您可以在阅读一个部分后选择试用 VITA,然后再回到下一部分学习更多内容。

本文中的代码可能会精简自示例下载中的代码,因此请打开示例应用程序以更全面地了解情况。

VITA 基础

为了理解 VITA 的基础知识,本节的目标是使用最少的 VITA 核心功能来运行一套非常简单的测试和一个非常基础的 Web 应用程序。请按照“基础示例解决方案”下载进行操作,其中包含以下项目:

  • Basic.VITA – 核心 VITA 管理的数据模型及相关打包。
  • Basic.Tests – 一些基本的 CRUD 测试,用于测试您的 VITA 管理的数据模型。创建一个匹配 `DbConnectionString` 配置值的测试 MS SQL 数据库。
  • Basic.DS – 一个 Web API 服务库及相关材料。
  • Basic.UI – 一个 MVC/AngularJS 单页 Web 应用程序,利用 Web API 服务。创建一个匹配 `DbConnectionString` 配置值的 MS SQL 数据库。

基础示例

我们的基础示例仅包含建筑物和房间,我们希望能够管理基本的建筑物和房间数据。我们期望的数据库架构如下所示:

数据模型

让我们创建一个数据模型并将其打包以供应用程序使用(有关详细信息,请参阅 `Basic.VITA` 项目)。定义一个数据模型来生成上述架构非常简单。我们需要:

  • 为每个表定义一个实体 `interface`。接口必须具有 `Entity` 特性才能成为有效的数据模型接口。我们将为 `Building` 和 `Room` 创建接口。
  • 将基本表列定义为实体接口属性。我们将添加上面所有非外键属性。
  • 为每个实体定义主键。我们将使用 Guid 主键,并将 `PrimaryKey` 和 `Auto` 特性添加到该属性,以实现自动生成的 Guid 键。
  • 在两端定义关系:建筑物的(仅获取)房间列表,以及房间对建筑物的引用。

以下是此数据模型的接口:

    [Entity]
    public interface IRoom
    {
        [PrimaryKey, Auto]
        Guid Id { get; set; }

        int Number { get; set; }

        IBuilding Building { get; set; }
    }

    [Entity]
    public interface IBuilding
    {
        [PrimaryKey, Auto]
        Guid Id { get; set; }

        string Name { get; set; }

        IList<IRoom> BuildingRooms { get; }
    }

就是这样!VITA 具有默认规则,用于将接口中的类型和名称映射到架构。您可以轻松覆盖任何这些默认规则,我们将在后面的部分介绍。

在我看来,那些通过具体类定义数据模型的 ORM 已经过时了。当今时代,我们需要使用可伸缩、松耦合的架构来构建实际的应用程序。我们需要将数据用作可模拟、可依赖注入的服务、工厂和其他库的一部分。如果我们的核心数据模型用接口来描述,这将容易得多!通过接口,您不会暴露 ORM 实现的复杂性,也不会开始将非数据相关细节打包到数据模型中。

我们很快就会开始访问我们这个小数据模型的 C# 数据,并演示 `BuildingRooms` 属性将如何根据 `IRoom` 中的 `Building` 引用自动填充。但首先,我们需要做一些配置工作。

实体模块和应用程序

好的,我们有了数据模型,然后呢?VITA 提供了一种有效的方式来打包您的数据相关功能,以便在您的应用程序中轻松使用。

首先,您创建一个或多个 `EntityModules`。`EntityModule` 本质上是相关实体的自包含组。设置实体模块本质上是将相关实体组注册到 `EntityArea`,我们通过注册我们的两个实体来做到这一点:

    public class DomainModule: EntityModule
    {
        public DomainModule(EntityArea area) : base(area, "DomainModule")
        {
            area.RegisterEntities( typeof(IBuilding)
                , typeof(IRoom));
        }
    }

那么,什么是 `EntityArea` 呢?`EntityArea` 本质上是注册到数据库架构的实体组(一个或多个模块)。我们将在稍后设置一个。

为了有效地利用我们所有的数据相关功能,我们将所有内容打包到一个 `EntityApp` 中。在我们的 `DomainApp` 中,我们添加一个区域(指定区域名称和架构名称),然后用该区域设置我们的模块。您可以根据需要定义和使用多个区域和模块。

    public class DomainApp: EntityApp
    {
        public DomainApp()
        {
            var domainArea = this.AddArea("Domain");
            var mainModule = new DomainModule(domainArea);
        }
    }

在我们的示例中,我们将所有这些打包到一个类库中(有关详细信息,请参阅 `Basic.VITA` 项目),以便我们可以轻松地配置和使用具有 VITA 管理的实体的应用程序。

访问和管理您的数据

现在我们想使用我们方便的小 `DomainApp` 组件,创建我们的架构,并进行一些实际的数据操作!让我们看看如何设置一些测试来执行这些操作(有关详细信息,请参阅 `Basic.Tests` 项目)。

为了运行一些测试,我们需要在测试开始时设置我们的 `DomainApp` 以供实际使用。设置 `EntityApp` 以供使用只有 3 个简单步骤:创建、初始化和连接(到您的数据库)。对于测试,我决定设置一个基类测试,在测试运行开始时设置 `DomainApp`(当然,您可以选择为每个测试执行此操作)。

    [TestClass]
    public abstract class BaseUnitTest
    {
        [AssemblyInitialize]
        public static void Initialize(TestContext testContext)
        {
            // set up application
            var protectedSection = (NameValueCollection)ConfigurationManager.GetSection("protected");
            DomainApp = new DomainApp();
            DomainApp.Init();
            var connString = protectedSection["MsSqlConnectionString"];
            var driver = MsSqlDbDriver.Create(connString);
            var dbOptions = MsSqlDbDriver.DefaultMsSqlDbOptions;
            var dbSettings = new DbSettings(driver, dbOptions, connString, modelUpdateMode: DbModelUpdateMode.Always);
            DomainApp.ConnectTo(dbSettings);
        }

        protected static DomainApp DomainApp { get; set; }
    }

当 `EntityApp` 被初始化并连接时,您的数据库(架构、存储过程等)将被创建/更新。请检查这些项是否在您的数据库中创建。

现在,请查看执行 `IRoom` 实例的 CRUD 操作的以下测试:

        [TestMethod]
        public void RoomCRUDTest()
        {
            IEntitySession session1 = DomainApp.OpenSession();
            IEntitySession session2;

            // create Room
            IRoom room1 = session1.NewEntity<IRoom>();
            room1.Number = 221;
            room1.Building = session1.NewEntity<IBuilding>();
            room1.Building.Name = "Building 1";

            session1.SaveChanges();
            Assert.IsNotNull(room1, "Create and save of IRoom item failed.");

            // read Room
            session2 = DomainApp.OpenSession();
            IRoom room2 = session2.GetEntity<IRoom>(room1.Id);
            Assert.IsNotNull(room2, "Retrieval of new IRoom item failed.");
            Assert.IsTrue(RoomTest.CompareItems(room1, room2), "Retrieved IRoom item match with created item failed.");

            // search Room
            session2 = DomainApp.OpenSession();
            room2 = (from i in session2.EntitySet<IRoom>()
                     where i.Number == room1.Number
                     select i).FirstOrDefault();
            Assert.IsNotNull(room2, "Search of new IRoom item failed.");

            // update Room
            room1.Number = 222;
            room1.Building.Name = "Building 1a";
            session1.SaveChanges();
            session2 = DomainApp.OpenSession();
            room2 = session2.GetEntity<IRoom>(room1.Id);
            Assert.IsNotNull(room2, "Retrieval of updated IRoom item failed.");
            Assert.IsTrue(RoomTest.CompareItems(room1, room2), "Retrieved IRoom item match with updated item failed.");

            // delete Room
            session1.DeleteEntity<IBuilding>(room1.Building);
            session1.DeleteEntity<IRoom>(room1);
            session1.SaveChanges();
            session2 = DomainApp.OpenSession();
            room2 = session2.GetEntity<IRoom>(room1.Id);
            Assert.IsNull(room2, "Delete of IRoom item failed.");
        }

我们首先为 `DomainApp` 打开一个我们将用于执行 CRUD 操作的会话。我们在测试中使用第二个会话来验证更新操作。

创建项目

请参阅上面测试中的“创建房间”部分。我们使用 `NewEntity` 调用来创建一个 `IRoom`,它将被跟踪为一个已创建(新)的项目。

    IRoom room1 = session1.NewEntity<IRoom>();

您可以根据需要填充 `IRoom`,然后保存会话的更改。如果您在调试器中,您会注意到在保存之前,`IRoom` 处于“已加载”状态,并且具有自动生成的 Guid,在保存之后,`IRoom` 处于“已加载”状态。

您会注意到一个 `IBuilding` 也被创建了。

    room1.Building = session1.NewEntity<IBuilding>();

我们所要做的就是设置对新 `IBuilding` 的引用。更改会自动跟踪,并且像外键值这样的细节会由我们处理。真方便!

如果您在编辑的数据中遇到问题,只需调用 `CancelChanges()` 即可轻松撤销会话中的更改。

检索项目

请参阅上面测试中的“读取房间”部分。使用 `GetEntity` 调用轻松按主键获取项目。在调试器中,您会注意到检索到的 `IRoom` 处于“已加载”状态。

搜索项目

请参阅上面测试中的“搜索房间”部分。这只是一个简单的 LINQ 查询,用于按房间号检索 `IRoom`。通过完整的 LINQ 支持,您可以构建复杂的查询,包括连接和分组、分页等。

更新项目

请参阅上面测试中的“更新房间”部分。由于您的实体是自跟踪的,我们所要做的就是更新任何我们想要的属性(包括引用)。完成后,保存会话的更改。

在调试器中,您会注意到更新的 `IRoom` 在保存更改之前处于“已修改”状态,在保存之后处于“已加载”状态。

删除项目

请参阅上面测试中的“删除房间”部分。使用 `DeleteEntity` 调用即可轻松标记项目以供删除。

    session1.DeleteEntity<IBuilding>(room1.Building);
    session1.DeleteEntity<IRoom>(room1);

 

与之前一样,完成后保存更改。在调试器中,您会注意到更新的 `IRoom`(及关联的 `IBuilding`)在保存更改之前处于“正在删除”状态,在保存之后处于“幻象”状态。

处理数据库更改

如果您还没有这样做,请查看您数据库中的生成架构。现在我们想做一些更改。

请继续向您的数据模型添加一些属性。在我这里,我向 `IRoom` 添加了 `Name` 和 `Capacity`。

    [Entity]
    public interface IRoom
    {
        [PrimaryKey, Auto]
        Guid Id { get; set; }

        int Number { get; set; }

        string Name { get; set; }

        int Capacity { get; set; }
       
        IBuilding Building { get; set; }
    }

我在“创建房间”测试中进行了相应的添加,以填充这些新属性。

    room1.Name = "My Room";
    room1.Capacity = 77;

继续重新运行测试。在测试运行期间,当 `EntityApp` 被初始化并连接时,架构更改(添加)会自动应用,并且测试通过。

继续添加和删除(您添加的内容)实体,然后重新运行测试。VITA 会自动应用您的更改。您可能遇到的唯一问题是,如果更改违反了现有数据的约束。我们稍后将处理这个问题。

在继续之前,请撤销您所有的更改。

RESTful Web 服务

VITA 框架与 MS Web API 框架完全集成,我们希望创建一些 RESTful 服务,以便我们在需要的地方使用它们。让我们开始在一个数据服务项目中构建一些服务(有关详细信息,请参阅 `Basic.DS` 项目)。请注意,我们将在下一个示例中介绍 slim API。

API 控制器

我们希望为我们数据模型中的每个实体构建一个 API 控制器来提供服务。使用 VITA 的 `BaseApiController` 可以轻松完成此过程。我们将逐步介绍为 `IRoom` 创建控制器。

    public class RoomsController : BaseApiController
    {
    }

请查看以下方法来搜索 `IRoom` 项目:

        [HttpGet, Route("api/rooms")]
        public QueryResults<RoomDto> GetRooms([FromUri] RoomQuery query)
        {
            var session = OpenSession();
            query = query ?? new RoomQuery();
            if (query.Take == 0) query.Take = 10;
            
            // Build where clause
            Guid buildingId;
            Guid.TryParse(query.BuildingId, out buildingId);
            var where = session.NewPredicate<IRoom>();
            where = where
                .AndIf(query.Number != 0, i => i.Number == query.Number.Value)
                .AndIf(buildingId != Guid.Empty, i => i.Building.Id == buildingId);
            
            // Build order by
            Dictionary<string, string> orderByMapping = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase)
            {
                { "id", "Id" },
                { "number", "Number" },
                { "building_name", "Building.Name" },
            };
            
            QueryResults<RoomDto> results = new QueryResults<RoomDto>(session.ExecuteSearch(where, query, i => i.ToDto(), null, orderByMapping));
            results.CanCreate = true;
            
            return results;
        }

VITA 提供了一个方便的 `ExecuteSearch` 方法,用于根据您的输入条件提供分页数据。第一步是使用 `NewPredicate` 来构建一个具有可选条件的 where 子句,如 `RoomQuery` 中所示。谓词构建提供了许多扩展方法,如 `True` 和 `AndIfNotEmpty`,用于构建 where 子句。接下来,构建一个用于排序的字典,其中字典键是排序名称,值是排序属性(可以是深度属性,如 `Building.Name`)。然后,调用 `ExecuteSearch` 会提供一个排序结果页面(结果包括总项目数)。请注意,输入查询有一个指定排序属性的 order by 属性。order 属性可以是字典中以逗号分隔的名称列表(在名称后附加 -desc 指定降序排序)。

我将不在此处显示详细信息,但在示例中,您会找到用于传输和提供数据的其他支持类:

  • 查询类(如 `RoomQuery`) – 这些类仅使传递搜索方法的可选搜索条件更加简单,如上面 `query` 参数所示。
  • DTO 类(如 `RoomDto`) – 这些类是数据模型实体的具体表示。属性基本上与相应接口(如 `IRoom`)中的属性匹配。
  • DTO 扩展(如 `RoomDtoExtensions`) – 这些扩展用于方便地将数据从 VITA 传输到 dto 对象等价物,如上面 `ToDto` 调用所示。

请查看以下控制器方法来获取 `IRoom` 项目:

       [HttpGet, Route("api/rooms/{id}")]
        public RoomDto GetRoom(Guid id)
        {
            var session = OpenSession();
            var item = session.GetEntity<IRoom>(id);
            if (item == null)
            {
                WebContext.CustomResponseStatus = HttpStatusCode.BadRequest;
                WebContext.ResponseBody = String.Format("Room with ID '{0}' not found.", id);
                return null; 
            }
            RoomDto itemDto = item.ToDto(true);
            Type[] blockingEntities;
            itemDto.CanDelete = itemDto.CanDelete && session.CanDeleteEntity<IRoom>(item, out blockingEntities);
            return itemDto;
        }

这里我们使用 `GetEntity` 通过 id 获取 `IRoom`。内置的 `WebContext` 属性和扩展方法可以轻松地提供具有预期错误代码和消息的 HTTP 友好响应。

这里要强调的一个功能是 `CanDeleteEntity` 调用。这个内置方法会检查是否有任何外键引用会阻止成功删除此项目。我创建了 `CanDelete` 属性,UI 可以利用它来禁用删除,如果它会导致外键冲突。

请查看以下控制器方法来创建和更新 `IRoom` 项目:

        [HttpPost, Route("api/rooms")]
        public RoomDto CreateRoom(RoomDto item)
        {
            return CreateUpdateRoom(item, create: true);
        }
        
        [HttpPut, Route("api/rooms")]
        public RoomDto UpdateRoom(RoomDto item)
        {
            return CreateUpdateRoom(item, create: false);
        }
        
        private RoomDto CreateUpdateRoom(RoomDto item, bool create)
        {
            var session = OpenSession();
            item.Validate(OpContext);
            OpContext.ThrowValidation(); //will throw if any faults had been detected; will return BadRequest with list of faults in the body
            IRoom updateItem;
            if (create)
            {
                updateItem = session.NewEntity<IRoom>();
            }
            else
            {
                updateItem = session.GetEntity<IRoom>(item.Id);
                OpContext.ThrowIfNull(updateItem, ClientFaultCodes.ObjectNotFound, "Room", "Room with ID '{0}' not found.", item.Id);
            }
            if (create)
            {
                
            }
            updateItem.Number = item.Number;
            updateItem.Building = session.GetEntity<IBuilding>(item.BuildingId);
            session.SaveChanges();
            return updateItem.ToDto(true);
        }

与测试示例一样,我们使用 `NewEntity` 或 `GetEntity` 来创建或获取要更新的 `IRoom`。VITA 提供了对内置操作上下文 `OpContext` 的扩展,可以轻松管理客户端故障并提供响应。DTO 类 `Validate()` 方法使用了一些验证扩展(下面的验证是针对 `IBuilding` 的)。

        public void Validate(OperationContext context)
        {
            context.ValidateNotEmpty(Name, "Name", "Name may not be empty.");
            context.ValidateMaxLength(Name, 50, "Name", "Name text is too long.");
        }

如果检测到任何故障,`ThrowValidation()` 扩展会抛出异常,并返回带有故障列表的 BadRequest。您还可以使用 `ThrowIfNull()` 等扩展来抛出特定的客户端故障。这里,如果请求更新的项目未找到,我们则抛出故障。

与测试示例一样,调用 `SaveChanges()` 来保存对自跟踪实体的更改,如果需要,则调用 `CancelChanges()`。

最后,请查看以下控制器方法来删除 `IRoom` 项目:

        [HttpDelete, Route("api/rooms/{id}")]
        public void DeleteRoom(Guid id)
        {
            var session = OpenSession();
            var item = session.GetEntity<IRoom>(id);
            OpContext.ThrowIfNull(item, ClientFaultCodes.ObjectNotFound, "Room", "Room with ID '{0}' not found.", id);
            session.DeleteEntity(item);
            session.SaveChanges();
        }
        }

与测试示例一样,我们使用 `DeleteEntity()` 来删除 `IRoom`,并使用 `SaveChanges()` 来执行实际的删除。与更新情况一样,如果请求删除的项目未找到,我们会抛出客户端故障。

请查看示例下载中的整体 API 控制器和支持类。

打包和配置

将 Web API 服务打包到一个类库中,以便根据需要进行配置和使用是一个好主意。配置服务的核心代码如下:

   public static class DomainWebApiConfig
    {
        public static void Register(HttpConfiguration <a>config</a>)
        {
            var app = DomainAppConfig.SetupApp();
            WebHelper.ConfigureWebApi(config, app);
            config.EnsureInitialized();
        }
    }

    public static partial class DomainAppConfig
    {
        public static DomainApp SetupApp()
        {
            // set up application
            var protectedSection = (NameValueCollection)ConfigurationManager.GetSection("protected");
            var domainApp = new DomainApp();
            domainApp.Init();
            var connString = protectedSection["MsSqlConnectionString"];
            var driver = MsSqlDbDriver.Create(connString);
            var dbOptions = MsSqlDbDriver.DefaultMsSqlDbOptions;
            var dbSettings = new DbSettings(driver, dbOptions, connString, modelUpdateMode: DbModelUpdateMode.Always);
            domainApp.ConnectTo(dbSettings);
            
            return domainApp;
        }
    }

请注意,`EntityApp` 的设置几乎与测试示例相同。

AngularJS 应用程序

本文并非关于构建 UI 应用程序,但我们需要构建一个基础应用程序来真正展示 VITA 的功能,尤其是 Web API 支持的功能。我将展示一些代码片段让您了解应用程序的结构,然后让您深入研究。

UI 应用程序是一个基本的管理单页应用程序。此应用程序的关键元素是:

  • MVC Home 控制器/视图 – 这是一个 mvc 应用程序,有一个 Home 控制器来呈现主页或主视图。这不是必需的 MVC 应用,因为 Angular 将用于完成所有工作,但我认为这是一个好主意,以防您将来需要传统的 MVC 控制器来执行某些功能。Angular 也与这些控制器配合良好。
  • AngularJS 模块 – 管理应用程序的整体模块,包含 angular 控制器、服务和(模板)视图等。
  • AngularJS 控制器、服务和模板 – 我们数据模型中的每个实体都有一个控制器、一个服务和一组模板,以提供管理该实体的 UI 功能。Angular 服务将向我们的 VITA 数据服务发出 Web API 调用,而数据服务将管理数据。

现在让我们开始看一些 UI 代码(有关详细信息,请参阅 `Basic.UI` 项目)。

利用我们的 Web API 服务

为了利用我们的类库 Web API 服务,我们只需要在 `MvcApplication`(global.asax)启动时注册我们的 `DomainWebApiConfig`。

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            DomainWebApiConfig.Register(GlobalConfiguration.Configuration);
           
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }

Angular 模块

在构建 Angular SPA 时,我个人非常喜欢 UI 路由和状态管理功能,这些功能可以轻松管理和更新多个视图。因此,这是示例应用程序采用的方法。

以下是 Angular 模块(`DomainApp.js`)的精简视图,仅包含与房间相关的信息:

var DomainApp = angular.module('DomainApp', ['ui.router', 'ui.bootstrap', 'angularValidator', 'ngCookies']);

// add controllers, services, and factories
DomainApp.controller('RoomsController', RoomsController);
DomainApp.service('RoomsService', RoomsService);

var configFunction = function ($stateProvider, $httpProvider, $locationProvider) {
   
    $stateProvider
        .state('roomSearch', {
            url: '/rooms?number&buildingId&orderBy&descending&page&pageSize',
            views: {
                "searchView": {
                    templateUrl: '/Templates/rooms/Search.html',
                    controller: RoomsController
                }
            }
        })
        .state('roomResults', {
            url: '/rooms/Results?number&buildingId&orderBy&descending&page&pageSize',
            views: {
                "detailView": {
                    templateUrl: '/Templates/rooms/Results.html',
                    controller: RoomsController
                }
            }
        })
        .state('roomGet', {
            url: '/rooms/get?id',
            views: {"detailView": {
                    templateUrl: '/Templates/rooms/Get.html',
                    controller: RoomsController
                }
            }
        })
        .state('roomCreate', {
            url: '/rooms/create?buildingId',
            views: {
                "detailView": {
                    templateUrl: '/Templates/rooms/Create.html',
                    controller: RoomsController
                }
            }
        })
        .state('roomUpdate', {
            url: '/rooms/update?id',
            views: {
                "detailView": {
                    templateUrl: '/Templates/rooms/Update.html',
                    controller: RoomsController
                }
            }
        })
        .state('roomDelete', {
            url: '/rooms/delete?id',
            views: {
                "detailView": {
                    templateUrl: '/Templates/rooms/Delete.html',
                    controller: RoomsController
                }
            }
        })
        .state('home', {
            url: '/'
        });
}
configFunction.$inject = ['$stateProvider', '$httpProvider', '$locationProvider'];

DomainApp.config(configFunction);

对于这种 UI 路由和状态管理方法,主页面有两个视图会被更新:`searchView` 和 `detailView`。对于每个状态,您需要定义 URL 和要更新的视图。对于每个视图,您需要定义模板的来源和要加载的 Angular 控制器。

Angular 控制器

请查看以下 Angular 控制器,它支持执行 `IRoom` 项目 CRUD 操作的视图。调用相应的服务函数来执行操作(下面的代码被精简以仅显示搜索,其他函数类似):

var RoomsController = function($scope, $stateParams, $state, $window, $location, RoomsService, BuildingsService) {
    // data for search operations
    $scope.searchQuery = {
        number: Number($stateParams.number) || 0,
        buildingId: $stateParams.buildingId || "00000000-0000-0000-0000-000000000000",
        orderBy: $stateParams.orderBy || '',
        descending: $stateParams.descending || 'false',
        page: $stateParams.page || 1,
        pageSize: $stateParams.pageSize || 10,
        totalPages: 0,
        filter: 'none'
    };
    
    // data to get search results
    $scope.searchResults = {
        items: null,
        totalPages: 0,
        totalItems: 0,
        hasResults: false,
        canCreate: true
    };
    
    // data to get an item
    $scope.itemQuery = {
        id: $stateParams.id || "00000000-0000-0000-0000-000000000000",
        itemFound: false
    };
    
    // data for create, update, and delete operations
    $scope.itemForm = {
        number: 0,
        buildingId: $stateParams.buildingId || "00000000-0000-0000-0000-000000000000",
        buildings: null,
        canEdit: false,
        canDelete: false
    };
    
    // status on any operation
    $scope.status = {
        isReadOnly: false,
        isError: false,
        errorMessage: '',
        isSuccess: false,
        successMessage: ''
    };
    
    $scope.navbarProperties = {
        isCollapsed: true
    };
    
    // search api
    $scope.search = function () {
        $scope.searchQuery.filter = '';
        if ($scope.searchQuery.number != 0) {
            if ($scope.searchQuery.filter != '') {
                $scope.searchQuery.filter = $scope.searchQuery.filter + ', ';
            }
            $scope.searchQuery.filter = $scope.searchQuery.filter + 'Number: ' + $scope.searchQuery.number;
        }
        if ($scope.searchQuery.buildingId != "00000000-0000-0000-0000-000000000000") {
            if ($scope.searchQuery.filter != '') {
                $scope.searchQuery.filter = $scope.searchQuery.filter + ', ';
            }
            $scope.searchQuery.filter = $scope.searchQuery.filter + 'Building Id: ' + $scope.searchQuery.buildingId;
        }
        if ($scope.searchQuery.filter == '') {
            $scope.searchQuery.filter = 'none';
        }
        var orderBy = $scope.searchQuery.orderBy;
        if ($scope.searchQuery.descending == 'true') {
            orderBy = orderBy + '-desc';
        }
        var result = RoomsService.searchRooms($scope.searchQuery.number, $scope.searchQuery.buildingId, orderBy, $scope.searchQuery.page, $scope.searchQuery.pageSize);
        result.then(function(result) {
            if (result.isSuccess) {
                $scope.searchResults.items = result.items;
                $scope.searchResults.totalPages = Math.ceil(1.0 * result.totalItems / $scope.searchQuery.pageSize);
                $scope.searchResults.totalItems = result.totalItems;
                $scope.searchResults.hasResults = true;
                $scope.searchResults.canCreate = result.canCreate;
            } else {
                $scope.status.isError = true;
                $scope.status.isSuccess = false;
                $scope.status.errorMessage = result.message;
            }
        });
    }
    
    $scope.refreshSearch = function () {
        $state.go('roomResults', {
            'number': $scope.searchQuery.number,
            'buildingId': $scope.searchQuery.buildingId,
            'orderBy': $scope.searchQuery.orderBy,
            'descending': $scope.searchQuery.descending,
            'page': $scope.searchQuery.page,
            'pageSize': $scope.searchQuery.pageSize
        });
    }
    
    // get api
    $scope.get = function (isEdit) {
        var result = RoomsService.getRoom($scope.itemQuery.id);
        result.then(function(result) {
            if (result.isSuccess) {
                $scope.status.isSuccess = true;
                $scope.itemForm.id = result.data.Id;
                $scope.itemForm.number = result.data.Number;
                $scope.itemForm.buildingId = result.data.BuildingId;
                $scope.itemForm.canEdit = result.data.CanEdit;
                $scope.itemForm.canDelete = result.data.CanDelete;
                if (isEdit == true && $scope.itemForm.canEdit == false) {
                    $scope.status.isReadOnly = true;
                }
                $scope.init();
            } else {
                $scope.status.isError = true;
                $scope.status.isSuccess = false;
                $scope.status.errorMessage = result.message;
            }
        });
    }
    
    // create api
    $scope.create = function () {
        var result = RoomsService.createRoom($scope.itemForm.number, $scope.itemForm.buildingId);
        result.then(function(result) {
            if (result.isSuccess) {
                $scope.status.isSuccess = true;
                $scope.status.isReadOnly = true;
                $scope.status.isError = false;
                $scope.status.successMessage = "Room item successfully created."
            } else {
                $scope.status.isError = true;
                $scope.status.isSuccess = false;
                $scope.status.errorMessage = result.message;
            }
        });
    }
    
    // update api
    $scope.update = function () {
        var result = RoomsService.updateRoom($scope.itemForm.id, $scope.itemForm.number, $scope.itemForm.buildingId);
        result.then(function(result) {
            if (result.isSuccess) {
                $scope.status.isSuccess = true;
                $scope.status.isReadOnly = true;
                $scope.status.isError = false;
                $scope.status.successMessage = "Room item successfully updated."
            } else {
                $scope.status.isError = true;
                $scope.status.isSuccess = false;
                $scope.status.errorMessage = result.message;
            }
        });
    }
    
    // delete api
    $scope.delete = function () {
        var result = RoomsService.deleteRoom($scope.itemQuery.id);
        result.then(function(result) {
            if (result.isSuccess) {
                $scope.status.isSuccess = true;
                $scope.status.isReadOnly = true;
                $scope.status.isError = false;
                $scope.status.successMessage = "Room item successfully deleted."
            } else {
                $scope.status.isError = true;
                $scope.status.isSuccess = false;
                $scope.status.errorMessage = result.message;
            }
        });
    }
}

RoomsController.$inject = ['$scope', '$stateParams', '$state', '$window', '$location', 'RoomsService', 'BuildingsService'];

Angular 服务

请查看以下 Angular 服务,它具有通过调用 VITA 增强的 Web API 服务来执行 `IRoom` 项目 CRUD 操作的函数。然后返回每个函数的成功和错误响应信息(请注意,您可以轻松访问响应数据、状态、标头和配置信息)。

var RoomsService = function ($http, $q) {
    this.searchRooms = function (number, buildingId, orderBy, page, pageSize) {
        var deferredObject = $q.defer();
        var results = {
            isSuccess: true,
            message: '',
            items: null,
            totalItems: 0,
            canCreate: true
        }
        var searchQuery = {
            Number: number,
            BuildingId: buildingId,
            OrderBy: orderBy,
            Skip: (page - 1) * pageSize,
            Take: pageSize
        };
        if (searchQuery.Skip < 0) searchQuery.Skip = 0;
        
        $http.get('/api/rooms', { params: searchQuery }).
            success(function (data) {
                results.items = data.Results;
                results.totalItems = data.TotalCount;
                results.canCreate = data.CanCreate;
                deferredObject.resolve(results);
            }).
            error(function (data, status, headers, config) {
                results.isSuccess = false;
                results.message = 'Could not search for Room items: ';
                if (typeof data == "string") {
                    results.message = results.message + ' ' + data;
                } else {
                    for (var i = 0; i < data.length; i++) {
                        results.message = results.message + ' ' + data[i].Message;
                    }
                }
                deferredObject.resolve(results);
            });
        
        return deferredObject.promise;
    };
    
    this.getRoom = function (id) {
        var deferredObject = $q.defer();
        var results = {
            isSuccess: true,
            message: '',
            data: null
        }
        
        $http.get('/api/rooms/' + id).
            success(function (data) {
                results.data = data;
                deferredObject.resolve(results);
            }).
            error(function (data, status, headers, config) {
                results.isSuccess = false;
                results.message = 'Could not get Room item:';
                if (typeof data == "string") {
                    results.message = results.message + ' ' + data;
                } else {
                    for (var i = 0; i < data.length; i++) {
                        results.message = results.message + ' ' + data[i].Message;
                    }
                }
                deferredObject.resolve(results);
            });
        
        return deferredObject.promise;
    };
    
    this.listRoom = function (id) {
        var deferredObject = $q.defer();
        var results = {
            isSuccess: true,
            message: '',
            data: null
        }
        
        $http.get('/api/roomslist', { params: { take: 100, id: id } }).
            success(function (data) {
                results.data = data;
                deferredObject.resolve(results);
            }).
            error(function (data, status, headers, config) {
                results.isSuccess = false;
                results.message = 'Could not get Room list:';
                if (typeof data == "string") {
                    results.message = results.message + ' ' + data;
                } else {
                    for (var i = 0; i < data.length; i++) {
                        results.message = results.message + ' ' + data[i].Message;
                    }
                }
                deferredObject.resolve(results);
            });
        
        return deferredObject.promise;
    };
    
    this.createRoom = function (number, buildingId) {
        var deferredObject = $q.defer();
        var results = {
            isSuccess: true,
            message: '',
            data: null
        }
        var itemData = {
            Number: number, 
            BuildingId: buildingId
        };
        
        $http.post('/api/rooms', itemData).
            success(function (data) {
                results.data = data;
                deferredObject.resolve(results);
            }).
            error(function (data, status, headers, config) {
                results.isSuccess = false;
                results.message = 'Could not create Room item:';
                if (typeof data == "string") {
                    results.message = results.message + ' ' + data;
                } else {
                    for (var i = 0; i < data.length; i++) {
                        results.message = results.message + ' ' + data[i].Message;
                    }
                }
                deferredObject.resolve(results);
            });
        
        return deferredObject.promise;
    };
    
    this.updateRoom = function (id, number, buildingId) {
        var deferredObject = $q.defer();
        var results = {
            isSuccess: true,
            message: '',
            data: null
        }
        var itemData = {
            Id: id, 
            Number: number, 
            BuildingId: buildingId
        };
        
        $http.put('/api/rooms', itemData).
            success(function (data) {
                results.data = data;
                deferredObject.resolve(results);
            }).
            error(function (data, status, headers, config) {
                results.isSuccess = false;
                results.message = 'Could not update Room item:';
                if (typeof data == "string") {
                    results.message = results.message + ' ' + data;
                } else {
                    for (var i = 0; i < data.length; i++) {
                        results.message = results.message + ' ' + data[i].Message;
                    }
                }
                deferredObject.resolve(results);
            });
        
        return deferredObject.promise;
    };
    
    this.deleteRoom = function (id) {
        var deferredObject = $q.defer();
        var results = {
            isSuccess: true,
            message: '',
            data: null
        }
        
        $http.delete('/api/rooms/' + id).
            success(function (data) {
                results.data = data;
                deferredObject.resolve(results);
            }).
            error(function (data, status, headers, config) {
                results.isSuccess = false;
                results.message = 'Could not delete Room item:';
                if (typeof data == "string") {
                    results.message = results.message + ' ' + data;
                } else {
                    for (var i = 0; i < data.length; i++) {
                        results.message = results.message + ' ' + data[i].Message;
                    }
                }
                deferredObject.resolve(results);
            });
        
        return deferredObject.promise;
    };
}

RoomssService.$inject = ['$http', '$q'];

管理员工具 UI

下面是管理员工具的屏幕截图。请继续深入研究,检查并运行应用程序来管理您的数据。

深入了解 VITA 模块

在 VITA 基础部分,我们已经介绍了 VITA 的许多核心功能。在此示例中,我们将深入研究一个更大的数据模型,并利用 VITA 的一个附加模块:*日志记录*。我们还将说明一些计算属性和视图,并使用 Slim API。请按照“Northwind 示例解决方案”下载进行操作,其中包含以下项目:

  • Northwind.VITA – VITA 管理的数据模型及附加模块和相关打包。此程序集还包括 Slim API 控制器。
  • Northwind.Tests – 一些基本的 CRUD 测试,用于测试您的 VITA 管理的数据模型。创建一个匹配 `DbConnectionString` 配置值的测试 MS SQL 数据库。
  • Northwind.DS – 一个 Web API 服务库及相关材料。
  • Northwind.UI – 一个 MVC/AngularJS 单页 Web 应用程序,利用 Web API 服务和附加模块。创建一个匹配 `DbConnectionString` 配置值的 MS SQL 数据库。

Northwind 示例

Northwind 是一个常见的示例数据库,我们将使用该熟悉结构作为此示例的数据模型。我们期望的数据库架构如下所示:

带有附加特性的数据模型

在构建实际应用程序时,我们需要能够定制我们的数据模型,以满足各种情况的严格要求。VITA 在这方面不会让人失望。请查看示例下载以了解此 Northwind 用例的完整数据模型。我将只展示一些摘录来演示一些允许您定制数据模型的特性。

数据模型示例

查看 `ICategory` 的示例数据模型接口。

    [Entity(Name="Category", TableName="Categories")]
    [Paged, OrderBy("CategoryName")]
    public partial interface ICategory
    {
        [Column("CategoryID"), PrimaryKey, ClusteredIndex(IndexName="PK_Categories"), Identity]
        int CategoryID { get; set; }
       
        [Column("CategoryName", Size = 15), Index(IndexName="CategoryName")]
        string CategoryName { get; set; }
       
        [Column("Description"), Nullable, Unlimited]
        string Description { get; set; }
       
        [OrderBy("ProductName")]
        IList<IProduct> Products { get; }
    }

实体级别特性

VITA 提供了许多可选的特性来定制您的实体/表是如何被管理的。其中一些特性包括:

  • Entity – 这是一个必需的特性,但您可以选择指定表名和/或实体类型的名称。
  • Paged – 对于较大的表,使用此特性来触发 VITA 生成带有分页的存储过程。
  • OrderBy – 使用此特性来触发 VITA 在列表存储过程中生成默认排序。

属性级别特性

VITA 提供了许多可选的特性来定制您的列/属性是如何被管理的。其中一些特性包括:

  • Column – 您可以定制属性的名称、*Size* 和特定数据库数据类型(*DbType* 或 *DbTypeSpec*)。
  • ClusteredIndex – 用于指定属性应为聚集索引,可以指定索引名称。对于较小的表,请改用实体级别的 *HeapTable* 特性,该特性也会影响 VITA 如何缓存此实体的数据。对于非聚集索引,请使用 *Index* 特性。
  • Identity – 用于指定标识主键属性。我们的基础示例使用了 *Auto* 特性来自动生成 Guid 主键。
  • Nullable – 用于指定属性是否可为空。
  • Unlimited – 用于指定数据长度无限制。
  • OrderBy – 对于集合,您可以指定默认排序。

数据模型示例

查看 `IOrderDetail` 的示例数据模型接口。

    [Entity(Name="OrderDetail", TableName="Order Details")]
    [PrimaryKey("Order,Product")]
    [ClusteredIndex("Order,Product", IndexName="PK_Order_Details")]
    [Paged]
    public partial interface IOrderDetail
    {
        [Column("UnitPrice", DbTypeSpec = "money", Scale = 4, Precision = 19)]
        decimal UnitPrice { get; set; }
       
        [Column("Quantity", DbType = DbType.Int16)]
        short Quantity { get; set; }
       
        [Column("Discount", DbTypeSpec = "real", Scale = 0, Precision = 24)]
        float Discount { get; set; }
       
        [EntityRef(KeyColumns = "OrderID")]
        IOrder Order { get; set; }
       
        [EntityRef(KeyColumns = "ProductID")]
        IProduct Product { get; set; }
    }

实体级别特性

这里展示了一些附加特性:

  • PrimaryKey – 对于复合主键,您可以在实体级别定义键中的属性。
  • ClusteredIndex – 对于复合聚集索引,您可以在实体级别定义索引中的属性。对于非聚集索引,请使用 *Index* 特性。

属性级别特性

这里展示了一些附加特性:

  • EntityRef – 用于显式指定外键列名。例如,对于 `Order` 属性,默认值将是 `Order_id`。

数据模型示例

查看 `IEmployee` 的(合并的)示例数据模型接口。

    [Entity]
    public partial interface IEmployee
    {
        [PrimaryKey, Identity]
        int EmployeeID { get; set; }
       
        string LastName { get; set; }
        
        string FirstName { get; set; }
       
        [ManyToMany(typeof(IEmployeeTerritory))]
        IList<ITerritory> Territories { get; }
       
        [EntityRef(KeyColumns = "ReportsTo"), Nullable]
        IEmployee Employee { get; set; }
        
        [Computed(typeof(EmployeeHelper), "GetEmployeeFullName"), DependsOn("LastName,FirstName")]
        string FullName { get; }
    }

多对多关系

某些数据库表正式表示多对多关系,如果这些表除了映射相关表之外不包含任何有用的信息,那么我们最好不要直接处理这些“映射”表。

VITA 对这些多对多关系提供了直接支持,如 `Territories` 属性上的 `ManyToMany` 特性所示,其中 `IEmployeeTerritory` 是一个映射实体/表。现在,我们只需考虑员工属于哪些区域,当您在(`Territories`)列表中使用 `Add` 或 `Remove` 时,VITA 会自动更新映射表。请参阅示例下载中的 `EmployeeCRUDTest` 来了解这一点。

计算属性和视图

计算属性和视图允许您以各种方式转换数据以满足应用程序的需求。

计算属性

请注意上面 `IEmployee` 数据模型项中的只读计算属性 `FullName`。设置计算属性非常方便。指定执行计算的类型和方法,以及计算所涉及的属性。下面是执行 `FullName` 属性计算的 `EmployeeHelper.GetEmployeeFullName()` 方法。

    public static class EmployeeHelper
    {
        public static string GetEmployeeFullName(IEmployee employee)
        {
            return employee.FirstName + " " + employee.LastName;
        }
    }

让我们在测试中使用此属性,请参阅下面 `EmployeeCRUDTest()` 测试方法的摘录。请注意,我们可以访问 `employee2.FullName` 并获得预期的名字和姓氏。

        [TestMethod]
        public void EmployeeCRUDTest()
        {
            IEntitySession session1 = DomainApp.OpenSession();
            IEntitySession session2;
            
            // create Employee
            IEmployee employee1 = EmployeeTest.GetTestEmployee(session1);
            employee1.FirstName = "John";
            employee1.LastName = "Doe";
            session1.SaveChanges();
            Assert.IsNotNull(employee1, "Create and save of IEmployee item failed.");
            
            // read Employee
            session2 = DomainApp.OpenSession();
            IEmployee employee2 = session2.GetEntity<IEmployee>(employee1.EmployeeID);
            Assert.IsNotNull(employee2, "Retrieval of new IEmployee item failed.");
            Assert.IsTrue(EmployeeTest.CompareItems(employee1, employee2), "Retrieved IEmployee item match with created item failed.");
            Assert.AreEqual(employee2.FullName, "John Doe");
        }

视图

设置视图需要更多的工作,但仍然很容易完成。视图作为 `EntityModule` 的一部分进行设置,如下所示:

   public class DomainModule: EntityModule
    {
        public DomainModule(EntityArea area) : base(area, "DomainModule")
        {
            RegisterEntities( typeof(ICustomer)
                , typeof(IProduct)
                , typeof(ICategory)
                , typeof(ICustomerCustomerDemo)
                , typeof(ICustomerDemographic)
                , typeof(IEmployee)
                , typeof(IEmployeeTerritory)
                , typeof(IOrderDetail)
                , typeof(IOrder)
                , typeof(IRegion)
                , typeof(IShipper)
                , typeof(ISupplier)
                , typeof(ITerritory));
            
            // IProductView setup
            var productQuery = from i in ViewHelper.EntitySet<IProduct>()
                select new
                {
                    ProductID = i.ProductID,
                    ProductName = i.ProductName,
                    QuantityPerUnit = i.QuantityPerUnit,
                    UnitPrice = i.UnitPrice,
                    UnitsInStock = i.UnitsInStock,
                    UnitsOnOrder = i.UnitsOnOrder,
                    ReorderLevel = i.ReorderLevel,
                    Discontinued = i.Discontinued,
                    CategoryName = i.Category.CategoryName,
                    CompanyName = i.Supplier.CompanyName,
                    ContactName = i.Supplier.ContactName,
                    ContactTitle = i.Supplier.ContactTitle,
                };
            RegisterView<IProductView>(productQuery, DbViewOptions.Materialized);
        }
    }

在这里,我们设置了一个 `IProductView` 视图,使用 `ViewHelper` 并基本上创建了 `IProduct` 的扁平化视图。然后,我们使用 `RegisterView` 将该视图注册为我们模块的一部分。

让我们在测试中使用此视图,请参阅下面 `ProductCRUDTest()` 测试方法的摘录。请注意,我们可以访问 `IProductView` 实体集,并从视图中获取预期的产品名称。

        [TestMethod]
        public void ProductCRUDTest()
        {
            IEntitySession session1 = DomainApp.OpenSession();
            IEntitySession session2;
            
            // create Product
            IProduct product1 = ProductTest.GetTestProduct(session1);
            product1.ProductName = "My Product";
            session1.SaveChanges();
            Assert.IsNotNull(product1, "Create and save of IProduct item failed.");
            
            // read Product
            session2 = DomainApp.OpenSession();
            IProduct product2 = session2.GetEntity<IProduct>(product1.ProductID);
            Assert.IsNotNull(product2, "Retrieval of new IProduct item failed.");
            Assert.IsTrue(ProductTest.CompareItems(product1, product2), "Retrieved IProduct item match with created item failed.");

            // read Product View
            var productView = session2.EntitySet<IProductView>().Where(i => i.ProductID == product2.ProductID).FirstOrDefault();
            Assert.IsNotNull(productView);
            Assert.AreEqual(product2.ProductName, productView.ProductName);

        }

附加模块

在此示例中,我们将使用一个附加的 VITA 模块:**日志记录**。VITA 模块是一个可重用组件,可为您的应用程序带来附加功能和支持表。向应用程序添加模块很容易。下面我们将日志记录实体应用程序添加到我们的 `EntityApp`:

    public class DomainApp: EntityApp
    {
        public DomainApp()
        {
            this.Version = "1.0.0.2";
            
            // add main area and module
            var domainArea = this.AddArea("Domain");
            MainModule = new DomainModule(domainArea);
            
            // add user transaction log, with extra tracking columns in "transaction" entities  
            var transLogStt = new TransactionLogModelExtender();
            // add columns CreatedIn and UpdatedIn - tracking user/date of create/update events
            transLogStt.AddUpdateStampColumns(new[]
            {
                      typeof(ICustomer)
                    , typeof(IProduct)
                    , typeof(ICategory)
                    , typeof(ICustomerCustomerDemo)
                    , typeof(ICustomerDemographic)
                    , typeof(IEmployee)
                    , typeof(IEmployeeTerritory)
                    , typeof(IOrderDetail)
                    , typeof(IOrder)
                    , typeof(IRegion)
                    , typeof(IShipper)
                    , typeof(ISupplier)
                    , typeof(ITerritory)
                },
                createIdPropertyName: "CreatedIn", updateIdPropertyName: "UpdatedIn");
            
            // Error log, operation log, web call log, model change log, incident log, persistent session
            this.LoggingApp = new LoggingEntityApp("log");
            LoggingApp.LinkTo(this);
    }

日志记录模块

日志记录模块是管理实际应用程序的强大构建块。整个日志记录模块包含多个您可以选择的模块,并且在此示例中,我们将所有这些配置为“Log”区域的一部分。以下小节描述了如何配置和使用这些日志记录模块。

事务日志

使用此模块记录数据库更新事务。要配置此模块,请添加新的 `TransactionLogModule` 并设置一些 `TransactionLogSettings`。请注意,在我们的设置中,我们将 `AddUpdateStampColumns` 配置为添加到我们的每个实体。

这有什么作用?请注意,所有表现在都有 `CreatedIn` 和 `UpdatedIn` 列。当记录被创建或更新时,这些列将被填充为相应数据库更新事务的 ID。将更新操作与整体事务关联是跟踪通常涉及对一个或多个表中的多个记录进行更新的一种更有效的方法。在表中添加跟踪列,如 CreatedDateTime、UpdatedDateTime、CreatedBy、UpdatedBy,是一种非常常见的模式。TransactionLog允许这样做,但使用对 TransactionId 的引用,并且事务记录引用日期时间以及当前用户。

另外,在您的配置的架构区域中,以下表将被添加到您的数据库:

  • TransactionLog – 每条记录记录了更新事务的详细信息,包括更改日期和详细信息,以及对用户和 Web 调用信息的链接。每个事务记录包含一个列表,列出事务涉及的所有实体(PK)。此日志可用于同步数据库时,以了解自上次同步以来哪些记录在数据库中发生了更改。

操作日志

使用此模块记录任何类型的数据库操作。要配置此模块,请添加新的 `OperationLogModule`。这会将以下表(在您的配置的架构区域中)添加到您的数据库:

  • OperationLog – 每条记录记录了 select 或 procedure 调用的详细信息,包括参数、日期和用户信息。此日志可以即时开启/关闭,或为特定用户开启,以便在生产站点上进行详细的调试。

错误日志

使用此模块记录可能发生的错误,无论是什么原因。要配置此模块,请添加新的 `ErrorLogModule`。这会将以下表(在您的配置的架构区域中)添加到您的数据库:

  • ErrorLog – 每条记录记录了异常的详细信息,包括日期、来源、消息详细信息,以及对 Web 调用和用户信息等的链接。

Web 调用日志

使用此模块记录 Web API 调用。要配置此模块,请添加新的 `WebCallModule`。这会将以下表(在您的配置的架构区域中)添加到您的数据库:

  • WebCallLog – 每条记录记录 Web API 调用的详细信息,包括日期、用户信息和位置信息、请求和响应信息,以及错误详情(如果发生错误)。对于错误,系统还会自动记录处理过程中发生的所有 SQL 调用。

事件日志

使用此模块记录一般事件。要配置此模块,请添加新的 `IncidentLogModule`。此模块会将以下表(在您的配置的架构区域中)添加到您的数据库:

  • IncidentLog – 每条记录显示登录失败和在 N 次失败尝试后禁用登录的详细信息。
  • IncidentAlert

Slim API

如果您只需要为自己的应用程序打包一套 RESTful Web 服务,Slim API 绝对是构建这些服务的绝佳 streamlined 方法。在任何库中构建 slim API 控制器,而无需 ASP.NET Web API 库的开销。`SlimApiController` 的设置与普通 Web API 控制器非常相似,主要区别在于使用了 Api 特性。下面是 `CategoriesController` 的方法签名,位于 `VITA.CodeFirst` 程序集中:

    public class CategoriesController : SlimApiController
    {
        [ApiGet, ApiRoute("categories")]
        public QueryResults<CategoryDto> GetCategories([FromUrl] CategoryQuery query)
        {
        }
        
        [ApiGet, ApiRoute("categorieslist")]
        public QueryResults<CategoryDto> GetCategoriesList([FromUrl] int take = 100, int categoryID = 0)
        {
        }
        
        [ApiGet, ApiRoute("categories/{categoryid}")]
        public CategoryDto GetCategory(int categoryID)
        {
        }
        
        [ApiPost, ApiRoute("categories")]
        public CategoryDto CreateCategory(CategoryDto item)
        {
        }
        
        [ApiPut, ApiRoute("categories")]
        public CategoryDto UpdateCategory(CategoryDto item)
        {
        }
        
        [ApiDelete, ApiRoute("categories/{categoryid}")]
        public void DeleteCategory(int categoryID)
        {
        }
    }

为了利用这些控制器,您需要在 `EntityApp` 中使用全局路由前缀来注册它们:

        public DomainApp()
        {
            this.Version = "1.0.0.2";
            
            // add main area and module
            var domainArea = this.AddArea("Domain");
            MainModule = new DomainModule(domainArea);
            
           // other enity application setup here...
            
            //api config
            base.ApiConfiguration.GlobalRoutePrefix = "slimapi";
            base.ApiConfiguration.RegisterControllerTypes(
                typeof(CustomersController),
                typeof(ProductsController),
                typeof(CategoriesController),
                typeof(CustomerCustomerDemosController),
                typeof(CustomerDemographicsController),
                typeof(EmployeesController),
                typeof(EmployeeTerritoriesController),
                typeof(OrderDetailsController),
                typeof(OrdersController),
                typeof(RegionsController),
                typeof(ShippersController),
                typeof(SuppliersController),
                typeof(TerritoriesController),
                typeof(ClientErrorController),
                typeof(LoggingDataController));
        }

在这些示例中的任何一个中,您都可以测试“普通”Web API 和 slim API 控制器。在此示例中,比较 `/api/categories` 和 `/slimapi/categories`。

测试和管理员工具

请继续深入研究,运行测试并运行管理员工具应用程序来管理您的数据,并查看您的信息和日志相关信息,以了解这些模块的实际运行情况。

深入了解 VITA 授权

在前几节中,我们已经介绍了 VITA 的许多核心功能和模块。在此示例中,我们将更深入地利用*授权*框架和支持模块*登录*。请按照“论坛示例解决方案”下载进行操作,其中包含以下项目:

  • Forums.VITA – VITA 管理的数据模型及附加模块和相关打包。
  • Forums.Tests – 一些基本的 CRUD 测试,用于测试您的 VITA 管理的数据模型。创建一个匹配 `DbConnectionString` 配置值的测试 MS SQL 数据库。
  • Forums.DS – 一个 Web API 服务库及相关材料。
  • Forums.UI – 一个 MVC/AngularJS 单页 Web 应用程序,利用 Web API 服务、附加模块和授权框架。创建一个匹配 `DbConnectionString` 配置值的 MS SQL 数据库。

论坛示例

我们的最终示例是一个论坛模型,其中有多种帖子类型,如讨论、问题和评论。我们期望的数据库架构如下所示:

数据模型

在此示例中,我们希望实现一个通用的帖子表,其主键与特定表相同,并且为了好玩,实现一个不同的表命名约定。这对 VITA 来说不是问题!请查看 `IDiscussion` 和通用的 `IPost` 的数据模型。

   [Entity(Name="Discussion", TableName="tblForums_Discussion")]
   [Paged, OrderBy("Title")]
    public partial interface IDiscussion
    {
        [PrimaryKey, EntityRef(KeyColumns = "PostID")]
        IPost Post { get; set; }
        
        [Column("Title", Size = 255), Index(IndexName="IX_Discussion_Title")]
        string Title { get; set; }
        
        [Column("DiscussionText"), Unlimited]
        string DiscussionText { get; set; }
        
        IList<IDiscussionReply> DiscussionReplies { get; }
    }

    [Entity(Name="Post", TableName="tblForums_Post")]
    [Paged, OrderBy("IntroText")]
    public partial interface IPost
    {
        [Column("PostID"), PrimaryKey, ClusteredIndex(IndexName="PK_tblForums_Post"), Auto]
        Guid PostID { get; set; }
       
        [Column("IntroText", Size = 1000), Nullable]
        string IntroText { get; set; }
       
        IList<IVote> Votes { get; }
       
        [ManyToMany(typeof(IPostTag))]
        IList<ITag> Tags { get; }
       
        [OrderBy("CommentText")]
        [OneToMany("CommentOnPost")]
        IList<IComment> CommentOnComments { get; }
       
        [EntityRef(KeyColumns = "MemberID")]
        IMember Member { get; set; }
    }

VITA 接口方法管理数据模型的另一个优点是,您的重点仅在于数据模型,而不是更高级的设计构造。通过这个论坛示例,您自然会从通用信息和专用信息的角度来考虑信息(`Discussion` 是一种 `Post`,`Comment` 也是一种 `Post`,等等),并且整个系统可能会反映这一点。VITA 支持任何类型简单或复杂的数据模型。但是,VITA 明智地不支持数据模型中的泛化/特化概念,因为这并非直接支持的关系数据库构造。您按照支持您的需求和最佳实践所需的精确方式定义数据模型,并在需要时将数据转换为更高级的接口/类。在此示例中,我实现了数据模型作为经典的 TPT 模式,并且仅仅在 dto 类中进行了通用/专用数据的扁平化(`Discussion` dto 包括基础 `Post` 信息等)。

附加模块

在进入授权之前,在此示例中,我们将使用一个附加的 VITA 模块:**登录**。下面我们将此模块添加到我们的 `EntityApp`(以及相关的必需日志记录应用程序):

    public class DomainApp: EntityApp
    {
        public DomainApp(string cryptoKey) : this()
        {
            var cryptoService = this.GetService<IEncryptionService>();
            var cryptoBytes = HexUtil.HexToByteArray(cryptoKey);
            if (cryptoService != null) cryptoService.AddChannel(cryptoBytes); //sets up default unnamed channel
        }

        public DomainApp()
        {
            var domainArea = this.AddArea("Domain");
            var mainModule = new DomainModule(domainArea);
           
            var loginArea = this.AddArea("Login");
           
            // add login functionality
            var loginStt = new LoginModuleSettings(passwordExpirationPeriod: TimeSpan.FromDays(180)); //uses BCrypt hashing
            loginStt.RequiredPasswordStrength = PasswordStrength.Medium; 
            // var loginStt = new LoginModuleSettings(passwordHasher: new Pbkdf2PasswordHasher()); // uses Pbkdf2 hasher - inferior method
            var loginModule = new LoginModule(loginArea, loginStt);
            //EncryptedData is used by login module
            var cryptModule = new EncryptedDataModule(loginArea);
            var templateModule = new TemplateModule(domainArea);
            
             this.LoggingApp = new LoggingEntityApp("log");
            LoggingApp.LinkTo(this);

           // add trigger to suspense login after 3 failed attempts within a minute
            var loginFailedTrigger = new Vita.Modules.Login.LoginFailedTrigger(this,
                failureCount: 3, timeWindow: TimeSpan.FromMinutes(1), suspensionPeriod: TimeSpan.FromMinutes(5));
            LoggingApp.IncidentLog.AddTrigger(loginFailedTrigger);
        }
    }

登录

使用此模块启用登录功能。要配置此模块,请添加新的 `LoginModule`,并带有 `LoginModuleSettings`。另请注意,您可以定义一个用于登录失败的触发器,带有超时时间和与您的事件日志关联的能力(以记录登录失败)。此模块会将以下表(在您的配置的架构区域中)添加到您的数据库:

  • Login – 每条记录记录登录帐户的关键信息,包括用户名和密码信息、日期和状态。
  • SecretQuestion – 每条记录包含一个用于帐户恢复的问题。
  • SecretAnswer – 每条记录记录用户对用于帐户恢复的秘密问题的答案。
  • TrustedDevice – 每条记录定义一个受信任的设备(我没有使用此功能)。
  • UserSession – 每条记录记录每个持久化会话的关键信息,包括用户、令牌和过期信息。
  • UserSessionLastActive – 每条记录记录给定用户会话最后一次活动的日期。

如何利用登录功能?通常,您希望将登录与数据模型中的用户实体关联起来。在我们的例子中,我们的用户实体是 `IMember`。

    [Entity]
    public partial interface IMember
    {
        [PrimaryKey, Auto]
        Guid MemberID { get; set; }
       
        string DisplayName { get; set; }
       
        string FirstName { get; set; }
       
        string LastName { get; set; }
       
        string EmailAddress { get; set; }
       
        UserType Type { get; set; } //might be combination of several type flags
       
        IList<IVote> Votes { get; }
       
        IList<IPost> Posts { get; }
    }

请注意,我们向此实体添加了一个 `UserType` 枚举属性。我们将在授权框架中使用它。

对于应用程序使用,我们需要一个完整的登录和注册流程,VITA 提供了我们可以随时使用的服务。下面,我们在注册期间将登录与 *IMember* 用户实体关联起来(有关更多详细信息,请参阅 `Forums.DS` 项目中的 `AuthenticationController`)。

        [HttpPost, Route("api/auth/register")]
        public LoginResponseDto Register(RegisterDto registerDto)
        {
            // create user
            var session = OpenSession();
            IMember member = session.NewEntity<IMember>();
            member.DisplayName = registerDto.DisplayName;
            member.FirstName = registerDto.FirstName;
            member.LastName = registerDto.LastName;
            member.EmailAddress = registerDto.EmailAddress;
            member.Type = UserType.Member;
            if (registerDto.IsAdmin)
                member.Type |= UserType.Administrator;
            session.SaveChanges();
           
            // create login
            var login = _loginManagementService.NewLogin(session, registerDto.UserName, registerDto.Password, userId: member.MemberID, loginId: member.MemberID);
            session.SaveChanges();
           
            // login
            LoginDto loginDto = new LoginDto { UserName = registerDto.UserName, Password = registerDto.Password };
            return Login(loginDto);
        }

在创建登录步骤中,我们将登录记录与我们的成员记录关联起来。现在我们可以使用我们的身份验证令牌登录并创建持久化会话。

        [HttpPost, Route("api/auth/login")]
        public LoginResponseDto Login(LoginDto loginDto)
        {
            //Login using LoginService
            var loginResult = _loginService.Login(loginDto.UserName, loginDto.Password);
            if(!loginResult.Success)
                return new LoginResponseDto() { ResultCode = "LoginFailed" };

            OpContext.User = loginResult.User;
            _sessionService.StartSession(OpContext);
            var userSession = OpContext.UserSession;
            var resp = new LoginResponseDto() {ResultCode = "Success", AuthenticationToken = userSession.Token };
            return resp;
        }

当然,我们也可以注销以结束我们的会话。

        [HttpDelete, Route("api/auth/login"), AuthenticatedOnly]
        public void Logout()
        {
            _loginService.Logout(OpContext.User);
            var userSession = OpContext.UserSession;
            if(userSession != null)
            {
                _sessionService.EndSession(OpContext);
            }
        }

授权框架

在 VITA 的所有附加功能中,我认为授权框架在众多框架中脱颖而出。通过这个框架,您可以轻松地使用实体资源、过滤器、权限和活动来定义规则,以精确确定用户可以做什么,甚至可以精确到属性级别。我们将在这里介绍一个基本场景。

授权角色和规则

对于我们的论坛应用程序,我们希望有 3 种类型的用户,具有以下规则:

  • 公共 – 一个未登录的用户,可以查看任何内容。
  • 会员 – 一个已登录用户,可以查看任何内容,并创建/编辑/删除自己的帖子。
  • 管理员 – 一个已登录用户,可以查看和创建/编辑/删除任何内容。

以下是 `DomainAuthorizationHelper` 类的核心内容,它定义了这些角色和规则:

   public static class DomainAuthorizationHelper
    {
        public static void EnsureInitialized()
        {
           var memberDataFilter = new AuthorizationFilter("MemberData");
            memberDataFilter.Add<IMember, Guid>((i, userId) => i.MemberID == userId);
            memberDataFilter.Add<IComment, Guid>((i, userId) => i.Post.Member.MemberID == userId);
            memberDataFilter.Add<IDiscussion, Guid>((i, userId) => i.Post.Member.MemberID == userId);
            memberDataFilter.Add<IDiscussionReply, Guid>((i, userId) => i.Post.Member.MemberID == userId);
            memberDataFilter.Add<IIssue, Guid>((i, userId) => i.Post.Member.MemberID == userId);
            memberDataFilter.Add<IIssueReply, Guid>((i, userId) => i.Post.Member.MemberID == userId);
            memberDataFilter.Add<IPost, Guid>((i, userId) => i.Member.MemberID == userId);
            memberDataFilter.Add<IPostTag, Guid>((i, userId) => i.Post.Member.MemberID == userId);
            memberDataFilter.Add<IVote, Guid>((i, userId) => i.Member.MemberID == userId);
            
            // Entity resources
            var entities = new EntityGroupResource("Entities"
                , typeof(IComment)
                , typeof(IDiscussion)
                , typeof(IDiscussionReply)
                , typeof(IIssue)
                , typeof(IIssueReply)
                , typeof(IPost)
                , typeof(IIssueStatus)
                , typeof(ITag)
                , typeof(IPostTag)
                , typeof(IVote));
            var members = new EntityGroupResource("Members", typeof(IMember));
            
            // Permissions
            var browseAll = new EntityGroupPermission("BrowseAll", AccessType.Read, entities, members);
            var register = new EntityGroupPermission("Register", AccessType.Create, members);
            var manageAccount = new EntityGroupPermission("ManageAccount", AccessType.CRUD, members);
            var manageEntities = new EntityGroupPermission("ManageEntities", AccessType.CRUD, entities);
            
            // Activities
            var browsing = new Activity("Browsing", browseAll);
            var registering = new Activity("Registering", register);
            var editing = new Activity("Editing", manageAccount, manageEntities);
            
            // Roles
            // Public role can browse through anything and register
            PublicUser = new Role("PublicUser", browsing, registering);
            // Member role can browse and edit own stuff
            MemberUser = new Role("MemberUser");
            MemberUser.ChildRoles.Add(PublicUser);
            MemberUser.Grant(memberDataFilter, editing);
            // Admin role can browse and edit anything
            AdminUser = new Role("AdminUser", editing);
            AdminUser.ChildRoles.Add(MemberUser);
            AdminUser.ChildRoles.Add(PublicUser);
        }
    }

我们按照以下方式配置了角色和规则:

  • 数据过滤器 – 数据过滤器是一个对象,它回答一个简单的问题:此实体 X 是否连接到用户 Y?如果是,则启用关联的权限。我们设置了一个 `memberDataFilter` 来过滤所有类型的帖子和当前登录成员的成员实体。我们从 `UserIdReader()` 获取当前用户 ID。
  • 实体资源 – 我们定义了 2 组实体资源:`members`(用于 `IMember` 实体)和 `entities`(用于所有其他内容)。我们希望允许公共用户在注册期间创建 `IMember` 记录。
  • 权限 – 我们定义了 4 个权限:`browseAll`(用于读取 `entities` 和 `members` 资源),`register`(用于创建 `members` 资源),`manageAccount`(用于编辑 `members` 资源),以及 `manageEntities`(用于编辑 `entities` 资源)。
  • 活动 – 我们定义了 3 个活动:`browsing`(用于利用 `browseAll` 权限),`registering`(用于利用 `register` 权限),以及 `editing`(用于利用 `manageAccount` 和 `manageEntities` 权限)。
  • 角色 – 我们的角色定义如下:
    • PublicUser – 公共用户可以执行 `browsing` 活动以查看任何内容,并可以执行 `registering` 活动以注册。
    • MemberUser – 会员可以执行公共用户活动,并可以对通过 `memberDataFilter` 的项目执行 `editing` 活动。
    • AdminUser – 管理员可以执行公共和会员用户活动,并可以对所有项目执行 `editing` 活动。

有意义吗?

为了将这些角色和规则连接起来,我们需要在我们的 `EntityApp` 中覆盖 `GetUserRoles`:

        public override IList<Role> GetUserRoles(UserInfo user)
        {
            DomainAuthorizationHelper.EnsureInitialized();
            var list = new List<Role>();
            switch(user.Kind)
            {
                case UserKind.Anonymous:
                    list.Add(DomainAuthorizationHelper.PublicUser);
                    return list;
                case UserKind.AuthenticatedUser:
                    var session = this.OpenSystemSession();
                    var iUser = session.GetEntity<IMember>(user.UserId);
                    var roles = DomainAuthorizationHelper.GetRoles(iUser.Type);
                    return roles;
            }
            return new List<Role>();
        }

利用授权

现在,让我们再次看看 `IDiscussion` 的几个 Web API 控制器方法(请参阅 `Forums.DS` 项目中的 `DiscussionsController`)。

        [HttpGet, Route("api/discussions/{postid}")]
        public DiscussionDto GetDiscussion(Guid postID)
        {
            var session = OpenSecureSession();
            var item = session.GetEntity<IDiscussion>(postID);
            if (item == null)
            {
                WebContext.CustomResponseStatus = HttpStatusCode.BadRequest;
                WebContext.ResponseBody = String.Format("Discussion with ID '{0}' not found.", postID);
                return null; 
            }
            DiscussionDto itemDto = item.ToDto(true);
            Type[] blockingEntities;
            itemDto.CanDelete = itemDto.CanDelete && session.CanDeleteEntity<IDiscussion>(item, out blockingEntities);
            return itemDto;
        }

        [HttpPost, Route("api/discussions"), AuthenticatedOnly]
        public DiscussionDto CreateDiscussion(DiscussionDto item)
        {
            return CreateUpdateDiscussion(item, create: true);
        }

现在我们已经建立了框架,我们可以直接使用几个功能:

  • 安全会话 – 现在我们可以使用 `OpenSecureSession()` 来为登录用户和公共用户打开安全会话。安全会话是与特定用户(当前登录用户)关联的会话,所有数据操作都会根据用户权限进行验证;它启用了 VITA 的实体访问授权,使我们能够利用我们的授权角色和规则。
  • 身份验证请求 – 现在我们可以使用 `AuthenticatedOnly` 特性来仅允许登录用户的请求。如果用户未通过身份验证,VITA 将抛出一个“需要身份验证”的异常,从而导致 BadRequest 响应。

现在,让我们看一下 `IDiscussion` DTO 扩展类:

   public static class DiscussionDtoExtensions
   {
        public static DiscussionDto ToDto(this IDiscussion discussion)
        {
            var discussionDto = new DiscussionDto()
            {
                Title = discussion.Title,
                DiscussionText = discussion.DiscussionText,
                PostID = discussion.Post.PostID,
                IntroText = discussion.Post.IntroText,
                CanEdit = true,
                CanDelete = true
            };
            var permissions = EntityHelper.GetEntityAccess(discussion);
            discussionDto.CanEdit = permissions.CanUpdate();
            discussionDto.CanDelete = permissions.CanDelete();
            return discussionDto;
        }
    }

这里我们使用 VITA 的 `EntityHelper` 来获取讨论项目的 `GetEntityAccess` 权限。我们可以查看这些权限来检查用户是否可以查看/读取/更新/删除相应数据,并根据这些权限执行某些操作。我们利用用户是否可以更新或删除数据作为 DTO 对象的 `CanEdit` 和 `CanDelete` 属性,以便 UI 可以绑定到这些属性,并根据权限提供或隐藏功能。

哇,使用如此强大的授权框架是多么容易!

带有登录和身份验证场景的 AngularJS 应用程序

感受授权框架工作方式的最佳方法是玩转示例管理员工具应用程序。注册为普通用户和管理员用户(您有能力让自己成为管理员!)。使用不同类型的用户登录(和登出)后,查看/编辑信息。

以下是公共(未登录)用户看到的讨论视图,只能查看信息。

以下是 Bob(一位非管理员会员)看到的同一视图。Bob 可以编辑他自己的讨论并创建新的讨论。

以下是管理员用户看到的同一视图。管理员用户可以创建和编辑任何内容。

请注意,如果 Bob(非管理员用户)试图搞鬼,并伪造更新服务调用来更新他不允许更新的项目(例如,使用 Fiddler),那么 VITA 授权将拦截并抛出 AccessDenied 异常,从而有效地取消该操作。

使用 VITA 进行实际项目

本文包括一些使用 VITA 的示例 Web API 应用程序。但对于在实际世界中构建大型、可伸缩的企业应用程序,VITA 的表现如何?

特点

在核心 ORM 功能、通用应用程序支持,特别是 Web API 支持方面,我认为 VITA 在现有的框架中名列前茅。如前所述,VITA 特别鼓励构建可伸缩、松耦合的架构。从功能角度来看,我完全有信心在实际的企业项目中使用 VITA。

开源和支持水平

企业在采用开源解决方案时经常担心的一个问题是支持。项目的活跃度如何?有多少安装?支持响应速度如何?

VITA 开源项目非常活跃,在可预见的未来不太可能成为死胡同。最近发布了 1.1 版本,并更新了 nuget 包。我知道还有一些附加功能正在开发中。我对 VITA 不会消失感到满意。

有多少安装以及 VITA 的用户有多少?我相信 VITA 已经在生产和开发中得到了积极使用,并且已经有多个应用程序在云中运行。据我从 Roman 那里了解,VITA 正在云中的多个生产服务器上运行,甚至被用于国际空间站(ISS)。

有多少项目贡献者提供支持?网站上没有列出其他开发者,如果我能获得更多这方面的数据,我会提供一些信息。但我可以从 Irony 项目的经验中说,Roman 是长远发展的,并且对问题和讨论非常积极,确保它们得到解决。我对 VITA 项目的期望也不低。

管理生产数据

在任何生产环境中,能够维护生产数据的完整性并能够应用任何所需的架构或数据更新都至关重要。目前生产环境中的方法是不让 VITA 自动更新您的架构。相反,请使用 VITA 的 vdbtool 生成数据库更改的 DDL 脚本。在将更改应用于生产之前,您可以编辑脚本以进行任何必要的更改。我计划对此进行一些测试,并在本文中更新一两个场景。

继续使用 VITA

以下是一些额外的资源,可以帮助您在未来使用 VITA 和/或了解更多 VITA 的功能和能力。

下载次数

在您的 .NET 应用程序中开始使用 VITA 最简单的方法是安装 VITA nuget 包。可用的包包括:

  • VITA – 此包包含核心 ORM 库和 MS SQL Server 驱动程序。
  • VITA.Modules – 包含日志记录和登录等模块(加密数据和第三方未在此处介绍)。
  • Vita.Web – 此包包括 ORM 功能与 Web API 堆栈的集成。
  • Vita.Data.xxx – 这些包用于支持其他数据库,如 MS SQL CE、My SQL、PostreSql 和 SQLite。

您也可以从 VITA github 站点 下载 VITA。

文档

VITA github 站点是 VITA 功能、能力和当前问题的文档的最佳来源。VITA codeplex 站点也可能对遗留讨论和问题有用。

示例

除了利用本文中的示例外,如果您从 VITA github 站点下载源代码,您可以查看并尝试 BookStore 示例,该示例比本文中的示例更详细地利用了一些功能。

利用最新的 VITA 功能

VITA 始终在不断发展,至少每月都会添加新功能。一个缺点(至少对我而言)是升级到最新版本的 VITA 时,现有代码可能会中断(主要是由于持续的重构)。我计划在版本 2 发布时更新本文,以介绍一些附加功能,并更新示例等。

在此期间,更新代码以适应 VITA 中最新更改和功能的最佳方法是什么?我执行以下步骤:

  1. 我查看 github 上的更新历史记录(对于 1.8.7 版本之前的版本,我在 codeplex 上查看),其中概述了新功能和可能破坏您代码的重构。我通常选择在有错误修复或我想要的新功能时进行升级。
  2. 下载最新的源代码。特别是,仔细查看(并运行)示例 BookStore 应用程序。应用程序和相关的单元测试经常更新以利用最新功能。
  3. 更新您的应用程序和测试,并同时参考以上两项。

VITA 数据库工具

VITA github 站点上的源代码下载还包含一个数据库工具(代码生成器),允许您从现有数据库中初步生成数据模型。这为您提供了持续工作的良好起点,尤其是在您有一个遗留数据库作为起点时。

Mo+ 模板

示例应用程序是使用 Mo+ 生成的。您可以使用的模板在“Mo+ VITA 模板”下载中,它们可以为您数据库/模型生成整体应用程序层,作为持续工作的绝佳起点。您可以使用源数据库作为您的 Mo+ 模型,从而采用“数据库优先”的方法来处理您的整体应用程序。无论如何,Mo+ 将根据您对 Mo+ 模型的更改,对您的所有代码进行正确的持续更新。请遵循下载中自述文件中的说明,以了解如何开始使用这些模板。

结论

我希望本文能让您对 VITA ORM 和 .NET 应用程序框架有一个很好的了解,并激发您更深入地研究和使用这个框架。请在下方发表您对 VITA 和/或本文或应用程序下载的任何想法。我个人迫不及待地想在未来的项目中使用 VITA!

 
© . All rights reserved.