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

如何使用 Owin 和 Nancy 提供可重用的 Web 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (20投票s)

2016年4月3日

CPOL

13分钟阅读

viewsIcon

34179

downloadIcon

705

如何使用 owin 和 nancy 作为表示框架创建一个 Web 应用程序,将其嵌入到 DLL 中并包含到许多网站中。

引言

在这篇文章中,我将向您展示如何使用 OWIN 并将一个 DLL 转换成一个网站。这个库将被集成到一个 MVC 应用程序中,以创建一个可以重用的独立网站部分。

我学习这项技术是在我计划创建 InfoPage 时,这是我的一个个人项目。InfoPage 只是一个页面,可以在 MVC 应用程序中显示一些有用的信息(构建号、更改日志、已加载的程序集...)。如果您无法想象,可以想想 php_info() 函数,然后尝试将其移植到 .net 环境并将其扩展到所有 .net 的需求。

对于那个项目,我想创建一个只需要一次点击就可以完成的事情。起初,我只设想了一个 nuget 包下载,但又担心要打包 CSS 和 js。我不想用大量的文件弄脏 InfoPage 将被安装的原始项目,也不想假设某些库已经存在或要求用户管理它们。此外,我想使用一种可以更新而不会覆盖用户所做更改的工具。

经过一些研究,我了解到 OWIN 应用程序是最好的解决方案,因为它将所有代码都嵌入到一个文件中(我可以用 nuget 进行打包),但是……如何创建一个 OWIN 模块?如何从 DLL 提供静态内容?如何渲染 html?

在这篇文章中,我将分享我从实践中学到的一些经验,并尝试回答这些以及更多关于该主题的问题。然后,当然,我们将实际看看我是如何使用该技术实现 InfoPage 的。

为什么选择 OWIN?

是的,OWIN 只是一个“标准”。您可以找到许多图表来解释 OWIN 的含义,但实际上,它只是定义了 .NET Web 服务器和 Web 应用程序之间的标准接口。OWIN 如何帮助我们?OWIN 的主要目的是**解耦**服务器和应用程序。这意味着您不能“使用 OWIN”进行开发,因为它只是一个规范(没有工具、没有框架、没有 DLL...)。

使用 OWIN 进行开发需要一个框架,这一点很清楚,它的名字叫 Katana。

Katana 是一个框架,用于在 ASP.NET 和 OWIN 规范之间建立桥梁。

所以,使用这项技术,您可以实现一个站点或服务,并将其作为 IIS 站点的一部分或以独立托管模式提供服务。后一种方法非常有趣,因为它允许您创建不需要 IIS 或任何 Web 服务器即可运行的独立服务。

 

为什么选择 Nancy?

在玩了一会儿 OWIN \ KATANA 后,我缺少一些模板引擎和一些通用的框架来帮助我进行表示。我做了一个快速搜索,找到了 Nancy,它看起来非常有趣。起初我没有问太多问题,因为在我看来它非常吸引人。它支持许多模板引擎(Razor 是其中之一),并且实现了 MVC 模式,这正是我想要的。所以我安装并测试了它。

Nancy 最初是一个用于构建基于 HTTP 的服务的轻量级框架,因此它不需要 System.Web 也不需要在 Web 服务器中运行。它具有非常强大的功能,但对我实现 InfoPage 项目来说,开箱即用的功能并不足够,所以我不得不在配置上做一些调整,并解决了一些我在过程中发现的小问题。

在那次经历之后,我学到了一些关于 Nancy 的东西,您可以在下一节中找到最重要的几点。

教程:如何开始使用 owin \ Nancy 网站

我从一个演示项目开始,在那里我试验了 Nancy 的最重要功能,您可以在我的个人 git-hub 帐户上找到完全可运行的示例。在这个示例中,我给自己设定了一些简单的目标:

  1. 在库中设置一个 Nancy \ Owin 站点
  2. 集成到基于 MVC 的 Web 应用程序中
  3. 从 DLL 提供静态内容
  4. 从主 Web 应用程序覆盖视图

让我们看看我如何用几个简单的步骤来完成它。

在库中设置一个 Nancy \ Owin 站点

首先,您需要创建一个 MVC 网站和一个解决方案内的库,当然,您还需要在库中添加 Nancy.Owin。这部分非常简单,我们只需要从 nuget 中包含 nancy.owin。安装此包将解决 Nancy 和 OWIN 的所有依赖关系,因此大部分工作只需单击一个按钮即可完成。之后,您应该会看到一个类似的解决方案:

Test owin nancy project tutorial how-to

Nancy 项目通过**约定**工作,这意味着您必须将事物放在正确的位置。标准文件夹是:

  • Views:包含任何渲染模板(Razor、SuperSimpleViewEngine...)的视图。
  • Model:包含 MVC 模型使用的所有类。
  • Contents:包含所有静态资源。放置在此处的所有资源(CSS、js、imgs)都必须标记为“嵌入资源”。
  • Modules:包含 Nancy“模块”,即一些控制器。

 

Nancy 使用一些类似于 MVC 控制器的东西,称为“模块”,您需要通过创建一个继承自 NancyModule 的类来实例化一个模块。在该类的构造函数中,您可以映射路径并配置使用哪个模型和哪个视图来处理该请求。考虑到 MVC,我们实现了一些类似于操作的方法,但在这种情况下,我们使用显式映射而不是路由,并使用 lambda 而不是函数。

一个非常简单的模块可能如下所示:

      public class WelcomeModule : NancyModule
    {
        public WelcomeModule()
        {
            string baseHome = ConfigurationManager.AppSettings["TestOwinNancy:home"];
            baseHome = (baseHome) ?? "home";
            Get["/"+baseHome] = _ => {
                var model = new { title = "We've Got Issues..." };
                return View["home.html", model];
            };
        }
    }

在这个示例中,我从配置中获取一个 URL,并使用它来提供 html。我的模型只是一个带有 title 的对象,我使用 home.html 视图通过此模板将模型转换为 html。

 

集成到基于 MVC 的 Web 应用程序中

为了将我们的模块集成到 Web 应用程序中,首先,我们需要通过引用项目或程序集来包含该库。这很容易。但是如何告诉 MVC 有东西需要发送到我们的 Nancy 模块呢?很简单,我们只需要在 **Startup.cs** 中添加一小段配置:

    
[assembly: OwinStartupAttribute(typeof(TestOwinNancy.Startup))]
namespace TestOwinNancy
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);

            app
           .UseNancy(options =>
           {
               options.Bootstrapper = new ResourceBootstrapper();
               options.PerformPassThrough = (context => context.Response.StatusCode == HttpStatusCode.NotFound);
           });

           app.UseStageMarker(PipelineStage.MapHandler);
        }
    }
}

 

这段代码只是告诉主 MVC 应用程序有一个 OWIN 模块需要集成,并将一些配置设置传递给 Nancy。在此示例中,有一些设置使用 ResourceBootstrapper 而不是标准的一个(它允许在引用的 DLL 中包含视图或其他资源),还有 PerformPassThrough 选项,它简单地定义当 Nancy 未处理该资源时,所有内容都将照常进行。

从 DLL 提供视图内容

这部分有点复杂,因为我们需要对配置进行一些调整。没什么特别的,只需遵循几个步骤。

  1. 首先,所有需要在运行时提供的资源都必须包含在 DLL 中。这意味着需要将每个资源标记为“嵌入资源”。这将**保持相对路径**将文件包含在 DLL 中。
  2. 我们需要告诉 Nancy 在 DLL 中查找文件,而不是在 Web 站点中查找。这可以通过实现我们的引导程序来完成。实现这一点的部分显示在下面的代码片段中,但请注意,我们稍后需要更多自定义...
     public class ResourceBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ConfigureApplicationContainer(TinyIoCContainer container)
        {
            
            base.ConfigureApplicationContainer(container);
            ResourceViewLocationProvider.RootNamespaces.Add(GetType().Assembly, GetType().Assembly.GetName().Name+".Views");

        }



        protected override NancyInternalConfiguration InternalConfiguration
        {
            get { return NancyInternalConfiguration.WithOverrides(OnConfigurationBuilder); }
        }

        private void OnConfigurationBuilder(NancyInternalConfiguration x)
        {
            x.ViewLocationProvider = typeof(_ResourceViewLocationProvider);
            
        }
    }

 

从 DLL 提供静态内容

我们需要让 ASP.NET 管理来自模块的请求,所以如果您还没有在应用程序中完成,您必须在 web.config 中放置 runAllManagedModulesForAllRequests="true"。这一步与 owin 或 Nancy 无关,但 ASP.NET 需要它来动态提供静态文件扩展名。

    public class ResourceBootstrapper : DefaultNancyBootstrapper
    {
        protected override void ConfigureApplicationContainer(TinyIoCContainer container)
        {
            
            base.ConfigureApplicationContainer(container);
            ResourceViewLocationProvider.RootNamespaces.Add(GetType().Assembly, GetType().Assembly.GetName().Name+".Views");
            //https://groups.google.com/forum/#!topic/nancy-web-framework/9N4f6-Y4dNA

        }


        protected override void ConfigureConventions(NancyConventions nancyConventions)
        {


            nancyConventions.StaticContentsConventions.Add(EmbeddedStaticContentConventionBuilder.AddDirectory("Content", GetType().Assembly, "Content"));
            base.ConfigureConventions(nancyConventions);
        }

        protected override NancyInternalConfiguration InternalConfiguration
        {
            get { return NancyInternalConfiguration.WithOverrides(OnConfigurationBuilder); }
        }

        private void OnConfigurationBuilder(NancyInternalConfiguration x)
        {
            x.ViewLocationProvider = typeof(_ResourceViewLocationProvider);
            x.StaticContentProvider = typeof(DefaultStaticContentProvider);
            
        }
    }

在第二步中,我们添加了 StaticContentProvider,并添加了一些约定来解释使用哪个文件夹来存储文件。

注意:在我写这篇文章的时候,我从 git-hub 上的官方仓库复制了 EmbeddedStaticContentConventionBuilder.cs 到我的演示应用程序,因为它似乎在 Nancy 框架中缺失。请在遵循我的方式之前检查一下,因为在未来的版本中它可能会被包含进来。

 

从主 Web 应用程序覆盖视图

以上所有设置都可以正常工作,但无法通过简单地覆盖主应用程序中的文件来允许您覆盖默认行为。在这种类型的应用程序中,可以预期通过在主项目中实现一个同名视图来覆盖它。经过一些搜索,我发现将一个与 DLL 中的文件同名的视图放在主应用程序的 Views 文件夹中会导致应用程序崩溃。这是因为 ResourceViewLocationProvider 的实现中有一个检查重复视图的代码会抛出异常。在我的情况下,足以扩展该类并覆盖这部分代码,我只是更改了这部分代码。

     if (resourceStreams.Count() == 1 && !RootNamespaces.ContainsKey(assembly))
            {
                //This is commented out to allow view override
                //var errorMessage =
                //    string.Format("Only one view was found in assembly {0}, but no rootnamespace had been registered.", assembly.FullName);

                //throw new InvalidOperationException(errorMessage);
            }

在您的项目中,您可以简单地从 git-hub 复制我的文件:git-hub

所以在此之后,您需要将一个与您想要覆盖的文件同名的文件放在主应用程序的视图文件夹中,以覆盖从库中提供的同名文件。

 

InfoPageLibrary:OWIN\NANCY 实战

实现

在研究完 OWIN 和 Nancy 之后,我得到了开始我最初项目所需的一切。所以首先我遵循了以上所有步骤(我将省略它们,以免过于冗长...),并得到了一个完全工作的项目,其中有一个用作测试的 Web 应用程序和一个核心库。这个库具有与最后一步相同的配置,因此它支持嵌入式资源、视图覆盖并且可以提供静态内容。在那时,将 bootstrap、jquery 添加到项目中以开始使用 OWIN DLL 中的 Nancy 模板是非常自然的。

之后,我的项目看起来像这个截图。

 

Info Page: How to use owin and nancy to create a web application

 

首先,我通过创建配置类和配置器类来处理配置问题。我的目标是通过调用函数传递一个动作来设置配置。这种方法有助于人们快速配置设置,并利用自动完成功能,我发现它非常有用。这就是如何使用,然后是一些代码片段来解释我如何实现的。

    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);

            InfoPageConfigurator.Configure(app, 
                x => {
                    x.BaseUrl = "custom-info";
                    x.ApplicationName = "My Sample Application";
                });
        }
    }
    
    //I defined a configuration class
    public class InfoPageConfiguration
    {
        public String BaseUrl { get; set; }
        public String ChangeLogPath { get; set; }
        public String LicensePath { get; set; }
        public String InfoPath { get; set; }
        public bool ShowInfo { get; set; }
        public bool ShowLicense { get; set; }
        public bool ShowChangeLog { get; set; }
        public Assembly MainAssembly { get; set; }
        public String ApplicationName { get; set; }
        public String ApplicationSubtitle { get; set; }
    }
    
    //Then I define a "configuration" class that apply configuration
    public static class InfoPageConfigurator
    {
        private static InfoPageConfiguration _current;
        public static InfoPageConfiguration Configuration { get { return _current ?? (_current = new InfoPageConfiguration()); } }

        public static void Configure( IAppBuilder app, Action conf)
        {

           app.UseNancy(options =>
          {
              options.Bootstrapper = new ResourceBootstrapper();
              options.PerformPassThrough = (context => context.Response.StatusCode == HttpStatusCode.NotFound);
          });


            InfoPageConfiguration settings = new InfoPageConfiguration();
            //... preset configuration here
            conf.Invoke(settings);
            //... user can change values as he prefer

            InfoPageConfigurator._current = settings;
           

        }
    }

 

在此之后,我们需要实现服务,首先我们从模型开始。这很简单,我只是创建了 InfoPageModel.cs 类,我在其中放置了我渲染所需的所有数据。

     public class InfoPageModel
    {
        public string ChangeLogs { get; set; }
        public string License { get; set; }
        public string Info { get; set; }
        public AssemblyMetadata MainAssembly { get; set; }
        public List LoadedAssemblies { get; set; }
        public InfoPageConfiguration Conf { get; set; }
    }

模型准备好后,我创建了一些辅助方法来用数据填充模型,这些数据是从配置和运行时值计算出来的。这将产生以下 Nancy 模块。

    public class WelcomeModule : NancyModule
    {
        public WelcomeModule()
        {
            string baseUrl = InfoPageConfigurator.Configuration.BaseUrl;
            baseUrl = (baseUrl) ?? "info";
            Get["/" + baseUrl] = _ =>
            {
                var model = InfoHelper.GetInfoPage(InfoPageConfigurator.Configuration);

                return View["home.html", model];
            };
        }
    }

关于视图“home.html”,我使用了标准的 Nancy 渲染引擎,我只在这里复制一小段代码来展示它的样子,完整版本可以在源代码中找到。

<div class="tab-pane " id="assembly" role="tabpanel">
<h2>Assembly info</h2>
@Each.LoadedAssemblies @EndEach

<table class="table table-bordered table-striped">
	<thead>
		<tr>
			<th>Title</th>
			<th>FullName</th>
			<th>Version</th>
			<th>Architecture</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td>@Current.Title</td>
			<td>@Current.FullName</td>
			<td>@Current.Version</td>
			<td>@Current.Architecture</td>
		</tr>
	</tbody>
</table>
</div>

<p>@If.Conf.ShowChangeLog</p>

<div class="tab-pane " id="changelogs" role="tabpanel">
<h2>Changelogs</h2>
@Model.ChangeLogs</div>

<p>@EndIf</p>

如何使用

InfoPage 的使用非常简单,您只需要从 nuget 下载、配置和使用它。

 

1. 从 nuget 下载

Install InfoPage library from nuget

2. 配置它

如果您不太在意自定义,只需写这个即可。当然,如果您好奇,只需输入 x. 然后按 CTRL+SPACE :-)

     InfoPageConfigurator.Configure(app, 
                x => {
                    x.ApplicationName = "My Sample Application";
                });
    

 

3. 开始跟踪更改日志

要开始跟踪更改日志,只需在您的项目中创建一个名为 changelog.md 的文件即可。

Solution special files for changlog track

您将使用 Markdown 支持在此文件中编写更改。更改日志的示例可以是:

    #2016-03-15 
    Minor changes to core application, new feature "XXX" was introduced
        * Css breaks home page: fixed
        * Home page not compatible with IE9: fixed
    
    #2016-03-10
    Just few bug fixed and some css edited. 

 

4. 开始使用 InfoPage

很简单。在浏览器中输入 /info,您就会看到类似这样的内容。您可以看到一些有关您应用程序的有用的信息。

您可以自定义 InfoPage 的行为设置配置。InfoPageConfiguration.cs 的所有属性都可以由用户更改。

属性 含义
BaseUrl 字符串 是 Info Page 响应的 URL。/info 是默认值。
ChangeLogPath 字符串 相对于应用程序根目录的更改日志文件的路径。此文件支持 markdown 语法。
LicensePath 字符串 相对于应用程序根目录的许可证文件的路径。此文件支持 markdown 语法。
InfoPath 字符串 相对于应用程序根目录的应用程序信息文件的路径。此文件支持 markdown 语法。
ShowInfo bool 默认情况下,如果找到 Info 文件,则为 true。可以编辑以强制隐藏文件,即使存在,或者无论如何都显示它。
ShowLicense bool 默认情况下,如果找到 License 文件,则为 true。可以编辑以强制隐藏文件,即使存在,或者无论如何都显示它。
ShowChangeLog bool 默认情况下,如果找到 Change 文件,则为 true。可以编辑以强制隐藏文件,即使存在,或者无论如何都显示它。
应用程序名称 字符串 这是应用程序名称。如果为空,将使用主程序集名称。此字段仅用于表示目的。
ApplicationSubtitle 字符串 这是将在信息页中显示的一个副标题。如果为空,将使用主程序集名称。此字段仅用于表示目的。将其设置为空字符串可隐藏。

 

然后您可以通过几种方式使用 InfoPage:

跟踪应用程序更改

是的,我们知道有仓库可以评论您的提交,但“更改”的含义更侧重于利益相关者和最终用户,而不是开发人员。在许多项目中,我们需要跟踪并让用户了解正在运行的软件版本包含哪些更改或新功能。这可以通过引入 InfoPage 并跟踪 changelog.md 文件中的最重要更改来轻松实现。在我的项目中,我为每次迭代写一行 h2 条目,并在其中描述了迭代中完成的更改。在有用时,我还添加一个项目符号列表来报告所有修复和小型更改。因此,发布后,最终用户或利益相关者可以看到发布了哪些功能或修复。

显示许可证

此功能相当自明。只需在应用程序的根目录中创建一个名为 license.md 的文件,并在其中复制您的许可证以向用户显示。

显示有关应用程序的信息:您放在 info.md 中的内容将显示在信息页的主页上,并且它类似于此自述文件。关于您可以在其中放置的内容没有书面规则,也没有长度限制。例如,在开源项目中,可能会有一些关于作者和项目的信息。

显示有关构建和程序集的信息

此功能是开箱即用的功能,对于管理多个环境非常有用,尤其是在您不基于持续集成进行部署时。实际上,只需浏览 infopage 路径,您就可以看到主程序集的版本以及所有带有版本号的已加载程序集。因此,只需查看它,您就可以了解环境之间是否存在差异,或者在生产环境中运行的代码是否是您预期的。

如何将其集成到我的应用程序中

最方便的方法是在某个地方放置一个链接来打开 infopage 路径,因为这是一个“系统”页面,不需要向最终用户显示。或者,您可以覆盖一个加载您 CSS 样式的视图,并将其集成到您的主题中,如果您需要的话。标准视图不使用 Razor,所以如果您想使用它,您需要包含 Nancy.Razor 扩展。请记住,这是一个 OWIN 应用程序,因此集成是有限的。该库背后的主要概念是易于按原样集成的东西,如果您需要更多自定义,请考虑在不使用 OWIN 的情况下将 InfoPage 集成到您的应用程序中。通过打开一个 issue 来问我。InfoPage 的最终通用方法是编辑视图,删除所有样式,并在您的应用程序中添加一个 iframe 来加载 InfoPage 路径。

InfoPage screenshot: info displayed at runtime

结论

在这篇文章中,我们学习了使用 Nancy 实现 OWIN 应用程序的基础知识,该应用程序可以作为 ASP.NET MVC 标准应用程序的一部分提供。这项技术对于创建可以重用和轻松共享的小型“独立”应用程序很有用,例如在 nuget 频道中。本文不是一个指南,但它是一个开始实践实现 OWIN 应用程序的良好入门点。

如果您想了解更多关于 InfoPage 项目的信息,可以查看 git-hub 项目,或者从 nuget 下载该库并进行尝试。

一如既往,欢迎提出建议和回答,这次是关于本文以及 InfoPage 项目。

历史

  • 2016-04-03:codeproject 提交
  • 2016-03-30:本文初稿
  • 2016-03-25:首次发布到 nuget
  • 2016-03-24:InfoPage 实现;
  • 2016-03-20:Nancy Owin 第一个教程

 

参考文献

 

© . All rights reserved.