从数据库或者其他位置加载ASP.NET MVC Views 视图 数据库中加载 cshtml


从数据库或者其他位置加载ASP.NET MVC Views 视图 数据库中加载 cshtml

对于绝大多数 ASP.NET Core MVC 应用程序,从文件系统定位和加载视图的传统方法可以很好地完成工作。但您也可以从包括数据库在内的其他来源加载视图。这在您希望为用户提供制作或修改 Razor 文件的选项但不想让他们访问文件系统的情况下很有用。本文着眼于创建组件以从其他来源获取视图所需的工作,使用数据库作为工作示例。

Razor 视图引擎使用称为 FileProviders 的组件来获取视图的内容。视图引擎将迭代其搜索视图的位置集合 (ViewLocationFormats),然后将这些位置依次呈现给每个已注册的 FileProvider,直到返回视图内容。在启动时,向视图引擎注册了 PhysicalFileProvider,该引擎旨在从每个 MVC 项目模板中的常用 Views 文件夹开始在各个位置查找物理 .cshtml 文件。  EmbeddedFileProvider 可用于从嵌入式资源中获取视图内容。如果您想将视图存储在另一个位置,例如数据库,您可以创建自己的  FileProvider并将其注册到视图引擎。

FileProviders 必须实现  IFileProvider 接口。  IFileProvider 接口指定以下成员:

C# 全选
IDirectoryContents GetDirectoryContents(string subpath);
IFileInfo GetFileInfo(string subpath);
IChangeToken Watch(string filter);

其中最重要的是  GetFileInfo 方法,它返回一个对象,该对象实现了表示文件实现的  IFileInfo 接口。  Watch 方法返回  IChangeToken 接口的实现。 当视图引擎第一次找到一个视图时,它必须编译它。 它缓存已编译的视图,以便不必为后续请求再次编译它。 视图引擎需要某种方式来通知原始视图已发生更改,以便可以使用最新版本刷新缓存。  IChangeToken 实例提供该通知。 因此,为了从数据库中获取视图,我们需要  IFileProvider 的实现、 IFileInfo 的实现和  IChangeToken 的实现。

数据库模式

下面说明了存储视图所需的数据库表的最小模式以及用于创建表的 DDL

从数据库或者其他位置加载ASP.NET MVC Views 视图 数据库中加载 cshtml

SQL 全选
CREATE TABLE [dbo].[Views](
    [Location] [nvarchar](150) NOT NULL,
    [Content] [nvarchar](max) NOT NULL,
    [LastModified] [datetime] NOT NULL,
    [LastRequested] [datetime]
) 

Location 字段包含视图的唯一标识符。 视图引擎使用子路径查找视图,因此使用它们来识别单个视图是有意义的。 因此,主页的 Location 值将是视图引擎期望为 Home 控制器的 Index 方法找到视图的路径之一,例如 /views/home/index.cshtml。 Content 字段包含视图文件中的 Razor 和 HTML。 LastModified 字段在创建视图时默认为 GetUtcDate,并在修改视图内容时更新。 每当视图引擎成功检索内容时,LastRequested 字段都会更新为当前的 UTC 日期和时间。 这两个字段用于计算自上次检索、编译和缓存文件以来是否发生任何修改。 您可以将 LastModified 的默认值设置为 GetDate(),然后在作为 CRUD 过程的一部分编辑文件时重置该值。

IFileProvider

C# 全选
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using System;
using System.IO;

namespace RazorEngineViewOptionsFileProviders
{
    public class DatabaseFileProvider : IFileProvider
    {
        private string _connection;
        public DatabaseFileProvider(string connection)
        {
            _connection = connection;
        }
        public IDirectoryContents GetDirectoryContents(string subpath)
        {
            throw new NotImplementedException();
        }

        public IFileInfo GetFileInfo(string subpath)
        {
            var result = new DatabaseFileInfo(_connection, subpath);
            return result.Exists ? result as IFileInfo : new NotFoundFileInfo(subpath);
        }

        public IChangeToken Watch(string filter)
        {
            return new DatabaseChangeToken(_connection, filter);
        }
    }
}

我已将我的实现命名为 DatabaseFileProvider。 它有一个构造函数,该构造函数采用一个字符串,该字符串表示数据库的连接字符串。 我没有提供 GetDirectoryContents 方法的实现,因为这个用例不需要。 如果找到与指定路径匹配的结果,GetFileInfo 方法将返回我的自定义 IFileInfo,或者返回 NotFoundFileInfo 对象,它告诉视图引擎尝试另一个提供程序或另一个视图位置。 Watch 方法返回我的自定义 IChangeToken 对象。 

IFileInfo

IFileInfo 接口具有以下成员:

C# 全选
public interface IFileInfo
{
    //
    // Summary:
    //     True if resource exists in the underlying storage system.
    bool Exists { get; }
    //
    // Summary:
    //     True for the case TryGetDirectoryContents has enumerated a sub-directory
    bool IsDirectory { get; }
    //
    // Summary:
    //     When the file was last modified
    DateTimeOffset LastModified { get; }
    //
    // Summary:
    //     The length of the file in bytes, or -1 for a directory or non-existing files.
    long Length { get; }
    //
    // Summary:
    //     The name of the file or directory, not including any path.
    string Name { get; }
    //
    // Summary:
    //     The path to the file, including the file name. Return null if the file is not
    //     directly accessible.
    string PhysicalPath { get; }

    //
    // Summary:
    //     Return file contents as readonly stream. Caller should dispose stream when complete.
    //
    // Returns:
    //     The file stream
    Stream CreateReadStream();
}

我已将源代码中的注释留在其中,因为它们很好地解释了每个成员的目的。 重要的是 Name、Exists、Length 属性和 CreateReadStream 方法。 这是 DatabaseFileInfo 类,它是 IFileInfo 的自定义实现,用于从数据库中获取视图内容:

C# 全选
using Microsoft.Extensions.FileProviders;
using System;
using System.Data.SqlClient;
using System.IO;
using System.Text;

namespace RazorEngineViewOptionsFileProviders
{
    public class DatabaseFileInfo : IFileInfo
    {
        private string _viewPath;
        private byte[] _viewContent;
        private DateTimeOffset _lastModified;
        private bool _exists;

        public DatabaseFileInfo(string connection, string viewPath)
        {
            _viewPath = viewPath;
            GetView(connection, viewPath);
        }
        public bool Exists => _exists;

        public bool IsDirectory => false;

        public DateTimeOffset LastModified => _lastModified;

        public long Length
        {
            get
            {
                using (var stream = new MemoryStream(_viewContent))
                {
                    return stream.Length;
                }
            }
        }

        public string Name => Path.GetFileName(_viewPath);

        public string PhysicalPath => null;

        public Stream CreateReadStream()
        {
            return new MemoryStream(_viewContent);
        }

        private void GetView(string connection, string viewPath)
        {
            var query = @"SELECT Content, LastModified FROM Views WHERE Location = @Path;
                    UPDATE Views SET LastRequested = GetUtcDate() WHERE Location = @Path";
            try
            {
                using (var conn = new SqlConnection(connection))
                using (var cmd = new SqlCommand(query, conn))
                {
                    cmd.Parameters.AddWithValue("@Path", viewPath);
                    conn.Open();
                    using (var reader = cmd.ExecuteReader())
                    {
                        _exists = reader.HasRows;
                        if (_exists)
                        {
                            reader.Read();
                            _viewContent = Encoding.UTF8.GetBytes(reader["Content"].ToString());
                            _lastModified = Convert.ToDateTime(reader["LastModified"]);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                // if something went wrong, Exists will be false
            }
        }
    }
}

真正的工作是在构造函数中调用的 GetView 方法中完成的。 它检查数据库中是否存在与视图引擎提供的文件路径匹配的条目。 如果找到匹配项,则将 Exists 设置为 true,并通过 CreateReadStream 方法将内容作为 Stream 提供。 对于这个示例,我选择使用普通的 ADO.NET,但也可以使用其他数据访问技术。

IChangeToken

链中的最后一个组件是 IChangeToken 的实现。 这负责通知视图引擎一个视图已被修改,并且缓存的版本应该被更新的版本替换。

C# 全选
using Microsoft.Extensions.Primitives;
using System;
using System.Data.SqlClient;

namespace RazorEngineViewOptionsFileProviders
{
    public class DatabaseChangeToken : IChangeToken
    {
        private string _connection;
        private string _viewPath;

        public DatabaseChangeToken(string connection, string viewPath)
        {
            _connection = connection;
            _viewPath = viewPath;
        }

        public bool ActiveChangeCallbacks => false;

        public bool HasChanged
        {
            get
            {

                var query = "SELECT LastRequested, LastModified FROM Views WHERE Location = @Path;";
                try
                {
                    using (var conn = new SqlConnection(_connection))
                    using (var cmd = new SqlCommand(query, conn))
                    {
                        cmd.Parameters.AddWithValue("@Path", _viewPath);
                        conn.Open();
                        using (var reader = cmd.ExecuteReader())
                        {
                            if (reader.HasRows)
                            {
                                reader.Read();
                                if (reader["LastRequested"] == DBNull.Value)
                                {
                                    return false;
                                }
                                else
                                {
                                    return Convert.ToDateTime(reader["LastModified"]) > Convert.ToDateTime(reader["LastRequested"]);
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                    }

                }
                catch (Exception)
                {
                    return false;
                }
            }
        }

        public IDisposable RegisterChangeCallback(Action<object> callback, object state) => EmptyDisposable.Instance;
    }

    internal class EmptyDisposable : IDisposable
    {
        public static EmptyDisposable Instance { get; } = new EmptyDisposable();
        private EmptyDisposable() { }
        public void Dispose() { }
    }
}

接口的关键成员是 HasChanged 属性。 this 的值是通过比较匹配文件条目的最后请求时间和最后修改时间来确定的。 如果文件自上次请求以来已被修改,则该属性设置为 true,这将导致视图引擎检索修改后的版本。

现在唯一要做的就是向视图引擎注册 DatabaseFileProvider 以便它知道使用它。 这是在 Startup.cs 的 ConfigureServices 方法中完成的:

C# 全选
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();
    services.Configure<RazorViewEngineOptions>(opts => 
        opts.FileProviders.Add(
            new DatabaseFileProvider(Configuration.GetConnectionString("DefaultConnection"))
        )
    );
}

有几点需要注意。 PhysicalFileProvider 将首先被调用,因为它首先被注册。 如果您在要检查的位置之一中有一个 .cshtml 文件,它将被返回,并且不会为该请求调用 DatabaseFileProvider(或任何后续提供程序)。 在其当前形式中,将为视图引擎检查的每个位置调用 IChangeToken。 出于这个原因,缓存数据库条目存在的路径可能是明智的,如果请求的路径不在缓存中,则防止执行数据库请求。

总结

Razor 视图引擎被设计为完全可扩展的,使您能够插入自己的 FileProvider,以便您可以从可以为其编写提供程序的任何源定位和加载视图。 本文展示了如何使用数据库作为源来做到这一点。 示例站点可从 GitHub 获得。 

参考

 

 

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:当CDN服务不可用时,前端有什么解决办法
下一篇:化繁为简,用几个例子介绍JavaScript异步处理async awite
评论列表

发表评论

评论内容
昵称:
关联文章

数据库或者其他位置ASP.NET MVC Views 视图 数据库中加 cshtml
asp.net core mvc修改cshtml试图热动态更新
ASP.NET MVC快速入门(一)
.NET MVC后台JS代码块
ASP.NET MVC 后台控制器向View前台传递数据的几种方式
asp.net - 在 ASP.NET Core MVC 中嵌套 TagHelper
ASP.NET MVCASP.NET Core MVC中获取当前URL/Controller/Action
网站迁移纪实:Web Form 到 Asp.Net Core (Abp vNext 自定义开发)
ASP.NET Core MVC 在过滤器ActionFilter中保存页面的生成的html静态页面文件
ASP.NET+MVC入门踩坑笔记 (一) 创建项目 项目配置运行 以及简单的Api搭建
C# asp.net mvc 创建虚拟目录
ASP.NET Core官网教程,资料查找
C# ASP.NET Core开发学生信息管理系统(一)
IIS初始化(预),解决第一次访问慢,程序池被回收问题
【推荐】Razor文件编译 ASP.NET Core
服务安装失败:未能文件或程序集
.NET IIS第一次访问慢,程序池被回收问题,IIS初始化(启用预)
ASP.NET Core开发者学习路线图
C# ASP.NET Core开发学生信息管理系统(二)
ASP.NET Core MVC中的路由约束

联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码