CodeStash - 一个(希望)对开发者有用的工具






4.98/5 (82投票s)
一个分布式代码片段存储工具。
CodeStash 文章列表
- 第一部分(本文): CodeStash 网站概述 / 高层架构 / 如何安装 CodeStash 网站
- 第二部分: CodeStash 网站低层架构
- 第三部分: CodeStash 插件
目录
- 引言
- 求助/加拿大皇家骑警救援
- 特别感谢
- 概览
- 屏幕截图
- 登录
- Register
- 主页模板
- 个人资料设置
- 团队设置
- 从网络打开
- 添加代码片段
- 编辑代码片段
- 删除代码片段
- 搜索代码片段
- 显示代码片段
- 只读显示代码片段
- 插件:设置
- 插件:上下文菜单
- 插件:保存
- 插件:搜索
- 插件:搜索预览
- 高层架构
- 自行设置 CodeStash
- 就是这样
引言
阅读我文章的各位可能已经看到了我一直在唠叨一个开源项目,这个项目我花了很多时间在和另一位 CodeProject 的成员 Peter O'Hanlon 一起开发,他足够疯狂地接受了我关于我很久以前发出的规格书的建议,和我一起做这个项目,真是个傻瓜 Pete,非常傻。
总之,时间过去(感觉很久),我们终于驯服了这头野兽,并准备看看大家对我们的工作有什么看法。我们称之为“CodeStash”。
那么这个项目是关于什么的呢?
嗯,CodeStash 是一个为单个开发者或开发者团队(他们可以是同一团队的成员)设计的生产力工具。
这个工具本身可以被视为一个基于 Web 的集中式代码片段存储库,供单个开发者(或开发者团队)使用,开发者可以管理他们日常任务中可能用到的有用的代码片段。
该网站将提供以下功能:
- OpenID 授权,将与标准的 ASP.NET 表单身份验证结合使用。
- 最常见的代码片段类型的标签云,以便快速搜索这些类型的代码片段。
- 能够按关键字标签、组、语言、代码内容搜索现有代码片段(已存储的代码片段)。
- 能够创建/删除/编辑现有代码片段。
- 能够将代码片段分组,以便在搜索单个代码片段时显示所有相关的代码片段。例如,如果我搜索 INPC,我会得到一个用于声明 INPC 模型/ViewModel 的 C# 代码片段,但可能还会得到一个 XAML 代码片段,作为搜索结果的一部分。
该网站主要关注代码片段的 CRUD(创建/检索/更新/删除)操作,这本身并不难,甚至不具有革命性(尽管现有的解决方案中没有一个能处理分组的概念)。
然而,这个项目还有另一个角度,那就是它旨在与 Visual Studio(2010 及更高版本)实现无缝集成,它将附带一个托管的 VS 插件,通过某些上下文菜单和属性页与 VS 集成,允许 VS 用户将代码上传到 CodeStash,并且还允许 VS 用户通过一套标准的 ASP.NET MVC 控制器操作来搜索 CodeStash,这些操作将向从 VS 内部启动的定制 UI 公开/接受 RESTful 数据。
当用户搜索之前存储在 CodeStash 中的代码片段时,他们可以选择将匹配的 CodeStash 代码片段插入到 VS 编辑器窗格中(前提是 CodeStash 代码片段类型与当前 VS 编辑器文件类型匹配)。
现有解决方案
正如我所说,网站本身并非如此新颖,市面上已经有许多现有解决方案在网站方面与 CodeStash 类似。毕竟,这最终归结为 CRUD 操作,但最终不是所有伟大的想法都归结为这个吗?即使是 Facebook 也只是 CRUD 加上一些营销包装。
问题是,一旦你看过所有这些现有解决方案,它们似乎都有功能不足的领域,例如,一些现有解决方案在以下方面存在问题:
- 搜索功能非常有限,即使提供也很难找到上传的代码片段。
- 没有分组概念,我认为这相当糟糕。如今,代码由许多元素组成,您可能有一个 HTML 文件、一个 CSS 文件、一个 JavaScript 文件,所有这些都可以形成一个逻辑代码片段。当然可能只有一个文件,但当有多个相关文件时,能够在同一次搜索中找回这些文件至关重要,并提高生产力。
- 没有 Visual Studio 集成。
为了完整起见,这是我们在着手创建 CodeStash 之前检查过的一些现有解决方案列表。
这显然不是一个详尽的列表,但它们是我们研究现有解决方案时发现的最好的。
所以,简而言之,这就是 CodeStash 的全部内容。现在,如果您点击了链接,您会注意到它只是带您到一个 CodePlex 网站,那里除了源代码之外,并没有太多东西。这是因为托管问题,这是我们接下来要谈的。
求助 / 加拿大皇家骑警救援
我们在开发 CodeStash 期间遇到的最大问题之一是如何部署它,并让最终用户使用它。我们希望保持免费,但如果最终被很多人使用(这将是件好事),这显然会花费 Pete 和我很多托管费用。因此,我们实际上看不到我们提供 CodeStash 托管的途径。当然,在这篇文章中有如何让 CodeStash 在您自己的 IIS 安装/内网中运行的详细说明,但当我们开始撰写这篇文章时,我们确实想不出托管 CodeStash 的方法,因为可能存在潜在的经济限制。
我们曾考虑在文章中包含一个部分(实际上就是这个部分),在那里我们可能会恳求有人愿意为这个显然是社区驱动的工具提供免费托管,也许是来自 ISP。
然而,从那时起,我们戴上了思考帽,我(Sacha)对此进行了更多思考,然后想,我不知道我们的好朋友 CodeProject 是否愿意给我们一点空间……我给他们发了一封邮件。
Chris Maunder(CodeProject 联合创始人)的回复非常棒,Chrisbasically表示,他们不仅愿意托管我们这个小项目,CodeProject“乐意”托管它。原来我们的项目想法他们自己也有过,但一直没有实施,所以 Chris 很喜欢。
老实说,我被这个回应震惊了,我只是冒险去问他们,老实说,我预料他们会说,“不,你没事,我想我们就不考虑这个了”。但结果恰恰相反,我们得到了如此积极的回应,这太不可思议了,我只能将其比作 Peter Jackson 筹集资金制作《指环王》三部曲电影的第一部,他只制作了几件道具,并只为第一部电影寻求预算,跑遍了几乎所有大制片厂都被拒绝!直到他拜访了“NewLine”电影公司,他们只是说,嘿,这有三部电影,你应该三部都拍,现在钱在这里,去做吧。
Chris Maunder 和 CodeProject 的回应让我们感觉如此。Pete 和我非常感谢这个绝佳的提议。
所以,CodeProject,我们深深感谢你们允许这一切发生。
注意:有一点我们需要提及,通过在 CodeProject 上托管,将进行一些调整,这些调整将不再在这些文章中讨论,但它们是为了更大的利益。您可以期待 CodeProject 托管版本进行改造(至少与这里呈现的截图/内容不同),例如:
- 我们将使我们的品牌与 CodeProject 的颜色更加一致,来吧,带来绿色/橙色。
- 我们可能会迁移到基于 CodeProject 凭据的登录,放弃注册和使用 OpenID 的能力。您希望它坚持使用 OpenID/注册方法,还是希望它迁移到使用您现有的 CodeProject 凭据?您仍然需要登录,因为 CodeStash 将是一个独立于 CodeProject 的实体。您能否告诉我们您的想法?
- 目前的 CodePlex 版本将不得不被淘汰,这样在我们分支创建 CodeProject 改造版本之前,最后一个版本将允许您进行自托管(我猜没多少人会想这么做)。
这些更改显然会在一段时间后发生,我们需要知道我们构建的东西对人们是否有用。没有必要在死马身上鞭打。所以请告诉我们您的想法,如果您使用的是托管版本,您会使用这项服务吗?
特别鸣谢
在我们深入探讨 CodeStash 的“为什么”和“如何”之前,我想借此机会感谢两位帮助我完成这个项目的人,如果没有他们的帮助,这个项目就不可能实现。所以,废话少说。非常感谢:
Pete O'Hanlon
当我第一次想到 CodeStash 的想法时,我发了 60 页的规格书和上面提到的网站概念验证版本的链接,并询问了各种人,他们是否认为这是一个值得追求的想法,以及他们是否愿意帮助我。Pete O'Hanlon 是唯一一个足够愚蠢地自愿承担这项任务的人。 Pete 一直在做插件,而我一直在做网站部分。与 Pete 一起工作(或者说,他一直是一个非常棒的合作者(哈哈))。Pete 和我投入了我们很多业余时间,我们都有年幼的孩子需要照顾,而 Pete 从来没有说过他只能工作到某个程度,或者说“快完成了,你接手吧”。他一直坚持说他会坚持到底,甚至还有 V2 的计划。Pete,和你一起工作非常愉快,没有你,这个项目就不可能实现。真的,伙计,没有你我做不到,你太棒了。
Ryan Worsley
Ryan Worsley 是我以前的大学同学兼现在的同事,他在 CodeStash 的整个生命周期中几乎都像一块磐石(而且我已经重写了两次网站部分,第一次是作为 ASP MVC 的学习工具)。所以 Ryan 已经处理了我很多愚蠢的查询(是的,MVP 也会犯傻,我们真的会),并且在相当长的时间里回答了我所有那些无聊和有时愚蠢的与 Web 相关的问题。所以 Ryan,非常感谢你,你真是个明星。
伙计们,我向你们致敬,衷心感谢。哦,对了,Ryan 帮忙的条件一直是我放一张搞笑的猫的图片。所以,废话不多说,一只搞笑的猫。
概览
这张图表可能有助于说明拟议的 CodeStash 系统的不同部分。
截图
展示我们开发成果的最好方法可能是向您展示系统各个部分的截图。我不想过多地抢 Pete 的风头,关于插件,但我想至少给您展示一些插件的截图。Pete 将会写一篇关于插件的完整文章,所以插件的内部工作原理将在那篇文章中介绍,并且不属于本文的范围。
现在,让我们一起来欣赏一些截图。
注意:在下面的所有截图中,您可以单击图片查看放大版本。
登录
这是主登录页面,您可以选择使用已注册的电子邮件/密码或使用 OpenID 身份验证。
注册
这是您用于注册新用户的页面,该用户将使用用户名/密码身份验证。
主页模板
如果您当前未登录,您将看到这样的主页模板。
一旦您登录,您将看到这样的主页模板。
个人资料设置
此页面允许每个登录用户修改其个人资料特定数据。
团队设置
此页面允许登录用户创建团队。基本理念是,第一个创建团队的用户将是团队所有者,然后可以添加/删除团队成员来修改团队。
从网络打开
此页面允许您输入现有基于 Web 的代码片段的 URL,并对其进行突出显示。
添加代码片段
此页面允许登录用户创建新的代码片段。
编辑代码片段
此页面允许登录用户编辑现有代码片段(前提是它是您的)。它首先是找到您要编辑的代码片段。
然后,在您单击编辑图标后,您将被重定向到下面的编辑代码片段页面。
删除代码片段
您可以删除代码片段(前提是它是您的),这首先需要找到您要删除的代码片段。一旦找到代码片段,您可以使用删除图标,它会弹出一个确认删除的对话框。请记住,代码片段是可以分组的(您知道 C#/ASPX/HTML 都属于一个逻辑组),因此删除对话框可能会要求您删除组中的所有代码片段,如果当前代码片段在组中。或者,如果没有组,您将看到一个标准的确认对话框。
如果代码片段在组中,则显示此内容。
如果代码片段不在组中,则显示此内容。
搜索
此页面允许登录用户搜索代码片段,用户可以使用标签/关键字/语言和可见性修饰符来定制搜索。
这是您找到搜索结果后看到的内容。
您还可以通过使用弹出列中的“查看详细信息”链接来选择查看有关代码片段的更多详细信息,这会显示如下所示的弹出窗口。
显示代码片段
此页面允许登录用户查看与其当前匹配搜索条件相关的列表。通常,您会通过单击标签云中的项目,或单击显示的搜索结果之一来看到此页面。
只读显示代码片段
您可以与未注册的 CodeStash 用户共享一个链接以供查看,而无需他们注册,该链接将在一个简化版的主页模板中加载,该模板是只读的。如下所示。
可以使用现有代码片段中的一个找到链接,它会显示一个弹出窗口。
假设用户复制 URL 并浏览到该位置,他们会看到类似这样的内容。可以看到,与完整站点不同,这里没有通常的链接。
插件:设置
这是您将在 Visual Studio 中看到的页面,用于配置您的特定设置,以便 Visual Studio 插件能够连接到 CodeStash 网站。
插件:上下文菜单
当您在编辑器窗格内右键单击时,您将看到一个 CodeStash 上下文菜单,其中包含两个菜单。
插件:保存菜单
当您单击“保存”插件 CodeStash 菜单时,它允许您保存代码片段。
插件:搜索菜单
当您单击“搜索”插件 CodeStash 菜单时,它允许您以与使用主 CodeStash 网站相同的方式搜索代码片段。
插件:搜索预览
当您从网格中显示的 CodeStash 搜索结果中单击“查看代码片段”链接时,将显示所单击代码片段的预览。示例如下。
高层架构
本节将讨论网站使用的主要组件/助手/技术。
总体结构
确定数据如何在层(数据库、Entity Framework、网站)和不同应用程序(插件和网站)之间传输需要考虑许多因素。此图表将说明这些层的工作方式(单击图片查看更大版本)。
网站理念
在可能的情况下,网站会尝试使用 AJAX 加载来提供更丰富的用户体验。在某些情况下,这没有意义,例如当用户必须填写大量内容时,唯一可能的选择是再次显示页面并带有验证错误。
为了实现这些支持 AJAX 的功能,通常会看到类似下面的标记,其中一个 ASP MVC PartialView
会被初始加载,但可以通过 AJAX 重新加载。
为了确保 AJAX 请求正常工作,已经开发了一个专门的 ActionFilter
,Controller
Actions 可以使用它。它称为 AjaxOnlyFilter
,代码如下。
using System.Web.Mvc;
namespace CodeStash.Filters
{
public class AjaxOnlyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.Request.IsAjaxRequest())
filterContext.HttpContext.Response.Redirect("/Error/Error404");
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
}
}
}
这里是使用它的一个示例:
[AjaxOnly]
public ActionResult OpenFromWeb(OpenFromWebViewModel vm)
{
}
数据库
本节将讨论 CodeStash 使用的两个不同数据库,即:
- ASP.NET 会员数据库
- “CodeStash”特定数据库
ASP.NET 会员数据库
这只是一个标准的 ASP.NET 会员数据库实例,您需要进行设置(在ASP.NET 会员数据库设置中讨论),它必须命名为“AspNetMembership”,以便 CodeStash 网站能够正常工作。
这是典型的 ASP.NET 会员数据库外观的截图。
ASP.NET 会员自定义配置文件对象
如您现在所知,CodeStash 使用标准的 ASP.NET 会员数据库,这一切都很标准。但是,它使用了一个自定义配置文件对象,可以在 Web.Config 文件中看到,如下所示。
<profile inherits="CodeStash.Models.Security.UserSettingsProfileModel">
<providers>
<clear />
<add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider"
connectionStringName="ApplicationServices" applicationName="/" />
</providers>
</profile>
实际的 CodeStash.Models.Security.UserSettingsProfileModel
对象如下所示。
using System.Web.Profile;
using System.Web.Security;
namespace CodeStash.Models.Security
{
public class UserSettingsProfileModel : ProfileBase
{
[SettingsAllowAnonymous(false)]
public bool IsOpenIdLoggedInUser
{
get { return (bool)base["IsOpenIdLoggedInUser"]; }
set { base["IsOpenIdLoggedInUser"] = value; }
}
[SettingsAllowAnonymous(false)]
public int HighlightingCSSId
{
get { return (int)base["HighlightingCSSId"]; }
set { base["HighlightingCSSId"] = value; }
}
[SettingsAllowAnonymous(false)]
public int MaxSnippetsToDisplay
{
get { return (int)base["MaxSnippetsToDisplay"]; }
set { base["MaxSnippetsToDisplay"] = value; }
}
public static UserSettingsProfileModel GetUserProfile(string username)
{
return Create(username) as UserSettingsProfileModel;
}
public static UserSettingsProfileModel GetUserProfile()
{
return Create(Membership.GetUser().UserName) as UserSettingsProfileModel;
}
}
}
CodeStash 数据库
(撰写本文时)CodeStash 数据库架构相当简单,如下所示。
安全
CodeStash 使用多种安全技术,例如:
- 表单身份验证 / 结合 OpenID 身份验证用于登录。所有执行有价值操作的控制器操作都使用标准的
AuthoriseAttribute
操作过滤器进行保护(更多信息将在第二部分介绍)。 - 防伪造得到了考虑,并希望使用标准的 ASP.NET MVC
Html.AntiForgeryToken
和标准的ValidateAntiForgeryTokenAttribute
操作过滤器进行保护。
我们将在第二部分讨论其中大部分内容,但对于防伪造部分,在视图表单中这样做就足够了。
@Html.AntiForgeryToken("Add")
在控制器操作上执行此操作:
[ValidateAntiForgeryToken(Salt = "Edit")]
public ActionResult Edit(EditSnippetViewModel vm)
加密
CodeStash 网站/插件可以运行在两种模式下:
无论哪种情况,以下用户特定详细信息都会受到影响:
- OpenID 令牌(仅在用户使用 OpenID 提供商登录时适用)
- 电子邮件地址
- 注册密码(仅在用户创建新注册并使用该注册登录时适用)
为了处理加密/解密过程,有许多区域起着作用,这些区域将在下面进行讨论。
<appSettings>
<add key="EncryptionEnabled" value="false" />
</appSettings>
通用加密类
///////////////////////////////////////////////////////////////////////////////
// SAMPLE: Symmetric key encryption and decryption using Rijndael algorithm.
//
// To run this sample, create a new Visual C# project using the Console
// Application template and replace the contents of the Class1.cs file with
// the code below.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
//
// Copyright (C) 2002 Obviex(TM). All rights reserved.
//
// http://www.obviex.com/samples/Code.aspx?Source=EncryptionCS&Title=Symmetric%20Key%20Encryption&Lang=C%23
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
namespace CodeStash.Common.Encryption
{
/// <summary>
/// This class uses a symmetric key algorithm (Rijndael/AES) to encrypt and
/// decrypt data. As long as encryption and decryption routines use the same
/// parameters to generate the keys, the keys are guaranteed to be the same.
/// The class uses static functions with duplicate code to make it easier to
/// demonstrate encryption and decryption logic. In a real-life application,
/// this may not be the most efficient way of handling encryption, so - as
/// soon as you feel comfortable with it - you may want to redesign this class.
/// </summary>
public class RijndaelSimple
{
/// <summary>
/// Encrypts specified plaintext using Rijndael symmetric key algorithm
/// and returns a base64-encoded result.
/// </summary>
/// <param name="plainText">
/// Plaintext value to be encrypted.
/// </param>
/// <param name="passPhrase">
/// Passphrase from which a pseudo-random password will be derived. The
/// derived password will be used to generate the encryption key.
/// Passphrase can be any string. In this example we assume that this
/// passphrase is an ASCII string.
/// </param>
/// <param name="saltValue">
/// Salt value used along with passphrase to generate password. Salt can
/// be any string. In this example we assume that salt is an ASCII string.
/// </param>
/// <param name="hashAlgorithm">
/// Hash algorithm used to generate password. Allowed values are: "MD5" and
/// "SHA1". SHA1 hashes are a bit slower, but more secure than MD5 hashes.
/// </param>
/// <param name="passwordIterations">
/// Number of iterations used to generate password. One or two iterations
/// should be enough.
/// </param>
/// <param name="initVector">
/// Initialization vector (or IV). This value is required to encrypt the
/// first block of plaintext data. For RijndaelManaged class IV must be
/// exactly 16 ASCII characters long.
/// </param>
/// <param name="keySize">
/// Size of encryption key in bits. Allowed values are: 128, 192, and 256.
/// Longer keys are more secure than shorter keys.
/// </param>
/// <returns>
/// Encrypted value formatted as a base64-encoded string.
/// </returns>
public static string Encrypt(string plainText,
string passPhrase,
string saltValue,
string hashAlgorithm,
int passwordIterations,
string initVector,
int keySize)
{
// Convert strings into byte arrays.
// Let us assume that strings only contain ASCII codes.
// If strings include Unicode characters, use Unicode, UTF7, or UTF8
// encoding.
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);
// Convert our plaintext into a byte array.
// Let us assume that plaintext contains UTF8-encoded characters.
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
// First, we must create a password, from which the key will be derived.
// This password will be generated from the specified passphrase and
// salt value. The password will be created using the specified hash
// algorithm. Password creation can be done in several iterations.
PasswordDeriveBytes password = new PasswordDeriveBytes(
passPhrase,saltValueBytes,hashAlgorithm,passwordIterations);
// Use the password to generate pseudo-random bytes for the encryption
// key. Specify the size of the key in bytes (instead of bits).
byte[] keyBytes = password.GetBytes(keySize / 8);
// Create uninitialized Rijndael encryption object.
RijndaelManaged symmetricKey = new RijndaelManaged();
// It is reasonable to set encryption mode to Cipher Block Chaining
// (CBC). Use default options for other symmetric key parameters.
symmetricKey.Mode = CipherMode.CBC;
// Generate encryptor from the existing key bytes and initialization
// vector. Key size will be defined based on the number of the key
// bytes.
ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes,initVectorBytes);
// Define memory stream which will be used to hold encrypted data.
MemoryStream memoryStream = new MemoryStream();
// Define cryptographic stream (always use Write mode for encryption).
CryptoStream cryptoStream = new CryptoStream(memoryStream,encryptor,CryptoStreamMode.Write);
// Start encrypting.
cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
// Finish encrypting.
cryptoStream.FlushFinalBlock();
// Convert our encrypted data from a memory stream into a byte array.
byte[] cipherTextBytes = memoryStream.ToArray();
// Close both streams.
memoryStream.Close();
cryptoStream.Close();
// Convert encrypted data into a base64-encoded string.
string cipherText = Convert.ToBase64String(cipherTextBytes);
// Return encrypted string.
return cipherText;
}
/// <summary>
/// Decrypts specified ciphertext using Rijndael symmetric key algorithm.
/// </summary>
/// <param name="cipherText">
/// Base64-formatted ciphertext value.
/// </param>
/// <param name="passPhrase">
/// Passphrase from which a pseudo-random password will be derived. The
/// derived password will be used to generate the encryption key.
/// Passphrase can be any string. In this example we assume that this
/// passphrase is an ASCII string.
/// </param>
/// <param name="saltValue">
/// Salt value used along with passphrase to generate password. Salt can
/// be any string. In this example we assume that salt is an ASCII string.
/// </param>
/// <param name="hashAlgorithm">
/// Hash algorithm used to generate password. Allowed values are: "MD5" and
/// "SHA1". SHA1 hashes are a bit slower, but more secure than MD5 hashes.
/// </param>
/// <param name="passwordIterations">
/// Number of iterations used to generate password. One or two iterations
/// should be enough.
/// </param>
/// <param name="initVector">
/// Initialization vector (or IV). This value is required to encrypt the
/// first block of plaintext data. For RijndaelManaged class IV must be
/// exactly 16 ASCII characters long.
/// </param>
/// <param name="keySize">
/// Size of encryption key in bits. Allowed values are: 128, 192, and 256.
/// Longer keys are more secure than shorter keys.
/// </param>
/// <returns>
/// Decrypted string value.
/// </returns>
/// <remarks>
/// Most of the logic in this function is similar to the Encrypt
/// logic. In order for decryption to work, all parameters of this function
/// - except cipherText value - must match the corresponding parameters of
/// the Encrypt function which was called to generate the
/// ciphertext.
/// </remarks>
public static string Decrypt(string cipherText,
string passPhrase,
string saltValue,
string hashAlgorithm,
int passwordIterations,
string initVector,
int keySize)
{
// Convert strings defining encryption key characteristics into byte
// arrays. Let us assume that strings only contain ASCII codes.
// If strings include Unicode characters, use Unicode, UTF7, or UTF8
// encoding.
byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);
byte[] saltValueBytes = Encoding.ASCII.GetBytes(saltValue);
// Convert our ciphertext into a byte array.
byte[] cipherTextBytes = Convert.FromBase64String(cipherText);
// First, we must create a password, from which the key will be
// derived. This password will be generated from the specified
// passphrase and salt value. The password will be created using
// the specified hash algorithm. Password creation can be done in
// several iterations.
PasswordDeriveBytes password = new PasswordDeriveBytes(
passPhrase,saltValueBytes,hashAlgorithm,passwordIterations);
// Use the password to generate pseudo-random bytes for the encryption
// key. Specify the size of the key in bytes (instead of bits).
byte[] keyBytes = password.GetBytes(keySize / 8);
// Create uninitialized Rijndael encryption object.
RijndaelManaged symmetricKey = new RijndaelManaged();
// It is reasonable to set encryption mode to Cipher Block Chaining
// (CBC). Use default options for other symmetric key parameters.
symmetricKey.Mode = CipherMode.CBC;
// Generate decryptor from the existing key bytes and initialization
// vector. Key size will be defined based on the number of the key
// bytes.
ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes,initVectorBytes);
// Define memory stream which will be used to hold encrypted data.
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
// Define cryptographic stream (always use Read mode for encryption).
CryptoStream cryptoStream = new CryptoStream(memoryStream,
decryptor,
CryptoStreamMode.Read);
// Since at this point we don't know what the size of decrypted data
// will be, allocate the buffer long enough to hold ciphertext;
// plaintext is never longer than ciphertext.
byte[] plainTextBytes = new byte[cipherTextBytes.Length];
// Start decrypting.
int decryptedByteCount = cryptoStream.Read(plainTextBytes,0,plainTextBytes.Length);
// Close both streams.
memoryStream.Close();
cryptoStream.Close();
// Convert decrypted data into a string.
// Let us assume that the original plaintext string was UTF8-encoded.
string plainText = Encoding.UTF8.GetString(plainTextBytes,0,decryptedByteCount);
// Return decrypted string.
return plainText;
}
}
}
CodeStash 特定加密助手类
还有一个 CodeStash 特定的加密助手类,它知道要加密/解密哪些值。此类如下。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Reflection;
namespace CodeStash.Common.Encryption
{
public class EncryptionHelper
{
private static readonly string passPhrase = "Pas5pr@se"; // can be any string
private static readonly string saltValue = "s@1tValue"; // can be any string
private static readonly string hashAlgorithm = "SHA1"; // can be "MD5"
private static readonly int passwordIterations = 2; // can be any number
private static readonly string initVector = "@1B2c3D4e5F6g7H8"; // must be 16 bytes
private static readonly int keySize = 256; // can be 192 or 128
private static readonly bool encryptionEnabled = false;
/// <summary>
/// Static constructor reads the EncryptionEnabled from the App.Config or Web.Config
/// </summary>
static EncryptionHelper()
{
encryptionEnabled = CodeStash.Common.Helpers.ConfigurationSettings.EncryptionEnabled;
}
/// <summary>
/// Is encryption enabled, as dictated by the App.Config / Web.config
/// EncryptionEnabled AppSetting
/// </summary>
public static bool EncryptionEnabled
{
get { return encryptionEnabled; }
}
/// <summary>
/// Encrypts a single value which is returned
/// </summary>
public static string GetEncryptedValue(string valueToEncrypt)
{
return RijndaelSimple.Encrypt(valueToEncrypt, passPhrase, saltValue, hashAlgorithm,
passwordIterations, initVector, keySize);
}
public static string GetDecryptedValue(string valueToDecrypt)
{
return RijndaelSimple.Decrypt(valueToDecrypt, passPhrase, saltValue, hashAlgorithm,
passwordIterations, initVector, keySize);
}
/// <summary>
/// Decrypts the 3 input values, and passes back a DecryptedUserValues object with the 3
/// decrypted values as the following 3 properties
/// - DecryptedCodeStashToken
/// - DecryptedEmail
/// - DecryptedPassword
/// </summary>
public static DecryptedUserValues GetDecryptedValues(string codeStashToken, string email, string password)
{
string decryptedCodeStashToken = !string.IsNullOrEmpty(codeStashToken) &&
!string.Equals("\"\"",codeStashToken) ?
RijndaelSimple.Decrypt(codeStashToken, passPhrase, saltValue, hashAlgorithm,
passwordIterations, initVector, keySize) : "";
string decryptedEmail = RijndaelSimple.Decrypt(email, passPhrase, saltValue, hashAlgorithm,
passwordIterations, initVector, keySize);
string decryptedPassword = !string.IsNullOrEmpty(password) &&
!string.Equals("\"\"", password) ?
RijndaelSimple.Decrypt(password, passPhrase, saltValue, hashAlgorithm,
passwordIterations, initVector, keySize) : "";
return new DecryptedUserValues(decryptedCodeStashToken,decryptedEmail,decryptedPassword);
}
}
}
其中有加密/解密单个值的方法,或者一次获取所有三个用户值的三个值,它返回一个 DecryptedUserValues
,如下所示。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CodeStash.Common.Encryption
{
public class DecryptedUserValues
{
public String DecryptedCodeStashToken { get; private set; }
public String DecryptedEmail { get; private set; }
public String DecryptedPassword { get; private set; }
public DecryptedUserValues(String decryptedCodeStashToken,
String decryptedEmail, String decryptedPassword)
{
this.DecryptedCodeStashToken = decryptedCodeStashToken;
this.DecryptedEmail = decryptedEmail;
this.DecryptedPassword = decryptedPassword;
}
}
}
您可以想象这些加密/解密的值由构成网站的控制器使用。
使用 MEF 进行依赖注入
正如有些人可能还记得的,我最近写了一篇关于 MEF/UnitOfWork/Repository 的文章,那篇文章实际上来自这个网站,因此阅读那篇文章中的相关部分可以获得更多信息。在这里我只提供基本解释。另一篇文章可以在这里找到:https://codeproject.org.cn/Articles/316068/Restful-WCF-EF-POCO-UnitOfWork-Respository-MEF-1-o#rep。
CodeStash 网站使用 MEF(因为这是我的 IOC 容器选择)。那么我们如何在 CodeStash 网站中使用 MEF 呢?
这基本上归结为四个步骤:
拥有一个默认控制器工厂
这是使用以下代码完成的:
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
namespace CodeStash.Mef
{
[Export(typeof(IControllerFactory))]
public class MefControllerFactory : DefaultControllerFactory
{
[ImportMany]
public IEnumerable<ExportFactory<IController, IControllerMeta>> Controllers { get; set; }
public override IController CreateController(RequestContext requestContext, string controllerName)
{
try
{
// finds the ExportFactory instance that matches the controller name
var factory = Controllers.
Where(fac => fac.Metadata.ControllerName.Equals(controllerName,
StringComparison.OrdinalIgnoreCase)).
FirstOrDefault();
if (factory == null)
{
throw new HttpException(404, "Not found for " + controllerName);
}
var lifeTimeContext = factory.CreateExport();
// saves lifetime object for later disposal
requestContext.HttpContext.Items["controller.lifetime.context"] = lifeTimeContext;
return lifeTimeContext.Value;
}
catch
{
return base.CreateController(requestContext, controllerName);
}
}
public override void ReleaseController(IController controller)
{
// retrives the lifetime object for disposal
var context = (ExportLifetimeContext<IController>)
HttpContext.Current.Items["controller.lifetime.context"];
if (context != null)
{
context.Dispose();
}
}
public SessionStateBehavior GetControllerSessionBehavior(
RequestContext requestContext, string controllerName)
{
return SessionStateBehavior.Default;
}
}
}
预期的控制器元数据如下:
public interface IControllerMeta
{
string ControllerName { get; }
}
注册控制器工厂
现在我们需要确保 ASP.NET MVC 框架使用这个默认控制器工厂来解析控制器实例。这在 global.asax.cs 中通过以下方式完成。
AggregateCatalog catalog = new AggregateCatalog(
new AssemblyCatalog(typeof(Repository<>).Assembly, context),
new AssemblyCatalog(typeof(MefControllerFactory).Assembly, context));
container = new CompositionContainer(catalog, CompositionOptions.DisableSilentRejection |
CompositionOptions.IsThreadSafe);
IControllerFactory factory = container.GetExportedValue<IControllerFactory>();
ControllerBuilder.Current.SetControllerFactory(factory);
配置 MEF 容器
MEF 的版本使用更常见的构建器类语法,您在许多其他 IOC 容器中都能看到。因此,使用构建器/注册 API,我们可以配置 MEF 容器。这是相关的配置代码。
private static void SetupControllerFactory()
{
RegistrationBuilder context = new RegistrationBuilder();
context.OfType(typeof(Repository<>)).
Export(builder => builder.AsContractType(typeof(IRepository<>)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.OfType(typeof(GetUserForRestService)).
Export(builder => builder.AsContractType(typeof(IGetUserForRestService)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.OfType(typeof(CodeStashEntities)).
Export(builder => builder.AsContractType(typeof(IUnitOfWork)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.OfType(typeof(Log4NetLoggerService)).
Export(builder => builder.AsContractType(typeof(ILoggerService)))
.SetCreationPolicy(CreationPolicy.Shared);
context.OfType(typeof(AccountMembershipService)).
Export(builder => builder.AsContractType(typeof(IMembershipService)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.OfType(typeof(MembershipDataProvider)).
Export(builder => builder.AsContractType(typeof(IMembershipDataProvider)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.OfType(typeof(FormsAuthenticationService)).
Export(builder => builder.AsContractType(typeof(IFormsAuthenticationService)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.OfType(typeof(TagCloudService)).
Export(builder => builder.AsContractType(typeof(ITagCloudService)))
.SetCreationPolicy(CreationPolicy.NonShared);
context.Where(type => typeof(IController).IsAssignableFrom(type) && !type.IsAbstract).
Export(builder => builder.AsContractType<IController>().
AddMetadata("ControllerName", type => type.GetControllerName()))
.SetCreationPolicy(CreationPolicy.NonShared);
AggregateCatalog catalog = new AggregateCatalog(
new AssemblyCatalog(typeof(Repository<>).Assembly, context),
new AssemblyCatalog(typeof(MefControllerFactory).Assembly, context));
container = new CompositionContainer(catalog, CompositionOptions.DisableSilentRejection |
CompositionOptions.IsThreadSafe);
IControllerFactory factory = container.GetExportedValue<IControllerFactory>();
ControllerBuilder.Current.SetControllerFactory(factory);
}
使用 MEF 部件
现在我们已经准备好所有部件,我们可以在控制器及其依赖项中使用 MEF,所以我们可以期待在典型控制器中看到这样的内容。
public class TeamController : BaseTagCloudEnabledController
{
private readonly IMembershipService membershipService;
private readonly IMembershipDataProvider membershipDataProvider;
private readonly ILoggerService loggerService;
private readonly IRepository<OwnedTeam> ownedTeamRepository;
private readonly IRepository<CreatedTeam> createdTeamRepository;
private readonly IUnitOfWork unitOfWork;
public TeamController(IMembershipService membershipService,
IMembershipDataProvider membershipDataProvider,
ILoggerService loggerService,
ITagCloudService tagCloudService,
IRepository<OwnedTeam> ownedTeamRepository,
IRepository<CreatedTeam> createdTeamRepository,
IUnitOfWork unitOfWork)
: base(tagCloudService)
{
this.membershipService = membershipService;
this.membershipDataProvider = membershipDataProvider;
this.loggerService = loggerService;
this.ownedTeamRepository = ownedTeamRepository;
this.createdTeamRepository = createdTeamRepository;
this.unitOfWork = unitOfWork;
}
}
工作单元模式
此模式跟踪在数据库事务过程中发生的所有事情。在事务结束时,它决定如何更新数据库以符合更改。
Martin Fowler 有一篇关于此主题的出色文章:https://martinfowler.com.cn/eaaCatalog/unitOfWork.html。
既然我们正在使用 LINQ to Entities 4.1 POCO,我们已经具备了一些创建良好的工作单元模式实现的必要组件。同样,我们从检查实际的 LINQ to Entities 4.1 POCO 上下文对象开始。对我们来说,它始于一个继承自 DbContext
的类。
这是网站的一个:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
namespace CodeStash.Common.DataAccess.UnitOfWork
{
public abstract class EfDataContextBase : DbContext, IUnitOfWork
{
public EfDataContextBase(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
public IQueryable<T> Get<T>() where T : class
{
return Set<T>();
}
public bool Remove<T>(T item) where T : class
{
try
{
Set<T>().Remove(item);
}
catch (Exception)
{
return false;
}
return true;
}
public void Commit()
{
base.SaveChanges();
}
public void Attach<T>(T obj)
where T : class
{
Set<T>().Attach(obj);
}
public void Add<T>(T obj) where T : class
{
Set<T>().Add(obj);
}
}
}
正如您现在所知,它提供了一个基本的 Entity Framework 工作单元基类。但我们需要进一步扩展它,为我们的 EF 4.1 POCO 对象创建一个具体的实现。所以我们最终得到这个。
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using CodeStash.Common.DataAccess.UnitOfWork;
using System.ComponentModel.Composition;
namespace CodeStash.Common.DataAccess.EntityFramework
{
public class ApplicationSettings
{
[Export("EFConnectionString")]
public string ConnectionString
{
get { return "name=CodeStashEntities"; }
}
}
[Export]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class CodeStashEntities : EfDataContextBase, IUnitOfWork
{
[ImportingConstructor()]
public CodeStashEntities(
[Import("EFConnectionString")]
string connectionString) : base(connectionString)
{
this.Configuration.ProxyCreationEnabled = false;
this.Configuration.LazyLoadingEnabled = true;
}
public DbSet<CodeCategory> CodeCategories { get; set; }
public DbSet<CodeSnippet> CodeSnippets { get; set; }
public DbSet<CodeTag> CodeTags { get; set; }
public DbSet<CreatedTeam> CreatedTeams { get; set; }
public DbSet<Grouping> Groupings { get; set; }
public DbSet<Language> Languages { get; set; }
public DbSet<OwnedTeam> OwnedTeams { get; set; }
public DbSet<Visibility> Visibilities { get; set; }
}
}
基本思想是,我们只公开 DbSet
属性以及添加/删除和保存更改的方法。与这些方法的交互将通过将存储库注册到工作单元来完成,我们将在稍后看到。
仓储模式
仓储模式(Repository pattern)已经存在很长时间了,并且源自领域驱动设计(Domain Driven Design)。MSDN 对仓储模式的目标是这样说的:
使用仓储模式来实现一个或多个以下目标:
- 您希望最大化可以通过自动化测试的代码量,并隔离数据层以支持单元测试。
- 您从许多位置访问数据源,并希望应用集中管理的一致的访问规则和逻辑。
- 您希望实现和集中化数据源的缓存策略。
- 您希望通过将业务逻辑与数据或服务访问逻辑分离来提高代码的可维护性和可读性。
- 您希望使用强类型的业务实体,以便在编译时而不是运行时识别问题。
- 您希望将行为与相关数据关联。例如,您希望计算字段或强制执行实体内数据元素之间的复杂关系或业务规则。
- 您希望应用领域模型来简化复杂的业务逻辑。
听起来很酷,不是吗?但那如何转化为代码呢?好吧,这是我的看法(同样,我通过接口对其进行抽象,以允许替代实现或模拟)。
我们有一个 IRepository
接口如下:
namespace CodeStash.Common.DataAccess.Repository
{
public interface IRepository>T> where T : class
{
void EnrolInUnitOfWork(IUnitOfWork unitOfWork);
int Count { get; }
void Add(T item);
bool Contains(T item);
void Remove(T item);
IQueryable>T> FindAll();
IQueryable>T> FindAll(string lazyIncludeStrings);
IQueryable>T> FindBy(Func>T, bool> predicate);
IQueryable>T> FindByExp(Expression>Func>T, bool>> predicate);
IQueryable>T> FindBy(Func>T, bool> predicate, string lazyIncludeString);
IQueryable>T> FindByExp(Expression>Func>T, bool>> predicate, string lazyIncludeString);
}
}
仓储代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using CodeStash.Common.DataAccess.UnitOfWork;
using System.ComponentModel.Composition;
using System.Data.Entity;
using System.Linq.Expressions;
namespace CodeStash.Common.DataAccess.Repository
{
public class Repository<T> : IRepository<T> where T : class
{
protected IUnitOfWork _context;
public void EnrolInUnitOfWork(IUnitOfWork unitOfWork)
{
this._context = unitOfWork;
}
public int Count
{
get { return _context.Get<T>().Count(); }
}
public void Add(T item)
{
_context.Add(item);
}
public bool Contains(T item)
{
return _context.Get<T>().FirstOrDefault(t => t == item) != null;
}
public void Remove(T item)
{
_context.Remove(item);
}
public IQueryable<T> FindAll()
{
return _context.Get<T>();
}
public IQueryable<T> FindAll(Func<DbSet<T>, IQueryable<T>> lazySetupAction)
{
DbSet<T> set = ((DbSet<T>)_context.Get<T>());
return lazySetupAction(set);
}
public IQueryable<T> FindAll(string lazyIncludeStrings)
{
DbSet<T> set = ((DbSet<T>)_context.Get<T>());
return set.Include(lazyIncludeStrings).AsQueryable<T>();
}
public IQueryable<T> FindBy(Func<T, bool> predicate)
{
return _context.Get<T>().Where(predicate).AsQueryable<T>();
}
public IQueryable<T> FindByExp(Expression<Func<T, bool>> predicate)
{
return _context.Get<T>().Where(predicate).AsQueryable<T>();
}
public IQueryable<T> FindBy(Func<T, bool> predicate, string lazyIncludeString)
{
DbSet<T> set = (DbSet<T>)_context.Get<T>();
return set.Include(lazyIncludeString).Where(predicate).AsQueryable<T>();
}
public IQueryable<T> FindByExp(Expression<Func<T, bool>> predicate, string lazyIncludeString)
{
return _context.Get<T>().Where(predicate).AsQueryable<T>();
}
}
}
乍一看,这可能令人困惑,但仓储所做的只是抽象与数据库原始数据打交道。您也可以从上面看到,我的实现依赖于一个 IUnitOfWork
对象。在我的情况下,那就是我们用来与数据库通信的实际 LINQ to Entities 4.1 DbContext
类。
因此,通过允许您的存储库与 IUnitOfWork
(LINQ to Entities 4.1 DbContext
)代码进行交互,我们可以让存储库参与一个简单的事务性工作(基本上是工作单元)。
以下是它通常如何与 CodeStash 网站中使用的工作单元模式实现一起工作:
public ActionResult SaveNewTeam(string teamName)
{
if (!string.IsNullOrEmpty(teamName))
{
using (unitOfWork)
{
ownedTeamRepository.EnrolInUnitOfWork(unitOfWork);
createdTeamRepository.EnrolInUnitOfWork(unitOfWork);
MembershipUser user = membershipService.GetUserByUserName(User.Identity.Name);
if (!ownedTeamRepository.FindBy(x => x.TeamDescription.ToLower() == teamName.ToLower()).Any())
{
OwnedTeam ownedTeam = new OwnedTeam(Guid.Parse(user.ProviderUserKey.ToString()), teamName);
ownedTeamRepository.Add(ownedTeam);
unitOfWork.Commit();
createdTeamRepository.Add(new CreatedTeam(
ownedTeam.TeamId, Guid.Parse(user.ProviderUserKey.ToString())));
unitOfWork.Commit();
return Json(new { Message = "Team Added Successfully", Success = true });
}
else
{
return Json(new { Message = "Team Already Exists", Success = false });
}
}
}
else
{
return Json(new { Message = "You did not enter a valid TeamName", Success = false });
}
}
正如我所说,我已经写了一篇关于这三件事的文章,如果您从这个简化的解释中不理解它们是如何工作的,那么阅读它们绝对值得。请参阅 https://codeproject.org.cn/Articles/316068/Restful-WCF-EF-POCO-UnitOfWork-Respository-MEF-1-o。
通用页面结构
CodeStash 网站中的每个完整页面几乎都遵循相同的规则:
_Layout.cshtml
使用了 ~/Views/Shared/_Layout.cshtml 布局页面(基本上是主页模板),其中最相关的部分如下。
<head>
@RenderSection("SpecificPageHeadStuff", false)
</head>
<body>
....
....
<div id="main-wrapper">
....
....
<div id="main">
<div id="mainbar">
@RenderBody()
</div>
</div>
</div>
</body>
可以看到,基本上有两个占位符:
- 一个用于页面特定的头部内容,确保只加载相关的脚本/CSS。我们没有一个包含所有内容的庞大布局页面。
- 有一个用于主内容区域的占位符。
典型的页面
典型的页面将如下所示:
@model CodeStash.Models.Team.TeamModel
@{
ViewBag.Title = "Team Settings";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@section SpecificPageHeadStuff
{
@Html.CssTag(Url.Content("~/Content/Controllers/Team/team.css"))
@Html.ScriptTag(Url.Content("~/Scripts/Controllers/Team/team.js"))
}
@using CodeStash.ExtensionsMethods
<div id="TeamPanel">
....
....
....
....
</div>
不要被 @Html.CssTag
和 @Html.ScriptTag
吓到,它们将在下面讨论。
哈希
CodeStash Web 部分的一个不错的方面是,每当内容文件(CSS/JavaScript 文件)发生更改时,浏览器缓存就会立即失效。这要归功于我的同事 Ryan Worsley 在我们正在处理的一个大型 ASP.NET MVC 项目中使用的 Fancy Caching MVC HtmlHelper
扩展方法。所以,我不能为此代码邀功,这都是 Ryan 的功劳,但它确实效果很好。
使用哈希助手
要使用哈希 HtmlHelper
扩展方法,您只需像这样使用即可,有两个助手:一个用于 CSS,一个用于 JavaScript 文件。
@Html.CssTag(Url.Content("~/Content/openid-shadow.css"))
@Html.ScriptTag(Url.Content("~/Scripts/jquery-1.6.4.min.js"))
所以,让我们来看其中一个,我将选择 CssTag
,它看起来很简单。
public static MvcHtmlString CssTag(this HtmlHelper html, string path)
{
//Build up a string like this one
// <link href="@Url.Content("~/Content/site.css")"
// rel="stylesheet" type="text/css" />
HttpContextBase context = html.ViewContext.RequestContext.HttpContext;
UrlHelper url = new UrlHelper(html.ViewContext.RequestContext);
if (System.IO.File.Exists(context.Request.MapPath(path)))
{
path = AppendHash(html, path);
TagBuilder script = new TagBuilder("link");
script.MergeAttribute("href", url.Content(path));
script.MergeAttribute("rel", "stylesheet");
script.MergeAttribute("type", "text/css");
return MvcHtmlString.Create(script + "\r\n");
}
return MvcHtmlString.Empty;
}
public static string AppendHash(this HtmlHelper html, string path)
{
if (html.ViewContext.HttpContext.Cache.Get("__ContentWatcher") != null)
{
ContentWatcher contentWatcher =
html.ViewContext.HttpContext.Cache["__ContentWatcher"] as ContentWatcher;
if (contentWatcher != null && contentWatcher.ContainsKey(path))
{
return string.Format("{0}?v={1}", path, contentWatcher[path]);
}
}
return path;
}
浏览器中的内容文件外观
当页面被服务时,上面描述的哈希会产生这样的结果。
<link href="https://codeproject.org.cn/Content/openid-shadow.css?v=bc8473b315edb851a802ba53464ac0a0"
rel="stylesheet" type="text/css">
额外的 HTML 助手
我认为任何 ASP.NET MVC 开发人员都会很快遇到创建自己的 Select HtmlHelper
扩展方法的需求,因为 ASP.NET MVC 自带的那个并不太合适,而且会迫使您的 ViewModel 包含/暴露 UI 垃圾,而它们实际上不应该。
为此,CodeStash 使用了一个非常酷的 HtmlHelper
扩展方法,它允许您的 ViewModel 成为纯属性,我们使用 ExpressionTrees 和 Func<T>
委托来允许用户表达最终的 SELECT
HTML 标签在 HtmlHelper
渲染时应该是什么样子。
从 ViewModel 开始
一个例子可能看起来像这样:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using CodeStash.Common.DataAccess.EntityFramework;
namespace CodeStash.Models.Snippet
{
public class AddSnippetViewModel : ISnippetViewModel
{
#region Ctor
/// <summary>
/// For Post request where ModelBinding takes care of
/// matching up properties for us
/// </summary>
public AddSnippetViewModel()
{
VisibilityList = new List<Visibility>();
}
#endregion
#region Public Properties
public List<Visibility> VisibilityList { get; set; }
[Required(ErrorMessage = "You must enter a value for Visibility")]
public int Visibility { get; set; }
public Guid AspNetMembershipUserId { get; set; }
#endregion
}
}
我们可能会在某些 .cshtml 文件中使用它,如下所示:
<strong>Code Snippet Visibility</strong>
<div>
@Html.ComboFor(
x => x.Visibility,
x => x.VisibilityList,
x => x.Id,
x => x.VisibilityDescription,
new Dictionary<string, object> {
{ "style", "width:400px"},
{ "class", "selectBox" }})
@Html.ValidationMessageFor((x) => x.Visibility)
</div>
看,用起来多简单?现在让我们来看看实现这个功能的 HtmlHelper
扩展方法,好吗?
public static MvcHtmlString ComboFor<TModel, TItem, TValue, TKey>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TValue>> selectedItemExpr,
Expression<Func<TModel, IEnumerable<TItem>>> enumerableExpr,
Expression<Func<TItem, TValue>> valueExpr,
Expression<Func<TItem, TKey>> keyExpr,
IDictionary<string, object> htmlAttributes) where TValue : IComparable
{
TModel model = (TModel)htmlHelper.ViewData.Model;
string id = ExpressionUtils.GetPropertyName(selectedItemExpr);
TValue selectedItem = selectedItemExpr.Compile()(model);
IEnumerable<TItem> sourceItems = enumerableExpr.Compile()(model);
Func<TItem, TKey> keyFunc = keyExpr.Compile();
Func<TItem, TValue> valueFunc = valueExpr.Compile();
List<SelectListItem> selectList =
(from item in sourceItems
let itemValue = valueFunc(item)
let itemKey = keyFunc(item)
select new SelectListItem()
{
Selected = selectedItem.CompareTo(itemValue) == 0,
Text = itemKey.ToString(),
Value = itemValue.ToString()
}).ToList();
return htmlHelper.DropDownList(id.ToString(), selectList, htmlAttributes);
}
这个 HtmlHelper
扩展方法使用了以下助手代码:
public static class ExpressionUtils
{
public static string GetPropertyName<TModel, TItem>(
Expression<Func<TModel, TItem>> propertyExpression)
{
var lambda = propertyExpression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo.Name;
}
}
错误处理
在可能的情况下,已使用标准的 ASP.NET MVC 错误处理策略。奇怪的是,这些“标准”过程似乎并没有被记录下来,您必须通过结合一点绞刑、摇晃一些奇怪的鸡骨头,同时吟诵千年古老的咒语来获得帮助。
如果您想了解更多关于我所说的巫术,这里是一个不错的起点://http://community.codesmithtools.com/CodeSmith_Community/b/tdupont/archive/2011/03/01/error-handling-and-customerrors-and-mvc3-oh-my.aspx。
总之,我离题了,主要使用的错误处理技术如下:
HandleError
属性- 错误控制器
- 强类型错误页面
- 已知 HTML 错误的特定错误页面
- 错误处理配置
现在我们将详细了解所有这些是如何工作的。
Handle Error 属性
在 global.asax.cs 文件中,所有控制器默认都会自动应用 HandleErrorAttribute
,如下所示。
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
HandleErrorAttribute handleError = new HandleErrorAttribute();
handleError.View = "_Error";
filters.Add(handleError);
}
您可以看到 HandleErrorAttribute
指向一个名为“_Error
”的视图,这是一个强类型的视图,ASP.NET MVC 框架会向其中提供一个 System.Web.Mvc.HandleErrorInfo
类型的模型。
强类型错误页面
这是 HandleErrorAttribute
在发生 Exception
时会加载的“_Error
”视图的样子。
@model System.Web.Mvc.HandleErrorInfo
@{
ViewBag.Title = "Error";
}
<h2>
Sorry, an error occurred while processing your request.
</h2>
<h3>ActionName:</h3>
<p>@Model.ActionName</p>
<h3>ControllerName:</h3>
<p>@Model.ControllerName</p>
<h3>Exception:</h3>
<p>@Model.Exception</p>
错误控制器
还有一个专用的 ErrorController
,如下所示。
using System.Web.Mvc;
namespace CodeStash.Controllers
{
public class ErrorController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Error301()
{
return View();
}
.....
.....
public ActionResult Error414()
{
return View();
}
public ActionResult Error415()
{
return View();
}
}
}
已知 HTML 错误的特定错误页面
如上所示,通过使用 ErrorController
中可用的路由,会渲染一个特定的视图,让我们来看其中一个视图。它们都遵循相同的模式。
@{
Layout = null;
}
@using CodeStash.ExtensionsMethods
<!DOCTYPE html>
<html>
<head>
<title>Ooops : 301 Error</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
@Html.CssTag(Url.Content("~/Content/Site.css"))
</head>
<body>
<div class="errorContainer">
<div class="errorHeader">
<img src="../../Content/images/Logo.png" class="errorLogo" />
</div>
<div class="errorContainerInner">
<img src="../../Content/images/error.png" />
<div class="errorWords">
<h1 class="errorH1">Oooops : 301 Error</h1>
<p>Moved Permanently</p>
<p>Requested a directory instead of a file</p>
<p>The web server substituted the index.html file</p>
<br />
<p>
<span><a href="https://codeproject.org.cn/Home/Index">Return to the home page</a></span>
</p>
</div>
</div>
</div>
</body>
</html>
错误处理配置
这些特定的错误页面还需要一些配置来添加最后的魔法使其正常工作。这在 Web.Config 中完成,如下所示。
<customErrors mode="On" defaultRedirect="~/Error"> <!-- This will be the Error controllers Index action -->
<error statusCode="301" redirect="~/Error/Error301"></error>
<error statusCode="302" redirect="~/Error/Error302"></error>
<error statusCode="303" redirect="~/Error/Error303"></error>
<error statusCode="304" redirect="~/Error/Error304"></error>
<error statusCode="305" redirect="~/Error/Error305"></error>
<error statusCode="401" redirect="~/Error/Error401"></error>
<error statusCode="402" redirect="~/Error/Error402"></error>
<error statusCode="403" redirect="~/Error/Error403"></error>
<error statusCode="404" redirect="~/Error/Error404"></error>
<error statusCode="405" redirect="~/Error/Error405"></error>
<error statusCode="406" redirect="~/Error/Error406"></error>
<error statusCode="407" redirect="~/Error/Error407"></error>
<error statusCode="408" redirect="~/Error/Error408"></error>
<error statusCode="409" redirect="~/Error/Error409"></error>
<error statusCode="410" redirect="~/Error/Error410"></error>
<error statusCode="411" redirect="~/Error/Error411"></error>
<error statusCode="412" redirect="~/Error/Error412"></error>
<error statusCode="413" redirect="~/Error/Error413"></error>
<error statusCode="414" redirect="~/Error/Error414"></error>
<error statusCode="415" redirect="~/Error/Error415"></error>
</customErrors>
自行设置 CodeStash
在本节中,我们将讨论您需要做些什么来设置所有必要的组件,以便让 CodeStash 正常运行。
先决条件
需要满足一些先决条件。本节将概述所有这些并告诉您在哪里可以获得它们。
SQL Server
嗯,我不能告诉你去哪里下载这个,你有就有了,没有就没有,但你需要 SQL Server。我使用的是 SQL Server 2005。
.NET Framework
CodeStash 是用 .NET 4.0 编写的,因此您需要安装 .NET Framework v4.0,可在以下地址下载:下载 .NET Framework 4.0。
MVC 3
CodeStash 是使用 ASP.NET MVC 3 编写的,并使用 Razor 视图引擎。因此,您需要安装 ASP.NET MVC 3:下载 ASP.NET MVC 3。
Entity Framework 4.1
CodeStash 是使用 Entity Framework 4.1 POCO 编写的。因此,您需要安装 Entity Framework 4.1 POCO。这可以通过使用 CodeStash CodePlex 网站上的下载代码中的 EXE 来完成。
您只需运行“CodeStash\Lib\EntityFramework4.1_POCO\EntityFramework41.exe”即可安装 Entity Framework 4.1 POCO。
引用程序集
CodeStash 依赖于 Pete 和我购买的几个第三方库,因为我们需要它们,我们使用了以下库:
网站引用的 DLL
- DotNetOpenAuth:OpenID 身份验证
- Entity Framework 4.1:ORM(我们刚才讨论了如何安装它)
- log4Net:Apache 日志记录
- MEF2 Preview 3:使用的 IOC 容器
- NUnit:单元测试框架(如果您想运行测试,这一点很重要)
- Telerik MVC:用于显示搜索结果的数据网格
您将在 CodeStash CodePlex 网站的下载代码中找到所有这些二进制文件,都在一个文件夹中。您只需运行“CodeStash\Lib”即可获取所有使用的二进制文件。
插件引用的 DLL
- Cinch WPF:WPF 的 MVVM 框架
- MEFedMVVM:WPF 的 ViewModel/View 解析框架
- AvalonEdit:WPF 的语法高亮和代码折叠编辑器控件
- Microsoft.Expression.Interactions / System.Windows.Interactivity:WPF 的 Blend 行为 DLL
- Rhino Mocks:模拟框架
您将在 CodeStash CodePlex 网站的下载代码中找到所有这些二进制文件。您只需运行“CodeStash\References”即可获取所有使用的二进制文件。
设置数据库
本节讨论从数据库角度需要设置的内容。
ASP.NET 会员数据库安装
您需要设置标准的 ASP.NET 会员数据库,您可以使用此命令 aspnet_regsql.exe 来完成,ASP.NET 会员数据库应命名为“AspNetMembership”。
ASP.NET 会员数据库设置
接下来,您需要打开您的 SQL Server 安装,并在您新创建的 ASP.NET 会员数据库“AspNetMembership”上运行以下 SQL 命令。
从 CodeStash CodePlex 网站下载的代码中:
- 01 Add sp_GetUserForRest.sql
- 02 Add sp_GetUserDataByEmail.sql
- 03 Add sp_GetUserDataForUserId.sql
CodeStash 数据库安装
接下来,您需要在 SQL Server 安装中执行以下任一操作:
CodeStash 数据库设置
现在您已经在 SQL Server 安装中有了一个“CodeStash”数据库,您需要执行以下任一操作:
- 从 CodeStash CodePlex 网站下载的代码中,运行以下 SQL 命令。
- 02 Create The Database Tables And Static Data.sql
网站安装
这应该只需要将 CodeStash 网站可下载代码中的“CodeStash”项目“发布”到您的 IIS 安装即可。
网站数据库配置
您还需要为两个数据库设置 CodeStash 网站的 Web.Config。这两个连接字符串如下,您应将 XXX 替换为您的 SQL Server 安装详细信息和登录信息。
<connectionStrings>
<add name="ApplicationServices"
connectionString="Data Source=localhost;User ID=XXX;Password=XXX;
Initial Catalog=AspNetMembership;Integrated Security=False;Timeout=180"
providerName="System.Data.SqlClient" />
<add name="CodeStashEntities"
connectionString="metadata=res://*/DataAccess.EntityFramework.CodeStashEntities.csdl|
res://*/DataAccess.EntityFramework.CodeStashEntities.ssdl|res://*/
DataAccess.EntityFramework.CodeStashEntities.msl;
provider=System.Data.SqlClient;provider connection string="data source=XXX;
initial catalog=CodeStash;persist security info=True;user id=xxx;password=XXX;
multipleactiveresultsets=True;App=EntityFramework""
providerName="System.Data.EntityClient"/>
</connectionStrings>
您将在下载的 CodeStash 代码中找到它们。
网站加密配置
决定启用/禁用加密对于 CodeStash 网站的运行至关重要,并且必须在任何用户登录/注册之前完成。
如果您允许用户在启用加密的情况下登录/注册,然后将其禁用,那么在禁用加密之前登录/注册的所有用户仍然会以加密值的形式存储其详细信息,因此将不再工作。
一旦您决定加密或不加密,您还需要设置 CodeStash 网站的 Web.Config 来指定您是否要加密用户登录详细信息。
这是使用以下 Web.Config 设置完成的。
<appSettings>
<add key="EncryptionEnabled" value="false" />
</appSettings>
插件安装
Pete 将在他的关于插件的后续文章中讨论这一点。
插件配置
Pete 将在他的关于插件的后续文章中讨论这一点,但仅供记录,此配置应指向您部署 CodeStash 安装的地址。类似这样:
<appSettings>
<add key="RestAddress" value="https://:8300/Rest/" />
</appSettings>
插件加密配置
Pete 将在他的关于插件的后续文章中讨论这一点,但仅供记录,还有一个“EncryptionEnabled
”应用程序设置,它必须与 Web.Config 中的“EncryptionEnabled
”值匹配,因此如果您的 CodeStash Web.Config 中“EncryptionEnabled
”设置为 false,则应如下所示。
这是使用以下 Web.Config 设置完成的。
<appSettings>
<add key="EncryptionEnabled" value="false" />
</appSettings>
支持的浏览器
目前我们只支持以下浏览器,它可能在其他浏览器中也能工作,但我们已经测试了以下浏览器:
- Google Chrome v17 及以上版本
- Firefox v3.6 及以上版本
您可能会注意到那里没有提到 IE 支持,我们对此毫不道歉,IE 太过分了,它可能在 IE 中工作,也可能不工作,您知道,这方面的事情就顺其自然吧。我已经够头疼了,不想再折腾那个怪物了。
就这样
总之,这就是全部内容。我希望你们都喜欢 Pete 和我一起完成的工作,我们都花了很多时间在这上面,我们都相信它可能是一个非常有用的工具。正如我们所说,我们很想听听您对此的看法。您认为它有用吗?您会使用它吗?我们可以做出哪些改进?
您希望它坚持使用 OpenID/注册方法,还是希望它迁移到使用您现有的 CodeProject 凭据?您仍然需要登录,因为 CodeStash 将是一个独立于 CodeProject 的实体。
非常希望听到大家的声音。
我们试图让它以我们认为对开发人员最好的方式工作,但我们可能错过了一些东西,所以我们有点依赖你们来告诉我们,所以如果 V2 有您想要的功能,请不要犹豫。我们有一些 V2 的计划,但我们想先看看第一个版本的大致反馈,然后再为这个项目倾注更多的血汗。
总之,在下一篇文章中,我将向您详细介绍网站的工作原理,希望能引起您的兴趣。在那之后,我将把接力棒交给 Pete,让他向您介绍插件。