深入浅出之Smarty模板引擎工作机制(一) - 曾是土木人 - 博客园

mikel阅读(1479)

来源: 深入浅出之Smarty模板引擎工作机制(一) – 曾是土木人 – 博客园

深入浅出Smarty模板引擎工作机制,我们将对比使用smarty模板引擎和没使用smarty模板引擎的两种开发方式的区别,并动手开发一个自己的模板引擎,以便加深对smarty模板引擎工作机制的理解。

在没有使用Smarty模板引擎的情况下,我们都是将PHP程序和网页模板合在一起编辑的,好比下面的源代码:

复制代码
<?php
$title="深处浅出之Smarty模板引擎工作机制";
$content="Smarty模板引擎原理流程图";
$auth="MarcoFly";
$website="www.MarcoFly.com";
?>
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><?php echo $title?></title>
</head>
<body>
<p>内容:<?php echo $content?></p>
<p>作者:<?php echo $auth?></p>
<p>网址:<?php echo $website?></p>
</body>
</html>
复制代码

输出到浏览器的结果截图:

查看HTML源代码:

复制代码
<!DOCTYPE HTML>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>深处浅出之Smarty模板引擎工作机制</title>

</head>

<body>

<p>内容:Smarty模板引擎原理流程图</p>

<p>作者:MarcoFly</p>

<p>网址:www.MarcoFly.com</p>

</body>

</html>
复制代码

程序比较小的情况下这种开发方式尚且不方便,一旦要开发一个大的WEB项目,就必须得使用到模板引擎。

使用模板引擎的情况下:
我们的开发方式将有所改变,美工人员只管做模板,后台开发人员专心写自己的程序。
一个web项目就可以分为模板文件PHP程序
比如:
美工人员就可以这样编辑网页模板文件:
index.dwt源代码

复制代码
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><{$title}></title>
</head>
<body>
<p>内容:<{$content}></p>
<p>作者:<{$auth}></p>
<p>网址:<{$website}></p>
</body>
</html>
复制代码
而后台WEB开发人员可以专注于PHP代码的书写:
index.php
复制代码
<?php
    include "./Smarty.ini.php";
    $title="深处浅出之Smarty模板引擎工作机制";
    $content="Smarty模板引擎工作机制流程图";
    $auth="MarcoFly";
    $website="www.MarcoFly.com";
    $tpl->assign("title",$title);
    $tpl->assign("content",$content);    
    $tpl->assign("auth",$auth);
    $tpl->assign("website",$website);
    $tpl->display("index.dwt");
?>
复制代码

从以上两段简单的演示代码可以看出,前台模板文件没有涉及到任何关于PHP的代码,只有几个看似陌生的标签<{$title}><{$content}>,而后台的php程序代码也没有涉及到前台的HMTL代码
      参考下图对比这两种开发方式的区别



通过对比,我们得出结论:在使用模板引擎后,原先需要使用PHP编写的地方,现在只需要用模板引擎提供标签的形式来代替了。
注:Smarty模板引擎默认的标签形式是{$xxx},如,{$title},{$content}
当然我们可以初始化为自己想要的标签形式,如我将其初始化为:<{$xxx}>的形式),如,<{$title}>、<{$content}>

不知各位看官有木有觉得奇怪,<{$title}>、<{$content}>根本就不是PHP的语法形式,那最终又是如何被输出到客户的浏览器中的,是否另有玄机?带着这个疑问,我们继续深究......
  其实,这一切的一切都是由Smarty模板引擎这双神秘的手在“暗中操作”着,经过Smarty模板引擎的“暗中操作”之后,起初的模板文件(index.dwt)经过Smarty“成功手术”之后,被改造为能在服务器端执行的PHP代码文件。
想看看模板文件(index.dwt)和后台的PHP程序(index.php)经过“手术”(即编译)之后的庐山真面目吗?
在此贴上经过模板引擎编译之后的编译文件的源代码:
复制代码
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title><?php echo $this->arr["title"] ?></title>
</head>
<body>
<p>内容:<?php echo $this->arr["content"] ?></p>
<p>作者:<?php echo $this->arr["auth"] ?></p>
<p>网址:<?php echo $this->arr["website"] ?></p>
</body>
</html>
复制代码
看到这里,各位看官是否恍然大悟,原来Smarty模板引擎的工作就是:将前台美工人员编写的模板文件(index.dwt)和后台开发人员编写的PHP程序(index.php)整合在一起,经过编译这一步骤之后,原先的模板标签被替换成了php代码。
为了方便大家理解,我简单的做了一张代码流程图:

如果你觉得很神秘,想更深入了解Smarty模板引擎是如何完成这一步骤的,可以看看深入浅出之Smarty模板引擎工作机制(二)

文章出自:WEB开发_小飞

转载请注明出处:http://www.cnblogs.com/hongfei/archive/2011/12/10/Smarty-one.html

知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案二 - 初久的私房菜 - 博客园

mikel阅读(1030)

来源: 知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案二 – 初久的私房菜 – 博客园

教程

01 | 模块化方案一

02 | 模块化方案二

其他教程预览

分库分表项目实战教程

Git地址: https://github.com/MrChuJiu/EasyLogger

01 | 前言

02 | 简单的分库分表设计

03 | 控制反转搭配简单业务

04 | 强化设计方案

05 | 完善业务自动创建数据库

06 | 最终篇-通过AOP自动连接数据库-完成日志业务

简介

开讲第二篇,本篇代码并非Copy的ABP,只是参考ABP的功能,进行的实现方案,让代码更加通俗易懂。代码的讲解思路和上一篇一样,但是不引用上篇的写法。

开始

第一步 基本操作

还是老样子,我们新建一个模块化接口类
新建接口 IAppModule (ps:项目中起的类名和方法名尽量对标ABP)

   /// <summary>
    /// 应用模块接口定义
    /// </summary>
    public interface IAppModule
    {
        /// <summary>
        /// 配置服务前
        /// </summary>
        /// <param name="context"></param>
        void OnPreConfigureServices();
        /// <summary>
        /// 配置服务
        /// </summary>
        /// <param name="context">配置上下文</param>
        void OnConfigureServices();
        /// <summary>
        /// 配置服务后
        /// </summary>
        /// <param name="context"></param>
        void OnPostConfigureServices();
        /// <summary>
        /// 应用启动前
        /// </summary>
        /// <param name="context"></param>
        void OnPreApplicationInitialization();
        /// <summary>
        /// 应用启动
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationInitialization();
        /// <summary>
        /// 应用启动后
        /// </summary>
        /// <param name="context"></param>
        void OnPostApplicationInitialization();
        /// <summary>
        /// 应用停止
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationShutdown();
    }

新建类 AppModule 继承 IAppModule

   public abstract class AppModule : IAppModule
    {
        public virtual void OnPreConfigureServices()
        {

        }

        public virtual void OnConfigureServices()
        {

        }

        public virtual void OnPostConfigureServices()
        {

        }

        public virtual void OnPreApplicationInitialization()
        {

        }

        public virtual void OnApplicationInitialization()
        {

        }

        public virtual void OnPostApplicationInitialization()
        {

        }
        public virtual void OnApplicationShutdown()
        {

        }
    }

第二步 预准备

这一步来完成ABP的DependsOnAttribute,通过特性进行引入模块,
这里参数 params Type[] 因为一个模块会依赖多个模块
新建类 DependsOnAttribute 继承 Attribute

/// <summary>
    /// 模块依赖的模块
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class DependsOnAttribute : Attribute
    {
        /// <summary>
        /// 依赖的模块类型
        /// </summary>
        public Type[] DependModuleTypes { get; private set; }

        public DependsOnAttribute(params Type[] dependModuleTypes)
        {
            DependModuleTypes = dependModuleTypes ?? new Type[0];
        }
    }

既然一个模块会包含多个模块的引用,那么就应该有一个存储的方式
新建类 ModuleDescriptor 该类来存储 自身和引用的其他模块

    /// <summary>
    /// 模块描述
    /// </summary>
    public class ModuleDescriptor
    {
        private object _instance;

        /// <summary>
        /// 模块类型
        /// </summary>
        public Type ModuleType { get; private set; }

        /// <summary>
        /// 依赖项
        /// </summary>
        public ModuleDescriptor[] Dependencies { get; private set; }

        /// <summary>
        /// 实例
        /// </summary>
        public object Instance
        {
            get
            {
                if (this._instance == null)
                {
                    this._instance = Activator.CreateInstance(this.ModuleType);
                }
                return this._instance;
            }
        }

        public ModuleDescriptor(Type moduleType, params ModuleDescriptor[] dependencies)
        {
            this.ModuleType = moduleType;
            // 如果模块依赖 为空给一个空数组
            this.Dependencies = dependencies ?? new ModuleDescriptor[0];
        }
    }

第三步 模块管理器

来到核心步骤,这里我们写模块管理器,白话就是存储模块和模块操作方法的一个类(同上一篇的StartupModulesOptions)
第一步肯定是模块的启动
我们新建 IModuleManager接口

 public interface IModuleManager : IDisposable
    {
        /// <summary>
        /// 启动模块
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        void StartModule<TModule>(IServiceCollection services)
            where TModule : IAppModule;
    }

紧跟新建类 ModuleManager 继承 IModuleManager, StartModule 先放在一边
这里的思路是:模块是从一个入口的根模块开始的慢慢的形成一个树状的引用关系,我们首先需要拿到所有的模块引用,并把他们从树叶为起点排列起来,依次注入。
(理解为A=>B=>C 那么注入的顺序应该是 C=>B=>A)

1.先来实现根绝入口递归获取所有的引用关系 我已经在方法中将每一步的注释都写上了

/// <summary>
        /// 获取模块依赖树
        /// </summary>
        /// <param name="moduleType"></param>
        /// <returns></returns>
        protected virtual List<ModuleDescriptor> VisitModule(Type moduleType) {

            var moduleDescriptors = new List<ModuleDescriptor>();
            // 是否必须被重写|是否是接口|是否为泛型类型|是否是一个类或委托
            if (moduleType.IsAbstract || moduleType.IsInterface || moduleType.IsGenericType || !moduleType.IsClass) {
                return moduleDescriptors;
            }

            // 过滤没有实现IRModule接口的类
            var baseInterfaceType = moduleType.GetInterface(_moduleInterfaceTypeFullName, false);
            if (baseInterfaceType == null)
            {
                return moduleDescriptors;
            }

            // 得到当前模块依赖了那些模块
            var dependModulesAttribute = moduleType.GetCustomAttribute<DependsOnAttribute>();
            // 依赖属性为空
            if (dependModulesAttribute == null)
            {
                moduleDescriptors.Add(new ModuleDescriptor(moduleType));
            }
            else {
                // 依赖属性不为空,递归获取依赖
                var dependModuleDescriptors = new List<ModuleDescriptor>();
                foreach (var dependModuleType in dependModulesAttribute.DependModuleTypes)
                {
                    dependModuleDescriptors.AddRange(
                        VisitModule(dependModuleType)
                    );
                }
                // 创建模块描述信息,内容为模块类型和依赖类型
                moduleDescriptors.Add(new ModuleDescriptor(moduleType, dependModuleDescriptors.ToArray()));
            }

            return moduleDescriptors;
        }
补: _moduleInterfaceTypeFullName 定义
        /// <summary>
        /// 模块接口类型全名称
        /// </summary>
        public static string _moduleInterfaceTypeFullName = typeof(IAppModule).FullName;
2.拿到依赖关系通过拓扑排序进行顺序处理 (ps:拓扑排序地址 https://www.cnblogs.com/myzony/p/9201768.html)

新建类 Topological 这块没啥特别要讲的根据链接去看下就好了

    /// <summary>
    /// 拓扑排序工具类
    /// </summary>
    public static class Topological
    {
        public static List<T> Sort<T>(IEnumerable<T> source, Func<T, IEnumerable<T>> getDependencies) {

            var sorted = new List<T>();
            var visited = new Dictionary<T, bool>();

            foreach (var item in source)
            {
                Visit(item, getDependencies, sorted, visited);
            }

            return sorted;
        }

        static void Visit<T>(T item, Func<T, IEnumerable<T>> getDependencies, List<T> sorted, Dictionary<T, bool> visited)
        {
            bool inProcess;
            var alreadyVisited = visited.TryGetValue(item, out inProcess);

            // 如果已经访问该顶点,则直接返回
            if (alreadyVisited)
            {
                // 如果处理的为当前节点,则说明存在循环引用
                if (inProcess)
                {
                    throw new ArgumentException("模块出现循环依赖.");
                }
            }
            else
            {
                // 正在处理当前顶点
                visited[item] = true;

                // 获得所有依赖项
                var dependencies = getDependencies(item);
                // 如果依赖项集合不为空,遍历访问其依赖节点
                if (dependencies != null)
                {
                    foreach (var dependency in dependencies)
                    {
                        // 递归遍历访问
                        Visit(dependency, getDependencies, sorted, visited);
                    }
                }

                // 处理完成置为 false
                visited[item] = false;
                sorted.Add(item);
            }
        }

    }

回到 ModuleManager 新建方法 ModuleSort

 /// <summary>
        /// 模块排序
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        /// <returns></returns>
        public virtual List<ModuleDescriptor> ModuleSort<TModule>() where TModule : IAppModule
        {
            // 得到模块树依赖
            var moduleDescriptors = VisitModule(typeof(TModule));
            // 因为现在得到的数据是从树根开始到树叶 - 实际的注入顺序应该是从树叶开始 所以这里需要对模块进行排序
            return Topological.Sort(moduleDescriptors, o => o.Dependencies);
        }
补:ModuleSort本来是个私有方法 后为了让模块使用者可以实现重写,请在 IModuleManager 加入
        /// <summary>
        /// 模块排序
        /// </summary>
        /// <typeparam name="TModule">启动模块类型</typeparam>
        /// <returns>排序结果</returns>
        List<ModuleDescriptor> ModuleSort<TModule>()
            where TModule : IAppModule;

3.模块已经可以通过方法拿到了就来实现 StartModule 方法 筛选去重 依次进行注入, 并最终保存到全局对象中

        /// <summary>
        /// 模块明细和实例
        /// </summary>
        public virtual IReadOnlyList<ModuleDescriptor> ModuleDescriptors { get; protected set; }

        /// <summary>
        /// 入口 StartModule 
        /// 我们通过传递泛型进来的 TModule 为起点
        /// 查找他的依赖树
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        /// <param name="services"></param>
        public void StartModule<TModule>(IServiceCollection services) where TModule : IAppModule
        {

            var moduleDescriptors = new List<ModuleDescriptor>();

            var moduleDescriptorList = this.ModuleSort<TModule>();
            // 去除重复的引用 进行注入
            foreach (var item in moduleDescriptorList)
            {
                if (moduleDescriptors.Any(o => o.ModuleType.FullName == item.ModuleType.FullName))
                {
                    continue;
                }
                moduleDescriptors.Add(item);
                services.AddSingleton(item.ModuleType, item.Instance);
            }
            ModuleDescriptors = moduleDescriptors.AsReadOnly();
        }
4.ModuleDescriptors既然存储着我们的所有模块,那么我们怎么执行模块的方法呢

入口通过调用下面的方法进行模块的方法调用

        /// <summary>
        /// 进行模块的  ConfigurationService 方法调用
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public IServiceCollection ConfigurationService(IServiceCollection services, IConfiguration configuration) {

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPreConfigureServices();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnConfigureServices();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPostConfigureServices();
            }

            return services;
        }
        /// <summary>
        /// 进行模块的  Configure 方法调用
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <returns></returns>
        public IServiceProvider ApplicationInitialization(IServiceProvider serviceProvider)
        {
            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPreApplicationInitialization();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnApplicationInitialization();
            }

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnPostApplicationInitialization();
            }

            return serviceProvider;
        }
        /// <summary>
        /// 模块销毁
        /// </summary>
        public void ApplicationShutdown()
        {
            // todo我觉得这里有点问题问 易大师
            //var modules = ModuleDescriptors.Reverse().ToList();

            foreach (var module in ModuleDescriptors)
            {
                (module.Instance as IAppModule)?.OnApplicationShutdown();
            }
        }

当然还漏了一个模块销毁,该方法在主模块被销毁的时候调用(ps: 我个人思路应该是从树叶开始进行,但是ABP对模块顺序进行了反转从根开始进行销毁,所以这里同上)

        /// <summary>
        /// 主模块销毁的时候 销毁子模块
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }

        protected virtual void Dispose(bool state)
        {
            this.ApplicationShutdown();

        }

第四步 Extensions

模块管理器写完了,那么这个方法如何调用呢来写我们的 Extensions
新建 RivenModuleServiceCollectionExtensions 类,让其完成ConfigurationService方法的模块调用

 /// <summary>
    /// 模块服务扩展
    /// </summary>
    public static class RivenModuleServiceCollectionExtensions
    {
        /// <summary>
        /// 添加Riven模块服务
        /// </summary>
        /// <typeparam name="TModule"></typeparam>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IServiceCollection AddRivenModule<TModule>(this IServiceCollection services, IConfiguration configuration)
            where TModule : IAppModule
        {
            var moduleManager = new ModuleManager();
            // 将模块都查询排序好
            moduleManager.StartModule<TModule>(services);
            // 调用模块 和 子模块的ConfigurationService方法
            moduleManager.ConfigurationService(services, configuration);
            // 注入全局的  IModuleManager
            services.TryAddSingleton<IModuleManager>(moduleManager);
            return services;
        }
    }

新建 RivenModuleIApplicationBuilderExtensions 类 ,让其完成Configuration方法的模块调用

 public static class RivenModuleIApplicationBuilderExtensions
    {
        /// <summary>
        /// 使用RivenModule
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <returns></returns>
        public static IServiceProvider UseRivenModule(this IServiceProvider serviceProvider)
        {
            var moduleManager = serviceProvider.GetService<IModuleManager>();

            return moduleManager.ApplicationInitialization(serviceProvider);
        }
    }

第五步 测试

新建一个测试项目,引入写好的模块化类库,在 ConfigureServices 中调用

services.AddRivenModule<MyAppStartupModule>(Configuration);

Configure 中调用

 app.ApplicationServices.UseRivenModule();

模块销毁演示(ps:这个是演示效果、实际是在项目停止的时候进行。)

 app.Map("/ApplicationShutdown", _ =>
            {
                _.Run((context) =>
                {
                    var moduleManager = app.ApplicationServices.GetService<IModuleManager>();
                    moduleManager.ApplicationShutdown();
                    return Task.FromResult(0);
                });
            });

补:

新建 MyAppStartupModule、TestModuleA、TestModuleB 继承AppModule。
MyAppStartupModule作为入口模块 引用 A => B 然后在模块方法中打印 Console.WriteLine 看效果

补充 给模块传递参数

新建 ApplicationInitializationContext 类

public class ApplicationInitializationContext
    {
        public IServiceProvider ServiceProvider { get; }

        public IConfiguration Configuration { get; }

        public ApplicationInitializationContext([NotNull] IServiceProvider serviceProvider, [NotNull] IConfiguration configuration)
        {
            ServiceProvider = serviceProvider;
            Configuration = configuration;
        }
    }

新建 ApplicationShutdownContext 类

 public class ApplicationShutdownContext
    {
        public IServiceProvider ServiceProvider { get; }

        public ApplicationShutdownContext([NotNull] IServiceProvider serviceProvider)
        {
            ServiceProvider = serviceProvider;
        }
    }

新建 ServiceConfigurationContext 类

 public class ServiceConfigurationContext
    {
        public IServiceCollection Services { get; protected set; }

        public IConfiguration Configuration { get; protected set; }


        public ServiceConfigurationContext(IServiceCollection services, IConfiguration configuration)
        {
            Services = services;
            Configuration = configuration;
        }

    }

修改 IAppModule 接口, 模块和实现都自己手动都同步一下

    /// <summary>
    /// 应用模块接口定义
    /// </summary>
    public interface IAppModule
    {
        /// <summary>
        /// 配置服务前
        /// </summary>
        /// <param name="context"></param>
        void OnPreConfigureServices(ServiceConfigurationContext context);

        /// <summary>
        /// 配置服务
        /// </summary>
        /// <param name="context">配置上下文</param>
        void OnConfigureServices(ServiceConfigurationContext context);

        /// <summary>
        /// 配置服务后
        /// </summary>
        /// <param name="context"></param>
        void OnPostConfigureServices(ServiceConfigurationContext context);

        /// <summary>
        /// 应用启动前
        /// </summary>
        /// <param name="context"></param>
        void OnPreApplicationInitialization(ApplicationInitializationContext context);

        /// <summary>
        /// 应用启动
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationInitialization(ApplicationInitializationContext context);

        /// <summary>
        /// 应用启动后
        /// </summary>
        /// <param name="context"></param>
        void OnPostApplicationInitialization(ApplicationInitializationContext context);

        /// <summary>
        /// 应用停止
        /// </summary>
        /// <param name="context"></param>
        void OnApplicationShutdown(ApplicationShutdownContext context);
    }

修改 ModuleManager的 ConfigurationService、ApplicationInitialization、ApplicationShutdown 方法给调用传递对应参数
这部分代码我就不贴了,会的大佬都能自己写,想看的去我的github直接下载源码看吧,麻烦老板们给点个星星!!!

项目地址

知识全聚集,逐个击破: https://github.com/MrChuJiu/Easy.Core.Flow

鸣谢

玩双截棍的熊猫

源地址:https://github.com/rivenfx/Modular

知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案一 - 初久的私房菜 - 博客园

mikel阅读(808)

来源: 知识全聚集 .Net Core 技术突破 | 如何实现一个模块化方案一 – 初久的私房菜 – 博客园

教程

01 | 模块化方案一

02 | 模块化方案二

其他教程预览

分库分表项目实战教程

Git地址: https://github.com/MrChuJiu/EasyLogger

01 | 前言

02 | 简单的分库分表设计

03 | 控制反转搭配简单业务

04 | 强化设计方案

05 | 完善业务自动创建数据库

06 | 最终篇-通过AOP自动连接数据库-完成日志业务

简介

模块化的介绍一共2篇

这一篇我们实现一个功能非常简单的StartupModules模块化。

第二篇我们来实现一个ABP的模块化效果。

思考

其实来简单想一下模块化的实验思路,写个接口=>模块类继承该接口=>项目启动反射检索=>调用接口实现。
那么具体到代码实践应该怎么写呢。

开始

第一步

第一步就是写一个模块化接口类的嘛!
新建类 IStartupModule

然后写一个反射检索全文谁继承了这个接口的方法
新建类 StartupModulesOptions


代码解释: Activator.CreateInstance 与指定参数匹配程度最高的构造函数来创建指定类型的实例
ps:白话文就是,你给我Type我给你创建个对应的实例
更一个有意思的是 Assembly.GetEntryAssembly()! 这个! 是不是很好奇怕
ps:我第一次看到这个语法也蒙了,问了好多人大家都没用过,这个语法同TS中的断言,是非null类型断言,意思就是我断言我这个方法返回的内容绝对不是null。

第二步

到这里来看我们是不是已经拿到了所有继承接口的模块那么怎么在该调用的地方调用呢,缺啥写啥

第三步

运行代码也有了,我该怎么调用呢。
接下来只要在 在Program 的 WebHost 调用.UseStartupModules() 流程就可以加载我们的 ConfigureServices 了

中间来插播一下

请让我掏出来一个器大的东西来说 他就是: IStartupFilter ,这个东西是个啥东西呢。来看一段源码

namespace Microsoft.AspNetCore.Hosting
{
    public interface IStartupFilter
    {
        Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
    }
}

这里我们看到了 Configure 他返回一个 IApplicationBuilder 他是怎么用的呢
我们新建一个空的Web项目的时候不知道有没有注意过 UseStaticFiles 这个函数

 public void Configure(IApplicationBuilder app)
        {
            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
            });
        }

在这个方法中,你可以直接使用方法提供的IApplicationBuilder参数,并且可以向其中添加各种中间件。使用IStartupFilter, 你可以指定并返回一个Action类型的泛型委托,这意味你除了可以使用方法提供的泛型委托配置IApplicationBuilder对象, 还需要返回一个泛型委托。

我们来看一段代码 Build(); 这个会调用BuildApplication方法

public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()    
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .Build();

        host.Run(); 
    }
}

private RequestDelegate BuildApplication()
{
    ..
    IApplicationBuilder builder = builderFactory.CreateBuilder(Server.Features);
    builder.ApplicationServices = _applicationServices;

    var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
    Action<IApplicationBuilder> configure = _startup.Configure;
    foreach (var filter in startupFilters.Reverse())
    {
        configure = filter.Configure(configure);
    }

    configure(builder);

    return builder.Build();
}

首先,此方法创建IApplicationBuilder的实例,该实例将用于构建中间件管道,并将ApplicationServices设置为已配置的DI容器。
接下来的代码块很意思。首先,从DI容器中获取了一个集合IEnumerable<IStartupFilter>
我们可以配置多个IStartupFilter来形成一个管道,所以这个方法只是从容器中取出它们。
现在我们通过循环遍历每个IStartupFilter(以相反的顺序),传入Startup.Configure方法,然后更新局部变量configure来创建Configure方法的管道。

第四步

我们自己如何来实现 一个IStartupFilter 让他帮我们调用 Configure。

第五步

在 WebHostBuilderExtensions类 UseStartupModules 方法 ConfigureServices 下用 IStartupFilter 注入实现
这样在Build() 的时候就会调用模块的方法了

ActivatorUtilities.CreateInstance<ModulesStartupFilter>(sp, runner) // 第二个参数是在创建实例的时候 给构造函数注入的第一个参数

测试一下

新建 Core Web项目 在 Program.cs
 Host.CreateDefaultBuilder(args)
 .ConfigureWebHostDefaults(webBuilder =>
    {
         // 进行模块映射
         webBuilder.UseStartupModules().UseStartup<Startup>();
});

在 Startup.cs ConfigureServices和Configure 下打一个 Console.WriteLine

新建 类 HangfireStartupModule 继承 IStartupModule
public class HangfireStartupModule : IStartupModule
{
        public void ConfigureServices(IServiceCollection services)
        {
            Console.WriteLine("HangfireStartupModule----ConfigureServices");
        }
        public void Configure(IApplicationBuilder app)
        {
            Console.WriteLine("HangfireStartupModule----Configure");
        }
}
结果

一个非常简单的模块化就完工了,当然这个是基础版本,只能拿来借鉴思路学习下。

补充模块的 ConfigureServices 和 Configure 传递上下文

新建类 ConfigureServicesContext 和 ConfigureMiddlewareContext

  public class ConfigureMiddlewareContext
    {
        public ConfigureMiddlewareContext(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, IServiceProvider serviceProvider, StartupModulesOptions options)
        {
            Configuration = configuration;
            HostingEnvironment = hostingEnvironment;
            ServiceProvider = serviceProvider;
            Options = options;
        }

        public IConfiguration Configuration { get; }

        public IWebHostEnvironment HostingEnvironment { get; }
        public IServiceProvider ServiceProvider { get; }

        public StartupModulesOptions Options { get; }
    }

    public class ConfigureServicesContext
    {
        public ConfigureServicesContext(IConfiguration configuration, IWebHostEnvironment hostingEnvironment, StartupModulesOptions options)
        {
            Configuration = configuration;
            HostingEnvironment = hostingEnvironment;
            Options = options;
        }

        public IConfiguration Configuration { get; }
        public IWebHostEnvironment HostingEnvironment { get; }
        public StartupModulesOptions Options { get; }
    }

修改 StartupModuleRunner 的方法

 public void ConfigureServices(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
        {
            var ctx = new ConfigureServicesContext(configuration, hostingEnvironment, _options);
            foreach (var cfg in _options.StartupModules)
            {
                cfg.ConfigureServices(services, ctx);
            }
        }

        public void Configure(IApplicationBuilder app, IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
        {
            using (var scope = app.ApplicationServices.CreateScope()) {
                var ctx = new ConfigureMiddlewareContext(configuration, hostingEnvironment, scope.ServiceProvider, _options);

                foreach (var cfg in _options.StartupModules)
                {
                    cfg.Configure(app, ctx);
                }
            }
               
        }

鸣谢

玩双截棍的熊猫、NETCore-大黄瓜

思路来源:https://github.com/henkmollema/StartupModules

友联: https://github.com/DestinyCore/Destiny.Core.Flow

smarty模板强制编译_翔宇的博客-CSDN博客

mikel阅读(988)

来源: smarty模板强制编译_翔宇的博客-CSDN博客

开发过程中碰到一个奇怪的现象,每次写完模板之后总是没有更新,一番搜索之后发现是smarty的缓存,并且项目的smarty配置被人修改过,囧。

参考这篇文章,解释下其中这两个参数:

/**
* force template compiling?
* @var boolean
*/
public $force_compile = false;
/**
* check template for modifications?
* @var boolean
*/
public $compile_check = true;
/**
* use sub dirs for compiled/cached files?
* @var boolean
*/

上面是smarty的默认配置
– $force_compile为true时每次用户访问页面都会重新编译模板,应设为false,减少io
– $compile_check为true时smarty会检查模板文件的变更,有变更会重新编译,应设为true

被人设置为false false了 =。= ,改回来就可以了

一款基于.NET Core的认证授权解决方案-葫芦藤1.0开源啦 - 福禄网络技术团队 - 博客园

mikel阅读(864)

来源: 一款基于.NET Core的认证授权解决方案-葫芦藤1.0开源啦 – 福禄网络技术团队 – 博客园

背景

18年公司准备在技术上进行转型,而公司技术团队是互相独立的,新技术的推动阻力很大。我们需要找到一个切入点。公司的项目很多,而各个系统之间又不互通,导致每套系统都有一套登录体系,给员工和客户都带来极大的不便。那么从登录切入进去无疑最合适,对于各个团队的技术改造成本也不大。所以我们团队第一个项目就是搭建一套统一登录认证授权系统,那么葫芦藤项目应运而生。

技术方案

后端框架:.NET Core3.1(后期会推出 .NET 5版本)

前端框架:React

数据库:mySQL(可根据实际情况,自由切换)

中间件:redis

详细功能

认证授权服务

基于IdentityServer4实现的协议,支持网站、本地应用、移动端、web服务等应用的认证授权逻辑。

单点登录登出

支持各种类型应用上的单点登录登出。开箱即用的基础用户管理模块,包括:注册、登录、手机验证码、忘记密码等。为了安全考虑,集成了腾讯图形验证码。

第三方登录(微信、钉钉)

完善的第三方登录支持。支持首次登录时绑定已存在用户或注册新用户后,自动绑定。

如何快速使用

1.下载代码

clone代码到本地。根目录结构如下:

20201103153907

其中,backend存放的是后端代码,frontend存放的是前端代码。

进入backend目录,使用Visual Studio打开解决方案。目录结构如下:

20201103154250

2.生成数据库

首先在Fulu.Passport.Web中找到appsettings.Development.json文件。编辑数据库连接字符串:

20201103155350

打开程序包管理器,切换默认项目为:Fulu.Passport.Web, 如下图所示:

20201106111334

然后在程序包管理器中执行如下命令:


Add-Migration Init

最后执行完成后,再执行如下命令:

update-database

执行完以上操作后,如没有报错,则会创建数据库,并会在Client表中创建一条测试数据,如下图所示:

20201103160408

3.按F5启动后端服务

注:由于项目中依赖redis来处理缓存,所以正式启动之前,需要将appsettings.Development.json文件里的redis配置改为你自己的。

4.启动前端

切换目录到frontend,在命令行中执行如下命令:

npm install

执行完毕后,执行如下命令:

npm run demo

执行结果如下图所示:

20201103161300

然后通过http://localhost:8080进行访问。界面如下所示:

20201103174200

至此,前后端服务已启动完毕,一个开箱即用的认证授权服务就完成了。

5.新客户端如何快速接入认证服务?

认证授权服务存在的意义就是提供统一的认证授权入口,有了这个服务后,每个新的客户端应用无需单独开发认证授权模块。下面就来一起看下如何快速将新应用接入到认证授权服务。(此处以 ASP.NET Core作为示例,其他语言大同小异)。

示例代码在sample文件夹中,如下图所示:

20201104165955

在正式接入之前,必须先申请应用。(此版本未提供应用管理服务)通过在数据库中添加示例信息,如下图所示:

20201104192124

示例SQL脚本:

INSERT INTO `fulusso`.`client`(`client_secret`, `full_name`, `host_url`, `redirect_uri`, `description`, `enabled`, `id`) VALUES ('14p9ao1gxu4q3sp8ogk8bq4gkct59t9w', '葫芦藤2', 'http://localhost:5003/', 'http://localhost:5003', NULL, 1, UUID());

其中,redirect_uri参数指的是从认证服务获取code之后,重定向的url。为了开发的方便,我们的认证服务中仅校验回调域名的域名,不会校验完整的地址。比如,你的redirect_uri为http://www.xxx.com/abc/aaa,则数据库中的redirect_uri字段填写http://www.xxx.com即可。

应用信息导入到数据库后,在Startup类的ConfigureServices方法中,添加如下代码:

services.AddServiceAuthorize(o =>
{
    o.AllowClientToken = true;
    o.AllowUserToken = true;
    o.OnClientValidate = false;
    o.Authority = "http://localhost:5000";
    o.ValidateAudience = false;
    o.ClientId = Configuration["AppSettings:ClientId"];
    o.ClientSecret = Configuration["AppSettings:ClientSecret"];
});

注:需添加Fulu.Service.Authorize项目引用,如下图所示:

20201104170401

然后在Configure方法中,添加如下代码:

 app.UseRouting();
 app.UseJwtAuthorize();
 app.UseAuthorization();

其中,UseJwtAuthorize是自定义的中间件,为了实现OAuth2.0的授权码的逻辑。
限于篇幅,具体代码不在此列出。可在代码仓库中查看。

到此为止,这个新应用就成功的接入到认证服务了。

当未登录的时候,访问此应用的页面会自动跳转到认证服务的login界面。登录之后,会重定向回登录之前的页面。如下图所示:

aa

下一版功能规划

1.更多的第三方平台的接入(QQ、微博等)

2.api授权服务

3.更安全的二次验证,集成google令牌

4.应用管理

等等~~~~,尽请期待。

体验

演示地址:https://account.suuyuu.cn/

代码仓库:https://github.com/fuluteam/fulusso

(3)ASP.NET Core3.1 Ocelot认证 - 暗断肠 - 博客园

mikel阅读(766)

来源: (3)ASP.NET Core3.1 Ocelot认证 – 暗断肠 – 博客园

.认证

当客户端通过Ocelot访问下游服务的时候,为了保护下游资源服务器会进行认证鉴权,这时候需要在Ocelot添加认证服务。添加认证服务后,随后使用Ocelot基于声明的任何功能,例如授权或使用Token中的值修改请求。用户必须像往常一样在其Startup.cs中注册身份验证服务,但是他们为每次注册提供一个方案(身份验证提供者密钥),例如:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    var authenticationProviderKey = "TestKey";
    services.AddAuthentication()
        .AddJwtBearer(authenticationProviderKey, x =>
        {
        });
}
复制代码

在此Ocelot认证项目示例中,TestKey是已注册此提供程序的方案。然后我们将其映射到配置中的Routes路由,例如:

复制代码
{
    "Routes": [
        {
            "DownstreamPathTemplate": "/api/customers",
            "DownstreamScheme": "http",
            "DownstreamHost": "localhost",
            "DownstreamPort": 9001,
            "UpstreamPathTemplate": "/customers",
            "UpstreamHttpMethod": [ "Get" ],
            "AuthenticationOptions": {
                "AuthenticationProviderKey": "TestKey",
                "AllowedScopes": []
            }
        }
    ]
}
复制代码

Ocelot运行时,它将查看Routes.AuthenticationOptions.AuthenticationProviderKey并检查是否存在使用给定密钥注册的身份验证提供程序。如果不存在,则Ocelot将不会启动,如果存在,则Routes将在执行时使用该提供程序。
如果对路由进行身份验证,Ocelot将在执行身份验证中间件时调用与之关联的任何方案。如果请求通过身份验证失败,Ocelot将返回http状态代码401。

2.JWT Tokens Bearer认证

Json Web Token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

2.1JWT令牌结构

在紧凑的形式中,JSON Web Tokens由dot(.)分隔的三个部分组成,它们是:
Header头、Payload有效载荷、Signature签名
因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz(Header.Payload.Signature)

2.1.1Header头

标头通常由两部分组成:令牌的类型,即JWT,以及正在使用的签名算法,例如HMAC SHA256或RSA。例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个JSON被编码为Base64Url,形成JWT的第一部分。

2.1.2Payload有效载荷

Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意,JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个JSON对象也要使用Base64URL算法转成字符串。

2.1.3.Signature签名

Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证消息在此过程中未被更改,并且,在使用私钥签名的令牌的情况下,它还可以验证JWT的发件人是否是它所声称的人。
把他们三个全部放在一起,输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比更加紧凑。
下面显示了一个JWT,它具有先前的头和有效负载编码,并使用机密签名。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoid3prNzAzIiwibmJmIjoiMTU5MjE0MzkzNyIsImV4cCI6MTU5MjE0Mzk5OCwiaXNzIjoiYXV0aC5qd3QuY2MiLCJhdWQiOiJkZW5nd3V8MjAyMC82LzE0IDIyOjEyOjE5In0
.4RiwhRy0rQkZjclOFWyTpmW7v0AMaL3aeve1L-eWIz0

其实一般发送用户名和密码获取token那是由Identity4来完成的,包括验证用户,生成JwtToken。但是项目这里是由System.IdentityModel.Tokens类库来生成JwtToken。最后返回jwt令牌token给用户。JwtToken解码可以通过https://jwt.io/中进行查看。

3.项目演示

3.1APIGateway项目

在该项目中启用身份认证来保护下游api服务,使用JwtBearer认证,将默认的身份验证方案设置为TestKey。在appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息:

复制代码
{
    "Audience": {
        "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
        "Iss": "http://www.c-sharpcorner.com/members/catcher-wong",
        "Aud": "Catcher Wong"
    }
}
复制代码

Startup添加身份认证代码如下:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    //获取appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息
    var audienceConfig = Configuration.GetSection("Audience");
    //获取安全秘钥
    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(audienceConfig["Secret"]));
    //token要验证的参数集合
    var tokenValidationParameters = new TokenValidationParameters
    {
        //必须验证安全秘钥
        ValidateIssuerSigningKey = true,
        //赋值安全秘钥
        IssuerSigningKey = signingKey,
        //必须验证签发人
        ValidateIssuer = true,
        //赋值签发人
        ValidIssuer = audienceConfig["Iss"],
        //必须验证受众
        ValidateAudience = true,
        //赋值受众
        ValidAudience = audienceConfig["Aud"],
        //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
        ValidateLifetime = true,
        //允许的服务器时间偏移量
        ClockSkew = TimeSpan.Zero,
        //是否要求Token的Claims中必须包含Expires
        RequireExpirationTime = true,
    };
    //添加服务验证,方案为TestKey
    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = "TestKey";
    })
    .AddJwtBearer("TestKey", x =>
        {
            x.RequireHttpsMetadata = false;
            //在JwtBearerOptions配置中,IssuerSigningKey(签名秘钥)、ValidIssuer(Token颁发机构)、ValidAudience(颁发给谁)三个参数是必须的。
            x.TokenValidationParameters = tokenValidationParameters;
        });
    //添加Ocelot网关服务时,包括Secret秘钥、Iss签发人、Aud受众
    services.AddOcelot(Configuration);
}
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //使用认证服务
    app.UseAuthentication();
    //使用Ocelot中间件
    await app.UseOcelot();
}
复制代码
3.1.1Identity Server承载JWT Token

在第二小节介绍JWT Token认证时候,我们都知道一般发送用户名和密码获取Token那是由Identity4来完成的,包括验证用户,生成JWT Token。也就是说Identity Server承载了JWT Token认证功能。为了使用IdentityServer承载Token,请像往常一样在ConfigureServices中使用方案(密钥)注册IdentityServer服务。如果您不知道如何执行此操作,请查阅IdentityServer文档。

复制代码
public void ConfigureServices(IServiceCollection services)
{
    var authenticationProviderKey = "TestKey";
    Action<IdentityServerAuthenticationOptions> options = o =>
        {
            o.Authority = "https://whereyouridentityserverlives.com";
            o.ApiName = "api";
            o.SupportedTokens = SupportedTokens.Both;
            o.ApiSecret = "secret";
        };
    services.AddAuthentication()
        .AddIdentityServerAuthentication(authenticationProviderKey, options);
    services.AddOcelot();
}
复制代码

在Identity4中是由Authority参数指定OIDC服务地址,OIDC可以自动发现Issuer, IssuerSigningKey等配置,而o.Audience与x.TokenValidationParameters = new TokenValidationParameters { ValidAudience = “api” }是等效的。

3.2AuthServer项目

此服务主要用于客户端请求受保护的资源服务器时,认证后产生客户端需要的JWT Token,生成JWT Token关键代码如下:

复制代码
[Route("api/[controller]")]
public class AuthController : Controller
{
    private IOptions<Audience> _settings;
    public AuthController(IOptions<Audience> settings)
    {
        this._settings = settings;
    }
    /// <summary>
    ///用户使用 用户名密码 来请求服务器
    ///服务器进行验证用户的信息
    ///服务器通过验证发送给用户一个token
    ///客户端存储token,并在每次请求时附送上这个token值, headers: {'Authorization': 'Bearer ' + token}
    ///服务端验证token值,并返回数据
    /// </summary>
    /// <param name="name"></param>
    /// <param name="pwd"></param>
    /// <returns></returns>
    [HttpGet]
    public IActionResult Get(string name, string pwd)
    {
        //验证登录用户名和密码
        if (name == "catcher" && pwd == "123")
        {
            var now = DateTime.UtcNow;
            //添加用户的信息,转成一组声明,还可以写入更多用户信息声明
            var claims = new Claim[]
            {
                //声明主题
                new Claim(JwtRegisteredClaimNames.Sub, name),
                    //JWT ID 唯一标识符
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    //发布时间戳 issued timestamp
                new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
            };
            //下面使用 Microsoft.IdentityModel.Tokens帮助库下的类来创建JwtToken

            //安全秘钥
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_settings.Value.Secret));

            //声明jwt验证参数
            var tokenValidationParameters = new TokenValidationParameters
            {
                //必须验证安全秘钥
                ValidateIssuerSigningKey = true,
                //赋值安全秘钥
                IssuerSigningKey = signingKey,
                //必须验证签发人
                ValidateIssuer = true,
                //赋值签发人
                ValidIssuer = _settings.Value.Iss,
                //必须验证受众
                ValidateAudience = true,
                //赋值受众
                ValidAudience = _settings.Value.Aud,
                //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
                ValidateLifetime = true,
                //允许的服务器时间偏移量
                ClockSkew = TimeSpan.Zero,
                //是否要求Token的Claims中必须包含Expires
                RequireExpirationTime = true,
            };
            var jwt = new JwtSecurityToken(
                //jwt签发人
                issuer: _settings.Value.Iss,
                //jwt受众
                audience: _settings.Value.Aud,
                //jwt一组声明
                claims: claims,
                notBefore: now,
                //jwt令牌过期时间
                expires: now.Add(TimeSpan.FromMinutes(2)),
                //签名凭证: 安全密钥、签名算法
                signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
            );
            //生成jwt令牌(json web token)
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            var responseJson = new
            {
                access_token = encodedJwt,
                expires_in = (int)TimeSpan.FromMinutes(2).TotalSeconds
            };
            return Json(responseJson);
        }
        else
        {
            return Json("");
        }
    }
}
public class Audience
{
    public string Secret { get; set; }
    public string Iss { get; set; }
    public string Aud { get; set; }
}
复制代码

appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息:

复制代码
{
    "Audience": {
        "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
        "Iss": "http://www.c-sharpcorner.com/members/catcher-wong",
        "Aud": "Catcher Wong"
    }
}
复制代码

3.3CustomerAPIServices项目

该项目跟APIGateway项目是一样的,为了保护下游api服务,使用JwtBearer认证,将默认的身份验证方案设置为TestKey。在appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息:

复制代码
{
    "Audience": {
        "Secret": "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==",
        "Iss": "http://www.c-sharpcorner.com/members/catcher-wong",
        "Aud": "Catcher Wong"
    }
}
复制代码

Startup添加身份认证代码如下:

复制代码
public void ConfigureServices(IServiceCollection services)
{
    //获取appsettings.json文件中配置认证中密钥(Secret)跟受众(Aud)信息
    var audienceConfig = Configuration.GetSection("Audience");
    //获取安全秘钥
    var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(audienceConfig["Secret"]));
    //token要验证的参数集合
    var tokenValidationParameters = new TokenValidationParameters
    {
        //必须验证安全秘钥
        ValidateIssuerSigningKey = true,
        //赋值安全秘钥
        IssuerSigningKey = signingKey,
        //必须验证签发人
        ValidateIssuer = true,
        //赋值签发人
        ValidIssuer = audienceConfig["Iss"],
        //必须验证受众
        ValidateAudience = true,
        //赋值受众
        ValidAudience = audienceConfig["Aud"],
        //是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
        ValidateLifetime = true,
        //允许的服务器时间偏移量
        ClockSkew = TimeSpan.Zero,
        //是否要求Token的Claims中必须包含Expires
        RequireExpirationTime = true,
    };
    //添加服务验证,方案为TestKey
    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = "TestKey";
    })
    .AddJwtBearer("TestKey", x =>
        {
            x.RequireHttpsMetadata = false;
            //在JwtBearerOptions配置中,IssuerSigningKey(签名秘钥)、ValidIssuer(Token颁发机构)、ValidAudience(颁发给谁)三个参数是必须的。
            x.TokenValidationParameters = tokenValidationParameters;
        });

    services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
    //使用认证服务
    app.UseAuthentication();
    app.UseMvc();
}
复制代码

在CustomersController下添加一个需要认证方法,一个不需要认证方法:

复制代码
[Route("api/[controller]")]
public class CustomersController : Controller
{
    //添加认证属性
    [Authorize]
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "Catcher Wong", "James Li" };
    }
    [HttpGet("{id}")]
    public string Get(int id)
    {
        return $"Catcher Wong - {id}";
    }
}
复制代码

3.4ClientApp项目

该项目是用来模拟客户端访问资源服务器整个认证流程测试项目,在Program主程序可以看到如下代码:

复制代码
class Program
{
    static void Main(string[] args)
    {
        HttpClient client = new HttpClient();

        client.DefaultRequestHeaders.Clear();
        client.BaseAddress = new Uri("http://localhost:9000");

        // 1. without access_token will not access the service
        //    and return 401 .
        var resWithoutToken = client.GetAsync("/customers").Result;

        Console.WriteLine($"Sending Request to /customers , without token.");
        Console.WriteLine($"Result : {resWithoutToken.StatusCode}");

        //2. with access_token will access the service
        //   and return result.
        client.DefaultRequestHeaders.Clear();
        Console.WriteLine("\nBegin Auth....");
        var jwt = GetJwt();
        Console.WriteLine("End Auth....");
        Console.WriteLine($"\nToken={jwt}");

        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");
        var resWithToken = client.GetAsync("/customers").Result;

        Console.WriteLine($"\nSend Request to /customers , with token.");
        Console.WriteLine($"Result : {resWithToken.StatusCode}");
        Console.WriteLine(resWithToken.Content.ReadAsStringAsync().Result);

        //3. visit no auth service
        Console.WriteLine("\nNo Auth Service Here ");
        client.DefaultRequestHeaders.Clear();
        var res = client.GetAsync("/customers/1").Result;

        Console.WriteLine($"Send Request to /customers/1");
        Console.WriteLine($"Result : {res.StatusCode}");
        Console.WriteLine(res.Content.ReadAsStringAsync().Result);

        Console.Read();
    }
    private static string GetJwt()
    {
        HttpClient client = new HttpClient();

        client.BaseAddress = new Uri( "http://localhost:9000");
        client.DefaultRequestHeaders.Clear();

        var res2 = client.GetAsync("/api/auth?name=catcher&pwd=123").Result;

        dynamic jwt = JsonConvert.DeserializeObject(res2.Content.ReadAsStringAsync().Result);

        return jwt.access_token;
    }
}
复制代码

运行项目看看测试结果:

结合代码,我们能看到当客户端通过Ocelot网关访问下游服务http://localhost:9000/api/Customers/Get方法时候,因为该方法是需要通过认证才返回处理结果的,所以会进行JWT Token认证,如果发现没有Token,Ocelot则返回http状态代码401拒绝访问。如果我们通过GetJwt方法在AuthServer服务上登录认证获取到授权Token,然后再访问该资源服务器接口,立即就会返回处理结果,通过跟而未加认证属性的http://localhost:9000/api/Customers/Get/{id}方法对比,我们就知道,Ocelot认证已经成功了!

4.总结

该章节只是结合demo项目简单介绍在Ocelot中如何使用JWT Token认证。其实正式环境中,Ocelot是应该集成IdentityServer认证授权的,同样的通过重写Ocelot中间件我们还可以把configuration.json的配置信息存储到数据库或者缓存到Redis中。

参考文献:
Ocelot官网

增势税票识别助手,可识别照片、扫描件、电子票、形成电子台帐。 - 一壶茶水 - 博客园

mikel阅读(1192)

来源: 增势税票识别助手,可识别照片、扫描件、电子票、形成电子台帐。 – 一壶茶水 – 博客园

 

该助手可以快速、准确的识别并读取发票相关字段信息,与传统的人工录入核对的方式相比,时效性更高,数据准确性更强。

同时,软件具备批量导出功能,数据读取字段与公司综项系统相一致,可以实现快捷的导入到综项系统中,大大减少操作人员的工作量。

首先本小工具使用C# winfrom 实现,其中主要是使用了百度智能云OCR文字识别技术,调用期官网接口,很简单,搭配NPOI Execl操作类库,

利用Spire.pdf类库,把pdf格式发票,转换为png图片格式。自动识别图片、pdf格式发票,发票可以用高拍仪、手机拍照、扫面件等都可以识别。

  其他说明:本程序借助百度智能云API作为基础的发票识别技术,识别准确率在98%以上,同时,由于百度智能云API每天免费授权识别限制在500次/天且可以对个人用户申请开放。

在授权管理中个人可以根据使用量的多少自行申请百度智能云API认证授权API key和Secret key,然后在发票识别助手中认证。

发票识别助手共分5个功能模块,操作相对很简单,第一步点击添加发票按钮,选择要识别的发票信息。注意说明:目前图片格式支持jpg、png、bmp,图片的长和宽要求最短边大于10px,

最长边小于2048px;图像编码后大小必须小于4M,建议不要超过1M;第二步点击识别发票按钮,系统开始识别发票信息,识别完成后,发票信息会自动生成;

 

介绍一下关键的代码:

一、获取百度云API token,这个是官方给的,直接拿过来用就可以了。

二、增值税票识别请求过程和参数传递,也是官方给的例子,自己按照需求修改一下就可以了。

 三、这里的部分是把pdf格式的发票,自动转换为png格式,提供出百度云api需要的文件格式。

 四、获取api返回的数据,输出到dataGridView中。

五、导出发票明细到EXECL表格中。

操作说明如下:

事务(进程 ID 51)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品_小白博客-CSDN博客

mikel阅读(1468)

来源: 事务(进程 ID 51)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品_小白博客-CSDN博客

所有死锁的原因可归结为资源的竞争
表现一:

一个用户A 访问表A(锁住了表A),然后又访问表B 另一个用户B 访问表B(锁住了表B),然后企图访问表A 这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B,才能继续,好了他老人家就只好老老实实在这等了 同样用户B要等用户A释放表A才能继续这就死锁了

解决方法:
    这种死锁是由于你的程序的'bug'产生的,除了调整你的程序的逻辑别无他法,仔细分析你程序的逻辑,
    1:尽量避免同时锁定两个资源
    2: 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源.
  • 1
  • 2
  • 3
  • 4
表现二:

用户A读一条纪录,然后修改该条纪录,这是用户B修改该条纪录这里用户A的事务里锁的性质由共享锁企图上升到独占锁(forupdate),而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁

解决方法:
    让用户A的事务(即先读后写类型的操作),在select 时就是用Update lock
    语法如下:
    select * from table1 (updlock) where ....
  • 1
  • 2
  • 3
  • 4

原文地址


NOLOCK

NOLOCK在概念上类似于READ UNCOMMITTED隔离级别,并且只针对于SELECT查询语句,它不会获取表的共享锁,换句话说不会阻止排它锁来更新数据行。当我们对表进行NOLOCK有什么好处呢?它能够提高并发性能,因为此时SQL Server数据库引擎不必去维护共享锁,由于不会对正在读取的表获取共享锁,所以可能导致未提交的事务也会被读取,所以此时缺点显而易见将导致脏读

SELECT COUNT(*) FROM Example WITH(NOLOCK)
  • 1
READPAST

当在表中用READPAST指定提示时此时SQL Server数据库引擎在返回结果集时将不会返回锁定的行或者数据页。它除了和NOLOCK一样不会导致查询阻塞外,因为不会返回锁定的行记录所以其优点好包括不存在脏读。但是其缺点则是因为不包含锁定的行记录但是很难保证结果集或者修改语句是否包含我们所必须需要返回的行。有可能在我们的业务逻辑中,需要返回我们必须需要的行。它的使用方式和NOLOCK一样

SELECT COUNT(*)FROM Example WITH(READPAST)
  • 1
UPDLOCK

UPDLOCK只是针对于表中的某一行记录来锁定从而阻止其他操作对该行的数据更新,说到这里想必我们已经明了,UPDLOCK是行级别,而排它锁则是表级别,二者不可同日而语。也就说当我们对某一行添加UPDLOCK提示时并不会阻塞其他查询操作

BEGIN TRAN
 select * from Example WITH (UPDLOCK) where SaleID = 1
此时我们再来开一个窗口进行查询,如下:
select * from Example
  • 1
  • 2
  • 3
  • 4
HOLDLOCK

使用HOLDLOCK提示时,此时查询将锁定表且被强制序列化,直到事务完成,才会被释放,其类似于SERIALIZABLE最高隔离级别

BEGIN TRAN
 select * from Example WITH (UPDLOCK,HOLDLOCK) where SaleID = 1
  • 1
  • 2

原文地址

 C#中List集合使用OrderByDescending方法对集合进行倒序排序 -

mikel阅读(2593)

来源: 【转载】 C#中List集合使用OrderByDescending方法对集合进行倒序排序 – 江湖逍遥 – 博客园

C#的List集合操作中,有时候需要针对List集合进行排序操作,如果是对List集合按照元素对象或者元素对象的某个属性进行倒序排序的话,可以使用OrderByDescending方法来实现,OrderByDescending方法属于List集合的扩展方法,方法的调用形式为使用Lambda表达式语句。

(1)对List<int>集合对象list1进行从大到小降序排序可使用下列语句:

List<int> list1 = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
list1 = list1.OrderByDescending(t => t).ToList();

(2)按List集合中的元素对象的某个具体属性进行倒序排序也可使用OrderByDescending方法。

我们需要对List<TestModel>集合对象testList进行排序,排序规则为按照对象属性Index降序排序。

首先看下TestModel的定义:

public class TestModel
{
public int Index { set; get; }

public string Name { set; get; }
}

对testList集合按元素对象的Index属性进行倒序排序可使用下列语句:

List<TestModel> testList = new List<ConsoleApplication1.TestModel>();
testList = testList.OrderByDescending(t => t.Index).ToList();

Web API 强势入门指南 - 微软互联网开发支持 - 博客园

mikel阅读(1030)

来源: Web API 强势入门指南 – 微软互联网开发支持 – 博客园

Web API是一个比较宽泛的概念。这里我们提到Web API特指ASP.NET Web API。

这篇文章中我们主要介绍Web API的主要功能以及与其他同类型框架的对比,最后通过一些相对复杂的实例展示如何通过Web API构建http服务,同时也展示了Visual Studio构建.net项目的各种强大。

目录

什么是 Web API

官方定义如下,强调两个关键点,即可以对接各种客户端(浏览器,移动设备),构建http服务的框架。

ASP.NET Web API is a framework that makes it easy to build HTTP services that reach a broad range of clients, including browsers and mobile devices. ASP.NET Web API is an ideal platform for building RESTful applications on the .NET Framework.

Web API在ASP.NET完整框架中地位如下图,与SignalR一起同为构建Service的框架。Web API负责构建http常规服务,而SingalR主要负责的是构建实时服务,例如股票,聊天室,在线游戏等实时性要求比较高的服务。

Picture20

 

为什么要用 Web API

Web API最重要的是可以构建面向各种客户端的服务。另外与WCF REST Service不同在于,Web API利用Http协议的各个方面来表达服务(例如 URI/request response header/caching/versioning/content format),因此就省掉很多配置。

Picture2

 

当你遇到以下这些情况的时候,就可以考虑使用Web API了。

  • 需要Web Service但是不需要SOAP
  • 需要在已有的WCF服务基础上建立non-soap-based http服务
  • 只想发布一些简单的Http服务,不想使用相对复杂的WCF配置
  • 发布的服务可能会被带宽受限的设备访问
  • 希望使用开源框架,关键时候可以自己调试或者自定义一下框架

功能简介

Web API的主要功能

1. 支持基于Http verb (GET, POST, PUT, DELETE)的CRUD (create, retrieve, update, delete)操作

通过不同的http动作表达不同的含义,这样就不需要暴露多个API来支持这些基本操作。

2. 请求的回复通过Http Status Code表达不同含义,并且客户端可以通过Accept header来与服务器协商格式,例如你希望服务器返回JSON格式还是XML格式。

3. 请求的回复格式支持 JSON,XML,并且可以扩展添加其他格式。

4. 原生支持OData

5. 支持Self-host或者IIS host。

6. 支持大多数MVC功能,例如Routing/Controller/Action Result/Filter/Model Builder/IOC Container/Dependency Injection。

Web API vs MVC

你可能会觉得Web API 与MVC很类似,他们有哪些不同之处呢?先上图,这就是他们最大的不同之处。

Picture1

详细点说他们的区别,

  • MVC主要用来构建网站,既关心数据也关心页面展示,而Web API只关注数据
  • Web API支持格式协商,客户端可以通过Accept header通知服务器期望的格式
  • Web API支持Self Host,MVC目前不支持
  • Web API通过不同的http verb表达不同的动作(CRUD),MVC则通过Action名字表达动作
  • Web API内建于ASP.NET System.Web.Http命名空间下,MVC位于System.Web.Mvc命名空间下,因此model binding/filter/routing等功能有所不同
  • 最后,Web API非常适合构建移动客户端服务

Web API vs WCF

发布服务在Web API和WCF之间该如何取舍呢?这里提供些简单地判断规则,

  • 如果服务需要支持One Way Messaging/Message Queue/Duplex Communication,选择WCF
  • 如果服务需要在TCP/Named Pipes/UDP (wcf 4.5),选择WCF
  • 如果服务需要在http协议上,并且希望利用http协议的各种功能,选择Web API
  • 如果服务需要被各种客户端(特别是移动客户端)调用,选择Web API

Web API 实战 (Web API + MongoDB + knockoutjs)

ASP.NET网站上有很多简单的Web API实例,看看贴图和实例代码你就明白怎么用了。这里我们通过一个稍微复杂一点的实例来展示下Web API的功能。

涉及技术

在我们的实例里面用到了:

服务URI Pattern

Action Http verb URI
Get contact list GET /api/contacts
Get filtered contacts GET /api/contacts?$top=2
Get contact by ID GET /api/contacts/id
Create new contact POST /api/contacts
Update a contact PUT /api/contacts/id
Delete a contact DELETE /api/contacts/id

准备工作

1. 下载并安装Mongo DB,步骤看这里

2. Mongo DB C# driver下载可以在nuget搜索mongocsharpdriver。

3. 如果想本地察看数据库中内容,下载MongoVUE

4. Knockoutjs下载可以在nuget搜索knockoutjs。

代码实现

1. 创建项目

创建MVC4 Web Application

1

在Project Template中选择Web API

2

然后项目就创建成了,Controllers里面有一个ValuesController,是自动生成的一个最简单的Web API Controller。

正如我们前面所说,里面引用的是System.Web.Http命名空间。

3

2. 创建model

在model里面添加Contact类

4

代码如下,其中BsonId需要mongocsharpdriver。

1
2
3
4
5
6
7
8
9
public class Contact
    {
        [BsonId]
        public string Id { get; set; }
        public string Name { get; set; }
        public string Phone { get; set; }
        public string Email { get; set; }
        public DateTime LastModified { get; set; }
    }

我们需要添加mongosharpdriver。

7

8

另外我们需要在Model中添加Repository,Controller通过该类来访问Mongo DB。

1
2
3
4
5
6
7
public interface IContactRepository {
        IEnumerable GetAllContacts();
        Contact GetContact(string id);
        Contact AddContact(Contact item);
        bool RemoveContact(string id);
        bool UpdateContact(string id, Contact item);  
    }

ContactRepository的完整实现如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ContactRepository : IContactRepository
    {
        MongoServer _server = null;
        MongoDatabase _database = null;
        MongoCollection _contacts = null;
        public ContactRepository(string connection)
        {
            if (string.IsNullOrWhiteSpace(connection))
            {
                connection = "mongodb://localhost:27017";
            }
            _server = new MongoClient(connection).GetServer();
            _database = _server.GetDatabase("Contacts");
            _contacts = _database.GetCollection("contacts");
            // Reset database and add some default entries
            _contacts.RemoveAll();
            for (int index = 1; index < 5; index++)
            {
                Contact contact1 = new Contact
                {
                    Email = string.Format("test{0}@example.com", index),
                    Name = string.Format("test{0}", index),
                    Phone = string.Format("{0}{0}{0} {0}{0}{0} {0}{0}{0}{0}", index)
                };
                AddContact(contact1);
            }
        }
        public IEnumerable GetAllContacts()
        {
            return _contacts.FindAll();
        }
        public Contact GetContact(string id)
        {
            IMongoQuery query = Query.EQ("_id", id);
            return _contacts.Find(query).FirstOrDefault();
        }
        public Contact AddContact(Contact item)
        {
            item.Id = ObjectId.GenerateNewId().ToString();
            item.LastModified = DateTime.UtcNow;
            _contacts.Insert(item);
            return item;
        }
        public bool RemoveContact(string id)
        {
            IMongoQuery query = Query.EQ("_id", id);
            WriteConcernResult result = _contacts.Remove(query);
            return result.DocumentsAffected == 1;
        }
        public bool UpdateContact(string id, Contact item)
        {
            IMongoQuery query = Query.EQ("_id", id);
            item.LastModified = DateTime.UtcNow;
            IMongoUpdate update = Update
                .Set("Email", item.Email)
                .Set("LastModified", DateTime.UtcNow)
                .Set("Name", item.Name)
                .Set("Phone", item.Phone);
            WriteConcernResult result = _contacts.Update(query, update);
            return result.UpdatedExisting;
        }
    }

3. 添加Controller

右键Controllers目录选择添加Controller

5

选择Empty API controller,将Controller命名为ContactsController

6

添加如下代码,可以看到Controller中的API方法名就是以http verb命名的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ContactsController : ApiController
    {
        private static readonly IContactRepository _contacts = new ContactRepository(string.Empty);
        public IQueryable Get()
        {
            return _contacts.GetAllContacts().AsQueryable();
        }
        public Contact Get(string id)
        {
            Contact contact = _contacts.GetContact(id);
            if (contact == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return contact;
        }
        public Contact Post(Contact value)
        {
            Contact contact = _contacts.AddContact(value);
            return contact;
        }
        public void Put(string id, Contact value)
        {
            if (!_contacts.UpdateContact(id, value))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }
        public void Delete(string id)
        {
            if (!_contacts.RemoveContact(id))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }
    }

4. 添加View

首先添加Knockoutjs库,

9

Knockoutjs通过MVVM模式来实现动态html绑定数据,如下图,其中View-Model是客户端的JavaScript object保存的model数据。

webapi_ef16

先打开HomeController,里面添加一个新的Action代码如下,因为我们要在MVC中对于ContactsController添加对应的View。

1
2
3
4
5
6
7
public ActionResult Admin()
        {
            string apiUri = Url.HttpRouteUrl("DefaultApi", new { controller = "contacts", });
            ViewBag.ApiUrl = new Uri(Request.Url, apiUri).AbsoluteUri.ToString();
            return View();
        }

然后右键Admin方法,选择添加View

10

选择Create strongly-typed view,在model class中选择Contact类。

11

添加View的完整代码,注意view中我们通过js去访问WebAPI,以及通过动态绑定将数据呈现在网页上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@model WebAPIDemo.Models.Contact
@{
    ViewBag.Title = "Admin";
}
@section Scripts {
  @Scripts.Render("~/bundles/JQueryval")
  <script type="text/JavaScript" src="@Url.Content("~/Scripts/knockout-2.3.0.js")"></script>
  <script type="text/JavaScript">
      function ProductsViewModel() {
          var self = this;
          self.products = ko.observableArray();
          var baseUri = '@ViewBag.ApiUrl';
          self.create = function (formElement) {
              // If valid, post the serialized form data to the web api
              $(formElement).validate();
              if ($(formElement).valid()) {
                  $.post(baseUri, $(formElement).serialize(), null, "json")
                      .done(function (o) { self.products.push(o); });
              }
          }
          self.update = function (product) {
              $.ajax({ type: "PUT", url: baseUri + '/' + product.Id, data: product });
          }
          self.remove = function (product) {
              // First remove from the server, then from the UI
              $.ajax({ type: "DELETE", url: baseUri + '/' + product.Id })
                  .done(function () { self.products.remove(product); });
          }
          $.getJSON(baseUri, self.products);
      }
      $(document).ready(function () {
          ko.applyBindings(new ProductsViewModel());
      })
  </script>
}
<h2>Admin</h2>
<div class="content">
    <div class="float-left">
    <ul id="update-products" data-bind="foreach: products">
        <li>
            <div>
                <div class="item">ID</div> <span data-bind="text: $data.Id"></span>
            </div>
            <div>
                <div class="item">Name</div>
                <input type="text" data-bind="value: $data.Name"/>
            </div>
            <div>
                <div class="item">Phone</div>
                <input type="text" data-bind="value: $data.Phone"/>
            </div>
            <div>
                <div class="item">Email</div>
                <input type="text" data-bind="value: $data.Email"/>
            </div>
            <div>
                <div class="item">Last Modified</div> <span data-bind="text: $data.LastModified"></span>
            </div>
            <div>
                <input type="button" value="Update" data-bind="click: $root.update"/>
                <input type="button" value="Delete Item" data-bind="click: $root.remove"/>
            </div>
        </li>
    </ul>
    </div>
    <div class="float-right">
    <h2>Add New Product</h2>
    <form id="addProduct" data-bind="submit: create">
        @Html.ValidationSummary(true)
        <fieldset>
            <legend>Contact</legend>
            @Html.EditorForModel()
            <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
    </form>
    </div>
</div>

接下来在_layout.cshtml中添加一个admin页面的链接如下

1
2
3
4
5
6
<ul id="menu">
    <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li>
    <li>@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)</li>
    <li>@Html.ActionLink("Admin", "Admin", "Home")</li>
</ul>

5. 测试与调试

大功告成,直接运行下我们的作品,我们的admin链接也显示在右上角,

12

Admin页面的样子,Contact list是动态加载进来的,可以通过这个页面做添加,修改,删除的操作。

13

通过IE network capture来查看请求内容,

重新加载页面,可以看到回复的格式为JSON,

14

JSON内容就是我们mock的一些数据。

image

接下来我们修改,删除,又添加了一条记录,可以看到使用了不同的http method。

image

通过前面安装的mongovue来查看下DB种的数据,先添加的user也在其中,令我感到欣慰。。。

image

其实还有两个有趣的实例,不过文章一写就长了,不好意思耽误大家时间,只好先放放,以后再写。