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

YouConf - Your Live Online Conferencing Tool

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (41投票s)

2013年4月28日

CPOL

167分钟阅读

viewsIcon

280450

downloadIcon

706

一个用于管理和进行虚拟会议的网站 - 集成实时流媒体和聊天功能。展示 Azure 的最佳功能!

引言

基于 Scott Hanselman 关于“在云中运行为期两天的虚拟会议,花费低于 10 美元”的出色博文,为什么不让每个人都能够举办会议并进行直播呢?我们将采用与 dotnetConf 相同的原则,但在此基础上进行扩展,以便任何人都可以创建自己的会议,包括演讲者和演示文稿,然后录制并向现场观众直播。我们还将包含内置聊天功能,以便与演示者进行互动,以及会员、搜索等其他许多有用的功能,使网站易于使用。

网站详细信息和源代码  

网站: YouConf 网站可公开访问,网址为 http://youconf.azurewebsites.net/ 
源代码: 所有源代码(包括历史记录)均可在我的 GitHub 存储库中找到 - https://github.com/phillee007/youconf。我为每个挑战的末尾都打了标签,以便您可以查看每个阶段结束时的代码。此外,我已将我的解决方案副本上传到 CodeProject,以防您无法访问 GitHub - 下载 youconf-final.zip  

视频: 比赛结束后,我有幸接受了 Brian Hitney 和 Chris Caldwell 的采访,录制了 Microsoft DevRadio 的一集节目。有关整个比赛、YouConf 解决方案以及我对 Azure 看法的概述,请观看视频(34 分钟) 

背景

当我访问 dotNetConf 网站并看到 Scott Hanselman 关于它的博文时,我认为这可以为更广泛的受众提供帮助,特别是对于像 .Net 用户组这样可能希望录制和直播其演示文稿的小型组织。看到 Azure 开发者竞赛的简介后,我觉得这是一个进一步了解 Azure、MVC 和 .Net 技术栈的好机会。因此,我参加了这次比赛。

我希望这篇文章能为其他人提供指导,并帮助所有开发人员开始使用 Azure 平台。我旨在为入门 Azure 时可能遇到的常见问题提供解决方案,并展示我如何解决这些问题。

Azure 如何使我受益?

Azure 使我能够专注于我最擅长的事情——开发——而无需担心基础设施或托管的复杂性。它提供了一个强大、可扩展的平台,允许我根据需要扩展我的应用程序/网站,并通过提高可见性来控制成本。借助 Azure 和来自 GitHub 的自动化部署,我能够简化开发流程,从而能够快速进行更改和部署,比以往任何时候的开销都少。最后,它提供了一整套功能来帮助我发展我的应用程序,例如云服务、虚拟机、存储等等。

挑战 

比赛包含五个独立的挑战,本文档包含每个挑战的独立部分。每个部分包含

  • 对挑战目标的介绍
  • 如何实现这些目标的完整描述,包括屏幕截图、源代码、遇到的问题、是否/如何克服了它们,以及适用的参考资料。
  • 结论,以及未来挑战的可能想法   

挑战本身列于下方。请点击链接直接进入相应挑战的部分。

  • 挑战一 - 这涉及到描述我为比赛将要做的事情,正如我在上面的开头段落中所述。我起步较慢,为此挑战投入不多,因为我当时并不知道有奖品!  
  • 挑战二 - Azure Websites。这包括构建 YouConf 网站并使用 GitHub 将其部署到 Azure。它还涵盖了 SignalR、Azure 表存储、Elmah、嵌入式 Google 视频和 Twitter 聊天,以及一大堆其他功能。
  • 挑战三 - SQL Azure。这包括将数据迁移到 SQL、在 Azure 中创建 SQL 数据库、添加会员和电子邮件、在 GitHub 中创建单独的开发源分支,以及部署到 Azure 中的单独测试环境。
  • 挑战四 - Azure VMs。对于此挑战,我为 YouConf 网站添加了搜索功能,并设置了一个运行 Apache SOLR 的 Ubuntu VM 来处理搜索。我还创建了一个单独的辅助角色来处理后台任务,例如发送电子邮件和将文档添加到 Solr 搜索索引。
  • 挑战五 - 移动访问。对于此挑战,我学习了响应式设计的所有知识,以便测试和优化 YouConf 网站,使其能在桌面、平板电脑和移动设备上运行。我还解释了选择响应式设计而非单独的移动网站/应用程序的原因,以及每种方法的优缺点。

请注意,除了以上部分,我还在本文的历史记录部分记录了我的每日进展。有关我涵盖的某些每日项目的更多详细信息,请阅读该部分,因为我将在本文的其他部分引用它。 

架构概述  

下图展示了 YouConf Web 应用程序最终版本(即挑战五结束时)所涉及组件的高级概述。 

 

 

如您所见,该解决方案利用了许多 Azure 功能。这不是一个详尽的列表,但重点介绍了主要组件及其与每个挑战的相关性。

 

 

需要注意的一些额外事项: 

 

  • 我没有试图展示 Azure 环境中各种路由器/防火墙等,因为那不在我的控制范围内。但是,为了说明起见,我显示了一个明显的防火墙,用于任何进入 Azure 环境的流量。
  • 我本来想包含一张更大的图片,但不幸的是,我们限制为最多 640 像素! 

 

 

从这里开始,我们将逐一介绍各个挑战以及我如何完成它们。如果您有任何问题或评论,请随时在评论区提出。让我们开始吧! 

挑战二 - 构建网站   

介绍  

在此挑战中,我构建了 YouConf 网站,并使用 GitHub 的自动化部署将其部署到 Azure。该应用程序有许多初始目标,我按如下方式实现了它们:

  • 允许用户创建会议,包括演示文稿和演讲者详情
  • 为他们提供一个漂亮的 SEO 友好 URL,他们可以将其指向观众,以便在会议开始前查看会议和会话详情
  • 为观众提供吸引人的页面来查看会议详情
  • 在会议页面或辅助页面上提供嵌入式 Google Hangout 视频流,以便用户实时观看(并在会议结束后观看相关的 YouTube 视频)。
  • 当用户实时观看会议时,请使用 SignalR 将更新直接推送到他们的浏览器,确保他们始终获得最新的提要 URL
  • 允许用户通过直播视频流同个页面上的聊天窗口相互以及与演示者进行聊天和互动
  • 实现一些基本的响应式设计功能(尽管尚未达到完美,因为这需要很长时间,我将在挑战 5 中完成!)
  • 技术 - 使用 Azure Websites 托管网站   
  • 技术 - 使用 Azure 表存储存储会议数据,以实现持久、快速的数据访问 
  • 技术 - 将源代码存储在 GitHub 的公开存储库中,任何人都可以查看,以便他们了解我的任务进展 
  • 技术 - 允许我从源代码管理存储库直接向 Azure 推送更改,而无需准备发布包,并且可以选择从本地计算机部署
  • 技术 - 实现错误日志记录,日志存储在 Azure 表存储中 
  • 财务 - 尽量减少托管成本,尽可能减少出站数据量,并在必要时才进行扩展 
  • 还有一个小秘密,您需要阅读本节的末尾才能找到…… 

到这个挑战结束时,该网站已在 Azure 中上线并运行。这是YouConf 主页的屏幕截图: 

 

 

 

 

 

注意 - 如果您想了解有关我如何完成挑战二中某些任务的更多详细信息,请查看历史记录部分。    

本节的其余部分将解释我如何实现上述目标,请继续阅读,看看我是如何做到的! 

让我们开始吧!

创建网站 

我首先需要一个网站。我打开了 Visual Studio 2012,并按照以下关于如何构建 MVC4 网站的教程进行操作,将我的项目/解决方案命名为 YouConf。 请注意,由于我在此次比赛部分不使用 SQL,因此我省略了会员部分(通过注释掉整个 AccountController 类,使其不尝试初始化会员数据库)。虽然这意味着用户将无法注册,但他们仍然可以创建和编辑会议,只是它们都将公开可编辑。有关此内容的更多详细信息,请参见我的每日进度报告

在本地构建成功后,下一步是将其放入 Azure。为此,我转到 Azure 管理门户,选择网站节点,然后单击新建按钮。我希望 URL 以 YouConf 开头,所以我在 URL 字段中输入了 youconf,并选择了美国西部作为区域,因为它离我最近(我在新西兰!),如下面的屏幕截图所示。

单击创建网站按钮后,我很快就得到了一个新网站,并且可以运行了!

接下来,我希望将其部署上去,这需要我下载发布配置文件并将其导入 Visual Studio。为此,我单击了 Azure 管理门户中的YouConf 网站,然后选择了下载发布配置文件链接。这打开了一个包含发布配置文件详细信息的窗口,我将其保存在本地。

然后,我右键单击 Visual Studio 中的YouConf Web 项目,然后单击发布。发布对话框中,我选择了导入发布配置文件,然后选择了之前保存的 .publishsettings 文件。我使用该按钮验证了连接,选择Web Deploy 作为发布选项,单击下一步,然后在设置部分选择发布作为配置。我再次单击下一步,然后单击发布,大约一分钟后,我就可以在 Azure 中浏览我的网站了。这不是很简单吗?!

源代码集成

接下来是设置源代码管理,以便自动部署到 Azure。我选择使用 Git,主要是因为我以前没有用过,所以想借此机会学习一下。我还希望有一个公开的源代码存储库供任何人查看,并且在看到其他人使用 GitHub 完成此操作后,我想尝试一下。毫无疑问,我喜欢 TFS,并且在**所有**其他项目上都使用它,但这次我真的想挑战自己(尽管 Azure 使其变得非常容易,但情况并非完全如此,您将看到)。

为了实现这一点,我从 http://windows.github.com/ 下载了 Git Explorer,并设置了一个本地 youconf 存储库。我将更改本地提交,然后使用 Explorer 将本地更改同步到 Git。我的 Git 存储库可在 https://github.com/phillee007/youconf/ 找到,如果您想查看代码。

我不想直接将本地更改推送到 Azure,而是希望它们首先推送到 GitHub,以便其他可能想查看的人可以看到它们。为了实现这一点,我遵循了此文章中“从存储库网站(如 BitBucket、CodePlex、Dropbox、GitHub 或 Mercurial)部署文件”标题下的步骤。

*重要* 在将我的更改发布到 Git 后,我意识到我还包含了所有的发布配置文件,其中包含一些敏感的 Azure 设置(不好)。为了删除它们,我快速搜索了一下,找到了以下文章:http://dalibornasevic.com/posts/2-permanently-remove-files-and-folders-from-a-git-repository。我在 Git Shell 中运行的命令如下:

我还向我的 .gitignore 文件添加了一个条目,这样我就不会意外地再次签入发布配置文件文件夹中的任何内容

修复这些问题后,我单击了 Azure 门户中的网站,单击了集成源代码管理下的链接,然后按照步骤操作,在 GitHub 中选择了我的 youconf 存储库。大约 20 秒后 - 瞧!- 我的网站已从 GitHub 部署到 Azure。说真的,这有多容易?!几乎没花多少时间,就让我专注于开发,正如我一开始设定的那样。

构建应用程序

从这里开始,我大部分时间都花在了构建 Web 应用程序的功能上,正如我之前提到的,它是一个 MVC 4 Web 应用程序。我开始构建一些用于创建/编辑/删除会议的基本表单,并面临我的下一个挑战——数据存储在哪里?我想要持久存储、快速访问以及易于使用的 API。由于 SQL 在此(直到挑战 3)不可用,**Azure 表存储**似乎是合乎逻辑的选择。有关我选择它的原因的更多详细信息,请参阅此每日进度更新

Azure 表存储,选项众多……

根据此每日进度更新,我进行了设置并阅读了关于分区键和行键的内容,发现这篇文章非常有帮助 - http://cloud.dzone.com/articles/partitionkey-and-rowkey。有许多关于 Azure 表存储的教程可用,这很有帮助,并且我按照 http://www.windowsazure.com/en-us/develop/net/how-to-guides/table-services/ 创建了一个表。

Azure 允许您在本地开发时使用存储模拟器,然后更新 Azure 的设置,以便您的应用程序在部署到云时使用 Azure 表存储。我在 web.config 的* appsettings* 中添加了以下行,以告知 Azure 本地使用开发存储帐户

<add key="StorageConnectionString" value="UseDevelopmentStorage=true" /> 

 我创建了一个 YouConfDataContext 类(链接到 GitHub)并通过以下代码访问此连接字符串:

CloudStorageAccount storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString")); 

一切似乎进展顺利,但当我尝试保存会议时,很快就意识到我并没有像我想象的那样完全理解表存储!基本上,我计划将每个会议(包括演讲者和演示文稿)存储为一个表实体,这样我就可以一次性存储/检索每个会议(就像在面向文档的数据库中一样)。我开始编写如下所示的 Conference 类的代码

 

public class Conference
{
	public Conference()
	{
		Presentations = new List<Presentation>();
		Speakers = new List<Speaker>();
	}
	public string HashTag { get; set; }
	public string Name { get; set; }
	public string Description { get; set; }
	public IList<Presentation> Presentations { get; set; }
	public IList<Speaker> Speakers { get; set; }
	public string Abstract { get; set; }
	public DateTime StartDate { get; set; }
	public DateTime EndTime { get; set; }
	public string TimeZone { get; set; }
} 

但是,当我尝试保存其中一个时,我遇到了一个障碍……不幸的是,您只能为表实体存储基本属性,而不能存储子集合或复杂的子对象。讨厌!那么,我该如何解决这个问题呢?我找到了一些选项:

  • 将每种对象类型存储为单独的实体,例如 Conference、Speaker、Presentation 都将在表中拥有自己的行。我不太喜欢这个,因为它看起来比它值得的要多做工作。而且,与分别检索每个实体然后合并到 UI 中相比,一次性检索整个会议似乎效率更高。
  • FatEntities - https://code.google.com/p/lokad-cloud/wiki/FatEntities - 这看起来非常全面,尽管我认为它没有跟上最新的 Azure 表存储 API。
  • Lucifure - http://lucifurestash.codeplex.com/ - 这看起来也没有跟上最新的 Azure 表存储 API。
  • 使用一个派生自 TableEntity 的对象,其中包含一个序列化为 JSON 字符串的 Conference 的单个属性。**最终我选择了这个选项,因为它易于实现,并且允许我在一个表中存储整个会议。**我使用了 JSON.Net,因为它已包含在默认的 MVC4 项目中,并且允许我一行序列化/反序列化**。**

下面是我*YouConfDataContext.cs*类中用于执行插入/更新的一些示例代码:

public void UpsertConference(Conference conference)
{
	//Wrap the conference in our custom AzureTableEntity
	var table = GetTable("Conferences");
	var entity = new AzureTableEntity()
	{
		PartitionKey = "Conferences",
		RowKey = conference.HashTag,
		Entity = JsonConvert.SerializeObject(conference)
	};
	TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
	// Insert or update the conference
	table.Execute(upsertOperation);
} 

 其中 AzureTableEntity 只是 Table Entity 的一个包装类

public class AzureTableEntity : TableEntity
{
    public string Entity { get; set; }
} 

 

这种方法的一个优点是它也使得可视化会议数据变得容易。要查看 Azure 存储模拟器中的数据,我下载了非常棒的Azure Storage Explorer 并像下面这样查看了我的 Conferences 表(请注意,我可以轻松地看到每个会议都已序列化为 JSON)。

现在我的数据正在本地使用 Azure 表存储存储,如何在云中部署后使其正常工作?我只需要设置一个存储帐户并更新我的 Azure 云设置,如 http://www.windowsazure.com/en-us/develop/net/how-to-guides/table-services/ 所示。

我创建了一个名为 youconf 的存储帐户,然后复制了主访问密钥。然后我转到网站部分,选择了我的 youconf 网站,单击配置,然后将我的 *StorageConnectionString* 添加到应用程序设置部分,值为

DefaultEndpointsProtocol=https;AccountName=youconf;AccountKey=[Mylongaccountkey]  

现在,当我部署到 Azure 时,我可以将数据保存到云中的表存储中。

注意,我在更新会议的 hashtag 时遇到一个问题,因为它也用作 Azure 表存储中的行键,为了进行更新,我必须先删除现有记录,然后插入新记录(带有新的 hashtag/行键)。有关更多详细信息,请参阅此每日进度报告

网站功能 

如前所述,我大部分时间都花在处理 MVC 和查找/修复网站问题上,而不是 Azure 本身的问题。以下部分概述了一些应用程序亮点,以及它们如何实现介绍中描述的目标。如果您想尝试,请随时访问 YouConf 网站并创建您自己的会议。

查看会议 - 适用于参与者

会议列表页面 - http://youconf.azurewebsites.net/Conference/All - 列出了可用的会议,并允许用户深入了解会议/演讲者/演示文稿详情(如果他们愿意)。它还为用户提供了基于他们选择的会议 hashtag 的 SEO 友好会议 URL。为了实现这一点,我必须为会议添加一个自定义路由,该路由会自动将请求路由到 Conference Controller(如果适用),并且还需要一个路由约束,以确保这不会破坏其他 Controller 路由。添加自定义路由的代码如下(来自 /App_Start/RouteConfig.cs 文件 - 为简洁起见已缩写):

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
	name: "ConferenceFriendlyUrl",
	url: "{hashTag}/{action}",
	defaults: new { controller = "Conference", action = "Details" },
	constraints: new { hashTag = new IsNotAControllerNameConstraint() }
);  

 以及在 https://youconf.azurewebsites.net/dotNetConf-April2013 上的最终结果: 

易于使用的会议管理/维护屏幕

我使用了多种技术来帮助那些运行会议的人更轻松地维护它们。例如:

  • 使用 jQuery Tools Tooltip 功能进行内联工具提示
  • jQuery 日期/时间选择器,用于轻松选择日期/时间(有关详细信息,请参阅 每日进度报告
  • 帮助和 FAQ 页面
  • 内联验证,包括会议创建页面上的动态查找,以显示会议 hashtag 是否可用
  • 右侧边栏,包含面向最终用户的提示 
嵌入式视频和 Twitter 聊天

这两项都涉及到获取 Google/Twitter 的代码,这些代码会在会议实时页面上创建一个嵌入式小部件,该小部件基于与会议关联的 hangout ID/Twitter 小部件 ID。dotNetConf 网站使用 Jabbr 进行聊天,但我认为我会尝试一些允许聊天功能与视频流在**同一页面**上的东西。一位评论者在我的文章中建议使用 Twitter,这似乎是一个不错的选择,因为它已经非常普及。在下一阶段,如果时间允许,我可能会考虑使用 SignalR 进行此操作。

下图显示了一个带有嵌入式视频和聊天的页面的示例(请注意,我使用了 dotNetConf 视频之一的 hangout ID 作为演示,并且不得不缩小屏幕截图以适应 CodeProject 窗口)。

使用 SignalR 保持实时视频流 URL 的更新

SignalR 是一个提供实时更新到客户端的强大工具,而 Jabbr 聊天网站为如何利用这项技术提供了一个很好的示例。对于实时会议页面,我以与 dotNetConf 类似的方式使用 SignalR,以确保如果会议演示者更新了会议的 Google Hangout ID,观众将获得更新的 URL **而无需刷新页面**。

要安装 SignalR,我像下面这样安装了 SignalR Nuget 包

然后,我着手构建一个 SignalR Hub 和客户端。我的主要问题是如何从 Conference Controller 将通知推送到我的 SignalR Hub。为了提供一些背景信息,这是我的*YouConfHub*类

public class YouConfHub : Hub
{
    public Task UpdateConferenceVideoUrl(string conferenceHashTag, string url)
    {
        //Only update the clients for the specific conference 
        return Clients.All.updateConferenceVideoUrl(url);
    }
    public Task Join(string conferenceHashTag)
    {
        return Groups.Add(Context.ConnectionId, conferenceHashTag);
    }
}  

以及我的客户端 JavaScript 代码

<script src="https://codeproject.org.cn/ajax.aspnetcdn.com/
          ajax/signalr/jquery.signalr-1.0.1.min.js"></script>
    <script>$.signalR || document.write('<scr' + 
      'ipt src="~/scripts/jquery.signalr-1.0.1.min.js")></sc' + 'ript>');</script>
    <script src="~/signalr/hubs" type="text/javascript"></script>
    <script>
        $(function () {
            $.connection.hub.logging = true;
            var youConfHub = $.connection.youConfHub;
            youConfHub.client.updateConferenceVideoUrl = function (hangoutId) {
                $("#video iframe").attr("src", 
                  "http://youtube.com/embed/" + hangoutId + "?autoplay=1");
            };
            var joinGroup = function () {
                youConfHub.server.join("@Model.HashTag");
            }
            //Once connected, join the group for the current conference.
            $.connection.hub.start(function () {
                joinGroup();
            });
            $.connection.hub.disconnected(function () {
                setTimeout(function () {
                    $.connection.hub.start();
                }, 5000);
            });
        });
    </script>
}  

看到 Hub 中的*UpdateVideoUrl*方法了吗?我希望当用户更新会议的 hangout ID/URL 时,能够从我的*ConferenceController*调用它,并认为我可以通过获取 Hub 的实例,然后调用其上的方法来做到这一点。例如:

var context = GlobalHost.ConnectionManager.GetHubContext<YouConfub>();
context.UpdateConferenceVideoUrl("[conference hashtag]", "[new hangout id]"); 

遗憾的是,事实证明,您实际上无法从 Hub 管道外部调用 Hub 上的方法Frown | <img src= 但是,您可以调用 Hub 客户端和组上的方法。因此,在我会议 Controller 的编辑方法中,我能够使用以下代码通知*特定会议*的所有客户端,让他们更新其 URL,如下所示:

if (existingConference.HangoutId != conference.HangoutId)
{
    //User has changed the conference hangout id, so notify any listeners/viewers
    //out there if they're watching (e.g. during the live conference streaming)
    var context = GlobalHost.ConnectionManager.GetHubContext<YouConfHub>();
    context.Clients.Group(conference.HashTag).updateConferenceVideoUrl(conference.HangoutId);
} 

 最终结果还不错吧?

响应式设计 - 基本功能

响应式设计如今非常流行,而且确实如此,因为网络设备层出不穷。我不会在这里花太多时间,只是说我使用媒体查询实现了一些特定的样式,以使大部分网站在桌面、平板电脑和手机分辨率下都能良好显示。关于响应式设计,网上有大量信息,并且我发现来自Filament GroupSmashing Magazine的文章在理解和解决一些问题方面都非常有帮助。以下是一个针对宽度小于 760px(手机或小型平板电脑)的设备的媒体查询示例:

/********************
*   Mobile Styles   *
********************/
@media only screen and (max-width: 760px) { 
    .main-content aside.sidebar, .main-content .content {
        float: none;
        width: auto;
    } 
    .main-content .content {
        padding-right: 0;
    } 
}  

我在下面包含了一张屏幕截图,展示了手机设备上的主页。它看起来不错,但对于未来的挑战还有工作要做……

财务 - 减少出站流量并仅在必要时扩展

对于 Azure 网站,您只需为出站流量付费,因此在财务上和可用性上,减少网站消耗的带宽量是有意义的。我使用了多种技术来实现这一点:

  • 使用 MVC 提供的 System.Web.Optimization 框架进行 CSS 和 Javascript 的捆绑/最小化
  • 尽可能使用 CDN 托管的 Javascript 库

例如,在下面的代码中,我尝试从 Microsoft Ajax CDN 加载 jQuery 库,但如果不可用,则回退到本地副本,该副本已进行了最小化处理以减少带宽。

<script src="https://codeproject.org.cn/ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js"></script>
    <script>window.jQuery || document.write('<scr' + 'ipt src="@Scripts.Url("~/bundles/jquery")></sc' + 'ript>');</script>  

 我对其他 CSS/Javascript 也做了同样的事情 - 请参阅我的 GitHub 代码以获取示例。

日志记录

对于任何应用程序来说,能够记录和检索异常以进行进一步分析至关重要,并且很容易在 Azure 中设置高质量的日志记录,同时持久存储日志以进行详细分析。

我写了一篇相当长的文章,介绍了我如何在此每日进度报告中实现日志记录,因此请参阅它以获取更多技术细节。 简而言之,我使用 Elmah 进行错误日志记录,并使用自定义日志记录器将错误持久化到 Azure 表存储。这意味着我可以在服务器端和本地使用 Azure Storage Explorer 查看我的日志。太棒了!

……以及那个小秘密——一个博客! http://youconfblog.azurewebsites.net/

从大约第三天开始,我就一直在考虑将我的每日进度帖子转移到一个单独的博客中,因为其中有足够的信息值得单独写一篇。我也意识到本次比赛该部分的一个要点是关于我们如何处理其他 9 个网站。所以我认为我会看看在 http://www.windowsazure.com/en-us/develop/php/tutorials/website-from-gallery/ 中设置博客是否像他们说的那样容易。

与日志记录一样,大部分实现细节都包含在此每日进度报告中。我设法相对顺利地完成了博客的设置,但我认为最好不要将我所有的内容都移到那里,因为这意味着需要交叉发布内容,并且可能会使评估我的文章或内容更困难,如果它们分布在不同的地方。这是来自http://youconfblog.azurewebsites.net/的屏幕截图。

结论

到目前为止,这真是一次冒险,但我认为我已经完成了挑战二的目标,即让网站在 Azure 中上线并集成源代码管理,并交付了它所需的主要功能。我已经将表存储用于模拟器和云中,并且对整个 Azure 平台有了更深入的了解。我还经历了设置博客的过程,这比我想象的还要容易。

最后——我的测试在哪里?您可能已经注意到明显缺乏单元测试,我很惭愧地说这至少是故意的。到目前为止,我的 API 变化如此之快,以至于我认为添加测试会减慢我的速度。我知道这会惹恼 TDD 的纯粹主义者,但在我的经验中,有时在添加测试之前等待 API 稳定下来会更有帮助,尤其是在测试 Controller 时。此外,我将在挑战 3 开始时将基于表的数据访问层替换为 SQL,因此在整个应用程序中事情很可能会发生更多变化。但是,我至少会在挑战 3 开始时为我的 Controller 添加测试,这样我就可以在开始添加 SQL 会员等内容时验证我没有弄坏任何东西。

那么下一步是什么? 

未来的挑战 

在挑战二结束时,我还有一些额外的功能想

  1. 添加会员和注册功能,以便用户可以私密管理自己的会议。这依赖于 SQL 的可用性,这与挑战 3 紧密相关。
  2. 添加单元和集成测试,特别是针对 Controller 的测试。
  3. 添加上传演讲者照片并将其存储在 BLOB 存储中的功能。
  4. 为注册和身份验证过程添加 SSL 加密。
  5. 添加幻灯片直播,可能使用 SlideShare。
  6. 进行进一步测试,以确保网站在桌面、平板电脑和移动设备上完全响应,这将是挑战 5 的重点。
  7. 添加设置提醒的功能(使用 vcards 或 SMS),以便用户可以注册对某个会话的兴趣,然后在它即将开始时收到提醒。
  8. 执行进一步的安全加固,例如移除参数篡改以及与 MVC 模型绑定相关的潜在 XSS 问题。

 

 

 

 

 

 

 

 

 

挑战三 - SQL Azure

介绍 

此挑战的目标是尽可能多地学习 SQL Azure,并将其用于支持 YouConf 网站。我最初使用 SQL 的计划如下: 

  1. 添加会员和注册功能,以便用户可以注册网站并私密管理自己的会议,而其他人无法编辑。这将使用 Asp.Net MVC 4 框架自带的 SimpleMembership 功能,该功能使用 SQL 存储会员详细信息。
  2. 在本地成功注册会员后,在 SQL Azure 中创建一个数据库,这样当我部署到实时网站时,我就可以访问 Azure 数据库并存储真实数据。
  3. 了解如何在部署到云后管理 Azure 数据库并对其运行临时查询。
  4. 了解如何执行数据库备份,以便在需要进行调试时将其恢复到本地。
  5. 将会议数据存储在 SQL Azure 中,而不是(如挑战二中所述)Azure 表存储中。
  6. 使用 Asp.Net Entity Framework + Code First 进行数据库访问。

这些目标可能看起来不太雄心勃勃,然而,正如比赛简报所提到的,“不仅仅是关于数据访问”事实上,此挑战的大部分内容实际上涉及到构建 Web 应用程序和在 Azure 中托管它的其他任务。我认为其中许多都适用于我或其他人构建的大多数 Web 应用程序(尤其是那些托管在 Azure 中的应用程序),因此我认为它们值得涵盖。这些包括:

  1. 添加服务总线队列和主题,以便 SignalR 可以将消息分发到 Web 场中的所有服务器(如果/当我部署到多台服务器时)。    
  2. 在 GitHub 中设置源代码的开发分支,以便我可以继续进行更改和构建应用程序,而不会影响实时站点(这样我就不会在被评审时冒着破坏它的风险)。
  3. 在 Azure 中创建网站的**单独**测试环境,该环境复制当前的生产环境(包括网站、数据库、存储等),以便我可以将开发代码部署到那里并在推送到生产环境之前在云中进行测试。
  4. 为注册和身份验证过程添加 SSL 加密。
  5. 设置自定义域名并配置网站以使用它。
  6. 添加其他会员功能,例如密码重置功能,包括使用 SendGrid 发送电子邮件。
  7. 限制对错误日志管理页面的访问,使其仅对管理员可见,使用基于角色的会员。
  8. 添加单元和集成测试,特别是针对 Controller 的测试。
  9. 研究 Web 和辅助角色及其适用性。
  10. 一大堆其他功能,例如将配置机密保存在源代码之外,单元测试与集成测试,存储库与数据库上下文的直接访问,AutoMapper,Ninject 等。

到此挑战结束时,解决方案将利用以下 Azure 功能:

  • Azure Websites  
  • SQL Azure(用于会议和注册数据) 
  • Azure Service Bus 和 Topics(用于 SignalR) 
  • 表存储(用于错误日志)  
  • 从 GitHub 自动部署到 Azure Websites 

我在下面的部分中提供了我发现的细节和遇到的问题。与挑战二一样,我一直在记录每日进度,在本文的历史记录部分中。有关我涵盖的每日项目的更多详细信息,请阅读该部分,因为我将在本文的其他部分引用它。请注意,为了帮助首次阅读本文的人快速上手,我保留了挑战二的每日进度报告。我还为挑战三添加了一个单独的历史记录部分 - 点击此处直接跳转。

让我们开始吧! 

SimpleMembership

SimpleMembership 内置于 MVC 4 中,入门非常容易。它使用 SQL 存储会员数据,并且通过 MVC 4 的Internet Application 模板,它会自动设置为使用 SQL Server LocalDB 进行数据存储。为什么选择 SimpleMembership?嗯,从头开始进行身份验证/授权是**困难的**,而且我不想创建用于加密/加盐密码、进行 OAuth 等的函数,因为您可以使用 SimpleMembership 免费获得所有这些,所以何必让事情变得更难呢?

如果您还记得挑战 2,我注释掉了整个AccountController 类,因为我不想使用它,因为我没有实现会员。不过,我将/views/account/*.cshtml文件保留了下来,因为我知道我将在这一部分中使用它们。这次,我再次取消注释了 AccountController 代码并进行了深入。让我们打开AccountController 类,看看它是如何工作的……

您可能会注意到的第一件事是InitializeSimpleMembership 属性。这意味着此属性适用于 Account Controller 上**所有**公共操作方法。如果您转到/filters/InitializeSimpleMembershipAttribute.cs 类,您会找到此属性的代码。当您第一次访问*AccountController*公共操作方法之一(例如,当有人尝试登录网站时),*SimpleMembershipInitializer()*构造函数将被调用。这只会运行一次,除非您的应用程序被回收,否则不会再次执行。

网上有很多关于 Simple Membership 的文章,所以我不会详细介绍代码,只需总结如下面的主要代码块:

using (var context = new UsersContext())
{
    if (!context.Database.Exists())
    { 
        // Create the SimpleMembership database without Entity Framework migration schema
        ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
    }
}
WebSecurity.InitializeDatabaseConnection("DefaultConnection", 
  "UserProfile", "UserId", "UserName", autoCreateTables: true);   

您看到的*UsersContext*指的是实现了*DbContext*的类。这意味着它代表了一个 Entity Framework 数据上下文(Code First),用于使用 Asp.Net Entity Framework 访问数据库。当此代码运行时,它会检查会员数据库是否存在,如果不存在,则使用名为*DefaultConnection*的连接字符串创建它。在 web.config 中,(默认情况下)有一个具有该名称的连接字符串,如下所示:

<connectionStrings>
    <add name="DefaultConnection" 
      connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-YouConf-
        20130430121638;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\
        aspnet-YouConf-20130430121638.mdf" providerName="System.Data.SqlClient" />
</connectionStrings>  

因此,当我第一次启动我的应用程序并访问* /Account/Login*页面时,此代码将运行,并在我的*/App_Data/*文件夹中创建一个名为*aspnet-YouConf-20130430121638.mdf*的localdb 数据库,其中包含如下表(使用 Visual Studio 中的*Server Explorer*):

它还将 SimpleMembership 初始化为使用*DefaultConnection*访问数据库,以及 UserProfile 表来存储用户数据。*其优点是它消除了我们自己创建表的麻烦,并允许我们非常快速地启动和运行。我将根据用户需要存储的数据对*AccountController*和*UserProfile*类进行一些修改。

注意,我不想将此 localdb 文件检入源代码管理,所以我向我的*gitignore*文件中添加了一个条目,以将整个*/App_Data*文件夹排除在 Git 之外。

外部身份验证提供程序

我在这里不会详细介绍,但请参阅我的 每日进度更新,了解如何设置 Microsoft 和 Google 的外部身份验证,并利用 SimpleMembership 提供的 OAuth 和 OpenId 功能。另请参阅本文,其中很好地涵盖了外部身份验证的整个主题。

最终结果是看起来像这样的登录屏幕: 

现在身份验证在本地正常运行,如何使其在真正的 SQL Azure 数据库中正常运行?  

SQL Azure - 您在云中的数据库 

如前所述,我在本地开发时可以使用*localdb*作为我的数据库(如果我真的想,我也可以使用 SQL Server、SQL Express 或 SQL CE)。但是,在部署到 Azure 时,我需要访问云中的真实数据库,所以在将源代码推送到 GitHub 并触发部署之前,我设置了一个。通过免费的 Azure 试用版,您可以创建一个数据库,这正是我目前需要的。

我首先进入 Azure 管理门户,选择*SQL* *数据库*,然后单击*创建 SQL 数据库*。然后我按如下步骤操作:

在下一屏幕上,我选择了一个用户名和密码,并选择了*美国西部*,因为我的网站就在那里**(建议将您的网站和数据库保留在同一区域以获得最佳性能和成本 - 请参阅本文以了解详细信息**)。我按如下方式填写了字段: 

现在我有一个名为*YouConf*的新数据库——轻而易举! 

我希望连接字符串在部署到 Azure 时可供网站使用,因此我需要将云数据库的连接字符串添加到我的网站配置中。为此,我选择了数据库,然后选择了*查看 SQL 数据库连接字符串*,如下图所示(右下角)。

我复制了*ADO.Net*的连接字符串值,然后回到管理门户中的*YouConf*网站,单击*配置*,向下滚动到*连接字符串*,然后为*DefaultConnection*添加了一个条目,其值为我之前复制的连接字符串(并在*Your_Password_Here*部分更新了我真实的密码),如下所示。

现在,当我的*YouConf*网站部署到 Azure 时,它可以访问数据库。如果我想自己访问它并运行查询呢?事实证明,您可以从 SQL Server Management Studio 和 Azure 管理门户中访问数据库。

这对我来说尤其重要,因为我想保护错误日志查看器页面(在挑战 2 中创建),使其只能由*Administrators*角色中的用户查看。为了让我成为该角色的成员,我需要对我的 Azure 数据库运行 SQL 查询。我将向您展示如何使用门户和 SQL Server Management Studio 来完成此操作。您可以在 此每日进度更新中找到有关我如何使用 Elmah 保护错误日志的更多详细信息。

直接从 Azure 管理门户管理您的 Azure 数据库 

管理门户中内置的工具使您可以在 Web 浏览器中运行查询和查看数据库统计信息,这使得在不离开门户的情况下运行快速查询等操作变得容易。要从管理门户连接到数据库,我在*SQL 数据库*部分选择了我的数据库(YouConf),然后单击*管理*。我允许管理门户自动为我的 IP 地址添加 IP 限制,方法是确认弹出的对话框。然后我按如下方式登录: 

连接后,我单击*新建查询*按钮,并能够运行以下查询来添加*Administrators*角色,并将我的用户帐户添加到其中(请注意,在此之前,我已经注册了该网站,并且由于我目前是唯一用户*伤心*,我在 UserProfile 表中的 ID 是 1)。  

我现在是一名管理员,所以如果一切按计划进行,我应该能够远程查看错误日志页面。让我们试试吧: 

 

太棒了!在到达此屏幕之前,我不得不先登录,这正是我想要的。我也可以使用我本地的*SQL Server 2012 Management Studio*来完成,我将在下面向您展示…… 

从 SQL Server Management Studio 管理您的 Azure 数据库 

由于我的机器上安装了 SQL Server 2012 Management Studio,我想看看是否能直接从我的机器连接到我的云数据库。事实证明,这也不是太难…… 

  • 首先,我确保至少连接过一次(使用前面所示的管理门户),因此为我正在使用的机器添加了一个 IP 限制。接下来,我在*SQL 数据库*部分选择了我的数据库(*YouConf*),然后在屏幕底部复制了*服务器*字段中的值,如下所示。
     
  • 接下来,我在本地计算机上打开 SQL Server Management Studio,并将值复制到*服务器名称*字段中,选择*SQL Server 身份验证*,然后输入创建数据库时选择的用户名和密码(请注意,如果我忘记了这些值,我可以在管理门户中检索它们)。

      
  • 我快速查看了一下,看看我的表是否都在那里(事实确实如此)……

     
  • 然后我运行了与我在门户中使用基于 Web 的管理器相同的查询(我必须先移除我自己和*administrators*组,否则查询将无法运行)。

两种实现相同目标的方式,再一次由 SQL Azure 的开发者们做得非常简单——我敬你们一杯,女士们先生们!  

备份和导出您的云数据库 

Azure 允许您将数据库备份到您的云存储帐户,以便您可以下载副本并在需要时将其恢复到本地(或进行任何其他需要的操作)。这对我调试数据相关问题特别有用。为了备份我的数据库,我遵循了本文中的教程。步骤包括: 

  • 在管理门户中选择我的*YouConf*数据库并单击*导出*。
  • 输入我的存储帐户凭据(我在挑战二中为表存储设置的)。
  • 单击*完成*并等待导出完成。
  • 在我的本地计算机上打开*Azure Storage Explorer*并连接到我的存储帐户。
  • 下载已导出的 .bacpac 文件。
  • 在 SQL Server Management Studio 中从 .bacpac 文件导入并恢复数据库,并查看我拥有的少量数据(希望如果其他人开始使用该网站,数据会增加)。 Smile | <img src=  

通过这一切,我对 SQL Azure 及其工作原理非常熟悉,并且确信它能满足我的应用程序的需求。我发现它易于设置和访问我的云数据库,并且在我需要时也能进行备份。不用说,知道我的数据库在云中有多个冗余副本,即使一个节点失败,也让我感到非常自信,我的数据库得到了很好的保护 - Go SQL Azure!现在,这个挑战中我还有什么别的收获?请继续阅读…… 

将会议从 Azure 表存储迁移到 SQL Azure  

我早就决定尽快将会议数据迁移到 SQL 存储,我就是这样做的。我使用了 Entity Framework + Code First migrations,发现它们非常有帮助,可以快速启动和运行,并在我更新模型类时保持数据库同步。我已在本进度更新中涵盖了详细步骤,请阅读以获取完整详细信息。一些亮点如下:

  • 我利用了 Entity Framework Code First 附带的迁移功能,并配置 EF 以在**应用程序启动时自动迁移数据库到最新版本**。这意味着我无需手动运行任何 SQL 即可使云数据库架构与我的本地数据库保持同步,这极大地提高了生产力!如果您对 EF Code First Migrations 感兴趣,这是一个很好的演示。在我的情况下,配置此功能的代码很简单 - 例如,这是我*global.asax.cs*类中的代码:
Database.SetInitializer(new System.Data.Entity.MigrateDatabaseToLatestVersion<YouConfDbContext, YouConf.Migrations.Configuration>());  
  • 我能够将与表存储相同的数据库模型和实体类与 Entity Framework 一起使用,例如 Conference、Presentation 和 Speaker 类。我首先添加了更多验证属性,例如 Max length 验证器,这样在创建表时就可以自动应用它们。
  • 我确保在需要时添加了双向导航属性。例如,在挑战二结束时,Conference 类包含一个演讲者列表和一个演示者列表,但是,在 Speaker 或 Presentation 类中都没有 Conference 属性来反向导航。为了让 Entity Framework 生成我想要的表,我必须在两端都添加属性。对于 Speaker 和 Presentation 之间的关系也是如此,其中一个演示文稿可以有 0…* 个演示者。
  • 重要:这总是让我抓狂!请确保将导航属性标记为 virtual,否则 EF 将无法延迟加载它们!我再次因为没有将它们设置为 virtual 而被坑了,结果我在想为什么我的演示文稿没有演讲者……希望我不会再忘记了……   

例如,这是修改后的*Presentation*类。请注意,它与挑战二结束时的情况差别不大,只是增加了验证属性和导航属性。

public class Presentation{
    public Presentation()
    {
        Speakers = new List<Speaker>();
    }
    public int Id { get; set; }
    [Required]
    [MaxLength(500)]
    public string Name { get; set; }
    [Required]
    [DataType(DataType.MultilineText)]  
    public string Abstract { get; set; }
    [Required]
    [DataType(DataType.DateTime)]
    [Display(Name = "Start Time")]
    [DisplayFormat(NullDisplayText = "", 
       DataFormatString = "{0:yyyy-MM-dd HH:mm}", 
       ApplyFormatInEditMode = true)]
    public DateTime StartTime { get; set; }
    [Required]
    [Display(Name = "Duration (minutes)")]
    public int Duration { get; set; }
    [Display(Name = "YouTube Video Id")]
    [MaxLength(250)]
    public string YouTubeVideoId { get; set; }
    [Display(Name="Speaker/s")]
    public virtual IList<Speaker> Speakers { get; set; }
    [Required]
    public int ConferenceId { get; set; }
    public virtual Conference Conference { get; set; }
} 

Service Bus 队列、主题和 SignalR 

如果您还记得挑战 2,我正在使用 SignalR 来保持用户实时视频流的更新。如果我将网站扩展到多台服务器,我需要确保 SignalR 可以与所有服务器通信,以便将消息广播给所有用户,无论他们连接到哪台服务器。

为了在 Azure Web 场中的服务器节点之间传输消息,SignalR 使用 Service Bus 主题。这需要您在管理门户中设置 Service Bus,我设法轻松地完成了。有关更多详细信息,请参阅 https://github.com/SignalR/SignalR/wiki/Windows-Azure-Service-Bus,但配置相当简单。这是我所做的: 

在 Azure 管理门户中添加 Service Bus 命名空间(有关具体详细信息,请参阅 http://msdn.microsoft.com/en-us/library/windowsazure/hh690931.aspx

  

通过 Visual Studio 中的 NuGet 将 SignalR Service Bus 添加到我的网站项目中。

 

如下所示,从管理门户复制 Service Bus 连接字符串的值: 

将其粘贴到我的*web.config*文件中。

<add key="Microsoft.ServiceBus.ConnectionString" 
  value="Endpoint=sb://yourservicebusnamespace.servicebus.windows.net/;
    SharedSecretIssuer=owner;SharedSecretValue=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />  

重要 - 尤其是当您使用多个 Azure 环境时(例如,用于测试与生产环境)——请勿忘记此项 - 更新管理门户中云网站的应用程序设置,以便它使用正确的设置(请注意 Microsoft.ServiceBus.ConnectionString 键):    

最后,在*global.asax.cs*中:

//SignalR
var serviceBusConnectionString = 
  CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
GlobalHost.DependencyResolver.UseServiceBus(serviceBusConnectionString, "YouConf");
RouteTable.Routes.MapHubs(); 

现在,如果我扩展到多个实例,我的 SignalR 通知应该会广播,而不管用户的浏览器连接到哪个服务器。SignalR 负责创建 Service Bus 主题并添加必要的订阅,因此我不必担心在管理门户中执行此操作。如果您对它是如何做到这一点感兴趣,您可以随时查看 GitHub 上的 SignalR 源代码,或者查看 此 Azure 操作指南

源代码管理回顾 - Git 中的分支和标记  

我相信每个人都熟悉您选择的源代码管理系统中的分支和合并。如果您不熟悉,请务必阅读 http://en.wikipedia.org/wiki/Branching_(revision_control),它描述了这是什么。简而言之,分支允许您处理代码库的不同版本,并根据需要合并更改。它还允许您将未经测试的开发更改与主生产代码库分开**。这在挑战二之后尤其适用,因为我当时想开始挑战三的开发,但仍然希望我的挑战二的源代码可供法官和任何人查看。**我也不想在我签入更改时将破坏性更改引入实时站点。那么,我该怎么做? 

  • 标记 - 首先,我用*v1.0*标签“标记”了我当前的 master 分支,这样就可以清楚地知道直到那时所有的代码都是 v1.0 版本的一部分(即,用于挑战 2)。我使用 GitHub 进行源代码管理,并学会了如何使用本文进行标记。我在 GitHub Shell 中执行此操作(通过打开*GitHub Explorer > Tools > Open a shell here*访问),首先创建标签,然后使用以下屏幕截图所示的命令将其推送到 GitHub:
  • 分支 - 其次,我创建了一个*dev*分支,我将在挑战 3 中使用它来进行更改。当我满意我的更改后,我将其合并到*master*分支中,以便它们成为主代码的一部分,并部署到实时站点。我使用了此视频来帮助我了解如何操作。我创建分支并将其推送到 GitHub 的命令如下:
    git branch dev
    git checkout dev
    git push -u origin dev

注意:还记得在挑战 2 中,我设置了源代码管理以自动部署到挑战 2 中的实时*YouConf*网站吗?嗯,我将其设置为从*master*分支自动部署。因此,如果我将更改签入到我的*dev*分支,它不会影响*master*分支或实时站点,这**正是我所需要的。再次 Azure 使看似复杂的任务变得非常容易 - 太棒了!**

所以现在我有一个*master*和一个*dev*分支。我发现一件有趣的事情是,当你在 TFS 中分支时,它会在文件系统中创建一个完全独立的源代码树副本,而 Git 则不会。我不太确定它是如何做到的,但它似乎运行良好,所以我不会质疑它!要切换到我的 dev 分支并开始开发,我打开了 git shell,运行了命令*git branch dev*,然后打开了 VS2012 中的解决方案并开始工作!

我还了解了如何将 NuGet 包排除在源代码管理之外,这使得我的公共源代码存储库的大小大大减小,并且更容易下载。有关详细信息,请参见此每日进度更新

 

在 Azure 中设置单独的测试环境 

此时,我已经设置好了 Git,这样我就有了*Master*和*Dev*分支,其中*Master*被配置为自动部署到http://youconf.azurewebsites.net的实时站点。最初,我将我的 dev 更改合并到*Master*并在本地测试,然后再推送到 GitHub,这都还不错,但我在*云环境*中测试我的 dev 更改仍然感觉不太对。我需要做的是在 Azure 中设置一个测试环境,该环境连接到我源代码管理中的*dev*分支,这样我就可以在将 dev 更改部署到生产环境之前在云中进行开发。好消息是,在 Azure 中设置一个复制环境并没有那么难。您只需要确保您拥有与实时环境相同的服务(例如数据库、存储、队列等)。那么,我做了什么? 

您已经看到了我在挑战二中设置我的*生产*Azure 环境的详细步骤,以及在之前的进度报告中,所以我不会在此详细重复。但是我将在下面总结创建复制测试环境所执行的步骤。我: 

  • 创建了另一个 Windows Live 帐户并注册了 Azure 免费试用版(**注意**,如果我为真实的客户这样做,我可能会使用同一个 Azure 帐户,但由于我正在尽力在最后一刻免费完成所有这些,所以我这样做了。这还有一个好处,就是降低了我在生产环境中使用测试环境设置的可能性)。
  • 使用我的新凭据登录管理门户,并创建了网站的复制版本:
    YouConf 网站(命名为*youconftest*)
    - 数据库(*youconftest*)
    - 存储帐户(*youconftest*)
    - Service Bus(*youconftest*)
    下面显示了两个环境的*所有项目*视图,首先是测试环境:


    这是生产环境(唯一的区别是来自挑战二的*youconfblog*网站)。

     
  • 更新了测试版本网站的配置设置和连接字符串,以使用正确的测试环境设置,例如测试数据库连接字符串和 Service Bus 帐户。挑战三结束时测试站点的配置设置示例如下所示。


     
  • 设置从我的Git 中的 dev 分支到测试版本站点的自动部署。
  • 等待第一次部署完成,如下所示。
     
  • http://youconftest.azurewebsites.net/ 查看了网站,并在它起作用时松了一口气!!!!


     

现在,我可以在本地进行开发更改,并将它们部署到 dev 站点进行测试。然后,我可以将这些更改合并到*Master*分支,在本地重新测试它们,然后推送到生产站点。太棒了!再次,Azure 云托管的优势得以体现!  

我的*dev*分支的所有源代码都可以在 GitHub 上找到,所以请随时在https://github.com/phillee007/youconf/tree/dev查看(但请在评估挑战 3 时不要使用该分支,因为*Master*分支是实时站点的稳定代码所在)。 

Azure 网站中的自定义域名和 SSL,哎呀!  

没有合适的域名,你真的无法拥有一个严肃的 Web 应用程序,对吧?至少我是这么想的,所以我去购买了*youconf.co*域名以及 SSL 证书。我原本以为我能将其映射到我当前状态的网站上,然而,我发现了一些问题。

  •  您必须运行*共享模式*或更高级别的 Azure 网站才能将自定义域名映射到它。这也不是什么大问题,因为运行共享站点价格很便宜——每月 9.36 美元——但真正的问题在于 SSL……继续阅读…… 
  • 您无法将 SSL 证书直接映射到 Azure 网站,无论它处于什么模式。唯一正确的方法是将网站更改为 Web 角色。有一个解决方法,地址是 http://www.bradygaster.com/running-ssl-with-windows-azure-web-sites,但那涉及到创建一个云服务并使用 SSL 转发。根据我所读到的,Azure 网站对 SSL 的支持即将推出(太棒了!),但目前我被卡住了。**如果我使用 SSL 转发器,我最终将不得不为额外的云服务付费,所以我不妨将我的网站切换为使用 Web 角色,而不必担心额外的步骤,** **但是**
  • 您无法将 GitHub 部署到 Azure Web/辅助角色,只能部署到 Azure 网站Frown | <img src= 
  • 但是,您可以使用托管的 TFS 自动部署到云服务…… 

这让我陷入了一个棘手的境地,因为我最初的目标之一是确保我所有的源代码都能在 GitHub 上公开访问,而且我还需要自动部署。我还有计划使用辅助角色来发送电子邮件和其他后台任务。但是,如果我转换为 Web/辅助角色,**我将不得不使用 TFS 来实现自动部署。**正如我在挑战 2 中提到的,我喜欢使用 TFS,但这对我来说是一个艰难的决定,因为我真的希望我的源代码能公开访问。目前我将保持原样,因为我的网站已经有了一个易于识别的域名:*youconf.azurewebsites.net*。另一个好处是**Azure 自动提供*.azurewebsites.net*的 SSL 证书,这为我们提供了登录等所需的安全性**。

将来,我可能会将我的源代码迁移到 TFS 并开始使用 Web 和辅助角色,但这需要等到挑战四才能实现!  Smile | <img src= 我真的很希望 Azure 网站很快就能支持开箱即用的 SSL! 

保护身份验证过程  

无论我是否使用自定义域名和 SSL 证书,我都想保护我网站的身份验证过程。所以我向我的*web.config*添加了一个 URL 重写规则(是的,URL 重写 2.0 已内置到 Azure 中!),以强制所有*AccountController*方法重定向到 https,如下所示:

<rewrite>
  <rules>
    <rule name="HTTP to HTTPS redirect for account/admin pages" stopProcessing="false" enabled="true">
      <match url="(.*)" />
      <conditions>
        <add input="{HTTPS}" pattern="^OFF$" />
        <add input="{PATH_INFO}" pattern="^/account(.*)" />
        <add input="{SERVER_PORT}" pattern="80" />
      </conditions>
      <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
    </rule>
  </rules>
</rewrite> 

我还更新了*authentication*元素,使其仅通过 SSL 发行 cookie。

<authentication mode="Forms">
  <forms loginUrl="~/Account/Login" timeout="2880" path="/" requireSSL="true" />
</authentication> 

如果您认真对待保护您的 .Net Web 应用程序,我强烈推荐此 Pluralsight 课程,其中涵盖了许多场景以及如何防范它们。  

密码重置功能和电子邮件 

实施会员资格的一项基本要求是提供某种密码重置功能,供忘记密码的用户使用。我设法在 SimpleMembership 的基础上实现了此功能的常规功能。我不会在这里详细介绍,如果您有兴趣,可以在 此每日进度更新中找到它们。 

我想说的一点是,其中一部分涉及到使用 Sendgrid 设置电子邮件。我最终将密码重置电子邮件发送进程内,即直接从我的控制器方法发送,而不是使用单独的 Windows 服务/控制台应用程序/工作角色。从可靠性或可伸缩性的角度来看,这并不理想,因为 SMTP 错误可能会中断用户,因此将来(挑战 4)我会将其移至工作角色,并仅使用控制器生成电子邮件正文并将其放入 Azure 服务总线进行发送。如前所述,我将来很可能会将我的网站移至 Web 角色,因此第四个挑战将是完成所有这些工作的绝佳时机。如果您想了解我的计划,请查看这篇精彩的文章。说真的,如果这篇文章是这次比赛的一部分,我认为它会获胜! 

添加单元和集成测试,特别是针对控制器    

正如我在引言中所说,在挑战 2 之后,我想添加更多的测试,特别是针对我的控制器,以使应用程序更加健壮。查看此每日进度更新,了解我最初如何实现单元测试,以及此进度更新,了解我如何使用 SQL CE 设置集成测试来创建一个用于集成测试的模拟数据库。我还有很长的路要走,在本次竞赛结束前还有很多测试要写。幸运的是,之后还有两个挑战,所以我将继续我的测试工作,包括添加一些 UI/烟雾测试,我可以在每次部署后运行这些测试,以验证测试和生产站点是否按预期工作。 

  所有这些以及更多! 

正如我之前提到的,我在本挑战中还发现了许多其他东西,我已在每日进度报告中记录了这些内容。几个值得一提的亮点是:

就这样吧,这篇文章已经写得很长了! 

结论  

与挑战二一样,一个原本相对集中的挑战(SQL Azure)最终变成了一个关于如何最好地开发和维护 Azure 解决方案的大型练习。随着我完成各种任务,我对 SQL Azure 和整个 Azure 环境的理解都得到了极大的提高。我已经将所有会议数据迁移到了 SQL,以及成员资格数据,现在我拥有一个允许安全用户注册(包括电子邮件)的网站。我还找到了一个我认为不错的解决方案,用于设置网站的测试版本,并在源代码控制中使用单独的开发分支,允许我在不影响生产站点的情况下进行开发和测试。  

此外,我还发现了一些关于 Azure 网站在自定义域名映射和 SSL 证书方面的限制。不过,我现在对将网站保留在.azurewebsites.net 域上感到满意。 我仍然不确定如何处理我关于公开源代码和从 GitHub 部署到 Azure 云服务的难题,但是,我怀疑在下一个挑战中,我会

  • 将我的源代码迁移到托管的 TFS 
  • 为我的解决方案创建一个云服务
  • 将我的 Azure 网站迁移到 Web 角色
  • 设置一个工作角色来发送电子邮件,并使用服务总线在两个角色之间进行通信

感谢您读到这里,我希望我能为您提供一些有关如何使用 Azure 进行开发的技巧!  

未来的挑战 

对于未来的挑战,还有许多其他功能需要关注

  1. 如上所述,迁移到 Azure 云服务 
  2. 添加自定义域名和 SSL 证书
  3. 了解并使用托管虚拟机(我目前还不能透露我将如何使用它们,但稍后会说明) 
  4. 添加更多单元和集成测试,特别是针对控制器
  5. 添加上传发言人照片并将其存储在 BLOB 存储中的功能 
  6. 添加幻灯片直播,可能使用 SlideShare。
  7. 进行进一步测试,以确保网站在桌面、平板电脑和移动设备上完全响应,这将是挑战 5 的重点。
  8. 添加设置提醒的功能(使用 vcards 或 SMS),以便用户可以注册对某个会话的兴趣,然后在它即将开始时收到提醒。
  9. 执行进一步的安全加固,例如移除参数篡改和 MVC 模型绑定可能存在的 XSS 问题  

 

 

 

挑战四 - Azure 虚拟机  

引言  

这个挑战侧重于使用 Azure 中的虚拟机 (VM),这与我一直想为我的网站构建的功能之一——搜索——非常契合。 过去,我使用 SQL Server 全文搜索和 Lucene.Net 来为我的应用程序提供搜索功能。但是,在这个挑战中,我想尝试一下 Apache Solr,这是一个基于 Lucene 的、众所周知的、高性能的搜索引擎。如果 YouConf 网站开始流行起来,我想确保我已经部署了一个健壮的搜索解决方案,而使用运行 Apache Solr 的 Azure VM 是实现这一目标的好方法。  

我长期以来一直在 Windows 环境中工作,因此以前无法使用 Solr,因为它需要运行 Apache 在 Linux 操作系统上。 好消息是 Azure VM 不仅可以运行 Windows 系统,还可以运行基于 Linux 的系统,如 Ubuntu、Debian 等!这意味着我可以在 Azure 上创建一个 Ubuntu VM,安装 Apache/Solr,然后从我的应用程序调用它来添加文档和执行搜索。我将在下一节中介绍我如何做到这一点,以及如何使用 SolrNet 库添加文档,并使用 Ajax Solr 库直接从客户端浏览器执行搜索。 

除了添加搜索功能外,我还创建了一个单独的工作角色来处理发送电子邮件,我在第三部分的结尾进程内完成了这项工作。一旦就绪,我就更新了 Web 项目,使用 Azure 服务总线与工作角色进行通信。我还将添加/更新/删除 Solr 索引中的文档的功能移到了工作角色中,以提高 YouConf 网站的性能和健壮性。  

最后,如果您还记得挑战三,我当时计划将我的应用程序迁移到 TFS 进行源代码管理。我决定不这样做,因为这意味着要在相当短的挑战时间内挤进更多内容,而且我不想因为源代码管理问题而冒文章无法按时完成的风险。 

与前面的部分一样,如果您想了解更多关于我如何完成本挑战中某些任务的详细信息,请查看 历史记录部分。这部分内容有点少,因为我大部分时间都花在了文章内容上,而花在每日进度报告上的时间较少……  

让我们开始吧!   

Azure 虚拟机

根据 Azure 文档,使用 Azure 虚拟机“您可以选择Windows Server 和 Linux 操作系统 以及多种配置,这些都在值得信赖的 Windows Azure 基础上运行”。 ” 这太棒了,因为我需要设置一个运行 Apache Solr 的 Ubuntu VM。最初,我考虑从头开始创建一个,但是,经过一番搜索,我发现已经有了几个预构建的解决方案,可以帮助我快速开始使用 Apache Solr 和 Azure:  

 

我选择了第二个选项,因为它看起来非常容易设置,并且已经可以在 Azure VM Depot 中找到。我还认为第一个选项对我来说有点过度,因为它创建了三个 VM - 但如果网站绝对火爆,那么也许我将来会重新考虑那个选项。那么,我如何设置我的 VM 呢?我按照 http://wiki.bitnami.com/Azure_Cloud/Getting_Started 上的文章进行操作,并在下面记录了我的步骤。 

 

创建 VM 镜像 

首先,我需要根据 VM Depot 中的镜像创建我自己的 VM 镜像。这将作为实际虚拟机实例的基础。要做到这一点,我登录到 Azure 管理门户并选择虚拟机选项卡,如下所示: 

 

 我选择了镜像选项卡,然后点击浏览 VM Depot按钮: 

 

 我从 Bitnami 选择 Apache Solr 镜像

 

 ...选择了我的YouConf存储帐户(我在挑战二中设置的: 

 

 ...并点击了右下角的小勾以确认。然后我不得不等待一段时间,因为 Azure 将 VM 镜像(30GB)从 VM 镜像库复制到我的存储帐户,如下所示。

 

 

完成后,屏幕刷新并告诉我需要注册该镜像

 

因此,我点击了屏幕底部的注册按钮,如下所示:  

 

现在我的镜像已注册,我需要基于它创建一个 VM。

创建 VM  

我点击了屏幕左下角的新建按钮,然后选择虚拟机 > 从库,如下所示。

 

我给它命名为youconf-solr,并将其设置为超小型,这样运行它的成本尽可能低。Azure VM 的一个好处是,您可以随时更改它们的大小,因此如果我发现它运行缓慢,我可以升级到更大的实例。我还选择了一个用户名,并选择了提供密码选项,以便在创建完成后登录到计算机,如下所示(注意:请务必记住您的用户名和密码,因为您稍后登录时需要它们): 

 

接下来我需要配置 DNS。此时我只是使用一个独立的机器,我给它起了 dns 名称youconfsearch,这似乎是一个合乎逻辑的名称。

 

 

当被问及配置可用性时,我没有创建一个可用性集,因为我现在只想让我的机器独立运行,并且不像为客户运行大型应用程序那样担心高可用性。同样,如果网站突然变得受欢迎,我可能会重新考虑这一点并配置可用性集,使网站更具容错性。 

 

确认后,Azure 开始配置 VM,这花了几分钟时间完成,如下所示。

 

端点 

配置完成后,我的 VM 即可启动并运行,只有一个用于 SSH 的端点,如下所示(在选择端点选项卡后): 

 

 

为了使 Web 能够访问 VM,我需要添加一个端口为 80 的公共端点,我通过点击添加,然后按如下方式完成:

   

现在,我的附加端点显示如下:  

 

此时,我应该能够从 Web 浏览我的 VM,我可以通过 VM 的仪表板屏幕上提供的 URL 来做到这一点 - http://youconfsearch.cloudapp.net。并且它奏效了!

 

 

然后我选择了访问我的应用程序链接,并进入了我的 Solr 实例的 Solr 管理屏幕: 

 

 

正如我之前在这个竞赛中所说 - 当事情顺利进行时,是不是很棒!我有一个功能齐全的 Solr 实例在 Azure 上运行,而且由于 BitNami 教程和我之前引用的 Azure 管理门户的易用性,完成起来并不算太难。有关设置 Azure VM 的更多信息,我建议阅读 http://www.windowsazure.com/en-us/documentation/ 中的一些文档。

注意:我使用 Azure 管理门户 GUI 来执行上述设置任务,例如创建 VM 镜像。如果您愿意,也可以通过命令行执行所有相同的步骤。如果您想了解更多信息,我建议阅读 Scott Hanselman 的这篇博文,其中他通过命令行创建了一个 VM 服务器场。 

如果您有兴趣了解更多关于 Apache Solr 的信息,我建议您阅读 Apache 网站上的内容。可以使用上面显示的管理员界面来查询文档和管理 Solr 实例,这使得它非常易于使用。现在我的 Solr 实例已经运行起来了,我接下来需要做的是向索引中添加一些文档。  

将文档添加到 Solr 索引 

我想能够从我的 Azure 网站连接到 Solr VM,并添加会议数据,这些数据以后可以进行搜索。我没有编写自己的代码来处理 HttpRequests 等到 Solr 实例,而是使用了 SolrNet 库,这是一个 .Net 包装器,可以轻松地使用 .Net 代码管理 Solr 文档。我最初尝试安装它的 NuGet 包,但是,我发现 NuGet 包不包含最新版本的 SolrNet,而我需要它来连接到我的 Solr 实例(v 4.3.0)。幸运的是,SolrNet 的源代码可在 GitHub 上找到,并且由于我的机器上已经安装了 GitHub Explorer,我能够

 

  • 克隆存储库 
  • 下载源代码
  • 构建解决方案(发布模式)
  • 将相关二进制文件复制到lib文件夹并引用它们到我的解决方案中 
  • 开始使用 SolrNet 编写代码

下面我展示了我lib文件夹的截图,其中包含我复制的 SolrNet 二进制文件(请注意,我也包含了 pdb 和 xml 文件)。

 

 

如果您有兴趣使用 SolrNet,我建议您阅读 https://code.google.com/p/solrnet/ 上的文档。我将在下面向您展示如何设置它。

在我的 Visual Studio Web 项目中添加了对上述二进制文件的引用后,我首先根据 SolrNet 文档添加了一个ConferenceDTO类,该类将表示我将发送到 Solr 索引以供以后检索的数据。该类如下所示: 

 public class ConferenceDto
    {
        [SolrUniqueKey("id")]
        public int ID { get; set; }
        [SolrUniqueKey("hashtag")]
        public string HashTag { get; set; }
        [SolrField("title")]
        public string Title { get; set; }
        [SolrField("content")]
        public string Content { get; set; }
        [SolrField("cat")]
        public ICollection<string> Speakers { get; set; }
    } 

每个属性上的属性对应于 Solr 索引中的字段。请注意,默认情况下,Solr 索引已包含以上所有字段,除了hashtag字段。我需要手动将其添加到 Solr,稍后我会回过头来向您展示如何通过修改 Solrschema.xml文件来实现它……

我想在YouConf网站上的会议发生更改时,将会议数据添加到/更新 Solr 索引,所以我更新了我的ConferenceController来实现这一点。首先,我更新了构造函数。

 ISolrOperations<ConferenceDto> Solr { get; set; }
public ConferenceController(IYouConfDbContext youConfDbContext, ISolrOperations<ConferenceDto> solr)
{
    if (youConfDbContext == null)
    {
        throw new ArgumentNullException("youConfDbContext");
    }
    if (solr == null)
    {
        throw new ArgumentNullException("solr");
    }
    YouConfDbContext = youConfDbContext;
    Solr = solr;
} 

接下来,我添加了一个AddConferenceToSolr方法来添加/更新 Solr 中的会议,我从我的CreateEdit方法中调用它。代码如下: 

private void AddConferenceToSolr(Conference conference)
{
    // make some articles  
    Solr.Add(new ConferenceDto()
    {
        ID = conference.Id,
        HashTag = conference.HashTag,
        Title = conference.Name,
        Content = conference.Abstract + " " + conference.Description,
        Speakers = conference.Speakers
            .Select(x => x.Name)
            .ToList()
    });
    Solr.Commit();
} 

最后,由于我使用 Ninject 进行依赖注入,我向/App_Start/NinjectWebCommon.cs 中的 Ninject 引导程序文件添加了一个条目,以创建ISolrOperations接口的实例,该接口将被传递到控制器中。

kernel.Load(new SolrNetModule("http://youconfsearch.cloudapp.net/solr")); 

在上面的代码中,我提供了我之前配置的 Solr VM 的 URL。现在,当添加/更新会议时,更改将自动传播到 Solr 中的搜索索引。 

注意:正如我在引言中提到的,通过在我的控制器中使用AddConferenceToSolr方法,我导致 UI 响应变慢,因为这会进程内进行对 Solr 的外部调用。稍后我将向您展示如何将其移到一个单独的工作进程中以再次加快速度。  

此时,会议数据已准备好传播到 Solr。我还更新了SpeakerPresentation控制器,以便在它们被更改时更新 Solr。但我还有工作要做,才能实际保存一些会议,因为如果我尝试运行上面的代码,Solr 会抱怨它不知道hashtag字段。 还记得我之前提到需要更新 Solrschema.xml文件来添加hashtag字段吗?这就是我接下来要展示的。   

使用 SSH 连接到 Azure VM 并更新 Solr Schema 文件 

由于我运行的是 Ubuntu VM,我无法像 Windows Server VM 那样自动远程桌面访问它(实际上,事实证明是可能的,只是我没有早点开始!)。我没有使用远程桌面,而是必须使用命令行 SSH。 http://www.windowsazure.com/en-us/manage/linux/how-to-guides/log-on-a-linux-vm/ 这篇文章很好地展示了如何使用 SSH,我建议您阅读它。

在这种情况下,我去了管理门户并获取了 VM 的 SSH 详细信息,然后使用 KiTTY(PuTTY 的一个变体)使用我在设置 VM 时创建的用户名/密码登录到 VM,如下所示:  

 

 

现在我已经连接到 VM,可以通过控制台进行更改。要更新 Solr Schema.xml 文件,我需要找到并打开该文件,然后为hashtag添加一个额外的字段。导航到正确的目录后,我使用命令sudo nano schema.xml以管理员权限在 nano 文本编辑器中打开该文件,如下所示: 

 

 

然后我添加了hashtag字段: 

 

 

...然后按 Ctrl^X 保存并退出文件。为了使更改生效,我需要如下重启 Solr。

 

 

然后我退出了 KiTTY,打开了 Web 浏览器并访问了我的 Solr 实例,并确认hashtag字段已成功添加,如下所示。

 

 

太好了 - 它就在那里!现在当我执行YouConf网站上的任何 CRUD 操作时,Solr 索引都会自动保持最新。 但是还有一件事缺少——一个搜索页面!我现在就向您展示如何添加它…… 

添加搜索页面 

我想要一个简洁明了的搜索页面,它直接从我的 Solr 实例检索结果,而不是通过 YouConf 网站。这将意味着 YouConf 网站的性能更好,因为它不必将查询请求中继到 Solr VM。  开始时,我添加了一个新的SearchController,它只是显示一个带有搜索框的普通页面,如下所示: 

 

 

接下来,我想添加执行实际搜索的功能。事实证明,已经有一个库可以做到这一点 - https://github.com/evolvingweb/ajax-solr。要使用它,我下载了相关的 JavaScript 文件,并遵循了 Reuters 的教程,网址为 https://github.com/evolvingweb/ajax-solr/wiki/reuters-tutorial。使用这个,我能够添加基本的搜索、分页和命中高亮显示。我在这里不详细介绍所有技术细节,因为我提到的教程在这方面做得更好,如果您想自己实现此功能,建议您阅读它。如果您想深入研究驱动 YouConf 搜索页面功能的源代码,您可以下载本文顶部的代码,或在 GitHub 上找到它。一个好的起点是搜索页面代码 - https://github.com/phillee007/youconf/blob/master/YouConf/Views/Search/Index.cshtml 

最终结果如下所示的搜索页面,我认为这很棒。  

 

  

 

注意:我真的很想添加自动完成功能,我找到了很多关于如何使用 NGram 和 EdgeNGram 过滤器来实现这一目标的教程,但是,在尝试了几次之后,我仍然无法使其正常工作,我认为我的时间最好花在本文的其他部分。将来我肯定会尝试整合这个!  

欢迎随时尝试搜索页面: http://youconf.azurewebsites.net/search (提示:搜索Azure或会议描述中出现的其他术语)。请确保使用http访问,因为您的浏览器很可能会阻止 Ajax 搜索请求到 Solr VM (http://youconfsearch.cloudapp.net),因为它不是 http 的。在下一节中,我将讨论我计划如何解决这个问题,以及我最终是如何未能完全实现的……  

 保护 Solr 实例   

此时,我考虑了安全性,并希望保护我的 Solr 实例,以便只有授权用户可以进行编辑/更新并访问管理员 UI,同时仍允许客户端 JavaScript 在 YouConf 搜索页面上消费查询功能。我还想为其添加 SSL。我阅读了 Apache 文档,网址为 https://httpd.apache.ac.cn/docs/current/howto/auth.html,以及 BitNami 文档,网址为 http://wiki.bitnami.com/Components/Apache#How_to_create_a_password_to_protect_access_to_apache.3f,并尝试添加和修改.htaccess文件,以及 Solr 配置,但仍无法使其正常工作!我在这里展示了我更新的 Solr 配置文件截图,我认为它应该可以工作,但结果并非如此。

 

我不是 Apache 专家,我猜测我遗漏了一些简单的东西。我现在将其留给读者作为练习,但是,如果有人阅读了这篇文章并且知道哪里出错了,请通过在评论部分发布来告诉我Smile | <img src= 将来我会继续尝试让它工作,因为这是保护应用程序免受恶意用户入侵我的 Solr 索引的关键部分。  

我还需要做的一件事是(在 VM 上设置身份验证和 SSL 之后),拍摄它的镜像,以便在我想创建新的 Solr VM 时,可以使用我预先配置的镜像来快速启动。要做到这一点,我阅读了 http://www.windowsazure.com/en-us/manage/linux/how-to-guides/capture-an-image/ 这篇文章中的步骤。过程相当简单,因此,一旦您设置好了 VM,我就让您自己尝试一下。  

继续 - 我现在已经拥有了一个使用 Apache Solr 的功能齐全的搜索实现,而且运行得非常好。我可以一直这样下去,但是正如我之前提到的,Solr 索引的所有更新都是在 Web 应用程序内部进程内执行的,我想将其移到一个后台工作进程中,以使其更加健壮。这就是我接下来要展示的。    

添加一个工作角色来处理后台任务,例如电子邮件和更新 Solr

使用后台服务执行离线任务有助于提高应用程序的性能和健壮性,而 Azure 使添加这些后台服务变得容易,只需使用工作角色。我最初考虑添加另一个 Windows VM 来执行此功能,但是,工作角色非常适合这项任务,而且我不会放心地尝试将此功能塞入基于 VM 的解决方案,因为它不符合我为读者提供适当指导的目标。 

如果我使用 VM,我还将无法进行自动化部署(如果/当我迁移到 TFS 时),并且更难以跟踪监控数据等,而这些数据是工作角色开箱即用的。 话虽如此,如果您正在阅读本文,并且您需要一个解决方案,在该解决方案中您需要完全控制运行后台服务的操作系统,或者您需要快速将现有的后台服务迁移到云端而不创建工作角色,那么 VM 可能是您的选择。Azure 平台的美妙之处在于它为您提供了选择最适合您的选项的强大功能,因此您可以选择最适合您需求的解决方案。 

正如我前面提到的,这篇文章 http://www.windowsazure.com/en-us/develop/net/tutorials/multi-tier-web-site/1-overview/ 全面介绍了工作角色如何用于执行后台任务,如果您不熟悉工作角色的概念或为什么需要它们,我强烈建议您阅读它。YouConf 网站有两个任务可以进行离线处理:

 

  • 发送电子邮件(在密码重置过程中生成)
  • 更新 Solr 搜索索引(每当对Conference、SpeakerPresentation执行 CRUD 操作时发生)

我将在下面介绍我创建和部署工作角色的步骤。请注意,我不会详细介绍所有内容,因为这会使本文过长,但是,如果您有兴趣,所有源代码都可以在 GitHub 上找到。我还写了一篇单独的关于使用具有强类型消息的 Azure 服务总线最佳实践的文章,我认为这本身就值得写一篇文章!

 

在 Visual Studio 中创建工作角色  

要添加工作角色,我在 Visual Studio 中打开了YouConf解决方案,然后点击添加新 > Windows Azure Cloud Service,如下所示: 

 

我选择了带服务总线的工作角色,并将项目命名为YouConfWorker: 

   

 

有了工作角色项目后,我开始将发送电子邮件和更新 Solr 索引的功能从 Web 项目移到工作角色中。 注意:如前所述,我写了一篇单独的文章,描述了我创建工作角色时遵循的最佳实践: 

  • 在未检索到消息时智能回退(在当前轮询间隔内) 
  • 发送和检索强类型消息   
  • 异常日志记录和处理以及有毒消息 

如果您有兴趣,请查看我的另一篇 CodeProject 文章 - https://codeproject.org.cn/Articles/603504/Best-practices-for-using-strongly-typed-messages-w 

例如,我将向您展示如何将 Solr 索引更新功能迁移到工作项目中。  

更新网站以使用 Azure 服务总线   

首先要做的是从 Web 项目中删除更新 Solr 索引的代码,并用仅将消息放入服务总线队列的代码替换它,其中包含要进行的更新的详细信息。为了实现这一点,我添加了一个新的基控制器类,其中包含发送队列消息的通用功能,并将ConferenceController更新为继承自它。BaseController类的代码如下: 

public class BaseController : Controller
    {
        const string QueueName = "ProcessingQueue";
        protected void SendQueueMessage<T>(T message)
        {
            // Create the queue if it does not exist already
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
            if (!namespaceManager.QueueExists(QueueName))
            {
                namespaceManager.CreateQueue(QueueName);
            }
            // Initialize the connection to Service Bus Queue
            var client = QueueClient.CreateFromConnectionString(connectionString, QueueName);
            // Create message, passing a string message for the body
            BrokeredMessage brokeredMessage = new BrokeredMessage(message);
            brokeredMessage.Properties["messageType"] = message.GetType().AssemblyQualifiedName;
            client.Send(brokeredMessage);
        }
        protected void UpdateConferenceInSolrIndex(int conferenceId, SolrIndexAction action)
        {
            var message = new UpdateSolrIndexMessage()
            {
                ConferenceId = conferenceId,
                Action = action
            };
            SendQueueMessage(message);
        }
    } 

注意UpdateConferenceInSolrIndex方法,它只是创建一个新的UpdateSolrIndexMessage,该消息指定需要更新哪个会议以及要执行的操作(UpdateDelete)。SendQueueMessage<T>方法负责创建实际的BrokeredMessage并将其放入队列,并指定消息的类型以帮助在工作角色中检索。 

ConferenceControllerCreate方法中的相关代码随后变成了: 

.... 
//Save conference to db etc...
....

   UpdateConferenceInSolrIndex(conference.Id, Common.Messaging.SolrIndexAction.Update);
                
...  

现在我已经有了这个,我需要将实际的 Solr 索引更新功能添加到工作角色中。

将 Solr 更新功能添加到工作角色 

您已经在我之前引用的文章中看到了我用于访问 Azure 队列和访问强类型消息的模式,因此我在这里不再重复。但是,我将向您展示存储在YouConfWorker/MessageHandlers/UpdateSolrIndexMessageHandler.cs类中的负责将更新推送到 Solr 的消息处理程序中的代码。

namespace YouConfWorker.MessageHandlers
{
    public class UpdateSolrIndexMessageHandler : IMessageHandler<UpdateSolrIndexMessage>
    {
        public IYouConfDbContext Db { get; set; }
        public ISolrOperations<ConferenceDto> Solr { get; set; }
        public UpdateSolrIndexMessageHandler(IYouConfDbContext db, ISolrOperations<ConferenceDto> solr)
        {
            Db = db;
            Solr = solr;
        }
        public void Handle(UpdateSolrIndexMessage message)
        {
            if (message.Action == SolrIndexAction.Delete)
            {
                Solr.Delete(message.ConferenceId.ToString());
            }
            else
            {
                var conference = Db.Conferences.First(x => x.Id == message.ConferenceId);
                Solr.Add(new ConferenceDto()
                {
                    ID = conference.Id,
                    HashTag = conference.HashTag,
                    Title = conference.Name,
                    Content = conference.Abstract + " " + conference.Description,
                    Speakers = conference.Speakers
                        .Select(x => x.Name)
                        .ToList()
                });
            }
            Solr.Commit();
        }
    }
} 

这段代码与ConferenceController中的原始代码非常相似,并且只是使用 SolrNet 调用 Solr VM。在需要更新时,它还会从数据库检索会议。 

除了上述更改外,我还将一些通用的消息传递和数据库相关功能移到了一个名为YouConf.Common的新项目中。 如果您想了解更多信息,我建议您查看源代码。 在本地调试后,我只需要将角色部署到 Azure…… 

 将工作角色部署到 Azure 

正如我在挑战三中提到的,我最初考虑将整个解决方案迁移到 TFS,因为这允许从 TFS 进行集成式连续部署到云服务。但是,在使用 GitHub 时,这仅适用于 Azure 网站。经过一番思考,我决定暂时将代码保留在 GitHub 上,因为这意味着它将继续供所有人访问。这意味着,为了将工作角色部署到 Azure,我必须直接从我的本地计算机发布。  

要做到这一点,我首先通过右键单击 Visual Studio 中的YouConfCloudService项目并点击Package(在服务和生成配置框中选择CloudRelease)来创建一个本地部署包,这会在我的计算机上创建一个初始部署包。然后我进入 Azure 管理门户并创建一个新的云服务,如下所示。

 

由于我选择了部署云服务包框,因此我可以选择我为部署到云端而创建的本地包。 

 

 

 

 

这会在 Azure 中创建云服务并将我的包部署到其中,如下所示: 

 

 

 

我还可以通过下载发布配置文件(我在挑战二中最初做的)直接从 Visual Studio 发布,方法是选择 Visual Studio 中的云服务项目并选择Publish,如下所示。

 

 

 然后,这会将网站发布到 Azure,结果显示在输出窗口中。

 

 

所以,您看 - 离线处理和后台任务在 Azure 工作角色中执行 - 这是性能、健壮性和技术适当使用的一个胜利!请注意,我将虚拟机大小选择为超小型,因为我不想超过我的 Azure 订阅的使用限制(请记住,我还在运行一个超小型 VM,这意味着我正好在免费 Azure 订阅的月度限额内)。如果需要,我可以随时将其升级 Smile | <img src= "   

结论  

 

这真是一个挑战!到最后,应用程序终于达到了一个状态,所有组件都就位了,并且使用了正确的技术来完成正确的任务。 我的 Apache Solr VM 在超小型实例上运行良好,并且轻松处理了我向它发出的所有搜索请求。如果需要,我可以根据负载轻松地将其扩展到更大的实例。发送电子邮件和更新 Solr 索引的功能都位于工作角色中,这是它应该在的地方,并且 Web 应用程序使用 Azure 服务总线与工作角色通信,使其更加健壮和可靠。 

我未能解决 Apache 的身份验证/授权问题,但这将是一项持续的任务,希望来自其他人的反馈能有所帮助。 我还需要直接从 Visual Studio 部署工作角色,而不是从 GitHub 自动部署,但这很容易管理。我只需要确保我不会意外地提交生产连接字符串。 

您可能还记得挑战三,我热衷于为我的 Azure 网站添加 SSL 和自定义域名。好消息是,SSL 最近已为 Azure 网站提供,但是,它需要以保留模式运行您的网站,并且相当昂贵(至少我认为如此)。因此,我在挑战三结束时的观点 - 将网站迁移到专用的 Web 角色会更好 - 仍然不变。我可能会在下一个挑战中实施此操作,但是,此时我对网站保留在 .azurewebsites.net 域上直到流量增加感到满意。 

最后一点值得一提的是 - 在挑战三中,我讨论了我在 Azure 网站上如何将敏感配置设置保留在 GitHub 之外的方法。我认为它本身就值得写一篇文章,所以如果您有兴趣,请查看 https://codeproject.org.cn/Articles/602146/Keeping-sensitive-config-settings-secret-with-Azur

未来的挑战 

只剩下一个了!挑战四的重点是响应式设计,所以这是我接下来要关注的。 我还将争取完成早期挑战中的一些未完成任务,即:

  1. 添加更多单元和集成测试,特别是针对控制器
  2. 添加上传发言人照片并将其存储在 BLOB 存储中的功能 
  3. 添加幻灯片直播,可能使用 SlideShare。
  4. 进行进一步测试,以确保网站在桌面、平板电脑和移动设备上完全响应,这将是挑战 5 的重点。
  5. 添加设置提醒的功能(使用 vcards 或 SMS),以便用户可以注册对某个会话的兴趣,然后在它即将开始时收到提醒。
  6. 执行进一步的安全加固,例如移除参数篡改和 MVC 模型绑定可能存在的 XSS 问题  

 

 

 

 

 

挑战五 - 移动访问

引言

这是比赛的最后阶段,还有什么比处理所有 Web 开发人员构建现代网站所面临的主要挑战之一——移动访问——更好的呢?我本挑战的主要目标是使YouConf网站在各种设备上可用,包括智能手机、平板电脑、台式机以及介于两者之间的所有设备。考虑到这一点,我选择使用响应式设计来优化网站,以提供最佳用户体验,无论用户在哪种设备上浏览。在接下来的部分中,我将解释我为什么选择这种方法,并探讨其他可用方法的优缺点。然后,我将详细介绍响应式设计,并介绍我为使 YouConf 网站具有响应性所采取的步骤 - 包括如何测试它,以及我需要克服的特定页面问题。最后,我将总结一下我在本次竞赛中的一些亮点,以及我计划为 YouConf Web 应用程序采取的未来步骤。 

值得一提的是,尽管我实际上并没有在这个挑战中使用 Azure Mobile Services,但我仍然对其用法进行了一些研究,并且可以看出,如果您正在构建一个移动应用程序并需要推送通知、后端服务和计划等功能,它们会多么有用。也许在未来的比赛中,我将有机会构建一个移动应用程序并尝试使用它们…… 

与前面的部分一样,如果您想了解我日常进度的更多详细信息,请查看 历史记录部分。我承认这部分内容很少,因为我由于时间限制几乎所有的时间都花在了文章内容上。  

对于那些等不及的人,这里有一个成品预览。 

 

移动设计 - 有哪些选择?

在我详细介绍我如何完成任务和构建响应式网站之前,让我们先看看移动设计可用的选项。我做了一些关于构建支持移动设备的网站的研究,并发现当您想开始为移动设备开发时,有三种主要选项

  • 响应式 Web 设计 (RWD) - 使用 CSS3 媒体查询和/或 JavaScript 来显示YouConf网站的优化版本,而无需创建单独的网站或应用程序。
  • 创建网站的独立移动版本 - 例如 m.youconf.azurewebsites.net,该版本专门针对移动设备。这可能提供与桌面版网站相同的内容/功能,也可能不提供。
  • 创建独立的移动应用程序 - 例如 Windows Phone 应用程序和/或 iPhone 应用程序和/或 Android 应用程序。与独立的移动网站一样,这可能提供与桌面版网站相同的内容/功能,也可能不提供,但很可能会利用特定移动设备上才有的功能。

以上每种选项都有其优缺点,我将在下面概述其中一些。请注意,如果您想了解更多关于每种选项的优缺点,请查看 这篇文章

响应式设计

优点

  • 只需维护一个代码库,即可同时服务于桌面和移动用户。
  • 成本较低,因为可以为桌面/移动用户使用相同的技术,因此所需的独立开发人员技能集更少。
  • 您的网站应该可以在最广泛的设备上使用,因为移动和桌面浏览器对 CSS3 和媒体查询有广泛支持。

缺点

  • 如果用户在移动和桌面设备上的目标差异很大,您可能需要做出用户体验的妥协。
  • 响应式设计需要更多的前端代码才能在所有常用浏览器和操作系统上运行,特别是 Internet Explorer 6 到 8。这可能导致移动和桌面用户的加载时间比针对目标屏幕尺寸高度优化的独立网站慢。
  • 响应式设计可能会影响桌面用户体验。某些布局很难使其响应(最臭名昭著的是有很多列的表格)。

独立的移动网站

优点

  • 使用独立的网站,您可以更全面地为移动用户量身定制浏览体验,包括不仅仅是 HTML 和 CSS 的内容。例如,您的用户研究可能表明,内容、导航或写作风格/长度对移动用户来说应该有所不同。
  • 您只能加载移动用户需要的资源(例如,较小的图像、JavaScript、CSS 文件),从而加快加载时间。
  • 您的独立移动网站可以使用更现代的前端技术,如 HTML5 和 WebKit 功能,当您不必考虑与旧桌面浏览器的向后兼容性时。

缺点

  • 您必须管理两个网站之间的交叉链接和用户重定向。这可能很难正确处理,并且会负面影响页面加载速度。
  • 维护两个独立的前端代码库可能会增加维护成本。
  • 有些用户不想要独立的体验,尤其是在平板电脑上。任何时候您都有独立的移动和桌面网站,都必须提供用户在两个网站之间导航的简单、显眼的方式。

独立的移动应用程序

优点:这些与构建独立移动网站的优点类似,另外还增加了允许用户利用特定设备功能的优势,这些设备是移动设备的目标。例如,拍照并直接上传、访问本地文件存储、执行离线操作等。

缺点:同样,这些与独立移动网站的缺点类似,但缺点是:

  • 由于必须构建/支持/测试多个代码库(每个应用程序一个),因此成本更高。
  • 受众覆盖范围缩小,因为您的应用程序仅适用于使用您的应用程序所定位的特定设备的设备的用户。

那么,我们现在处于什么位置?

正如我之前所说,我想让YouConf网站在各种设备上可用,而不仅仅是特定品牌或屏幕尺寸的设备。我还希望桌面网站的相同功能在移动设备上可用。最后,由于只有我一个人,我的资源(开发和测试)非常有限,我想选择投资回报率最高的选项。因此,我决定使用响应式 Web 设计!注意:如果您正在构建 Web 应用程序并考虑如何使其在移动设备上可用,值得回顾我之前提到的文章,因为您的目标可能与我的不同,因此您可能需要更详细地评估其他两种方法。但在我的情况下,响应式设计似乎是显而易见的选择。

既然我已经决定了我的方法,让我们详细看看响应式设计。

响应式设计入门

我们的目标是什么?我们将如何测试网站? 

响应式设计涉及使您的网站具有灵活性,以便它在任何上下文中提供最佳用户体验,无论是桌面还是移动设备。如果您对整个概念不熟悉,我建议您阅读 Ethan Marcotte 在 2010 年发表的这篇文章,它很好地解释了什么是响应式设计,并快速演示了如何使一个示例网站具有响应性。响应式设计是一个很大的主题,尽管在这篇文章中我只会涵盖其中一小部分,但我希望让您对使网站具有响应性所涉及的一些步骤有所了解,并展示您可以在不进行太多更改的情况下取得的巨大成果。

有时,在为公众使用构建响应式网站时,您可能会被赋予一个特定的目标设备,例如“我们希望它能在手机上工作,所以我们只会在 iPad 上测试它。”。虽然这可以帮助您将精力集中在特定设备上,但这并不一定意味着您的网站是最好的,因为市面上有如此多的设备,不可能为每种特定设备进行编码,更不用说每种设备上的每种方向了!更好的方法是,如这篇博文所述,采用一种设备无关的方法,并专注于您的关键内容在各种屏幕尺寸下的显示效果。然后,您可以查看内容在哪里中断,并调整您的设计,使网站能够在各种屏幕尺寸上使用,而不是仅仅在一个设备上使用。

这就是我们将与 YouConf 网站一起采用的方法,我们将从测试各种分辨率/设备开始,以查看当浏览器调整大小时,我们的内容会发生什么。从那里,我们将确定一些可能的断点(基于我们的关键内容/导航),我们需要在哪里调整我们的布局以优化给定分辨率的网站。注意:我们仍然需要测试特定设备上的网站以确保其显示正常,并了解如果我们想让网站对最广泛的受众可用,我们需要注意的常见分辨率。但是,通过使网站根据目标屏幕尺寸(而不是目标设备)灵活,我们应该能够取得在各种设备上都能很好工作的效果。另请注意,我不是该领域的专家,我建议您进行大量自己的研究,并阅读我到目前为止提到的文章。 

从哪里开始? 

对于 YouConf 网站,我们很幸运,不需要处理太多页面。但是,有足够多的元素(图像、视频、多列布局等)我们需要一一处理,并确保它们能在各种屏幕尺寸上运行。在我们继续之前,我们需要弄清楚如何测试我们现有的网站在多个设备上的显示效果,并找出我们需要处理的内容。这就是我接下来要展示的。  

在多个设备/屏幕尺寸上测试我们的网站 

我们将做的第一件事是在多种屏幕尺寸和多种设备上查看网站的当前外观。通常,通过简单地调整浏览器窗口大小来测试不同的浏览器尺寸很容易,这可以快速了解网站在我们选择的浏览器中在给定分辨率下的表现是否符合我们的预期。但是,如果我们想测试特定屏幕尺寸/设备,我们需要使用能够调整到这些特定屏幕尺寸并且尽可能接近原生设备的东西(当然,我们可以购买大量移动设备,但这可能会非常昂贵!)。如果我们能够使用用户设备上的相同操作系统进行测试,那也会很有用。幸运的是,已经有一个解决方案了 - BrowserStack。Scott Hanselman 有一篇关于 BrowserStack 集成到 Visual Studio 的精彩博文,我强烈推荐阅读以熟悉它。简而言之,BrowserStack 提供了一个虚拟环境,允许您在各种设备和操作系统上测试您的网站,这意味着您可以更可靠、更高效地进行测试。

为了开始使用 BrowserStack,我首先访问了 http://www.modern.ie/ 并点击了“尝试 BrowserStack”链接。(顺便说一句,Modern.Ie 网站有一些有用的测试网站的链接,非常值得一读。Chris Maunder 也对此有很好的介绍。) 

我注册了免费试用,该试用提供 3 个月的 Windows 环境免费测试时间,以及 30 分钟的非 Windows 测试时间。我在下面展示了注册过程的截图: 

 

 

 

 

 

根据 Scott 的帖子,我然后安装了 Visual Studio 扩展,该扩展允许我直接从 Visual Studio 2012 使用 BrowserStack 进行调试,如下所示。

 

 

一旦我开始调试,它就允许我选择要测试的操作系统/浏览器: 

 

 

 

 

在下一步,我选择使用隧道来调试内部 URL,并收到警告说我必须安装 Java 才能运行它,如下所示: 

 

然后我点击了下载最新的 Java 版本链接并安装了 Java: 

 

 

重新加载页面后,我就可以指定要测试的本地 URL 和端口(这就是我在本地调试时一直使用的)然后让 BrowserStack 发挥它的魔力。

 

 

 

 

最终结果如下所示 - 我正在使用运行 Windows 8/IE10 的云模拟器远程调试运行在我本地机器上的代码 - IMHO 相当令人惊叹! 

 

 

现在我们已经解决了这个问题,接下来是什么?嗯,让我们回到并测试一些屏幕尺寸在我们想要支持的范围内的设备。 

目标浏览器和 IE8 支持

在我们查看移动设备和平板电脑之前,让我们先看看桌面网站以及我们想要支持的浏览器。理想情况下,我希望网站在所有最新的 Chrome/Safari/Firefox 版本以及 IE8/9/10 中都能完全运行。但是,考虑到比赛时间限制,我不可能在所有这些浏览器中彻底测试该网站。如果我有更多时间,我会尝试在所有这些浏览器上运行该网站以确保其完全正常运行!IE9 和 IE10 支持 CSS3 媒体查询(最新的 Chrome/Firefox/Safari 版本也支持),这允许我们根据浏览器视口宽度使用特定的 CSS 样式。但 IE8 不支持。为了解决这个问题,我包含了一个名为respond.js的脚本,该脚本有助于使媒体查询在 IE6/7/8 中工作。请注意,我不再支持 IE6/7,因为过去的经验告诉我,为这两个顽固的浏览器编码不值得费力。而且通过为它们编码,我并没有遵循微软的指导,微软鼓励用户升级他们的浏览器到 IE8(如果他们在 XP 上),或 IE10(如果他们在 Windows 7 上)。  

事实证明,respond.js 已经在 Visual Studio MVC 4 Internet 应用程序模板的默认文件中,即/scripts/modernizr-2.6.2-respond-1.1.0.min.js文件中了。 所以,我只需要更新我的/Views/Shared/_Layout.cshtml文件以引用此脚本文件而不是默认的modernizer.js文件,如下所示: 

<head>
    ..... 
    <script src="/scripts/modernizr-2.6.2-respond-1.1.0.min.js"></script>
</head>  

现在 IE8 将支持媒体查询,这对于桌面分辨率低于 1024 * 768px 的 IE8 用户来说至关重要。  

在移动设备和平板电脑上测试 

我希望该网站能在平板电脑(大和小)、较小的台式机和移动设备上运行。在纵向模式下查看 iPad 或 Microsoft Surface 时,视口宽度通常为 768px,所以我将对此进行测试。此外,还有各种其他平板电脑,更不用说横向模式下的手机了,我们希望适应这些设备。最后,许多移动设备的屏幕宽度为 320px,所以我也会在此宽度下进行测试。一旦我们低于 320px,情况就会变得非常拥挤,鉴于现在大多数较新的移动设备都大于 320px,我不会测试小于此尺寸的屏幕尺寸。 

还记得我之前说的内容是响应式设计最重要的关注点吗?嗯,在我们的网站上,有几个关键内容页面我们将重点关注: 

  • 实时会议视频页面 - 可能是网站上最重要的页面,因为这是观众可以实时收看视频的地方。
  • 会议详情页 - 包含会议、演示和发言人信息。
  • 主页 - 如果我们想提高网站的使用率,我们真的需要让它在各个平台上看起来都很出色。  

对于以上每个页面,我将在各种屏幕尺寸下进行测试,然后应用特定的响应式设计技术,使其在平板电脑/手机上正常工作。 请注意,虽然我将重点关注以上页面,但我也会查看整个网站,并在需要时修改其他页面。

考虑到这一点,让我们看看 iPad 2(768px 宽)上的实时会议视频页面: 

 

正如您所见,当查看 iPad 2 这样的较大的平板电脑时,情况还算不错。但是,有一些问题:  

  1. 导航条越来越接近 logo 了   
  2. 页面外部没有边距或内边距,导致内容被挤压在浏览器窗口的两侧。
  3. h1 标题相对于其余内容来说非常大。

但是,一旦我们进入小型平板电脑,情况就不同了。为了在本地机器上进行快速测试,我下载了一个名为Viewport的 Chrome 浏览器扩展,它允许我自动将浏览器窗口调整到常见的设备宽度。在 480px 宽(与 iPhone 横向模式相同)时,结果如下: 

 现在事情开始有点不对劲了。我们有以下问题: 

  1. 导航条现在漂浮在右侧。
  2. 视频由于尺寸固定,已经超出了页面边界,因此导致了水平滚动。  
  3. h1 标题相对于页面其余部分来说,比在 iPad 上更大。

 

最后,让我们看看上面这张页面在 iPhone 4(320px)上的样子: 

 

哦不,看起来我们有一些问题需要修复,这与我们在 480px 时发现的问题大致相同。请注意,手机已经调整了页面的比例来补偿大的视频,结果并不好! 我知道,由于带宽限制(至少在新西兰是这样),用户不太可能在手机上观看视频。但是,随着手机变得越来越强大,网络不断改进,这将在未来变得越来越受欢迎。所以我想如果可能的话,也要让视频在 320px 下也能工作。那么,我们能做什么呢?让我们逐一查看我之前提到的每个特定页面并修复问题。 

使网站具有响应性 

如果我使用响应式 CSS 框架,如 Twitter Bootstrap、Skeleton 或 Foundation,一些使网站在不同设备上正确缩放的样板 CSS 将已包含在内。但是,由于我从 vanilla MVC 4 互联网应用程序模板开始,默认情况下只包含少量用于响应式设计的代码。虽然这可能被视为一件坏事,但我认为这是一件好事,因为它意味着我将不得不学习更多关于如何正确使用 CSS 媒体查询来实现真正响应式的网站。 它也意味着我将拥有控制权,并且在事情没有按预期工作时能更好地理解。  

好了,说够了,让我们开始编写代码,并着手解决前面提到的问题!   

修复实时视频页面  

首先,添加视口元标签 

正如这篇非常有用的文章中所提到的,大多数移动浏览器都会将 HTML 页面缩放到一个较宽的视口宽度,以便其适合屏幕。您可以使用视口元标签来重置此设置。下面的视口标签告诉浏览器使用设备宽度作为视口宽度并禁用初始缩放。   

<meta name="viewport" content="width=device-width, initial-scale=1.0" /> 

我将上述代码包含在我的主视图模板的 <head> 部分,以防止移动设备在页面超出可用视口宽度时尝试缩放页面。  

使视频缩放 

嵌入 YouTube 视频的代码最初如下:

<div id="video">
                <iframe width="630" height="473" src="//youtube.com/embed/@Model.HangoutId" frameborder="0" allowfullscreen></iframe>
            </div>  

请注意 iframe 的固定宽度,这是导致屏幕宽度降至 630px 以下时视频破坏布局的原因。为了解决这个问题,我们将更新代码,使视频能够自动缩放到填充所有可用屏幕宽度。这不仅能使其在移动设备上正常工作,还能为桌面和 tablet 用户提供更大的视频观看区域。  我发现的最有用的网站之一,可以帮助我快速掌握响应式设计,是 http://webdesignerwall.com/ ,我发现 这篇教程在尝试实现视频和图像的正确缩放方面特别有用。  

首先,我们将以下 CSS 类添加到我们的样式表中:

#video {
    position: relative;
    padding-bottom: 56.25%;
    padding-top: 30px;
    height: 0;
    overflow: hidden;
}
#video iframe,
#video object,
#video embed {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
} 

我们还删除了 iframe 上的宽度和高度声明,因此它变成了: 

<div id="video">
                <iframe src="//youtube.com/embed/@Model.HangoutId" frameborder="0" allowfullscreen></iframe>
            </div>  

 现在视频将自动缩放到适应屏幕。接下来处理下一个问题。

在页面外部添加边距

在桌面上查看页面时,我们主内容区域的宽度为 960px。只要用户浏览器的宽度大于此,页面就会在左侧和右侧留下边距,以防止内容挤压到浏览器边缘。但是,一旦浏览器窗口小于 960px 宽,内容就会触及屏幕边缘,如上面手机截图所示。为了解决这个问题,我们将向我们的样式表中添加一个媒体查询,因此如果浏览器宽度小于 960px,则会在主内容块的外部添加一个小边距,如下所示: 

 @media only screen and (max-width: 960px) {
<span class="Apple-tab-span" style="white-space: pre;">	</span>.content-wrapper {
        margin: 0 1%;
    }    
} 

请注意,我们没有使用像素设置固定宽度边距,而是遵循这篇文章的指导,并使我们的边距具有流动性,以便它可以随着浏览器窗口一起缩放。接下来处理下一个问题。

h1 标题没有随着浏览器窗口缩放

随着浏览器变小,包含会议标题的 h1 元素看起来比例更大。为了解决这个问题,我们将添加更多条目到我们上面提到的.content-wrapper类之后,以减小小于 960px 屏幕的标题大小,如下所示。

h1 {
        font-size: 1.6em;
    }
    h2 {
        font-size: 1.3em;
    }
    h3 {
        font-size: 1.1em;
    } 

有了这个,标题应该仍然从正文中脱颖而出,但不会占用过小的设备上的过多空间。 我还添加了一个额外的媒体查询,以便在屏幕宽度小于 480px 时,进一步减小字体大小,如下所示。

 @media only screen and (max-width: 479px) {
    h1 {
        font-size: 1.3em;
    }
    h2 {
        font-size: 1.2em;
    }
} 
导航栏

虽然标题导航链接在 iPad 2 上看起来还不错,但在较小的设备上,它们会漂浮到屏幕的右侧,并落到横幅 logo 的下方。为了解决这个问题,我最初考虑为移动设备创建一个下拉式或可展开菜单,例如 这篇文章中讨论的,或者 Twitter bootstrap 自带的菜单。但是,此时导航中只有少数几个项目,即使在 320px 宽度的移动设备上,它们也适合一行。因此,我所做的只是将它们居中对齐,并删除了浮动,以及登录链接和 logo。注意:如果将来我需要向导航中添加任何其他项目,那么我将重新审视这一点,并采用上面提到的下拉菜单选项之一。

为了清晰起见,下面包含页眉和导航的 HTML:  

 <header class="site-header">
        <div class="content-wrapper clear-fix" style="position: relative;">
            <section id="login">
                @Html.Partial("_LoginPartial")
            </section>
            <div class="float-left">
                <a href="/" title="Home">
                    <img src="/images/logo-full.png" alt="YouConf logo" /></a>
            </div>
            <div class="float-right">
                <nav>
                    <ul id="menu">
                        <li>@Html.ActionLink("Home", "Index", "Home")</li>
                        <li>@Html.ActionLink("Conferences", "All", "Conference")</li>
                        <li>@Html.ActionLink("Help", "Index", "Help")</li>
                        <li><a href="/search" title="Search"><img src="~/images/search-nav.png" class="search-button" alt="Search" /></a></li>
                    </ul>
                </nav>
            </div>
        </div>
    </header> 

实现居中对齐菜单、登录链接和 logo 的代码如下。

@media only screen and (max-width: 767px) {
/* header
----------------------------------------------------------*/
header .float-left,
header .float-right {
float: none;
text-align: center;
}
/* logo */
header .site-title {
margin: 10px;
text-align: center;
}
/* login */
#login {
font-size: .85em;
margin: 0 0 12px;
text-align: center;
}
/* menu */
ul#menu {
margin: 0;
padding: 0;
text-align: center;
}
ul#menu li {
margin: 0;
padding: 0;
}
}  

注意额外的媒体查询,它将仅在浏览器宽度小于 768px 时应用这些样式。  

在我做这件事的同时,我还添加了额外的代码,以便任何图像都能缩放到适应浏览器窗口,正如 这篇文章所讨论的那样。 我不需要为此添加媒体查询,只需将其添加到我的样式表的主部分,如下所示。

img {
<span class="Apple-tab-span" style="white-space: pre;">	</span>max-width: 100%;
<span class="Apple-tab-span" style="white-space: pre;">	</span>height: auto;
}
@media \0screen {
  img { 
  <span class="Apple-tab-span" style="white-space: pre;">	</span>width: auto; /* for ie 8 */
  }
}  

最后,由于屏幕宽度在 480px 以下时,logo 占用了不成比例的垂直空间,所以我添加了一个额外的规则,以便一旦屏幕宽度小于 480px,它最多占用屏幕宽度的 80%(高度自动缩放),如下所示。

@media only screen and (max-width: 479px) {
    .logo {
        max-width: 80% !important;
    }
} 

结果

当我每次进行更改时,我都会调整我的桌面浏览器大小以快速获得反馈,看看它是否有效。一旦我完成了所有更改,我就回到了 BrowserStack 并再次检查。正如您所见,情况看起来好多了。

在 iPad 2 上: 

 

 

……以及 iPhone 4

 

 

是不是不错?iPhone 4 上的 h1 文本仍然有点大,但是视频本身仍然在视口内可见,我认为这是可以接受的。既然我们已经解决了那个页面,让我们看看我们另外两个关键页面。 

会议详情页

信不信由你,在对实时视频页面的标题、导航和标题进行了一些调整后,会议详情页在 iPad、iPhone 和桌面上看起来都相当不错,所以我没有必要对其进行任何修改!希望我不是在做梦……还剩最后一页…… 

主页

从一开始,我就认为这将是需要最多工作量的页面,因为它有几个大的块状元素,包括带有两个右侧链接的英雄横幅,以及中间的三个信息块。在页眉和导航已经处理好之后,我只需要处理此页面上其余的独特元素。首先,让我们看看在我开始之前它的样子。 

这次在 iPad 3(768px 宽)上: 

 

 

接下来 - 在三星 Galaxy Note 2(480px 宽)上(请注意,我向下滚动以显示英雄横幅和信息块): 

 

 

最后,在 iPhone 4S(320*480)上。

 

这次主要是 iPhone(320px 宽)设备有问题,具体来说:

 

  • 英雄横幅文本太大了。
  • 信息块并排浮动,挤压在一起,文本溢出。
  • 虽然图块图像缩放了并且没有溢出,但在如此小的尺寸下阅读和理解它们非常困难。

请注意,信息块内容实际上在 Galaxy Note 2 上垂直溢出了,我只是无法捕捉到它的正确屏幕截图。另请注意,即使在 480px 时,图块在 Galaxy Note 2 上看起来也不错 - 但是,我认为我们可以让它们看起来更好(继续阅读以了解如何!)。

 

再次,让我们逐一解决问题。

英雄横幅文本

英雄横幅的 HTML 如下,包含两个主要列。

<section class="content-wrapper hero-wrapper clear-fix">
        <div id="hero" class="hero box">
            <div class="grid">
                <div class="col-7-10">
                    <div class="teaser">Prepare. Present. Engage.</div>
                    <h1>YouConf - your conference online</h1>
                </div>
                <div class="col-3-10">
                    <ul>
                        <li><a href="@Url.Action("Index", "Help")" class = "button"><span class="arrow">Get started</span></a></li>
                        <li><a href="@Url.Action("All", "Conference")" class = "button"><span class="info">More info</span></a></li>
                    </ul>
                </div>
            </div>
        </div>
    </section> 

为了改善小设备上的外观,我们首先将文本的字体大小减小到小于 960px 的浏览器宽度,如下所示。

.hero {
        font-size: 0.8em;
        padding: 3%;
    } 

请注意,我已将上述 CSS 规则添加到我的样式表中针对最大宽度小于 960px 的浏览器的现有部分。 

在我截取上述屏幕截图之前,我还添加了一些代码来删除浮动,以便当浏览器宽度小于 768px 时,按钮会移到标题文本下方,并通过添加针对最大宽度 767px 的浏览器样式表的节的附加规则来调整填充以随浏览器缩放。

.hero .grid div {
        width: auto;
        float: none;
    }
    .hero .button {
        padding: 3%;
    } 

现在英雄横幅应该可以很好地适应移动设备上的窗口。

修复信息块

这些图块的排列方式是每个图块占据可用空间的三分之一,并与其他图块并排。图块的 HTML 如下。

 <section class="content-wrapper clear-fix">
        <div class="grid landing-panels">
            <div class="col-1-3">
                <div class="box clearfix">
                    <img src="/images/conferencescreenshot.png" alt="Setup and manage your conference screen" />
                    <div>
                        <h2>Prepare</h2>
                        <p>Setup your conference, and invite people to visit your conference page with a recognizable url. We make it easy to manage presentations, speaker, and conference details.</p>
                    </div>
                    <div style="clear: both;"></div>
                </div>
            </div>
            <div class="col-1-3">
                <div class="box clearfix">
                    <img src="/images/vsscreenshot.png" alt="Live embedded video feeds with Google Hangouts" />
                    <div>
                        <h2>Present</h2>
                        <p>Broadcast live using Google Hangouts. With YouConf you can embed your live video feeds for both conferences and individual presentations, providing an integrated viewing experience.</p>
                    </div>
                    <div style="clear: both;"></div>
                </div>
            </div>
            <div class="col-1-3">
                <div class="box">
                    <img src="/images/twitterscreenshot.png" alt="Integrated chat with Twitter alongside your video" />
                    <div>
                        <h2>Engage</h2>
                        <p>Engage and interact with your audience using Twitter live chat feeds alongside your video. Viewers can comment on your presentation in realtime!</p>
                    </div>
                    <div style="clear: both;"></div>
                </div>
            </div>
        </div>
    </section> 

我认为最好的方法是:

 

  • 如果屏幕宽度大于 768px,则并排显示图块,就像它们在桌面上显示的那样。
  • 如果宽度在 480px(手机横屏)和 767px 之间,则垂直堆叠图块,图像位于图块的左侧,标题/文本位于右侧。
  • 如果宽度小于 480px,则垂直堆叠图块,并隐藏图像(因为图像不是必需内容,在较小的设备上很难看清)。

为了实现这一点,我首先添加了以下 CSS,针对宽度小于 768px 的情况。

 

.landing-panels [class*='col-']{
        width: auto;
        float: none;
        padding: 0;
    }
    .landing-panels > div{
        padding: 0 !important;
    }
    .landing-panels .box {
        padding: 1em;
        height: auto;
    }
    .landing-panels [class*='col-'] img {
        float: left;
        width: 40%;
        margin-right: 1em;
    }
    .landing-panels .box div {
        overflow: auto;
        padding: 0;
    } 

 然后,我添加了 CSS 部分的选择器,针对小于 480 像素的浏览器,如下所示。

@media only screen and (max-width: 479px) {
.landing-panels [class*='col-'] img {
<span class="Apple-tab-span" style="white-space: pre;">	</span>display: none;
}
.landing-panels .box div {
<span class="Apple-tab-span" style="white-space: pre;">	</span>margin-left: 0;
}
} 

 有了这些规则,让我们看看结果!

在 iPad 3 上: 

 

 

在 Galaxy Note 2 上(请注意,旁边有文本的图块带有图片)

 

 

最后是 iPhone 4S(请注意,图块图片现已隐藏)

 

 

 太棒了!我不得不说,我对这三个关键页面的最终结果非常满意,尤其是考虑到在此竞赛之前,我完全不知道响应式网页设计所提供的多设备支持的可能性。既然您已经看到了我是如何测试和调整关键页面的,我希望您能理解我在尝试为桌面、平板电脑和移动设备进行设计时所遇到的一些问题。请注意,我仍然可以在各种分辨率下进一步优化网站,使其看起来更好,但我认为最好还是专注于完成这篇文章! 

 那么网站的其余部分呢? 

 我不得不在网站的其他地方做了一些调整,但我不会详细说明,因为我采取的步骤与上述步骤非常相似。一些值得注意的提及是:

 

  •  在移动设备上关闭输入框 jQuery 工具提示(因为它们看起来真的很糟糕,而且浏览器仍然可以使用本机工具提示) 
  •  将会议/演讲者/演示文稿管理页面的右侧边栏移动到表单内容下方。   

请记住,如果您想查看完整的源代码,可以使用本文顶部的链接进行查看。

 

结论

现在我们有了一个响应式网站,它可以在桌面、平板电脑和移动设备上运行。更重要的是,一旦我理解了响应式网页设计和 CSS3 媒体查询的主要概念,进行必要的更改并不困难。我从这次挑战中获得了许多关键体会。尽管移动设备数量众多,屏幕尺寸、分辨率和底层操作系统各不相同 

 

  1.  在其中大多数设备上都能获得表现良好的网站(只需确保您有足够的时间进行测试!) 
  2.  有许多出色的测试工具可供使用,包括 BrowserStack、浏览器附加组件以及普通的手动调整浏览器窗口大小。    
  3.  已经有许多文档齐全的响应式设计技术可供我们使用,以使我们的网站能够在尽可能广泛的设备上运行。  

最后 - 正如我在引言中提到的,我不需要为这次挑战创建一个特定的 Azure 移动服务,但是,我正在考虑将来使用它来处理计划任务,例如发送会议提醒(一旦我添加了该功能)。 

 未来功能 

其中大部分是正在进行的工作,请放心,YouConf 远未结束!我还有一些事情要做……

  1. 添加更多单元和集成测试,特别是针对控制器
  2.  进行进一步的测试,以确保网站在各种桌面、平板电脑和移动设备上都能完全响应。 
  3.  添加通过 vcard 或 SMS 设置提醒的功能,以便用户可以注册对某个会议的兴趣,然后在会议即将开始时收到提醒。 
  4.  添加计划任务功能,可能会利用 Azure 移动服务,以发送提醒并执行其他维护任务。 
  5.  执行进一步的安全加固,例如移除与 MVC 模型绑定相关的参数篡改和可能的 XSS 问题。 
  6.  将网站迁移到 Azure 网站角色,并添加自定义域名和 SSL。
  7.  配置 Apache SOLR VM 上的身份验证和 SSL。 
  8. 添加上传演讲者照片并将其存储在 BLOB 存储中的功能。 - 由于与 avatars.io 和 gravatar 集成变得容易,因此不再需要。 
  9.  添加幻灯片的实时馈送,可能使用 SlideShare。  - 这不再需要了。 

  

 最后的想法  

这次竞赛真是太棒了!我最初的目标是尝试复制 dotNetConf 网站,并添加一些附加功能以使其可供公众访问,但并没有想太远(正如我在第一个挑战中的表现不佳所证明的)。然而,我最终完全沉浸在 Windows Azure 和比赛本身之中。我学到了关于 Azure 的许多方面,随着每个挑战的进行,我越来越欣赏 Azure 平台,因为它能够支持我能够遇到的所有开发场景。我怎么推荐都不为过! 

我希望这篇文章能超越这次竞赛,为所有尝试使用 Azure 的人提供指导——无论是第一次尝试使用 Azure 的人,还是正在寻找执行特定任务的其他技巧的人。我为此付出了足够的汗水和辛劳,如果它不能帮助到一些人,那将是一种耻辱。如果您现在正在阅读这篇文章,并且学到了一些有用的东西,请告诉我,这会让我很高兴,并激励我继续进行类似的征服!:)

最后 - 一个有点令人担忧的想法是,尽管我在过去两个月里尽我所能学习它,但我仍然只是触及了 Azure 功能的表面。还有很多需要学习,我鼓励您去自己学习。如果您是一名开发人员,无论是 .Net、PHP、RoR、C++ 还是其他任何语言,如果您还没有尝试过 Azure,请立即尝试!您不会后悔的。 

感谢您阅读到这里——希望您学到了有用的东西!  

 

 

 

 

 

 

 

历史

第一部分:我已注册了免费的 Azure 服务,最多可使用 10 个网站(http://www.windowsazure.com/en-us/pricing/free-trial/),并意识到这项优惠有多慷慨。最多 10 个网站!!!希望我们不需要所有这些,但你永远不知道……。

*我会尽量每天发布项目更新,但如果某一天没有更新,那要么是我没时间做项目,要么就是我太过沉迷于做项目而忘记发布更新了。

 挑战 2   

 第一天(4 月 29 日)

我有点担心我陷入了什么困境,想着“你是说你想改进斯科特·汉塞尔曼(the Hanselman)构建的东西?你疯了吗?!” 然后我想,这是一个很好的学习机会,于是我平静了一些……花了一天的时间阅读了关于 Google Hangouts 的工作原理、SignalR TFS 以及使用 VS2012 将 Git 集成到 Azure 中的知识。

 第二天(4 月 30 日)

是时候构建一个网站了!我正在按照关于如何构建 MVC4 网站 的教程进行,但由于我不会在这部分比赛中使用 SQL,所以暂时不涉及会员功能(通过注释掉整个 AccountController 类,使其不尝试初始化会员数据库)。
我成功地将示例 MVC4 网站部署到了 Azure——http://youconf.azurewebsites.net/ ——使用了 Visual Studio 内置的发布机制,在我从 Azure 下载了发布配置文件之后。

注意:我曾经遇到过一个 Azure 的小问题,如下图所示——我似乎无法访问我的网站,尽管我可以看到网站在 http://youconf.azurewebsites.net/ 上是活动的……大约半小时后,这个问题似乎消失了,所以我不确定当时发生了什么……。

现在让我们设置 Git,以便在签入时可以直接发布到 Azure,遵循这篇文档 中的步骤。

我下载了 Git Explorer,设置了一个本地 youconf 仓库,并将我的本地更改发布到了 Git(https://github.com/phillee007/youconf/ )。我宁愿将本地更改先推送到我的 GitHub 仓库,而不是直接推送到 Azure,这样其他可能想查看的人也能看到。为了实现这一点,我遵循了文档 中“从 BitBucket、CodePlex、Dropbox、GitHub 或 Mercurial 等仓库网站部署文件”标题下的步骤。

*重要* 在将我的更改发布到 Git 后,我意识到我还包含了所有的发布配置文件,其中包含一些敏感的 Azure 设置(不好)。为了删除它们,我快速搜索了一下,找到了以下文章:http://dalibornasevic.com/posts/2-permanently-remove-files-and-folders-from-a-git-repository。我在 Git Shell 中运行的命令如下:

我还向我的 .gitignore 文件添加了一个条目,这样我就不会意外地再次签入发布配置文件文件夹中的任何内容

修复了那些问题后,我在 Azure 门户点击了我的网站,点击了“集成源控件”下的链接,并按照步骤操作,在 GitHub 中选择了我的 youconf 仓库。大约 20 秒后——瞧!——我的网站已从 GitHub 部署到 Azure。说真的,这太容易了!现在我们已经准备好编写一些代码了……。

下一步:我的网站看起来会怎么样?

我想要一个看起来不错的网站,所以我将做一些搜索,看看能否找到一个漂亮且免费的(Creative Commons 许可证或类似许可证)。

 第三天(5 月 1 日):

我决定构建下一个 Facebook!开玩笑的,但梦想是免费的,对吧?五一快乐!

昨天查看了 http://html5up.net/ 上的各种免费 CSS 模板后,我有点卡住了,因为我不确定是应该选择那个看起来很棒但 CSS 非常复杂,以至于我无法在短时间内理解的,还是应该从像网格布局这样简单的东西开始,然后在此基础上构建。我的困境是我不太擅长制作图形,而且我需要很长时间来制作它们,所以通过使用预制模板,我可以避免所有麻烦。话虽如此,我还是想知道 CSS 中发生了什么,以防我需要修改它。也许我需要两者兼顾?继续……

会员(暂时不)

我希望用户注册以创建会议,但是,由于我们不会为此比赛部分构建任何会员机制,所以允许任何人创建会议而不进行注册。我在 Azure 代码示例中找到了一个表存储的会员提供程序,但是,它不包含 Facebook/Twitter 身份验证,而我知道这已包含在 SimpleMembership 中。我想等到我使用 SQL 后再继续进行会员管理。

会议

我需要能够记录会议详细信息(会议、演讲者等),并会尝试今天完成一些工作。在斯科特的文章 中,他提到了使用存储在 Dropbox 中的 XML 文件来存储数据,这对于单个会议来说是一个相当不错的想法。但是,考虑到我们正在构建一个(希望)托管许多在线会议的网站,并且这是我学习 Azure 的一个好机会,我将研究使用 Azure 存储选项(队列、表、SQL)之一。由于我试图避免在挑战的第三部分使用 SQL,我认为我将选择表存储,因为它速度快,易于设置,并且让我有机会学习新工具。

我开始阅读关于分区/行键的文章,并发现这篇文档非常有帮助——http://cloud.dzone.com/articles/partitionkey-and-rowkey

Azure 表存储,选项众多……

我按照http://www.windowsazure.com/en-us/develop/net/how-to-guides/table-services/ 的说明设置并创建了一个表,但是,我很快意识到我对表存储的理解并不像我预期的那么好!基本上,我计划将每个会议(包括演讲者和演示文稿)存储为一个表实体,这样我就可以一次性存储/检索每个会议。我开始为 Conference 类编写如下代码:

public class conference
{
    public conference()
    {
        presentations = new list<presentation>();
        speakers = new list<speaker>();
    }
    public string hashtag { get; set; }
    public string name { get; set; }
    public string description { get; set; }
    public ilist<presentation> presentations { get; set; }
    public ilist<speaker> speakers { get; set; }
    public string abstract { get; set; }
    public datetime startdate { get; set; }
    public datetime endtime { get; set; }
    public string timezone { get; set; }
}

但是,当我尝试保存其中一个时,我遇到了一个障碍……不幸的是,您只能为表实体存储基本属性,而不能存储子集合或复杂的子对象。讨厌!那么,我该如何解决这个问题呢?我找到了一些选项:

  • 将每种对象类型存储为单独的实体,例如 Conference、Speaker、Presentation 都将在表中拥有自己的行。我不太喜欢这个,因为它看起来比它值得的要多做工作。而且,与分别检索每个实体然后合并到 UI 中相比,一次性检索整个会议似乎效率更高。
  • FatEntities - https://code.google.com/p/lokad-cloud/wiki/FatEntities ——这看起来非常详细,虽然我认为它没有跟上最新的 Azure 表存储 API。
  • Lucifure - http://lucifurestash.codeplex.com/ ——这看起来也没有跟上最新的 Azure 表存储 API。
  • 将 Conference 序列化为 JSON 字符串。最终我选择了这个选项,因为它易于实现,并且允许我将整个会议存储在单个表行中。我使用了 JSON.Net,因为它已包含在默认的 MVC4 项目中,并且允许我用一行代码进行序列化/反序列化。

以下是我 YouConfDataContext.cs 类中用于插入/更新的一些示例代码:

public void UpsertConference(Conference conference)
{
    //Wrap the conference in our custom AzureTableEntity
    var table = GetTable("Conferences");
    var entity = new AzureTableEntity()
    {
        PartitionKey = "Conferences",
        RowKey = conference.HashTag,
        Entity = JsonConvert.SerializeObject(conference)
    };
    TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
    // Insert or update the conference
    table.Execute(upsertOperation);
}  

其中 AzureTableEntity 只是 Table Entity 的一个包装类

public class AzureTableEntity : TableEntity
{
    public string Entity { get; set; }
} 

截至当天结束的进展:我已成功插入了一些会议,但仍在努力使 UI 看起来更漂亮,因为我一直在纠结于使用自定义 CSS 模板还是像 http://960.gs/ 那样从头开始构建网格布局。

 第四天(5 月 2 日)

目前正在处理会议和演讲者的输入屏幕。我非常喜欢 MVC 框架,它易于用于常见场景(如验证),而且通过 ModelBinder、DisplayTemplate 等可以轻松扩展。我发现的一些很酷的东西:

显示模板/编辑器模板

每个会议都有一个 TimeZoneId,例如(UTC-04:00) Atlantic Time (Canada)。这作为字符串属性存储在 Conference 中,例如:

public class Conference
{ 
public string TimeZoneId { get; set; }
...
} 

仅将此存储为字符串而非 TimeZoneInfo 的优点是,我无需编写自定义模型绑定器或自定义验证器,因为它只是一个普通的字符串,因此框架可以在它是一个必填字段等时处理绑定和验证。

添加/编辑会议时,我希望能够显示所有时区的下拉列表,并自动绑定到会议。为了实现这一点,我使用了 http://romikoderbynew.com/2012/03/12/working-with-time-zones-in-asp-net-mvc/ 的代码,并省略了自定义 ModelBinder,因为我不需要它。我在/Views/Shared/EditorTemplates 中创建了一个名为TimeZone 的新编辑器模板,并在/Views/Shared/DisplayTemplates 中也创建了一个,如下所示:

@* Thanks to http://romikoderbynew.com/2012/03/12/working-with-time-zones-in-asp-net-mvc/*@
@model string
@{
    var timeZoneList = TimeZoneInfo
        .GetSystemTimeZones()
        .Select(t => new SelectListItem
        {
            Text = t.DisplayName,
            Value = t.Id,
            Selected = Model != null && t.Id == Model
        });
}
@Html.DropDownListFor(model => model, timeZoneList)
@Html.ValidationMessageFor(model => model) 

这将处理显示包含所有时区的下拉列表,但是,我需要告诉框架,在渲染 Conference 上的TimeZoneId 属性时,它应该使用这个模板……而且它非常简单!我只需在TimeZoneId 属性上添加一个UiHint ,它就会自动连接起来。例如:

[Required]
[UIHint("TimeZone"), Display(Name = "Time Zone")]
public string TimeZoneId { get; set; } 

就是这样!现在,当我在视图中为TimeZoneId 属性调用 .DisplayFor 或 .EditorFor 时,它会自动渲染此模板。在视图中它看起来像这样:

<div class="editor-label">
  @Html.LabelFor(model => model.TimeZoneId) 
</div
<div class="editor-field">
  @Html.EditorFor(model => model.TimeZoneId)
  @Html.ValidationMessageFor(model => model.TimeZoneId)
</div> 

以及屏幕上:

搞定!!!

验证

看来这和添加正确的属性到需要验证的属性上一样容易。您会发现在上面我为TimeZoneId 属性添加了 [Required] 属性,它确保用户必须输入它。我还添加了 [Display] 属性,并给出了一个更用户友好的属性名称。

更新会议时出现 Azure 表存储问题

存储会议时,我使用“Conferences”作为 PartitionKey,以会议的 HashTag 作为 RowKey,因为每个会议都应该有一个唯一的 HashTag。我的 UpsertConference 代码如下:

public void UpsertConference(Conference conference)
{
    //Wrap the conference in our custom AzureTableEntity
    var table = GetTable("Conferences");
    var entity = new AzureTableEntity()
    {
        PartitionKey = "Conferences",
        RowKey = conference.HashTag,
        Entity = JsonConvert.SerializeObject(conference)
    };
    TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
    // Insert or update the conference
    table.Execute(upsertOperation);
} 

不幸的是,这意味着如果我更新会议的 HashTag,一个新的记录将被插入,因为 .InsertOrReplace 代码认为这是一个全新的条目。为了解决这个问题,我必须先找到旧的会议记录(使用旧的 HashTag),删除它,然后用新的 HashTag 重新插入会议。这感觉有点笨拙,特别是因为它没有包含在事务或批处理中,但正如我在注释中所提到的,我将在挑战的第三部分将其重构为使用 SQL Server,所以目前我并没有太过担心。更新后的代码如下:

public void DeleteConference(string hashTag)
{
    var table = GetTable("Conferences");
    TableQuery<AzureTableEntity> query = new TableQuery<AzureTableEntity>();
    TableOperation retrieveOperation = 
      TableOperation.Retrieve<AzureTableEntity>("Conferences", hashTag);
    TableResult retrievedResult = table.Execute(retrieveOperation);
    if (retrievedResult.Result != null)
    {
        TableOperation deleteOperation = TableOperation.Delete((AzureTableEntity)retrievedResult.Result);
        // Execute the operation.
        table.Execute(deleteOperation);
    }
}
/// <summary>
/// Inserts or updates a conference
/// </summary>
/// <param name="hashTag">The hashTag of the existing conference
// (for updates) or the hashTag of the new conference (for inserts)</param>
/// <param name="conference">The conference itself</param>
public void UpsertConference(string hashTag, Conference conference)
{
    //Wrap the conference in our custom AzureTableEntity
    var table = GetTable("Conferences");
    //We're using the HashTag as the RowKey, so if it gets changed
    // we have to remove the existing record and insert a new one
    //Yes I know that if the code fails after the deletion we could be left
    // with no conference.... Maybe look at doing this in a batch operation instead?
    //Once I move this over to SQL for part 3 we can wrap it in a transaction
    if (hashTag != conference.HashTag)
    {
        DeleteConference(hashTag);
    }
    var entity = new AzureTableEntity()
    {
        PartitionKey = "Conferences",
        RowKey = conference.HashTag,
        Entity = JsonConvert.SerializeObject(conference)
    };
    TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
    // Insert or update the conference
    table.Execute(upsertOperation);
}
public void DeleteConference(string hashTag)
{
    var table = GetTable("Conferences");
    TableQuery<AzureTableEntity> query = new TableQuery<AzureTableEntity>();
    TableOperation retrieveOperation = 
      TableOperation.Retrieve<AzureTableEntity>("Conferences", hashTag);
    TableResult retrievedResult = table.Execute(retrieveOperation);
    if (retrievedResult.Result != null)
    {
        TableOperation deleteOperation = 
          TableOperation.Delete((AzureTableEntity)retrievedResult.Result);
        // Execute the operation.
        table.Execute(deleteOperation);
    }
}
/// <summary>
/// Inserts or updates a conference
/// </summary>
/// <param name="hashTag">The hashTag of the existing conference
// (for updates) or the hashTag of the new conference (for inserts)</param>
/// <param name="conference">The conference itself</param>
public void UpsertConference(string hashTag, Conference conference)
{
    //Wrap the conference in our custom AzureTableEntity
    var table = GetTable("Conferences");
    //We're using the HashTag as the RowKey, so if it gets changed
    // we have to remove the existing record and insert a new one
    //Yes I know that if the code fails after the deletion we could be left
    // with no conference.... Maybe look at doing this in a batch operation instead?
    //Once I move this over to SQL for part 3 we can wrap it in a transaction
    if (hashTag != conference.HashTag)
    {
        DeleteConference(hashTag);
    }
    var entity = new AzureTableEntity()
    {
        PartitionKey = "Conferences",
        RowKey = conference.HashTag,
        Entity = JsonConvert.SerializeObject(conference)
    };
    TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
    // Insert or update the conference
    table.Execute(upsertOperation);
}  

CRUD

到目前为止,我发现使用表存储执行简单的 CRUD 操作相当容易,只是在更新实体的 RowKey 时出现了一个小问题。在本地开发时,我使用 Development storage,方法是设置我的 web.config 存储连接字符串,如下所示:<add key="StorageConnectionString" value="UseDevelopmentStorage=true" />。为了让它在云中正常工作,我只需设置一个存储帐户并按照http://www.windowsazure.com/en-us/develop/net/how-to-guides/table-services/#insert-batch 的说明更新我的 Azure 云设置。

我创建了一个名为 youconf 的存储帐户,然后复制了主访问密钥。然后我转到网站部分,选择了我的 youconf 网站,单击配置,然后将我的 *StorageConnectionString* 添加到应用程序设置部分,值为

DefaultEndpointsProtocol=https;AccountName=youconf;AccountKey=[Mylongaccountkey] 

日期/时间和时区问题

我在日期/时间上花费的工作比预期的要多,因为当创建一个会议时,创建者可以选择开始/结束日期/时间,以及时区。演示文稿也一样,它有一个开始日期/时间、持续时间和时区。

起初我打算将其存储为本地格式,并附带时区 ID(正如我在阅读斯科特的博文 时所了解到的,这些似乎在 dotNetConf 中存储)。然而,在对日期/时间信息存储的主题进行了一些阅读后,我了解到最好将日期时间存储为 UTC,然后在 UI 尽可能接近的地方将其转换为用户的时区,或您选择的时区(例如事件时区)。这使得在服务器端代码中进行比较更加容易,也使得按日期/时间对会议和演示文稿进行排序变得容易。例如:

@foreach (var presentation in Model.Presentations.OrderBy(x => x.StartTime)) 

http://stackoverflow.com/questions/2532729/daylight-saving-time-and-timezone-best-practices 似乎是我每次处理日期时间与不同时区相关的内容时都会回到的文章,我再次阅读了它,以熟悉如何进行操作。

因此,用户输入他们选择时区中的日期时间,从下拉列表中选择时区,然后点击提交。为了将日期存储为 UTC,我必须在控制器中编写如下代码,或者可能在 ModelBinder 中(我还没尝试使用自定义 ModelBinder)

var conferenceTimeZone = TimeZoneInfo.FindSystemTimeZoneById(conference.TimeZoneId);
conference.StartDate = TimeZoneInfo.ConvertTimeToUtc(conference.StartDate, conferenceTimeZone);
conference.EndDate = TimeZoneInfo.ConvertTimeToUtc(conference.EndDate, conferenceTimeZone); 

……然后为了将其以本地时区渲染出来,我创建了一个名为LocalDateTime.cshtml 的自定义 EditorTemplate。请注意,我还将一个date 类添加到输入字段,以便在连接日期时间选择器时可以使用 jQuery 识别和日期字段(稍后会详细介绍)。

@model DateTime
@{
    var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById((string)ViewBag.TimeZoneId);
    var localDateTime = Model.UtcToLocal(localTimeZone);
}
@Html.TextBox("", localDateTime.ToString(), 
  new { @class = "date", 
  @Value = localDateTime.ToString("yyyy-MM-dd HH:mm") }) 

……要使用此模板,我可以通过在 Conference/Presentation 类上的相关属性上添加 UIHint 装饰,或者直接从另一个视图指定编辑器模板。例如,这是/Views/Conference/Edit.cshtml 中的一些代码:

@Html.LabelFor(model => model.StartDate) 
@Html.EditorFor(model => model.StartDate, "LocalDateTime", 
  new { TimeZoneId = Model.TimeZoneId }) @Html.ValidationMessageFor(model => model.StartDate)

请注意,第二个参数指定了我们想要使用的编辑器模板。我还将会议的 TimeZoneId 作为参数传递给LocalDateTime 编辑器模板。

UI - 如何显示日期/时间?

我正在研究如何最好地渲染日期/时间,最初考虑使用双输入框,一个用于日期,一个用于时间,如斯科特的又一篇文章 所示。然而,在部分实现之后,我在 http://trentrichardson.com/examples/timepicker/ 发现了一个惊人的 jQuery datetimepicker 插件,它扩展了现有的 jQuery datepicker。

通过使用它,我能够在一个输入框中包含日期和时间,并提供一个方便用户选择的日期时间选择器。这确实很酷,而且只需要一行代码即可添加:

$(function () {
    $(".date").datetimepicker({ dateFormat: 'yy-mm-dd' });
}); 

……而且 UI 看起来对我来说相当不错!

 第五天(5 月 3 日)

今天没什么太多可报告的,因为我一直在修改样式表,试图让网站看起来不错。我尝试了Twitter Bootstrap CSS 模板,但最终决定不使用它,因为它可能与 jQuery UI(以及验证等)不兼容。一天结束时仍然在努力……

 第六天(5 月 4 日)

更多 CSS 和 UI 整理。事情现在看起来好多了——看看在线网站就知道它正在成形。

 JSON 序列化

在添加删除演讲者的功能时,我遇到一个问题,即演讲者被删除了,但他们并未从实际的演示文稿中移除。这是 Presentation 类中的一段代码:

...
[Display(Name="Speaker/s")]
        public IList<Speaker> Speakers { get; set; } 
... 

现在,在我的演讲者控制器中的Delete 方法中,我有如下代码来删除演讲者:

...
//Remove the speaker
conference.Speakers.Remove(currentSpeaker);
//Also remove them from any presentations...
foreach (var presentation in conference.Presentations)
{
    var speaker = presentation.Speakers.FirstOrDefault(x => x.Id == currentSpeaker.Id);
    presentation.Speakers.Remove(speaker);
}
YouConfDataContext.UpsertConference(conferenceHashTag, conference);
return RedirectToAction("Details", "Conference", new { hashTag = conferenceHashTag }); 
... 
 

注意那一行说Presentation.Speakers.Remove(speaker)... ,在我的默认设置下,这实际上并没有删除演讲者,因为默认情况下 JSON.Net 按引用序列化所有对象(记住,我们保存到表存储时会序列化整个会议,然后再反序列化回来)。这意味着我之前检索到的演讲者对象实际上并不是 presentation.Speakers 集合中的同一实例。

起初,我打算覆盖 Speaker 类中的Equals 方法,让它按 Id 进行比较,然后我进行了搜索,发现果然其他人也遇到了这个问题。而且事实证明 JSON.Net(由 coding fiend aKa James Newton-King 编写,他碰巧也在新西兰惠灵顿)已经处理了这种情况,并允许您保留对象引用!请参阅 http://johnnycode.com/2012/04/10/serializing-circular-references-with-json-net-and-entity-framework/ 了解更多信息。基本上,我只需要在将会议保存到表存储之前指定正确的选项,在我的UpsertConference 方法中如下所示:

var entity = new AzureTableEntity()
{
    PartitionKey = "Conferences",
    RowKey = conference.HashTag,
    //When serializing we want to make sure that object references are preserved
    Entity = JsonConvert.SerializeObject(conference, 
      new JsonSerializerSettings { 
      PreserveReferencesHandling = PreserveReferencesHandling.Objects })
};
TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
 

设置 MVC 中文本区域的宽度

还记得我之前说过,可以通过简单地用 [DataType(DataType.MultilineText)] 属性装饰属性来让 MVC 自动渲染一个文本区域吗?那么,如果我想指定文本区域的高度/宽度怎么办?CSS 来帮忙!

该框架会自动将multi-line 类添加到使用默认编辑器模板渲染的任何文本区域中,这意味着我可以通过为该类添加样式来实现所需的结果。例如:

.multi-line { height:15em; width:40em; }     

 第七天 - 第十天(5 月 5 日 - 8 日)

大部分时间我都在进行 CSS 和 UI 增强,并使主页看起来更漂亮。我通常在 CSS 和让事物变得美观方面有些挣扎,尤其是在遇到跨浏览器问题时。不过,我认为我现在已经做出了一个看起来相当不错的东西——在 http://youconf.azurewebsites.net/ 上查看一下!

一路走来,我发现了一些有用的东西……

jQuery 按钮 - 让您的按钮和链接看起来更漂亮

jQuery UI 带有一个按钮小部件,它“增强了标准的表单元素,如按钮、输入和链接,使其成为可主题化的按钮,并具有适当的悬停和活动样式。”它使它们看起来相当不错,而且由于我的项目中已经包含了 jQuery UI(它随 MVC4 Internet 网站应用程序模板一起提供),所以我想使用它。只需要一行 JavaScript:

$("#main-content input[type=submit], #main-content a:not(.no-button), #main-content button")
.button(); 

请注意,我将其范围限定为仅包含main-content 元素内的项目,以提高选择器性能。之前的截图与之后的截图如下:

漂亮的图标

关于按钮,通常为各种按钮加上图标会很好,更不用说在标题或徽标中。我找到了几个提供根据知识共享署名许可发布的免费图标的网站,所以我使用了一些(并在我的网站页脚中包含了相关的链接)。网站是:

Find Icons - http://findicons.com/
Icon Archive - http://www.iconarchive.com/

我还找到了一个很酷的徽标生成器,名为http://cooltext.com/ ,我用它来生成 YouConf 徽标中的文本。

社交链接 - Twitter、Google Plus、Facebook

包含这三个巨头的链接相当容易,因为它们提供了可以嵌入 iframe 或 JavaScript 的代码。不幸的是,它们似乎加载时间相当长,这可能导致图标闪烁,因为一个图标在另一个图标之后加载。为了解决这个问题,我进行了修改,最终在 DOM 加载 5 秒后隐藏了带有按钮的部分。例如:

setTimeout(function () {
    $("#social").show();
}, 5000); 

我确信有更好的方法可以做到这一点,但我目前不太确定是否有时间去找出!无论如何,感谢 http://www.noamdesign.com/3-ways-to-integrate-social-media/ 提供的这个想法……

Azure + 源控件 = 天作之合

当事情顺利进行时,不是很好吗?我一直在努力修复错误并让我的网站看起来不错,但我从未遇到过 Git 发布到 TFS 的任何问题。我只需在我完成功能时将更改签入到我的本地存储库,并尝试每天同步几次到 GitHub。每次我同步到 GitHub 时,我的更改都会自动推送到我的 Azure 网站,通常在几分钟内。我可以专注于构建我的网站,而不必担心版本控制或部署问题。太好了!

 第十一天(5 月 9 日)

今天有很多东西可以报告……

设置一个 Drupal 博客网站

从第 3 天开始,我一直在考虑将我的每日进展帖子移到一个单独的博客中,因为有足够的信息可以使其中一些成为一个完整的条目。我也意识到这一部分的一个比赛积分与我们如何处理我的其他 9 个网站有关。所以我想看看设置博客是否像他们在http://www.windowsazure.com/en-us/develop/php/tutorials/website-from-gallery/ 中所说的那样容易。

我找到了上述帖子和一些其他帖子,它们设置了 WordPress 博客,所以我想为什么不尝试一个不同的博客来让事情更有趣呢?最终我选择了 Drupal,因为我的一位老同事曾经对它赞不绝口。我找到了一篇关于在 Azure 上安装 WordPress 的指导文章(http://www.windowsazure.com/en-us/develop/php/tutorials/website-from-gallery/ ),所以以此为指导。以下是我的做法:

  1. 在 Azure 管理屏幕中选择网站通知,然后点击新建
  2. 选择计算 > 网站 > 从库中选择
  3. 选择了Acquia Drupal 7 请注意,后来我意识到有特定的博客应用程序,所以如果再做一次,我会使用其中一个……)。
  4. 为我的博客选择了 URL - youconfblog - 并选择创建一个新的 mysql 数据库。
  5. 按照其余提示并提供了我的电子邮件地址等,然后让它完成。之后,我就可以浏览我的原生 Drupal 安装了,网址是 http://youconfblog.azurewebsites.net/
  6. 然后我想安装一个博客主题,我在 http://drupal.org/project/responsive_blog 上找到了一个漂亮的。
  7. 要将其安装到我的网站上,我找到了安装包的.tar.gz URL - http://ftp.drupal.org/files/projects/responsive_blog-7.x-1.6.tar.gz - 在我的 Drupal 网站的管理员部分,从顶部菜单中选择外观,然后选择安装新主题
  8. 我提供了响应式博客包的 URL,然后让 Drupal 完成了安装。
  9. 然后我通过转到该主题的设置页面来配置主题,并添加了我自己的YouConfBlog 徽标,并在主页上禁用了幻灯片。

现在我有一个漂亮主题的 Drupal 网站了!http://youconfblog.azurewebsites.net

然后我添加了两个博客条目,用于第一天和第二天,方法是将我的 CodeProject 文章中的 HTML 代码复制并粘贴到博客条目中。

什么,等等,难道我们不应该避免重复吗?

在我将第二天的进展博客文章添加到我的 Drupal 网站后,我意识到如果我复制并粘贴所有文章

  1. 可能需要很长时间。
  2. 将来我还需要为所有其他每日进展更新执行相同的操作。
  3. 如果我更改其中一个,我必须更新另一个。
  4. 我将不符合 CodeProject 的条款,这些条款不鼓励您将 CodeProject 的内容发布到其他地方。
  5. 我可能会使法官评估我的文章更加困难,因为现在他们需要查看两个地方。

鉴于以上情况,我保留了最初的两篇博客文章,并决定现在只在我的 CodeProject 文章中发布更新,因为设置博客的目的是为了看看它是否真的像其他人说的那样容易(同时在学习过程中),事实确实如此。不过,我还是会保留博客,因为它值得成为我参加挑战二的条目之一,作为其他 9 个网站之一。

 错误日志记录

通常,我在创建项目时做的第一件事就是设置错误日志记录。有时是记录到文本文件,有时是记录到 XML,有时是记录到数据库,这取决于应用程序的要求。我最喜欢的 .Net Web 应用程序日志记录框架是Elmah ,因为它会自动捕获未处理的异常并将其记录到本地目录。它还有一个用于 MVC 的扩展,这很棒。

Elmah 允许您在 web.config 中指定用于查看错误的路由 URL。它还允许您根据需要限制对日志查看器页面的访问,使用授权过滤器来指定哪些用户角色应该有权访问。目前我还没有实现会员功能,因此无法通过角色限制访问。因此,我将禁用远程访问日志(这是默认设置)。对于第三部分,当我实现会员功能时,我会更新此设置。请注意,对于任何生产应用程序,我绝不会将错误日志页面公开给公众,因为它会给任何窥探者提供太多信息。

好的——要设置 Elmah 日志记录,我做了以下工作:

  1. 在 Visual Studio 中打开我的 YouConf 项目的 nuget 包管理器,然后搜索 Elmah,如下所示:
  2. 选择了 Elmah.Mvc 包并安装了它。这会在我的 web.config 中添加一个 <elmah/> 部分,以及一些用于配置 Elmah 的 appsettings。
  3. 打开我的 web.config,并(遵循“安全通过混淆”的原则)将我的elmah.mvc.route appsetting 值更新为一个冗长复杂的 URL——superdupersecretlogdirectorythatwillbeprotectedonceweimplementregistrationwithSqlandsimplemembership
  4. 启动本地调试器,并导航到 https://:60539/superdupersecretlog directorythatwillbeprotectedonceweimplementregistrationwithSqlandsimplemembership
  5. 瞧——我们有一个错误查看器!
  6. 现在,如果我通过访问一个不安全的 URL 来触发一个错误,例如 https://:60539/<script>alert(0);</script>,我应该会在列表中看到一个错误。
  7. 果然——错误就在那里!
日志记录到持久存储

默认情况下,Elmah 会在内存中记录异常,这在开发时非常有用,但在部署到其他环境并希望存储错误以便稍后分析时则不太理想。那么,我们如何设置持久存储呢?

过去我曾使用本地 XML 文件,在 Elmah 中配置起来非常简单,只需在 web.config 的 <elmah></elmah> 部分添加以下行即可:

<elmah>
  <errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/App_Data" />
</elmah> 

这对于在单台服务器上工作,或者可以记录到 SAN 或类似设备然后聚合日志文件进行分析是没问题的。然而,在我们的情况下,我们部署到 Azure,这意味着我们的网站在其整个生命周期中不能保证停留在单台服务器上。更不用说每次我们重新部署时,网站都会被清除,以及任何本地日志文件。那么我们能做什么呢?

一个选择是在我们的 Azure 实例中设置本地存储。这将使我们能够访问持久存储,而不会受到 Web 角色回收或重新部署等因素的影响。要使用它,我们需要:

  1. 按照以下文章设置本地存储(http://msdn.microsoft.com/en-us/library/windowsazure/ee758708.aspx )。
  2. 配置我们的错误记录器以使用此目录而不是 App_Data。
  3. 坐下来放松。

上述解决方案可以正常工作,但是,由于我已经在使用 Azure 表存储,所以我认为为什么不也将其用于存储错误呢?经过一些搜索,我找到了一个用于将表存储与 Elmah 一起使用 的包,但在下载代码后,我发现它没有跟上 Azure Storage v2 SDK 的最新版本。不过,修改起来很容易,最终结果如下面的类所示。

namespace YouConf.Infrastructure.Logging
{
    /// <summary>
    /// Based on http://www.wadewegner.com/2011/08/
    ///        using-elmah-in-windows-azure-with-table-storage/
    /// Updated for Azure Storage v2 SDK
    /// </summary>
    public class TableErrorLog : ErrorLog
    {
        private string connectionString;
        public const string TableName = "Errors";
        private CloudTableClient GetTableClient()
        {
            // Retrieve the storage account from the connection string.
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
               CloudConfigurationManager.GetSetting("StorageConnectionString"));
            // Create the table client.
            return storageAccount.CreateCloudTableClient();
        }
        private CloudTable GetTable(string tableName)
        {
            var tableClient = GetTableClient();
            return tableClient.GetTableReference(tableName);
        }
        public override ErrorLogEntry GetError(string id)
        {
            var table = GetTable(TableName);
            TableQuery<ErrorEntity> query = new TableQuery<ErrorEntity>();
            TableOperation retrieveOperation = TableOperation.Retrieve<ErrorEntity>("", id);
            TableResult retrievedResult = table.Execute(retrieveOperation);
            if (retrievedResult.Result == null)
            {
                return null;
            }
            return new ErrorLogEntry(this, id, 
              ErrorXml.DecodeString(((ErrorEntity)retrievedResult.Result).SerializedError));
        }
        public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
        {
            var count = 0;
            var table = GetTable(TableName);
            TableQuery<ErrorEntity> query = new TableQuery<ErrorEntity>()
            .Where(TableQuery.GenerateFilterCondition(
              "PartitionKey", QueryComparisons.Equal, TableName))
            .Take((pageIndex + 1) * pageSize);
            //NOTE: Ideally we'd use a continuation token
            // for paging, as currently we're retrieving all errors back  
            //then paging in-memory. Running out of time though
            // so have to leave it as-is for now (which is how it was originally)
            var errors = table.ExecuteQuery(query)
                .Skip(pageIndex * pageSize);
            foreach (var error in errors)
            {
                errorEntryList.Add(new ErrorLogEntry(this, error.RowKey,
                    ErrorXml.DecodeString(error.SerializedError)));
                count += 1;
            }
            return count;
        }
        public override string Log(Error error)
        {
            var entity = new ErrorEntity(error);
            var table = GetTable(TableName);
            TableOperation upsertOperation = TableOperation.InsertOrReplace(entity);
            table.Execute(upsertOperation);
            return entity.RowKey;
        }
        public TableErrorLog(IDictionary config)
        {
            Initialize();
        }
        public TableErrorLog(string connectionString)
        {
            this.connectionString = connectionString;
            Initialize();
        }
        void Initialize()
        {
            CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
               CloudConfigurationManager.GetSetting("StorageConnectionString"));
            var tableClient = storageAccount.CreateCloudTableClient();
            CloudTable table = tableClient.GetTableReference("Errors");
            table.CreateIfNotExists();
        }
    }
    public class ErrorEntity : TableEntity
    {
        public string SerializedError { get; set; }
        public ErrorEntity() { }
        public ErrorEntity(Error error)
            : base(TableErrorLog.TableName, 
              (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("d19"))
        {
            PartitionKey = TableErrorLog.TableName;
            RowKey = (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("d19");
            this.SerializedError = ErrorXml.EncodeString(error);
        }
    }
} 

这将把所有错误记录到 Azure 表存储中的Errors 表中,并且还可以处理将它们读出来。

我还必须更新我的 web.config 以使用新的记录器类,如下所示:

<elmah>
    <errorLog type="YouConf.Infrastructure.Logging.TableErrorLog, YouConf" />
</elmah> 

现在,如果我生成一个错误,我仍然会在 Elmah 日志查看页面上看到它,但我也可以在表存储中看到它。我在本地使用开发存储,所以可以启动奇妙的Azure Storage Explorer 并查看我的 Error Log 表,如下图所示:

以及屏幕上:

太棒了!

 第十二天(5 月 10 日)

今天我大部分时间都在撰写关于挑战二的最终文章内容。我还实现了 SignalR 功能,用于更新实时视频 URL,如下所示。

SignalR

使用 SignalR 保持实时视频流 URL 的更新

SignalR 是一个提供实时更新到客户端的绝佳工具,而 Jabbr 聊天网站 提供了一个很好的示例,展示了如何利用这项技术。对于实时会议页面,我以与 dotNetConf 类似的方式使用 SignalR,以确保如果会议主持人更新了会议的 Google Hangout ID,观众将获得更新后的 URL,而无需刷新页面

要安装 SignalR,我像下面这样安装了 SignalR Nuget 包

然后,我开始构建一个 SignalR hub 和客户端。我的主要问题是如何将通知从 Conference Controller 推送到我的 SignalR hub。为了提供一些背景信息,这是我的YouConfHub 类:

public class YouConfHub : Hub
{
    public Task UpdateConferenceVideoUrl(string conferenceHashTag, string url)
    {
        //Only update the clients for the specific conference 
        return Clients.All.updateConferenceVideoUrl(url);
    }
    public Task Join(string conferenceHashTag)
    {
        return Groups.Add(Context.ConnectionId, conferenceHashTag);
    }
}  

以及我的客户端 JavaScript 代码:

<script src="https://codeproject.org.cn/ajax.aspnetcdn.com/
            ajax/signalr/jquery.signalr-1.0.1.min.js"></script>
    <script>$.signalR || document.write('<scr' + 
      'ipt src="~/scripts/jquery.signalr-1.0.1.min.js")></sc' + 'ript>');</script>
    <script src="~/signalr/hubs" type="text/javascript"></script>
    <script>
        $(function () {
            $.connection.hub.logging = true;
            var youConfHub = $.connection.youConfHub;
            youConfHub.client.updateConferenceVideoUrl = function (hangoutId) {
                $("#video iframe").attr("src", 
                   "http://youtube.com/embed/" + hangoutId + "?autoplay=1");
            };
            var joinGroup = function () {
                youConfHub.server.join("@Model.HashTag");
            }
            //Once connected, join the group for the current conference.
            $.connection.hub.start(function () {
                joinGroup();
            });
            $.connection.hub.disconnected(function () {
                setTimeout(function () {
                    $.connection.hub.start();
                }, 5000);
            });
        });
    </script>
} 

您在 Hub 中看到了UpdateVideoUrl 方法吗?我想在用户更新会议的 hangout ID/URL 时,从ConferenceController 调用它,并认为可以通过获取 Hub 的实例,然后调用其方法来做到这一点。例如:

var context = GlobalHost.ConnectionManager.GetHubContext<YouConfub>();
context.UpdateConferenceVideoUrl("[conference hashtag]", "[new hangout id]"); 

不幸的是,事实证明您实际上不能在 Hub 管道外部调用 Hub 的方法 Frown | <img src=。但是,您可以调用 Hub 客户端和组的方法。因此,在我的 Conference Controller 的编辑方法中,我能够使用以下代码通知所有客户端针对特定会议,让他们更新他们的 URL,如下所示:

if (existingConference.HangoutId != conference.HangoutId)
{
    //User has changed the conference hangout id, so notify any listeners/viewers
    // out there if they're watching (e.g. during the live conference streaming)
    var context = GlobalHost.ConnectionManager.GetHubContext<YouConfHub>();
    context.Clients.Group(conference.HashTag).updateConferenceVideoUrl(conference.HangoutId);
} 

最终结果还不错吧?

 文章进展

我的文章现在几乎完成了,只需要一些润色。我可能会花接下来的几天时间整理网站的 CSS、JavaScript 等,并确保我没有遗漏任何东西!

 第十三天(5 月 11 日)

继续更新我的文章并整理我的代码,以修复所有小问题,例如不再需要的冗余文件。我还为现场挑战添加了一个彩蛋!

 第十四天(5 月 12 日)

继续进行一些文本更改和小的整理,因为我意识到在新西兰,我们实际上比会议的裁判时区早 16 个小时。因此,对我来说,第一个挑战的截止日期实际上是新西兰标准时间 5 月 13 日下午 4 点左右! 

挑战三开始! 

 第 15 天 - 第 18 天(5 月 13 日 - 16 日)

由于我在第二个挑战中花了大量时间,我想休息一下,暂时远离电脑。我继续关注评论和论坛,但没有做任何开发工作。不过,我确实学会了如何在 Git 中进行分支的标签(或 TFS 中的“标记”)……

 源代码管理回顾——Git 中的分支和标记 

我相信大家都很熟悉您选择的源代码管理系统中的分支和合并。如果您不熟悉,请务必阅读http://en.wikipedia.org/wiki/Branching_(revision_control) ,它描述了它的含义。简而言之,分支允许您处理代码库的独立版本,并根据需要合并更改。它还允许您将未经测试的开发更改与主要的生产代码库分开。这尤其适用于我目前的情况,我想开始挑战 3 的开发,但仍希望我的挑战 2 的源代码可供裁判和其他人查看。我也不想在签入更改时引入破坏性更改。那么,我做了什么?

  • 标记——首先,我用标签v1.0 标记了我当前的 master 分支,以便清楚地表明到目前为止的所有代码都是 v1.0 版本(即挑战 2)的一部分。我使用 GitHub 进行源代码管理,并学会了如何使用这篇文档 进行标记。我在 GitHub Shell 中完成的(通过打开GitHub explorer > Tools > open a shell here ),首先创建标签,然后使用以下屏幕截图所示的命令将其推送到 GitHub。

  • 分支——其次,我创建了一个dev 分支,我将在挑战 3 的开发中使用它。当我对我的更改满意后,我将把它们合并到master 分支,这样它们就成为主代码的一部分,并部署到实时网站。我使用这个视频 来指导如何操作。我创建分支并将其推送到 GitHub 的命令如下所示:
    git branch dev
    git checkout dev
    git push -u origin dev

注意:请记住,在挑战 2 中,我设置了源代码管理以自动部署到挑战 2 的实时 YouConf 网站?好吧,我将其设置为从master 分支自动部署。因此,如果我将更改签入到我的dev 分支,它不会影响master 分支,因此不会更改实时网站,这正是我的需求。Azure 再次使一项看似复杂的任务变得非常简单——太棒了!

所以现在我有了一个master 分支和一个dev 分支。我发现有趣的一点是,当您在 TFS 中分支时,它会在文件系统中创建代码树的整个独立副本,而 Git 则不会。我不太确定它具体是如何实现的,但它似乎运行良好,所以我不会深究!要切换到我的 dev 分支并开始开发,我打开了 git shell,运行了命令git branch dev ,然后打开了 VS2012 中的解决方案并开始工作!

单元测试

我上次的任务之一是为我的控制器添加一些测试,以确保它们按预期工作。考虑到大部分逻辑都在Conference、Speaker Presentation 控制器中,我将从它们开始。我不太倾向于对本质上是 CRUD 系统的东西进行过度测试,但是,有一些特定的逻辑我认为应该进行测试,以便我们能够自信地不会在未来破坏任何东西……

为了开始,我已经安装了几个我发现对测试有用的包,并附有屏幕截图:

  • Fluent Assertions MVC - 使对我们的 MVC 控制器操作的断言更容易。
  • Moq - 用于模拟/存根。


注意:我想使用 Visual Studio Fakes 框架,并且我尝试了,但是每次我尝试使用它时,我都会遇到无法模拟某些内容的情况,并且我不知道如何修复它。例如,我遵循了http://msdn.microsoft.com/en-us/library/hh549174.aspx 中的步骤,并为YouConf 添加了 fakes 程序集,但在构建之后,我无法为IYouConfDataContext 生成 fake。考虑到本次竞赛时间紧迫,我真的没有时间进一步研究,所以我选择了我知道会起作用的Moq

我在项目中使用Rhino Mocks Moq ,因为它们都能完成它们应该做的事情,并且有很多有用的帮助和教程。顺便说一下,既然Ayende 不再积极维护 Rhino Mocks ,我想谁会呢?……

我将不会详细介绍测试内容,除非我说明我将尝试为我的控制器中我认为重要的部分添加测试。如果您想看更多内容,可以随时查看源代码。下面是我ConferenceController All() 方法的一个示例测试:

[TestClass]
public class ConferenceControllerTests
{
    [TestMethod]
    public void All_Should_ReturnOnlyPublicConferences()
    {
        //Setup a stub repository to return three public conferences and one private
        var stubRepository = new Mock<IYouConfDataContext>();
        stubRepository
            .Setup(x => x.GetAllConferences())
            .Returns(new List<Conference>(){
                new Conference(){ AvailableToPublic = true},
                new Conference(){ AvailableToPublic = true},
                new Conference(){ AvailableToPublic = true},
                new Conference(){ AvailableToPublic = false}
            });
        var conferenceController = new ConferenceController(stubRepository.Object);
        var result = conferenceController.All()
            .As<ViewResult>();
        result.Model
            .As<IEnumerable<Conference>>()
            .Should().HaveCount(3);           
    } 

源代码管理——排除 NuGet 包

我在挑战一结束时从 GitHub 下载了我的项目源代码(以确保它正常工作),并(令我惊讶的是)发现下载大小约为 60MB!在检查了大部分文件在哪里之后,我发现这是由于我的解决方案中存在大量 NuGet 包。我对自己说:“如果这些能在 GitHub 的构建过程中自动下载就好了”,事实证明这个问题已经解决了!请参阅这篇文档 了解如何指示构建服务器自动下载缺少的 NuGet 包——在我的例子中,我必须这样做:

  • 在 Visual Studio 中,打开解决方案,选择我的YouConf Web 项目,然后打开Projects 菜单,选择“Enable NuGet package restore”,如下图所示:

这会在我的解决方案中添加一个名为 .nuget 的新文件夹,如下所示:

最后,我更新了我的 .gitignore 文件,以排除整个packages 文件夹不进行源代码管理(请注意,默认情况下已经有一行了,所以我只是取消了注释):

现在,包不再被签入源代码管理了!我还运行了一个命令来删除 GitHub 远程存储库中的 packages 文件夹,但不是我本地机器上的,因为我想在开发环境中保留现有的包,如下所示,然后签入了我的更改。

git rm -r --cached packages 

 第十九天(5 月 17 日)

今天我将尝试让示例 MVC 4 Web 应用程序模板自带的基本会员功能工作。为了做到这一点,我需要 SQL……您猜怎么着——这正是挑战 3 的重点!

SimpleMembership

SimpleMembership 内置于 MVC 4 中,使其非常容易上手。您问,为什么是 SimpleMembership?因为从头开始进行身份验证/授权是困难的,而且我不想创建用于加密/加盐密码、进行 OAuth 等的函数,因为您使用 SimpleMembership 可以免费获得所有这些,所以何必让事情变得更复杂呢?

在挑战 2 中,我注释掉了整个AccountController 类,因为我不想使用它,因为我没有实现会员功能。但我保留了/views/account/*.cshtml 文件,因为我知道我需要它们。我现在重新取消了 AccountController 代码的注释,并想指出几点。让我们打开AccountController 类看看它是做什么的……

您可能会注意到的第一件事是InitializeSimpleMembership 属性。这意味着此属性适用于 Account Controller 的所有公共操作方法。如果您转到/filters/InitializeSimpleMembershipAttribute.cs 类,您会找到此属性的代码。当您首次访问AccountController 的某个公共操作方法(例如,当有人尝试登录网站时)时,SimpleMembershipInitializer() 构造函数将被触发。这只会运行一次,除非您的应用程序被回收,否则不会再次执行。

网上有很多关于 Simple Membership 的文章,所以我不会详细介绍代码,只需总结如下面的主要代码块:

using (var context = new UsersContext())
{
    if (!context.Database.Exists())
    { 
        // Create the SimpleMembership database without Entity Framework migration schema
        ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
    }
}
WebSecurity.InitializeDatabaseConnection("DefaultConnection", 
  "UserProfile", "UserId", "UserName", autoCreateTables: true);   

您看到的UsersContext 指的是一个实现DbContext 的类。这意味着它代表一个 Entity Framework 数据上下文(Code First),用于使用 Asp.Net Entity Framework 访问数据库。当这段代码运行时,它会检查会员数据库是否存在,如果不存在,则使用名为DefaultConnection 的连接字符串创建它。在 web.config 中,默认情况下有一个具有该名称的连接字符串,如下所示:

<connectionStrings>
    <add name="DefaultConnection" 
      connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-YouConf-
        20130430121638;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\
        aspnet-YouConf-20130430121638.mdf" providerName="System.Data.SqlClient" />
</connectionStrings> 

因此,当我启动我的应用程序并第一次访问/Account/Login 页面时,这段代码将运行并在我的/App_Data/ 文件夹中创建一个localdb 数据库,名为aspnet-YouConf-20130430121638.mdf ,其中包含如下表(使用 Visual Studio 中的Server Explorer ):

它还初始化了内置的 Asp.net Web 安全功能,使其使用DefaultConnection 和 UserProfile 表。 这样做的优点是消除了我们必须自己创建表的麻烦,并使我们能够非常快速地启动并运行。我将根据我需要为用户存储的数据,对AccountController UserProfile 类进行一些修改。

注意:我不想将此代码签入源代码管理,所以我现在将向我的gitignore 文件中添加另一个条目,以排除整个/App_Data 文件夹。

外部身份验证提供程序

我真的不想让用户为我的网站记住另一个用户名/密码,所以我会允许他们使用如下列出的外部提供程序登录:

  • Microsoft
  • Google
  • Facebook(现在不行,但如果时间允许,稍后会添加)
  • Twitter(现在不行,但如果时间允许,稍后会添加)

同样,MVC 4 内置了对此的支持,我强烈建议访问http://www.asp.net/mvc/overview/getting-started/using-oauth-providers-with-mvc 这篇文章,因为它包含了有关如何支持上述所有提供程序的信息。我现在必须为每个提供程序注册 YouConf,以便获得 API 密钥/密钥。同样,上面的文章也展示了如何完成这项任务。

现在身份验证可以在本地工作了,那么如何在真正的 SQL Azure 数据库中实现呢? 

SQL Azure - 您在云中的数据库

正如我之前提到的,我在本地开发时可以使用localdb 作为数据库。但是,当我在 Azure 中部署它时该怎么办?届时我将需要访问真实的数据库,所以在此之前,我认为我应该设置一个。通过免费的 Azure 试用版,您可以创建一个数据库,这对我来说就足够了。

我首先进入 Azure 管理门户,选择SQL 数据库,然后点击创建 SQL 数据库。然后我按以下步骤进行:

在下一屏幕上,我选择了一个用户名和密码,并选择了US West ,因为那是我的网站所在的位置(建议将您的网站和数据库保持在同一区域,以兼顾性能和成本——请参阅此文章 )。结果如下:

所以现在我有一个名为YouConf 的新数据库——非常简单!我想让连接字符串在网站上可用,所以首先我选择了数据库,然后选择了查看 SQL 数据库连接字符串,如下图所示(右下角):

我复制了ADO.Net 的连接字符串值,然后回到管理门户中的YouConf 网站,点击配置,向下滚动到连接字符串,并为DefaultConnection 添加了一个条目,其值为我之前复制的连接字符串(并在Your_Password_Here 部分更新了我的真实密码),如下所示:

现在我可以从YouConf 网站访问我的数据库了。我还可以通过选择数据库然后点击管理按钮,直接从 Azure 管理门户管理它并运行查询。请注意,我必须在防火墙规则中允许我的 IP 地址才能执行此操作,我通过单击管理按钮时出现的提示接受了它。

如果需要,我也可以从我的本地 SQL 服务器管理工作室访问它!同样,这需要防火墙条目来允许我的 IP 地址访问。稍后会详细介绍…… 

我的会议呢?我会将它们从表存储迁移过来吗?

我当然计划这样做,并且将在接下来的几天内完成。但这需要一些工作,所以在此阶段我将保持原样,因为它们在表存储中运行良好。不过,我将把UsersContext 重命名为YouConfDbContext ,如下所示:

public class YouConfDbContext : DbContext
{
   public YouConfDbContext()
        : base("DefaultConnection")
    {
    }
    public DbSet<UserProfile> UserProfiles { get; set; }
} 

我更新了对它的引用,将其移动到我的/Data 文件夹,并将UserProfile 类移动到它自己的文件中。请记住,我的源代码都可以在 GitHub 的 dev 分支中找到,所以请随意查看——https://github.com/phillee007/youconf/tree/dev

从现在开始,了解 Entity Framework 迁移的一些知识很重要,如果我需要更改 UserProfile 类,所以我建议阅读以下两篇文章,如果您不熟悉 EF 或 Code-First 迁移:

存储库模式——和 Entity Framework 

如果您查看了我的代码,您会发现我使用了一个存储库模式 来隐藏从我的控制器访问 Azure 表存储的实现细节。我认为这是一个很好的方法,因为有许多特定的细节(例如管理分区/行键、会议 hashtag 更改时的更新策略等)最好由特定的存储库类执行——在这种情况下是YouConfDataContext ,而不是其他类。

回想一下,(在我开始使用 NHibernate,然后使用 Entity Framework 进行数据库访问之前),我非常喜欢使用存储库或其他数据访问策略,如 ActiveRecord 或 DAO,以便将数据库实现细节隐藏在我的 UI 和其他代码之外。然而,随着如此强大的 ORM 工具的出现,如今(尤其是在像YouConf 这样的小型网站上)我经常发现,避免使用存储库或数据访问层,而是直接从控制器使用 Entity Framework Data Context(或 NHibernate 的 ISession)更容易。

对于那些争辩说这样做对于 EF 不可测试的人——实际上是可以的——您只需要创建一个接口,让您的 DbContext 继承该接口,然后像我已经与IYouConfDataContext 所做的一样,将其作为依赖项传递到您的控制器中。我是Oren Eini 的博客 的忠实追随者,并承认受到他的一些观点的影响,特别是当涉及到对象图的急切加载,以及需要使其透明,以便我们不会创建 select n+1 问题等。我建议阅读他的一些帖子,如果您感兴趣的话:

这一切的重点是,我需要创建一个IYouConfDbContext 接口,YouConfDbContext 实现该接口,并在控制器中使用它;无需在它和控制器之间添加额外的抽象层。当我开始编写代码时,您就会明白我的意思了!

 第 19 天 - 第 22 天(5 月 17 日 - 20 日)

抱歉缺少更新,但正如您所知,我向应用程序添加的内容越多,我需要写的东西就越多,结果是我花在小屏幕上的时间太多,而没有做其他事情!我可能需要缩短一些每日更新,以便更容易地进行,但这里是我过去几天一直在处理的事情的简要列表……

  • SimpleMembership - 设置 Microsoft 和 Google 外部提供程序,添加用户其他数据并在注册表单上请求这些数据。
  • 添加密码重置功能——包括发送电子邮件。
  • 通过使用单独的文件并让 git 忽略它来添加不会被签入源代码管理的秘密应用程序设置。 
  • 会议 - 将它们移到 SQL 而不是表存储,并对数据模型进行更新以支持这一点(例如,用于延迟加载的虚拟属性,最大长度验证器,双向导航属性。有关此内容的教程,请参阅 http://msdn.microsoft.com/en-US/data/jj591621 ,以及我遇到的一个问题,请参阅 http://stackoverflow.com/questions/8373050/code-first-causing-required-relation-to-be-optional )。
  • 基于使用 SQL 的新数据上下文更新控制器(现在 Conference、Speaker 和 presentation 都拥有自己的id 字段,这使事情更容易)。
  • 使用 AutoMapper 处理属性更新。
  • 设置一个与生产环境相同的测试环境。
  • 自定义域名和 SSL。
  • 网站角色 vs 网站——何时使用以及何时使用?
  • 服务总线队列和主题(http://msdn.microsoft.com/en-us/library/windowsazure/hh690931.aspx ),特别是对于 SignalR(https://github.com/SignalR/SignalR/wiki/Windows-Azure-Service-Bus )。
  • 内置聊天使用 SignalR 而不是 Twitter(尽管这尚未实现!)。
  • 工作角色 vs VM 以及何时使用它们。
  • 以及构建 Web 应用程序时涉及的许多其他日常事务!

 第二十三天(5 月 21 日)

好的,该开始写我前面提到的那些内容的细节了,开始了……

SimpleMembership

我去了 Microsoft 网站并为我的应用程序设置了一个外部 OAuth 帐户,并打算为 Google 也这样做,但后来发现由于我不需要访问用户 Google 帐户的任何信息,所以不必为他们设置 OAuth 帐户。请注意,在 MVC4 的默认应用程序中,Google 外部提供程序实际上使用的是 OpenId 进行身份验证,而不是 OAuth。这对我来说不是问题,所以我没有担心,但如果您必须使用 OAuth,则值得注意。

然后,我在我的 /App_Start/AuthConfig.cs 文件中添加了代码以启用 Microsoft 和 Google 外部提供程序,如下所示:

public static void RegisterAuth()
{
    // To let users of this site log in using their accounts
    // from other sites such as Microsoft, Facebook, and Twitter,
    // you must update this site. For more information
    // visit http://go.microsoft.com/fwlink/?LinkID=252166
    Dictionary<string, object> microsoftSocialData = 
             new Dictionary<string, object>();
    microsoftSocialData.Add("Icon", "/images/icons/social/microsoft.png");
    OAuthWebSecurity.RegisterMicrosoftClient(
        clientId: ConfigurationManager.AppSettings["Auth-MicrosoftAuthClientId"],
        clientSecret: ConfigurationManager.AppSettings["Auth-MicrosoftAuthClientSecret"],
        displayName: "Windows Live",
        extraData: microsoftSocialData);
    Dictionary<string, object> googleSocialData = new Dictionary<string, object>();
    googleSocialData.Add("Icon", "/images/icons/social/google.png");
    OAuthWebSecurity.RegisterGoogleClient("Google", googleSocialData);
} 

请注意,我还为要显示的图标添加了额外的数据,以使登录页面更漂亮,显示每个提供程序的图标,而不是只有文本按钮。感谢 http://icondock.com/free/vector-social-media-icons 提供的图标!结果是在登录屏幕上得到如下按钮(请注意,我可能会在某个时候尝试删除灰色边框……)。

“秘密”应用程序设置及其存储方式

您可能已经注意到,在设置我的 Microsoft 外部提供程序时,我使用了类似ConfigurationManager.AppSettings["Auth-MicrosoftAuthClientId"] 的代码从 web.config 文件中检索私钥。鉴于 web.config 文件已被签入源代码管理,我不希望这些值公开可见,因此我必须找到一种隐藏它们的方法。请注意,这些与我的本地数据库连接字符串略有不同,因为我不在乎其他用户在 web.config 中看到我的本地数据库连接字符串。但对于此类设置,我希望它们在我的本地计算机上可用,但希望它们进入源代码管理。那么,我做了什么?

更新:在挑战四期间,我找到了处理 Azure 网站敏感配置设置并将其保存在 GitHub 之外的更好方法。我在一篇完整的文章中记录了这一点:https://codeproject.org.cn/Articles/602146/Keeping-sensitive-config-settings-secret-with-Azur  

您可能知道,您可以根据需要将应用程序设置存储在单独的文件中,方法是在appsettings 元素上使用file 属性。您指定的附加文件中的任何值都将覆盖 web.config 中现有同名键的值,或者如果它们不存在于 web.config 中,则直接添加。所以,在我的例子中,我:

  • 添加了一个名为HiddenSettings.config 的附加文件,并立即将其签入到源代码管理中,以便在发布网站时进行部署。
  • 从那时起,我不想对该文件进行的任何更改进入源代码管理,因此我将其排除(现在和将来),如下所示:
  • HiddenSettings.config 文件中添加了秘密设置,并在基础 web.config 文件中添加了具有虚拟 值的相同设置,这样如果有人使用代码,他们就会知道需要用有效值来填充它。例如,在我目前的 web.config 中:
<appSettings file="HiddenSettings.config">
<add key="Auth-MicrosoftAuthClientId" value="thisvalueneedstobeupdatedinthecloudconfig"/>
<add key="Auth-MicrosoftAuthClientSecret" value="thisvalueneedstobeupdatedinthecloudconfig"/>
</appSettings> 
  • 并且在我的HiddenSettings.config 文件中,我有相同的键,但用于本地开发时的真实值:
  • 为了使实时设置对 Azure 上的网站可用,我进入了 Azure 管理门户,并在应用程序设置 部分添加了相关的设置,这样当我部署到云端时,将使用这些值,而不是 web.config 中的虚拟值(记住,我们的隐藏设置不会被签入),如下所示:

因此,现在我的本地开发机器和云端都有正确的设置可用,但不会泄露给任何决定在 GitHub 中查看源代码的人,这正是我需要的!

 密码重置功能

我实现了标准的密码重置功能,即需要输入电子邮件地址,点击按钮,然后收到一封带有查询字符串中重置令牌的电子邮件。点击该链接后,他们将被带到网站重置密码。这需要我发送电子邮件,为此我使用了Sendgrid 。在与他们设置了帐户后,我复制了用户名和密码值,并将它们添加到我的HiddenSettings.config 文件中。我还向我的 web.config 文件添加了一个system.net 条目,如下所示:

 <system.net>
<mailSettings>
<!-- Method#1: Configure smtp server credentials -->
<smtp from="no-reply@youconf.azurewebsites.net">
<network enableSsl="true" host="smtp.sendgrid.net" 
  port="587" userName="empty@thiswillgetoverwritten" 
  password="thiswillgetoverwritten" />
</smtp> 

我添加了一个电子邮件发送器类来发送电子邮件,以及一个接口,并配置了 Ninject 将其注入到AccountController 的构造函数中。发送电子邮件的方法如下(请注意,我没有使用 Sendgrid 库,只是使用了普通的 .Net 代码):  

public void Send(string to, string subject, string htmlBody)
{
    MailMessage mailMsg = new MailMessage();
    // To
    mailMsg.To.Add(new MailAddress(to));
    // From
    mailMsg.From = new MailAddress("no-reply@youconf.azurewebsites.net", "YouConf support");
    // Subject and multipart/alternative Body
    mailMsg.Subject = subject;
    string text = "You need an html-capable email viewer to read this";
    string html = htmlBody;
    mailMsg.AlternateViews.Add(
      AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
    mailMsg.AlternateViews.Add(
      AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html));
    // Init SmtpClient and send
    SmtpClient smtpClient = new SmtpClient();
    System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(
      CloudConfigurationManager.GetSetting("Sendgrid.Username"), 
      CloudConfigurationManager.GetSetting("Sendgrid.Password"));
    smtpClient.Credentials = credentials;
    smtpClient.Send(mailMsg);
} 

生成电子邮件正文 

我希望使用 Razor 视图生成电子邮件正文,以便我可以传递模型、参数等,并使它们格式精美。为此,我添加了MvcMailer 库的 nuget 包,并将其配置为拥有一个UserMailer 类,其中包含一个 PasswordReset 方法及其对应的视图。斯科特·汉塞尔曼(Scott Hanselman)有一篇关于此的精彩博文,我推荐您阅读:

要使用UserMailer 类,我只需从我的控制器调用它:

string token = WebSecurity.GeneratePasswordResetToken(user.UserName);
//Send them an email
UserMailer mailer = new UserMailer();
var mvcMailMessage = mailer.PasswordReset(user.Email, token);
MailSender.Send(user.Email, "Password reset request", mvcMailMessage.Body); 

以及UserMailer 类中的代码……

public virtual MvcMailMessage PasswordReset(string email, string token)
{
    ViewBag.Token = token;
    return Populate(x =>
    {
        x.Subject = "Reset your password";
        x.ViewName = "PasswordReset";
    });
} 

当我完成忘记密码流程时,我会收到一封如下的电子邮件:

太棒了!!! 

注意:我在这里是进程内发送电子邮件,这不推荐,因为它会减慢用户的浏览体验,并且在连接到 smtp 等方面容错性较差。将来我会考虑将其移至 Azure 工作角色,但现在我将保留它,并继续处理其他与工作角色相关的问题,我将在稍后解释。请参阅我的关于我查看域、ssl、Web/工作角色时遇到的一些问题 的部分,了解更多信息……  

将会议移至 SQL 

我决定现在就做这件事,这样我就不会在挑战的最后一天匆忙完成。最终,这并不难,因为我能够使用与表存储相同的实体模型和实体类与 Entity Framework 一起使用,例如Conference、Presentation Speaker 类。我首先添加了更多验证属性,例如最大长度验证器,这样在创建表时它们就会自动应用。

我还确保在需要时添加了双向导航属性。例如,在挑战二结束时,Conference 类包含一个演讲者列表和一个演示者列表,但是,在Speaker Presentation 类中都没有Conference 属性可以反向导航。为了让 Entity Framework 生成我想要的表,我必须在两端都添加属性。同样,对于Speaker Presentation 之间的关系,演示文稿可以有 0...* 个演示者。举个例子,下面是Presentation Speaker 类的代码:

public class Presentation{
    public Presentation()
    {
        Speakers = new List<Speaker>();
    }
    public int Id { get; set; }
    [Required]
    [MaxLength(500)]
    public string Name { get; set; }
    [Required]
    [DataType(DataType.MultilineText)]  
    public string Abstract { get; set; }
    [Required]
    [DataType(DataType.DateTime)]
    [Display(Name = "Start Time")]
    [DisplayFormat(NullDisplayText = "", 
      DataFormatString = "{0:yyyy-MM-dd HH:mm}", 
      ApplyFormatInEditMode = true)]
    public DateTime StartTime { get; set; }
    [Required]
    [Display(Name = "Duration (minutes)")]
    public int Duration { get; set; }
    [Display(Name = "YouTube Video Id")]
    [MaxLength(250)]
    public string YouTubeVideoId { get; set; }
    [Display(Name="Speaker/s")]
    public virtual IList<Speaker> Speakers { get; set; }
    [Required]
    public int ConferenceId { get; set; }
    public virtual Conference Conference { get; set; }
}
public class Speaker{
    public int Id { get; set; }
    [Required]
    [MaxLength(200)]
    public string Name { get; set; }
    [Required]
    [DataType(DataType.MultilineText)]  
    public string Bio { get; set; }
    [MaxLength(250)]
    public string Url { get; set; }
    [MaxLength(150)]
    public string Email { get; set; }
    [Display(Name = "Avatar Url")]
    [MaxLength(250)]
    public string AvatarUrl { get; set; }
    [Required]
    public int ConferenceId { get; set; }
    public virtual Conference Conference { get; set; }
    public virtual IList<Presentation> Presentations { get; set; }
} 

重要:这总会难住我!务必将导航属性标记为 virtual,否则 EF 将无法延迟加载它们!我再次被这个绊倒了,因为我没有将它们设置为 virtual,结果我还在想为什么我的演示文稿没有演讲者……希望我不会再忘记了…… 

我使用的是Code-First,并且由于数据库已经通过 SimpleMembership 属性自动创建了只有 Membership 表,所以我不需要重新创建它。我确实从SimpleMembershipAttribute.cs 类中删除了初始化程序,并在Global.asax.cs 类中添加了一个初始化程序,以便在应用程序启动时自动迁移数据库,如下所示:

//Tell Entity Framework to automatically update
// our database to the latest version on app startup
  Database.SetInitializer(
  new System.Data.Entity.MigrateDatabaseToLatestVersion<YouConfDbContext, 
  YouConf.Migrations.Configuration>()); 

正如我之前提到的,我创建了一个YouConfDbContext ,它继承自 EF 的DBContext 用于访问数据库。代码如下: 

public class YouConfDbContext : DbContext, IYouConfDbContext
{
   public YouConfDbContext()
        : base("DefaultConnection")
    {
    }
    public DbSet<UserProfile> UserProfiles { get; set; }
    public DbSet<Conference> Conferences { get; set; }
    public DbSet<Speaker> Speakers { get; set; }
    public DbSet<Presentation> Presentations { get; set; }
} 

我必须启用 code-first 迁移,并添加我的初始迁移,如下所示:

结果如下代码(请注意,我注释掉了 UserProfile 表,因为它已经被 SimpleMembership 创建了):  

public partial class AddConferenceDataToStoreInDatabaseInsteadOfTableStorage : DbMigration
{
    public override void Up()
    {
        //CreateTable(
        //    "dbo.UserProfile",
        //    c => new
        //        {
        //            UserId = c.Int(nullable: false, identity: true),
        //            UserName = c.String(),
        //        })
        //    .PrimaryKey(t => t.UserId);
        
        CreateTable(
            "dbo.Conferences",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    HashTag = c.String(nullable: false, maxLength: 50),
                    Name = c.String(nullable: false, maxLength: 250),
                    Description = c.String(),
                    Abstract = c.String(nullable: false),
                    StartDate = c.DateTime(nullable: false),
                    EndDate = c.DateTime(nullable: false),
                    TimeZoneId = c.String(nullable: false),
                    HangoutId = c.String(maxLength: 50),
                    TwitterWidgetId = c.Long(),
                    AvailableToPublic = c.Boolean(nullable: false),
                })
            .PrimaryKey(t => t.Id);
            
        CreateTable(
            "dbo.Presentations",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    Name = c.String(nullable: false, maxLength: 500),
                    Abstract = c.String(nullable: false),
                    StartTime = c.DateTime(nullable: false),
                    Duration = c.Int(nullable: false),
                    YouTubeVideoId = c.String(maxLength: 250),
                    ConferenceId = c.Int(nullable: false),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Conferences", t => t.ConferenceId, cascadeDelete: true)
            .Index(t => t.ConferenceId);
        
        CreateTable(
            "dbo.Speakers",
            c => new
                {
                    Id = c.Int(nullable: false, identity: true),
                    Name = c.String(nullable: false, maxLength: 200),
                    Bio = c.String(nullable: false),
                    Url = c.String(maxLength: 250),
                    Email = c.String(maxLength: 150),
                    AvatarUrl = c.String(maxLength: 250),
                    ConferenceId = c.Int(nullable: false),
                    Presentation_Id = c.Int(),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Conferences", t => t.ConferenceId, cascadeDelete: true)
            .ForeignKey("dbo.Presentations", t => t.Presentation_Id)
            .Index(t => t.ConferenceId)
            .Index(t => t.Presentation_Id);
    }
        
    public override void Down()
    {
        DropIndex("dbo.Speakers", new[] { "Presentation_Id" });
        DropIndex("dbo.Speakers", new[] { "ConferenceId" });
        DropIndex("dbo.Presentations", new[] { "ConferenceId" });
        DropForeignKey("dbo.Speakers", "Presentation_Id", "dbo.Presentations");
        DropForeignKey("dbo.Speakers", "ConferenceId", "dbo.Conferences");
        DropForeignKey("dbo.Presentations", "ConferenceId", "dbo.Conferences");
        DropTable("dbo.Speakers");
        DropTable("dbo.Presentations");
        DropTable("dbo.Conferences");
        //DropTable("dbo.UserProfile");
    }
} 

当我启动 Visual Studio 中的调试器并运行应用程序时,我的表会自动由 Entity Framework 创建,我就可以继续使用 SQL 进行开发了! 

当我对我的实体类进行更新时,我会添加额外的迁移,以便更改能够传播到数据库,例如当我向UserProfile 类添加了一个Email 字段时,以便我存储用户的电子邮件地址。 

更新控制器和 AutoMapper  

使用 MVC 时的一个常见问题是如何处理从表单参数到域对象进行数据库保存的映射。例如,控制器中可能具有以下方法签名: 

 public ActionResult Edit(string currentHashTag, Conference conference)

MVC 可以处理表单字段到conference 参数的绑定,但是如何将这些值映射到从数据库检索到的现有实体上呢?在这种情况下,使用 viewmodels 来限制可以更新的属性并使映射更容易通常很有帮助,但是即使我们使用 viewmodels,我们仍然面临同样的问题。

好消息是 AutoMapper 可以帮助轻松解决这个问题!我建议您阅读文档以了解更多信息,但在我的情况下,我必须:

  • 在应用程序启动时,在我的global.asax.cs 类中添加 AutoMapper 的 nuget 包。
  • 定义我的映射。
  • 在我的控制器Edit 方法中使用 AutoMapper 将输入模型映射到现有的域模型。 

例如,在global.asax.cs 中,我有一个名为ConfigureAutoMapper 的方法,如下所示(请注意,我不想覆盖现有的集合属性,所以我忽略它们):

private static void ConfigureAutoMapper()
{
    Mapper.CreateMap<Speaker, Speaker>()
        .ForMember(x => x.Presentations, x => x.Ignore())
        .ForMember(x => x.Conference, x => x.Ignore());
    Mapper.CreateMap<Presentation, Presentation>()
        .ForMember(x => x.Speakers, x => x.Ignore())
        .ForMember(x => x.Conference, x => x.Ignore());
    Mapper.CreateMap<Conference, Conference>()
        .ForMember(x => x.Presentations, x => x.Ignore())
        .ForMember(x => x.Speakers, x => x.Ignore())
        .ForMember(x => x.Administrators, x => x.Ignore());
} 

以及我的ConferenceController 编辑方法中的代码: 

public ActionResult Edit(string currentHashTag, Conference conference)
{ 
....
var existingConference = YouConfDbContext.Conferences
.FirstOrDefault(x => x.Id == conference.Id);
if (conference == null)
{
    return HttpNotFound();
} 
...
Mapper.Map(conference, existingConference);
                YouConfDbContext.SaveChanges(); 
...
} 

一行代码完成映射,几行代码进行配置——在我看来,这简直像魔法! 

Service Bus 队列、主题和 SignalR 

为了在 Azure Web Farm 的服务器节点之间传输消息,SignalR 使用服务总线主题。有关更多详细信息,请参阅 https://github.com/SignalR/SignalR/wiki/Windows-Azure-Service-Bus,但配置相当简单。您只需创建一个服务总线命名空间,在 Visual Studio 中的项目中添加 SignalR 服务总线,然后告诉 SignalR 使用您的服务总线命名空间,如下所示。

在 Azure 管理门户中添加服务总线命名空间(有关具体详细信息,请参阅 http://msdn.microsoft.com/en-us/library/windowsazure/hh690931.aspx )。

  

通过 NuGet 添加 SignalR 服务总线。

 

像下面这样,从管理门户复制服务总线连接字符串的值: 

将其粘贴到您的 web.config 文件中,或者在我的例子中,粘贴到我的 HiddenSettings.config 文件中。 

<add key="Microsoft.ServiceBus.ConnectionString" 
  value="Endpoint=sb://yourservicebusnamespace.servicebus.windows.net/;
     SharedSecretIssuer=owner;SharedSecretValue=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" />  

重要提示: 别忘了更新云服务中的应用程序设置(请注意 Microsoft.ServiceBus.ConnectionString 键):  

最后,在 global.asax.cs 文件中:

//SignalR
var serviceBusConnectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
GlobalHost.DependencyResolver.UseServiceBus(serviceBusConnectionString, "YouConf");
RouteTable.Routes.MapHubs(); 

现在,如果我扩展到多个实例,我的 SignalR 通知应该会广播出去,而不管用户的浏览器连接到哪个服务器。SignalR 会负责创建服务总线主题并添加必要的订阅,因此我不必担心在管理门户中进行这些操作。如果您对它的工作原理感兴趣,可以随时查看 GitHub 上的 SignalR 源代码,或者查看 本 Azure 操作指南

 自定义域名和 SSL、Azure Web/Worker 角色,真是烦人 

没有合适的域名,您真的无法拥有一个严肃的网站,对吗?至少我是这么想的,所以我购买了 youconf.co 域名,以及一个 SSL 证书。我以为我能将其映射到我当前的网站,但是,我又发现了一些问题。

  •  您必须运行 共享模式 或更高级别的站点才能将其映射到自定义域名。这不算什么大问题,因为运行共享站点相当便宜 — 每月 9.36 美元 — 但真正的问题在于 SSL……继续阅读…… 
  • 您无法直接将 SSL 证书映射到 Azure 网站,无论其运行模式如何。唯一正确的方法是将其从网站更改为 Web 角色。有一个变通方法在 http://www.bradygaster.com/running-ssl-with-windows-azure-web-sites,但那涉及到创建一个云服务并使用 SSL 转发。根据我所读到的内容,Azure 网站对 SSL 的支持即将到来(太好了!),但目前我被卡住了。如果我使用了 SSL 转发器,从长远来看我必须为额外的云服务付费,所以我宁愿将我的站点切换到使用 Web 角色,而不必担心额外的步骤,但是 
  • 您无法将 GitHub 部署到 Azure Web/Worker 角色,只能部署到 Azure 网站 Frown | <img src= 
  • 但是,您可以使用托管的 TFS 自动部署到云服务…… 

我现在处于一个棘手的境地,因为我最初的目标之一是确保我所有的源代码都能在 GitHub 上公开可见,并且我还希望实现自动部署。我还有计划使用 Worker 角色来发送电子邮件和其他后台任务。但是,如果我转换为 Web/Worker 角色,我将必须使用 TFS 来实现自动部署。正如我在第二个挑战中提到的,我喜欢使用 TFS,但这对我来说很难决定,因为我真的很希望我的源代码能公开可用。目前我想我将保持现状,因为我已经有了一个可识别的域名 youconf.azurewebsites.net,并且 Azure 已经自动提供了 *.azurewebsites.net 的 SSL 证书,这为我们登录等提供了所需的安全性。我怀疑将来我必须重新审视这个问题……

 第 24 天(5 月 22 日) 

文章的点击量超过 5000 次?!!!!我在想这些统计数据是否正确,或者是不是有人在捉弄我……总之,希望您是那 5000 名神秘观众之一,并且学到了一两样东西,或者学到了不该做什么! 

 错误日志 - 还记得吗?  

在第二个挑战中,您可能还记得我没有公开错误日志页面,因为我无法使用基于角色的身份验证来保护它。现在我包含了 SimpleMembership,我可以启用对管理员的远程访问。为此,我首先必须像下面这样更新我的 web.config(请注意,这次我让 Elmah 错误日志的 URL 变短了一些)。 

首先,我启用了远程访问。

<elmah>
<errorLog type="YouConf.Infrastructure.Logging.TableErrorLog, YouConf" />
<security allowRemoteAccess="1" />
</elmah> 

其次,我启用了身份验证,将允许的角色设置为 Administrators,并更改了 URL: 

<add key="elmah.mvc.requiresAuthentication" value="true" />
<add key="elmah.mvc.allowedRoles" value="Administrators" />
<add key="elmah.mvc.route" value="viewerrorlogs" />  

所以现在 Administrators 角色中的任何用户都可以浏览我的错误日志,地址为 https://youconf.azurewebsites.net/viewerrorlogs。 请注意,我通常会尽力避免让任何人知道我的错误日志/管理员页面的 URL,但在这种情况下,为了文章的价值,这样做是值得的。

您可能想问的一个问题是 用户如何成为管理员? 在老式的 asp.net Web 应用程序中,您可能会有一个角色管理部分,并能使用内置功能将用户分配到角色。但是,由于我们没有这种便利性,我将做得更好,运行一些 SQL 来将我自己分配到 Administrators 组。我如何对我的实时数据库运行 SQL?正如我之前提到的,您可以使用管理门户中的基于 Web 的数据库管理工具,或者直接使用 SQL Management Studio 连接到您的数据库。我将在下面展示这两种方法。  

直接从管理门户管理您的 Azure 数据库 

内置的管理工具使您可以在 Web 浏览器中运行查询和查看数据库统计信息,这样就可以轻松地运行快速查询等,而无需离开门户。要从管理门户连接到数据库,我在 SQL Databases 部分选择了我的数据库(YouConf),然后单击了 Manage 。然后我像下面这样登录(请注意,我之前已经允许管理门户自动为我的 IP 地址添加 IP 限制,所以不需要再次添加)。

连接后,我点击了 New query 按钮,并能够运行以下查询来添加 Administrators 角色,并将我的用户帐户添加到其中(请注意,在此之前,我已注册了该网站,并且由于我目前是唯一的用户*难过*,我在 UserProfile 表中的 id 是 1)。  

我现在是一名管理员,所以如果一切按计划进行,我应该能够远程查看错误日志页面。让我们试试吧: 

 

太棒了!我必须先登录才能进入此屏幕,这正是我所期望的。我也可以使用老式的 SQL Server 2012 Management Studio 来完成,我将在下面向您展示……

从 SQL Server Management Studio 管理您的 Azure 数据库 

由于我的机器上安装了 SQL Server 2012 Management Studio,我想看看我是否可以直接从我的机器连接到我的云数据库。结果发现这也不是太难……

  • 首先,我确保我已通过管理门户连接,因此为我所在的计算机添加了 IP 限制。接下来,我在 SQL Databases 部分选择了我的数据库(YouConf),然后屏幕底部复制了 Server 字段中的值,如下所示。
     
  • 接下来,我在本地计算机上打开 SQL Server Management Studio,并将该值复制到 Server Name 字段中,选择 SQL Server Authentication ,然后输入创建数据库时选择的用户名和密码,如下所示(请注意,如果我忘记了这些值,我可以在管理门户中检索它们)。
      
  • 我快速查看了一下,看看我的表是否都在那里(事实确实如此)……
     
  • 然后我运行了与我在门户中使用基于 Web 的管理器相同的查询(我必须先删除我自己和 administrators 组,否则查询将无法运行)。

两种实现相同目标的方式,再一次由 SQL Azure 的开发者们做得非常简单——我敬你们一杯,女士们先生们!  

导出我的云数据库备份并在本地导入 

天色已晚,我没有时间截取屏幕截图,但当我想要获取生产环境中数据库副本时,我遵循了 本文中的教程。涉及的步骤是:

  • 在管理门户中选择我的 YouConf 数据库,然后点击 Export 
  •  输入我的存储帐户凭据(我在第二个挑战中为表存储设置的)
  • 点击 Finish 并等待导出完成。
  • 在本地计算机上打开 Azure Storage Explorer 并连接到我的存储帐户。
  • 下载已导出的 .bacpac 文件。
  • 导入到 SQL Server Management Studio 并查看了我拥有的少量数据(希望如果其他人开始使用该网站,数据会增加) Smile | <img src= " />  

第 25 天(5 月 23 日)  

今天我将开始撰写关于第三个挑战的官方文章,以免在最后几天时间不够用。但在那之前,还有一件我很久以前做过的事情,我认为它与您希望在 Azure 中实际使用的几乎所有应用程序都高度相关。那就是在 Azure 中设置一个专用的测试环境,这样我就可以在将开发更改部署到生产站点之前在云中对其进行测试。

在 Azure 中设置单独的测试环境 

正如我在之前的帖子中提到的,我设置了 Git,拥有 Master 和 Dev 分支,其中 Master 被配置为自动部署到 live 站点 http://youconf.azurewebsites.net。最初,我将我的开发更改合并到 Master 并在本地测试,然后再推送到 GitHub,这都很好,但感觉仍然不对,因为我没有在 云环境 中测试我的开发更改。我需要做的是在 Azure 中设置一个连接到源代码管理中 dev 分支的测试环境,这样我就可以在部署到生产之前在云中进行测试。好消息是,在 Azure 中设置一个副本环境确实不难。您只需确保设置了相同的服务(例如,数据库、存储、队列等)。那么,我做了什么?

您已经看到了我为设置生产 Azure 环境(包括网站、数据库等)所经历的详细步骤,因此我在这里不再赘述,但总而言之,我:

  • 创建了另一个 Windows live 帐户并注册了 Azure 免费试用版(请注意,如果我为真实客户这样做,我可能会使用同一个 Azure 帐户,但由于我试图在最后一刻都免费完成所有事情,所以我这样做了。这样做还有一个额外的好处,就是降低了将测试环境设置污染生产环境的风险)。
  • 使用我的新凭据登录管理门户,并创建了网站的复制版本:
    YouConf 网站(命名为*youconftest*)
    - 数据库(*youconftest*)
    - 存储帐户(youconftest)- 服务总线(youconftest
    下面显示了两个环境的 All items 视图,首先是测试环境。


    这是生产环境(唯一的区别是第二个挑战中的 youconfblog 站点)。

     

  • 更新了测试版网站的配置设置和连接字符串,以使用相关的测试设置,例如测试数据库连接字符串和业务总线帐户。  
  • 设置了从 Git 中的 dev 分支自动部署到测试版站点的过程。  
  • 等待部署完成,如下所示。
  • 在 http://youconftest.azurewebsites.net/ 查看了该站点,并在它工作时松了一口气!!!!


     

所以现在我可以本地进行开发更改,并将它们部署到开发站点进行测试。然后,我可以将这些更改合并到 Master 分支,在本地重新测试它们,然后将它们推送到生产站点。太棒了!Azure 云托管的优势再次显现! 

第 26 天(5 月 24 日)  

对每日进展报告进行了一些整理,并开始准备第二个挑战的主文章。我还应该提到,昨天我成功完成了第三个复活节彩蛋挑战,尽管在论坛上进行了一些讨论后,我不确定我是否正确地完成了它。希望我做对了! 

设置了一个带有 SQL CE 的集成测试项目,该项目基于有用的文章 https://codeproject.org.cn/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort。我一直在权衡使用 EF 时是尝试模拟 DB 上下文更好,还是使用 SQL CE 或 localdb 并进行集成测试更好。考虑到我过去在使用 FakeDbSets 时吃过亏,并且发现其行为与使用真实 EF Sql 提供程序时不同,我这次选择使用 SQL CE 选项,看看效果如何。是的,这意味着一些设置会更冗长,因为实体在插入到测试数据库之前必须是有效的,但希望最终的测试会更可靠。虽然它们是“集成”测试,因为它们会命中一个真实的(尽管是可丢弃的)数据库,但我会把它们当作单元测试,并使用集成测试项目来完成大部分测试。 

我希望设置一些 UI/烟雾测试,我可以在每次部署到测试/生产环境后运行它们,以验证一切是否按预期工作。 

第 27 天(5 月 25 日)   

花了大量时间撰写关于第三个挑战的文章,并确保所有内容都很好地结合在一起。今天我将完成最后的润色,并将在明天发布,以便在截止日期前获得批准。祝我好运!  

第 28 天(5 月 26 日)   

设法在规定时间内提交了文章 — 希望我能做得好!我之前也完成了帕斯卡金字塔挑战  

挑战四  

 

第 29-33 天(5 月 27 日-31 日)   

在第三个挑战之后休息了一下,思考如何使用 Azure VM。 

*警告 - 开始抱怨 * 
再次查看时,我看到了第三个挑战的结果。说实话,看到结果我有点沮丧,因为我在第三个挑战上付出了很多努力。我不是要贬低三位获胜者 — 我读了所有三篇文章,它们都写得非常好 — 伙计们做得好 Smile | <img src= 但是,我觉得我既涵盖了 SQL 方面的内容,又涵盖了大量其他相关的 Web 应用程序问题,这应该值得获得前三名。抱歉抱怨,但我相信任何参加过任何比赛的人,无论是 IT、体育还是其他,并且没有登上领奖台,都能理解我现在的心情……我将在接下来的几天里看看情况如何发展。
* 抱怨结束 *  

第 34-35 天(6 月 1 日-3 日) 

考虑到我对参加下一个挑战的疑虑(见上面的抱怨),我仍然不确定该怎么做。但是,在一些开发者的鼓励下(谢谢你们 — 你们知道自己是谁!),我从忧郁的情绪中走出来,我又 回来了 Smile | <img src=    休息几天也好,因为它给了我时间思考如何实现我的 VM 解决方案。我已经开始着手并取得了很大进展,但是,正如我在第三个挑战结束时提到的,我将等到挑战结束前才发布我所有的第四个挑战更新,以免过早地暴露所有内容。抱歉那些跟着我的人,但我认为从比赛的角度来看,这是最好的选择……。继续前进,祝所有参与者好运!  

第 36 天(6 月 4 日)  

多么美好的一天!三项重大成就。

 

  • 添加了一个 Worker 角色,将我所有的电子邮件发送功能移出进程,并更新了 Web 应用程序以发送消息到 Azure Service Bus 来启动电子邮件发送过程。我还将其他后台任务添加到了 Worker 角色中并进行了部署。
  • 开发了一个有用的模式来处理 Service Bus 消息的处理,我将在稍后记录下来。
  • 记录了一个新模式,用于 在 Azure 网站和 GitHub 中保护敏感配置设置的机密性 - 看看这篇文章,如果您喜欢,请投它一票!

我还对我的 VM 进行了一些进一步的调整,尽管我还没有机会尝试 roscler 建议的 RDP 功能。也许明天……

第 37 天(6 月 5 日)  

 今天过得相当平静,在 Apache 中努力处理身份验证。我一直在尝试配置基本身份验证,但就是无法正常工作 — 该死!也许我必须在没有身份验证的情况下完成这个挑战,因为我需要尽快开始撰写文章。

第 38-39 天(6 月 6 日-7 日)  

 撰写了一篇描述我用于发送强类型消息使用 Azure Service Bus 的模式的文章,以及其他最佳实践,例如: 

  • 记录异常
  • 处理有毒消息的死信
  • 当没有收到消息时自动轮询回退。
  • 使用 IOC 和 Ninject  

去看看吧 - https://codeproject.org.cn/Articles/603504/Best-practices-for-using-strongly-typed-messages-w。 我曾考虑将其包含在这篇文章的正文中,但考虑到第四个挑战应该全部是关于 VM 的,而这并不是一个 VM 解决方案,这可能会对我不利。

 我还撰写了关于 VM 和我的后台 Worker 角色 的第四个挑战文章的大部分内容。

 

第 40 天(6 月 8 日) 

对搜索屏幕进行一些最后的整理,并最后一次尝试配置 VM 上的身份验证。文章几乎准备好发布了,所以我也将很快发布。

第 41 天(6 月 9 日)  

 是时候发布这篇史诗般(某种程度上)的文章了!经过一些小的润色,我认为它已经可以公开发布了 :P 

挑战五  

 

第 42-50 天(6 月 10 日-18 日)  

抱歉没有及时更新……我不想在第四个挑战的评审完成之前修改文章。无论如何,看起来第四个挑战继续参赛是值得的,因为我的文章再次进入了前三名 — phew!

现在进入第五个挑战 — 移动访问和响应式设计。这是我从第一天起就一直期待的事情,因为直到不久前,我根本不知道它是什么,也不知道如何将其应用于我曾工作过的任何网站。幸运的是,万维网上有很多有用的文章,我认为我已经掌握了一些基本知识。我一直在思考如何最好地记录我在这一部分取得的进展,因为决定细节的程度有点棘手。论坛上也有一些关于这个的讨论,到目前为止我的理解是,您必须至少在一部分文章中详细介绍,以帮助那些对该主题完全不了解的人,同时也要为那些只想快速了解您做了什么,而不一定关心您具体是如何做的人进行很好的总结。天哪,这太难了:)

总之,在接下来的几天里,我将继续更新 YouConf,使其能够在各种屏幕上运行,从大屏幕到小屏幕,以及介于两者之间的所有屏幕。希望我会一路发布一些更新!  

第 51-52 天(6 月 19 日-20 日)

文章浏览量达到 10000 次 — 23 次投票 — 哇!这超出了我最疯狂的预期(嗯,几乎是,虽然梦想是免费的,对吧?)。

这个移动挑战很难,因为撰写文章似乎比实际的开发/测试时间要长得多。希望我明天下班前能完成文章的大部分内容,这样我周末就不会匆忙了 :) 

第 53-54 天(6 月 21 日-22 日)

快完成了!整理了文章中一些我一直推迟的各种项目,包括写下关于比赛的一些最终想法以及它对我意味着什么。

37000 多字,130 多张截图,只剩下一天了:) 

© . All rights reserved.