从数据库或者其他位置加载ASP.NET MVC Views 视图 数据库中加载 cshtml
对于绝大多数 ASP.NET Core MVC 应用程序,从文件系统定位和加载视图的传统方法可以很好地完成工作。但您也可以从包括数据库在内的其他来源加载视图。这在您希望为用户提供制作或修改 Razor 文件的选项但不想让他们访问文件系统的情况下很有用。本文着眼于创建组件以从其他来源获取视图所需的工作,使用数据库作为工作示例。
Razor 视图引擎使用称为 FileProviders
的组件来获取视图的内容。视图引擎将迭代其搜索视图的位置集合 (ViewLocationFormats
),然后将这些位置依次呈现给每个已注册的 FileProvider,直到返回视图内容。在启动时,向视图引擎注册了 PhysicalFileProvider
,该引擎旨在从每个 MVC 项目模板中的常用 Views 文件夹开始在各个位置查找物理 .cshtml 文件。 EmbeddedFileProvider
可用于从嵌入式资源中获取视图内容。如果您想将视图存储在另一个位置,例如数据库,您可以创建自己的 FileProvider
并将其注册到视图引擎。
FileProviders
必须实现 IFileProvider
接口。 IFileProvider
接口指定以下成员:
IDirectoryContents GetDirectoryContents(string subpath);
IFileInfo GetFileInfo(string subpath);
IChangeToken Watch(string filter);
其中最重要的是 GetFileInfo
方法,它返回一个对象,该对象实现了表示文件实现的 IFileInfo
接口。 Watch
方法返回 IChangeToken
接口的实现。 当视图引擎第一次找到一个视图时,它必须编译它。 它缓存已编译的视图,以便不必为后续请求再次编译它。 视图引擎需要某种方式来通知原始视图已发生更改,以便可以使用最新版本刷新缓存。 IChangeToken
实例提供该通知。 因此,为了从数据库中获取视图,我们需要 IFileProvider
的实现、 IFileInfo
的实现和 IChangeToken
的实现。
数据库模式
下面说明了存储视图所需的数据库表的最小模式以及用于创建表的 DDL
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
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 接口具有以下成员:
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 的自定义实现,用于从数据库中获取视图内容:
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 的实现。 这负责通知视图引擎一个视图已被修改,并且缓存的版本应该被更新的版本替换。
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 方法中完成的:
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 获得。
参考