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

ASP MVC5 & Azure 表存储脚手架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2014年8月23日

CPOL

8分钟阅读

viewsIcon

27898

downloadIcon

384

在ASP.NET MVC5中生成Microsoft Azure Table Storage实体

引言

使用Microsoft ASP.NET MVC借助诸如Scaffolding之类的技术构建功能性的业务系统很容易,只要您使用Microsoft SQL Server。我发现让业务用户转向Microsoft Azure Table Storage是一场不可战胜的遭遇,尤其当他们听说开发时间会更长时。

借助Microsoft Visual Studio 2013 Update 2,Microsoft使MVC Scaffolding 扩展能够开发,从而允许开发人员扩展脚手架系统的行为。

我已使用这些方法开发了一个生成Microsoft Azure Table Storage实体控制器和视图的脚手架。

源代码可在GitHub上找到

安装Visual Studio扩展

Visual Studio Gallery下载Azure Table Storage Scaffolder扩展。在Visual Studio 2013中,打开工具,然后单击扩展和更新菜单项。

下载扩展后,请按照步骤将其安装到您的Visual Studio 2013副本中。

完成此步骤后,该扩展将可用于任何ASP.NET MVC项目。

创建新的MVC项目

要开始生成Azure Table Storage实体,请在Visual Studio 2013中创建一个新的ASP.NET Web应用程序

在ASP.NET One Project向导中,选择MVC模板,然后选择MVCWeb API核心引用。

项目创建后,使用程序包管理器控制台安装以下NuGet程序包。

install-package WindowsAzure.Storage
install-package BlueMarble.Shared
install-package BlueMarble.Shared.Azure
install-package KendoUIWeb

 

添加模型

接下来,我们可以添加几个模型,我们希望为其生成控制器和视图。在此示例中,我们将使用一个简单的书籍+作者示例。我们将有两个表,一个列出作者,另一个列出书籍。每本书都必须有一个作者。

作者实体只有一个属性,即作者姓名。

public class Author : BlueMarble.Shared.Azure.Storage.Table.Entity
{
    public Author() : base() { }
    public Author(string publicId) : base(publicId) { }

    public string Name { get; set; }
}

图书实体有两个属性,即书名和对作者实体的引用。

public class Book : BlueMarble.Shared.Azure.Storage.Table.Entity
{
    public Book() : base() { }
    public Book(string publicId) : base(publicId) { }

    public string Name { get; set; }

    [RelatedTable(Type = typeof(WebApplication18.Models.Author))]
    public string AuthorPublicId { get; set; }
}

在图书实体中,对作者实体的引用由字段上方的RelatedTable属性确定。该字段必须是字符串,因为它将存储作者表的PublicId值。PublicId由实体内部生成,它是PartitionKey和RowKey的组合,并应用了Base64编码。

为了便于识别任何给定列中存储的数据,建议在相关字段名称后添加PublicId后缀。

添加存储上下文

与Entity Framework中可用的DataContext组件非常相似,您必须为脚手架提供一个StorageContext。StorageContext是一个继承自BlueMarble.Shared.Azure.Storage.Table.StorageContext的类,它使脚手架能够用所需的表属性和方法来扩展脚手架。这是通过为每个实体创建一个部分类来实现的。

public partial class StorageContext : BlueMarble.Shared.Azure.Storage.Table.StorageContext
{
    public StorageContext(Microsoft.WindowsAzure.Storage.CloudStorageAccount StorageAccount)
        : base(StorageAccount)
    {
    }

    public StorageContext()
        : base(new Microsoft.WindowsAzure.Storage.CloudStorageAccount(
            new Microsoft.WindowsAzure.Storage.Auth.StorageCredentials(
                Properties.Settings.Default.StorageAccountName,
                Properties.Settings.Default.StorageAccountKey), true))
    {
    }

    public override void InitializeTables()
    {
        base.InitializeTables();
    }
}

由于存储上下文的生成依赖于部分类,因此您创建的StorageContext必须是部分类。您需要实现两个构造函数和一个方法(InitializeTables)。构造函数允许您的代码轻松创建绑定到不同存储帐户的存储上下文。

您还需要为StorageAccountNameStorageAccountKey创建两个应用程序属性。这可以在项目属性设置选项卡上完成。

InitializeTables方法查找所有用InitializeTable属性装饰的方法并执行它们。这些方法在脚手架过程中被自动生成,并允许Azure存储中每个表的自动初始化。

几乎可以生成代码了

在App_Start/BundleConfig.cs文件中包含以下行以启用KendoUI。

bundles.Add(new ScriptBundle("~/bundles/kendo").Include(
            "~/Scripts/kendo/2014.1.318/kendo.web.min.js"));

bundles.Add(new StyleBundle("~/Content/kendocss").Include(
            "~/Content/kendo/2014.1.318/kendo.common-bootstrap.min.css",
            "~/Content/kendo/2014.1.318/kendo.bootstrap.min.css"));

Azure Table Storage Scaffolder使用KendoUI Web版来呈现用户界面组件,例如下拉列表和日期时间选择器。它还使用KendoUI MVVM组件来数据绑定控件。

更新Views/Shared/_Layout.cshtml文件中的<head>标签,以包含kendocss,使其全局可用。KendoUI脚本作为脚手架过程的一部分添加到需要它们的每个页面上。

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css")
    @Styles.Render("~/Content/kendocss")
    @Scripts.Render("~/bundles/modernizr")
</head>

生成代码

右键单击“解决方案资源管理器”中的任何项,然后选择添加 > 新建脚手架项

接下来,选择使用Microsoft Azure Table Storage的Razor Pages

在脚手架屏幕上,选择要为其生成代码的所有模型以及存储上下文。

您可以选择仅生成控制器、API控制器、存储上下文、视图或脚本。首次执行时,最好从所有必需的组件开始。

结果

视图

以下视图是脚手架过程产生的输出。

目录

index视图使用datatables.net和Reactive Extensions来呈现数据网格。这使得当屏幕空间有限时,网格可以折叠未使用的列。

详细说明

details视图使用HTML5 label和span元素以及data-bind属性来呈现内容。内容使用KendoUI MVVM进行数据绑定,并在HTML加载到浏览器后使用JavaScript从API检索数据。

删除

delete视图使用HTML5 label和span元素以及data-bind属性来呈现内容。内容使用KendoUI MVVM进行数据绑定,并在HTML加载到浏览器后使用JavaScript从API检索数据。
 

未使用。

edit视图使用HTML5 label和input元素以及data-bind属性来呈现内容。内容使用KendoUI MVVM进行数据绑定,并在HTML加载到浏览器后使用JavaScript从API检索数据。

Create

create视图使用HTML5 label和input元素以及data-bind属性来呈现内容。内容使用KendoUI MVVM进行数据绑定,并在HTML加载到浏览器后使用JavaScript从API检索数据。

生成的代码

以下代码是从脚手架过程中生成的。

控制器 (Controller)

控制器代码被保持在最低限度以呈现视图。控制器内部不执行任何功能代码,这是为了确保页面尽快传递到用户的浏览器。然后,这使得浏览器能够查询API,并使用户感觉网站更加响应。

public class AuthorController: Controller
{
 public async Task<ActionResult> Index()
 {
  return View();
 }

 public async Task<ActionResult> Create()
 {
  return View();
 }

 public async Task<ActionResult> Edit(string id)
 {
  ViewBag.PublicId = id;

  return View();
 }

 public async Task<ActionResult> Details(string id)
 {
  ViewBag.PublicId = id;

  return View();
 }

 public async Task<ActionResult> Delete(string id)
 {
  ViewBag.PublicId = id;

  return View();
 }

 public void Dispose()
 {
 }
}

API控制器

API控制器代码是使用StorageContext读写Azure Storage数据的地方。它实现了Get(全部)、Get(特定)、Post(插入)、Put(更新)和Delete HTTP命令。

public class AuthorController: ApiController
{
 protected StorageContext db = new StorageContext();

 [HttpGet]
 public async Task<IEnumerable<Author>> Get()
 {
  return db.GetAuthors();
 }

 [HttpGet]
 public async Task<Author> Get(string id)
 {
  var privateEntity = BlueMarble.Shared.Azure.Storage.Table.Entity.GetPrivateEntity(new Author(id));

  return db.GetAuthor(privateEntity.PartitionKey, privateEntity.RowKey).GetPublicEntity<Author>();
 }

 [HttpPost]
 public string Post(Author entity)
 {
  if (ModelState.IsValid)
  {
   db.InsertAuthor(entity);

   entity.PublicId = entity.GetPublicId();
   return entity.GetPublicEntity<Author>().PublicId;
  }

        return string.Empty;
 }

 [HttpPut]
 public void Put(string id, Author entity)
 {
  if (ModelState.IsValid)
  {
   db.UpdateAuthor(entity.GetPrivateEntity<Author>());
  }
 }

 [HttpDelete]
 public async Task Delete(string id)
 {
  var privateEntity = BlueMarble.Shared.Azure.Storage.Table.Entity.GetPrivateEntity(new Author() { PublicId = id });

  var entity = db.GetAuthor(privateEntity.PartitionKey, privateEntity.RowKey);
   
        if (entity == null)
            throw new System.Exception("Author entity not found, delete failed.");

  await db.DeleteAuthorAsync(entity);
 }

 protected override void Dispose(bool disposing)
    {
        db.Dispose(disposing);
        base.Dispose(disposing);
    }
}

存储上下文

存储上下文代码是用于扩展项目StorageContext类的部分类。此类包含将CloudTable对象初始化到存储帐户中的所有逻辑。它还包含针对存储帐户执行CRUD操作的所有逻辑。

public partial class StorageContext
{
 #region Services

 #region Constants

 internal partial class Constants
 {
  internal partial class StorageTableNames
  {
   public const string Authors = "authors";
  }

  internal partial class StoragePartitionNames
  {
   public const string Author = "author";
  }
 }

 #endregion

 #region Initialize Table

 [InitializeTable]
    public void InitializeAuthorTables()
    {
        Authors = CloudTableClient.GetTableReference(Constants.StorageTableNames.Authors);
        Authors.CreateIfNotExists();
    }

 public CloudTable Authors { get; set; }

 #endregion

 #region Data Access Methods

 public IQueryable<Author> GetAuthors()
    {
        var partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.NotEqual, string.Empty);

        var query = new TableQuery<Author>().Where(partitionKeyFilter);

        var collection = Authors.ExecuteQuery(query);

        return collection.AsQueryable();
    }

 public async Task<IQueryable<Author>> GetAuthorsAsync()
    {
        var partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.NotEqual, string.Empty);

        var query = new TableQuery<Author>().Where(partitionKeyFilter);

        var returnList = await ExecuteSegmentedQueryAsync<Author>(query);

        return returnList.AsQueryable();
    }

 public IQueryable<Author> GetAuthors(string PartitionKey)
    {
        var partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, PartitionKey);

        var query = new TableQuery<Author>().Where(partitionKeyFilter);

        var collection = Authors.ExecuteQuery(query);

        return collection.AsQueryable();
    }

 public async Task<IQueryable<Author>> GetAuthorsAsync(string PartitionKey)
    {
        var partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, PartitionKey);

        var query = new TableQuery<Author>().Where(partitionKeyFilter);

        var returnList = await ExecuteSegmentedQueryAsync<Author>(query);

        return returnList.AsQueryable();
    }

    public Author GetAuthor(string PartitionKey, string RowKey)
    {
        var partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, PartitionKey);
  var rowKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, RowKey);

  var queryFilter = TableQuery.CombineFilters(partitionKeyFilter, TableOperators.And, rowKeyFilter);

        var query = new TableQuery<Author>().Where(queryFilter);

        var collection = Authors.ExecuteQuery(query);

        return collection.FirstOrDefault();
    }

    public async Task<Author> GetAuthorAsync(string PartitionKey, string RowKey)
    {
        var partitionKeyFilter = TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, PartitionKey);
  var rowKeyFilter = TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, RowKey);

  var queryFilter = TableQuery.CombineFilters(partitionKeyFilter, TableOperators.And, rowKeyFilter);

        var query = new TableQuery<Author>().Where(queryFilter);

        var returnList = await ExecuteSegmentedQueryAsync<Author>(query);

        return returnList.FirstOrDefault();
    }

    public void InsertAuthor(Author Author)
    {
        Author.PartitionKey = GetAuthorPartitionKey(Author);
  Author.RowKey = GetAuthorRowKey(Author);
  Author.PublicId = Author.GetPublicId();

        Insert<Author>(Author, Authors);
    }

    public async Task InsertAuthorAsync(Author Author)
    {
        Author.PartitionKey = GetAuthorPartitionKey(Author);
  Author.RowKey = GetAuthorRowKey(Author);
  Author.PublicId = Author.GetPublicId();

        await InsertAsync<Author>(Author, Authors);
    }

    public void InsertAuthorBatch(IEnumerable<Author> Authors)
    {
        InsertBatch<Author>(Authors, this.Authors);
    }

    public async Task InsertAuthorBatchAsync(IEnumerable<Author> Authors)
    {
        await InsertBatchAsync<Author>(Authors, this.Authors);
    }

    public void UpdateAuthor(Author Author)
    {
        Replace<Author>(Author, Authors);
    }

    public async Task UpdateAuthorAsync(Author Author)
    {
        await ReplaceAsync<Author>(Author, Authors);
    }

    public void DeleteAuthor(Author Author)
    {
        Delete<Author>(Author, Authors);
    }

    public async Task DeleteAuthorAsync(Author Author)
    {
        await DeleteAsync<Author>(Author, Authors);
    }

 #endregion

    #endregion
}

存储上下文键助手

存储上下文键助手是一个部分类,它通过提供生成每个实体PartitionKey和RowKey属性的方法来扩展项目StorageContext。

由于PartitionKey和RowKey属性带来的要求以及它们如何影响Azure存储中的存储,您必须在生成这些文件后进行编辑。这些助手在生成代码后的默认行为是允许存储一条记录。任何后续记录都将被拒绝,并返回409 Conflict HTTP响应。

public partial class StorageContext
{
 #region Key Helpers
  
    public string GetAuthorPartitionKey(Author Author)
    {
        return GetPartitionKey(Constants.StorageTableNames.Authors);
    }

    public string GetAuthorPartitionKey()
    {
        return GetPartitionKey(Constants.StorageTableNames.Authors);
    }

    public string GetAuthorRowKey(Author Author)
    {
        return GetRowKey();
    }

    public string GetAuthorRowKey()
    {
        return GetRowKey();
    }

 #endregion
}

BlueMarble.Shared.Azure.Storage.Table.Entity有一个公共属性ModelGuid,它在构造函数中用Guid.NewGuid()初始化。要启用多条记录,请将代码修改为如下所示:

public string GetAuthorRowKey(Author Author)
{
    return GetRowKey(Author.ModelGuid.ToString());
}

public string GetAuthorRowKey(string ModelGuid)
{
    return GetRowKey(ModelGuid);
}

这使用每个实体的唯一ModelGuid来定义RowKey。即使所有数据都在同一个分区中,如Constants.StorageTableNames.Authors中的常量值所定义,每个实体也将有一个唯一的ModelGuid,因此RowKey。

我原本打算让脚手架模板预先应用此逻辑,但后来放弃了这个想法。原因是,当涉及到Azure存储时,您必须为每个实体考虑PartitionKey和RowKey逻辑。这不像SQL Server那样,您可以简单地依赖于外键和自动递增的标识列。为了交易成本和性能,识别正确的分区和行策略非常重要。

JavaScript

生成两个JavaScript文件,一个用于所有插入、更新、删除和数据绑定功能。另一个由需要数据绑定组合框的窗体使用。

/* *************************************** */
/* Author api data access */
/* *************************************** */

var AuthorsData = AuthorsData || (function(){

 return {
  AuthorsDataSource: new kendo.data.DataSource({
   transport: {

    read: {
     url: '/api/Author',
     dataType: 'json'
    }
   
   },
   schema: {

    model: {
     Name: 'Name',
    }

   }
  })

 };
}());

以下JavaScript文件包含所有用于数据绑定到窗体以及执行所有插入、更新和删除行为的逻辑。

/* *************************************** */
/* Author api data access */
/* *************************************** */

var AuthorData = AuthorData || (function(){
   

 return {
  PublicId: 'NOTSET',
  Delete: function(e) {

   var that = this;

   e.preventDefault();

            $.ajax({
                url: '/api/Author/' + that.PublicId,
                type: 'DELETE',
                dataType: 'json',
                success: function(data) {

                    window.location.href = '/Author/';

                },
                error: function(error) {

                    that.LogError(error);

                }
            });

  },
  Update: function(e) {

   var that = this;

      e.preventDefault();

   $.ajax({
    url: '/api/Author/' + that.PublicId,
    type: 'PUT',
    dataType: 'json',
    data: {
     Name: that.Name,
     PublicId: this.PublicId
    },
    success: function(data) {

     window.location.href = '/Author/Edit/' + that.PublicId;

    },
    error: function(error) {

     that.LogError(error);

    }
   });

  },
  Create: function(e) {

   var that = this;

   e.preventDefault();

   $.ajax({
    url: '/api/Author',
    type: 'POST',
    dataType: 'json',
    data: {
     Name: that.Name,
     PublicId: that.PublicId
    },
    success: function(data) {

     window.location.href = '/Author/Details/' + data;

    },
    error: function(error) {

     that.LogError(error);

    }
   });

  },
  Load: function (form) {

   var that = this;

   that.ViewModel.loading = true;
   that.ViewModel.loaded = false;

   that.Bind(form);

   $.ajax({
    url: '/api/Author/' + that.PublicId,
    type: 'GET',
    dataType: 'json',
    success: function (data) {

     that.LoadEntity(data, form);

    },
    error: function (error) {

     that.LogError(error);

    }
   });

  },
  LoadEntity: function(data, form){

   var that = this;
   that.ViewModel.Name = data.Name;
   that.ViewModel.PublicId = data.PublicId;

   that.ViewModel.loading = false;
   that.ViewModel.loaded = true;

   that.Bind(form);

  },
  ViewModel: kendo.observable({
   PublicId: '',
  
   Name: '',
   hasChanges: false,
   saving: false,
   saved: false,
   creating: false,
   created: false,
   deleting: false,
   deleted: false,
   loading: true,
   loaded: false,
   error: false,
   errorMessage: '',
   update: undefined,
   delete: undefined,
   create: undefined
    
  }),
  Bind: function(form) {

      kendo.bind(form, this.ViewModel);

  },
  LogError: function(error) {

   var that = this;
   
   that.ViewModel.error = true;
   that.ViewModel.errorMessage = error.responseJSON.ExceptionMessage;

   console.log(error);

  },
  Init: function(publicId) {
   
   var that = this;

      that.PublicId = publicId;

   that.ViewModel.PublicId = publicId;

   that.ViewModel.Name = '';
    
   that.ViewModel.update = that.Update;
   that.ViewModel.delete = that.Delete;
   that.ViewModel.create = that.Create;
  }
 };
}());

视图

脚手架过程生成的Index、Create、Edit、Details和Delete视图,其行为与Microsoft的标准Entity Framework脚手架生成的视图大致相同。

唯一的区别是,这些视图不依赖于标准控制器来填充数据和处理回发。这些视图使用KendoUI MVVM将控件绑定到数据,并使用jQuery ajax调用来填充数据和处理窗体回发。

历史

在此处保持您所做的任何更改或改进的实时更新。

© . All rights reserved.