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

使用 Blazor 和 OData 快速构建数据驱动的 Web 应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (3投票s)

2019 年 11 月 21 日

CPOL

14分钟阅读

viewsIcon

5383

使用 Blazor 和 OData,通过流畅的 C# LINQ 在服务器和客户端之间实现无缝通信,构建数据驱动的 .NET 应用程序(类似于 GraphQL,但没有 JSON)。

Build Data-Driven Web Apps Blazing Fast with Blazor and OData

很久以前,有一项名为 WCF RIA Services 的技术。它通过在客户端和服务器之间搭建一座神奇的桥梁,使我们的生活更加轻松。在后台,它生成了一组端点和代理,它们协同工作,在整个堆栈中提供无缝的用户体验。在 Silverlight 应用中使用领域对象的 LINQ 查询会生成适当的 API 调用到服务器,这些调用会过滤、传输和反序列化数据。

想象一下,如果在一个 REST 端点上实现高级过滤和排序,就像编写这段流畅的代码并在任何浏览器客户端中运行一样简单:

return _client.For<To do>
   .Filter(todo => !todo.Complete)
   .Orderby(todo => todo.Description);

“给我一个未标记为完成的待办事项列表,并按描述排序。”

这将生成适当的服务器调用来获取过滤和排序后的列表(而不是将整个列表传输到网络然后再进行过滤)。

如果你可以使用完全相同的“待办事项”模型在客户端中,包含业务逻辑和验证,为用户提供一个添加和更新列表的表单,那会怎么样?

别担心。这在今天是可能的。不,我不是在说要费力构建一个华而不实的 GraphQL 客户端并发送巨大的 JSON 有效负载来获取你想要的东西。我指的是开箱即用,利用 Blazor 的强大功能,以及使用非常稳定且成熟的 OData。你的后端是 SQL ServerNoSQL Cosmos DB 实例还是内存缓存,都没关系。

The Todo App

“待办事项”应用程序

感兴趣吗?让我们通过一个简单的“todo”应用程序来探讨如何实现。

OData:GraphQL 的前身

GraphQL 是当今一项热门技术。它使前端/客户端开发人员能够查询数据并塑造结果,例如仅检索可用属性的子集和/或遍历复杂的对象图。尽管 GraphQL 作为一项“较新”的技术引起了广泛关注,但 .NET 开发人员在过去十多年中一直能够获得它所提供的功能。Project Astoria 于 2007 年 7 月公开发布。我最近主持了一个 Channel 9 的 On .NET OData 节目,如果你不熟悉 OData 或需要复习,可以在这里观看。

为了使演示保持独立,我创建了一个服务器项目来提供 OData 端点。客户端可以轻松地与任何现有的 OData 端点配合使用,而不管交付它的服务器或技术是什么。示例应用程序的服务器部分是 .NET Core 2.1 项目,它为 To do 存储库公开了一个 OData API。你可以在这里访问源代码:

以下是 To do 项的定义。它定义在一个由服务器和客户端共享的 .NET Standard 类库中。请注意,我使用了数据注解来要求描述并提供验证失败时显示的错误消息。我还通过公开 MarkComplete 操作来提供一些业务逻辑,该操作会将 Complete 设置为 true 并设置 MarkedComplete 的日期。

public class To do
{
   public To do()
   {
      Created = DateTime.UtcNow;
   }
   public int Id { get; set; }
   public bool Complete { get; set; }
   public DateTime Created { get; set; }
   public DateTime? MarkedComplete { get; set; }

   [Required(ErrorMessage = "Description is required.")]
   public string Description { get; set; }
   public void MarkComplete()
   {
      if (!Complete)
      {
         Complete = true;
         MarkedComplete = DateTime.UtcNow;
      }
   }
}

在这个示例中,我使用 Entity Framework Core 来设置数据上下文,因为我可以指定一个内存数据库来保持示例的简单性。只需更改一个简单的配置,就可以切换到更持久、生产级的数据库,如 SQL Server 或 Cosmos DB。

public class TodoContext : DbContext
{
   public TodoContext(DbContextOptions options) : base(options)
   {
   }
   public DbSet<To do> TodoList { get; set; }
}

尽管 OData 可以与任何数据源配合使用,并且不需要 Entity Framework,但使用 Entity Framework 可以更轻松地生成 OData 控制器。

Startup.cs 中,添加了 OData 服务:

services.AddOData();

该配置创建了一个实体数据模型来公开 To do 类的操作。

app.UseMvc(routes =>
{
   routes.EnableDependencyInjection();
   routes.Select().OrderBy().Filter().Count();
   var builder = new ODataConventionModelBuilder();
   builder.EntitySet<To do>("todos").EntityType.Filter().Count().Expand().OrderBy().Select();
   routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
   routes.MapRoute(
      name: "default",
      template: "{controller=Home}/{action=Index}/{id?}");
});

端点将以 /odata 开头,而不是传统的 Web API /api 前缀。实体集支持过滤、求和、展开相关对象(在此示例项目中未演示)和排序。

控制器继承自 ODataController

[EnableCors]
[ODataRoutePrefix("todos")]
public class TodosController : ODataController
{
}

我只是启用了 CORS。生产应用程序应实施 CORS,只允许生产中实际使用的域和方法。

现在,请做好准备。添加一个支持投影、过滤和排序等功能的新增端点的复杂程度如下:

[EnableQuery]
[ODataRoute]
public IEnumerable<To do> GetTodoList()
{
   return _context.TodoList;
}

此时,我可以构建一个 OData 查询。在这里,我使用跨平台的 .NET Core 全局 HTTPREPL 工具来查询我的项目。注意在结果中,我得到了我所要求的(并且仅限于)内容:

Example OData Query

HTTPREPL 查询 OData

“给我未完成的项目。只返回描述和创建日期,并按创建日期降序排序,最近创建的项目排在最前面。”

数据注解会自动由控制器解析,因此插入带验证的项目(以防止服务器插入没有描述的 To do 实体)的代码如下所示:

[ODataRoute]
public async Task<IActionResult> Post([FromBody] To do todo)
{
   if (!ModelState.IsValid)       // auto-magic
   {
      return BadRequest(ModelState); // with an explicit message!
   }
   _context.TodoList.Add(todo);
   await _context.SaveChangesAsync();
   return CreatedAtAction("GetTodo", new { id = todo.Id }, todo);
}

随意 浏览其他控制器操作(例如,注意 PATCH 端点何时调用 MarkComplete 操作)。现在,一个 proper OData 客户端已经完成,让我们看看构建一个可以在任何现代浏览器(无论是在桌面、笔记本电脑、平板电脑还是手机上)上运行的 Web 客户端需要做什么。

Blazor:浏览器中的无插件 .NET

如果你还不熟悉 Blazor,我已经写过 几篇 Blazor 文章了。看看它们吧!

在本节中,我涵盖了从构建可重用的视图组件到使用 MVVM 模式实现验证的方方面面。如果你只想跳到重点,看看如何使用流畅的 C# 编写查询,请随意跳到 简单 OData 客户端

我创建了一个 Blazor 客户端项目,并添加了一个对共享 To do 模型的引用。然后我创建了三个 Razor 视图组件。第一个是 TodoErrors,用于显示错误消息(类显示我可以轻松地与现有的 JavaScript 和 CSS 库(如 Bootstrap)进行互操作)。

@foreach(var error in Errors)
{
   <div class="alert alert-danger">@error</div>
}
@code {
   [Parameter]
   public List<string> Errors { get; set; }
}

在这里,我需要指出,我知道有内置的 Blazor 控件可以自动处理验证。我故意“自己动手”来展示一些稍后将介绍的 Blazor 功能。

接下来,我处理了显示实际的 To do。我倾向于从简单的开始然后分层服务,因此视图组件设计为接受操作和实体作为参数。在测试时,我可以硬编码一些内容,稍后将其与“真实”服务连接起来。

<div>
<button @onclick="Delete">Delete</button>&nbsp;
@if(CurrentItem.Complete)
{
   <strong>@CurrentItem.Description</strong>
   Completed: @CurrentItem.MarkedComplete
}
else
{
   <button @onclick="Markcomplete">Done</button>
   @CurrentItem.Description
}
Created: @CurrentItem.Created
</div>
@code {
[Parameter]
public To do CurrentItem { get; set; }

[Parameter]
public Action Markcomplete { get; set; }

[Parameter]
public Action Delete { get; set; }
}

最后是主列表组件。这里有一些标题文本和始终可用的选项:

<h2>To do List</h2>

Show Completed:
<input type="checkbox" @bind-value="ViewModel.ShowCompleted" />

Sort By Created:
<input type="checkbox" @bind-value="ViewModel.SortByCreated" />

哇,那是什么?是的,我偷偷塞了一个视图模型。目前,它公开了一些标志。默认情况下,我只显示未完成的项目。默认排序按描述(升序),你可以选择显示最近创建的项目(因此是日期降序)。如果你选择显示所有项目,我会选择性地提供此选项。

@if (ViewModel.ShowCompleted && !ViewModel.SortByCreated)
{
   Completed then Created
   <input type="checkbox" @bind-value="ViewModel.SortByCompleted" />
}

这里有一个简单的标签和一个输入框来输入新项目。

Enter new item:
@if (ViewModel.ValidationHasErrors)
{
   <TodoErrors Errors="@ViewModel.Errors" />
}

<input name="newItem" type="text"
   @ref="InputBox"
   @attributes="inputattributes"
   @bind-value="ViewModel.NewDescription"
   @bind-value:event="oninput" />&nbsp;
<button @attributes="btnattributes"
   @onclick="@(async () => await ViewModel.AddNewAsync())">
   Add
</button>

几点说明

  • @attributes 绑定很酷。我可以绑定到一个键/值对的字典,它们将被应用为属性。因此,我正在手动操作验证状态,而不是使用内置控件,这更简单,因为它“就是能用”。
  • @bind- 属性表示用于数据绑定的属性,oninput 事件确保在用户键入时更新绑定,而不是在字段失去焦点时更新(这是默认行为)。
  • 我将 @attributes 用于按钮,以便在输入无效或异步操作正在进行时禁用它。
  • 我正在与服务器通信,因此我的大部分方法都是异步的。这很容易实现,正如你所见:我将事件“提升”为一个异步事件并等待该方法。

这是项目列表的模板。

@if (todos == null || ViewModel.Loading)
{
   <strong>Loading...</strong>
}
else
{
   @foreach (var todo in todos)
   {
      <TodoShow CurrentItem="@todo"
         Markcomplete="@(async () => await ViewModel.MarkdoneAsync(todo))"
         Delete="@(async () => await ViewModel.DeleteAsync(todo))" />
   }
}

 

最终的“网格”看起来像这样:

Todo grid

待办事项网格

“吉姆,我是一名程序员,不是设计师。”

如果列表为 null(如果没有项目则为空)或视图模型上的某个属性指示异步操作正在进行,我会显示加载提示。我在视图组件中为单个项目公开了一些 Action 参数,因此我将它们作为异步调用进行连接,并将当前项目传递给视图模型。

在组件初始化后,我加载初始列表并设置属性更改通知以更新按钮状态。这会在用户输入文本时触发,因此我只想在需要时刷新列表(而不是在每次按键时)。我假设视图模型会在 TodosAsync 每次需要刷新列表时引发属性更改通知。StateHasChanged 会导致模板重新渲染。

protected override async Task OnInitializedAsync()
{
   todos = (await ViewModel.TodosAsync()).ToList();
   ViewModel.NewDescription = string.Empty;
   ViewModel.PropertyChanged += async (o, e) =>
   {
      CheckButton();
      if (e.PropertyName.Equals(nameof(ViewModel.TodosAsync)))
      {
         todos = (await ViewModel.TodosAsync()).ToList();
      }
      StateHasChanged();
   };
   await base.OnInitializedAsync();
}

最后一部分代码是处理输入框和按钮的属性。

private void CheckButton()
{
   btnattributes.Clear();
   inputattributes.Clear();
   if (ViewModel.Loading)
   {
      btnattributes.Add("disabled", "");
      inputattributes.Add("disabled", "");
   }
   else if (ViewModel.ValidationHasErrors)
   {
      btnattributes.Add("disabled", "");
      inputattributes.Add("class", "alert-danger");
   }
}

我假设 viewmodel 会指示是否存在验证错误,因此我可以向你展示如何手动进行验证,使用 To do 类上定义的数据注解。

在你的应用中加入 MVVM

此时,视图已设置好,我们只需要一个 viewmodel 来连接数据。我做的第一件事是设置一个接口来模拟服务器调用。该接口应该不言自明。在此示例中,我限制了可用过滤器,但我也可以将过滤器公开为 IQueryable 以获得更大的灵活性。

public interface ITodoDataAccess
{
   Task<IEnumerable<Todo>> GetAsync(bool showAll,
      bool byCreated, bool byCompleted);
   Task<Todo> GetAsync(int id);
   Task<Todo> AddAsync(Todo itemToUpdate);
   Task<Todo> UpdateAsync(Todo item);
   Task<bool> DeleteAsync(Todo item);
}

在担心调用 API 或与服务器通信之前,我创建了一个 MockTodoData 实现,你可以在 这里查看。它使用内存列表来满足接口。因为 Blazor 内置了依赖注入 (DI),我可以在 Startup.cs 中这样注册视图模型和模拟服务:

services.AddSingleton<ITodoDataAccess, MockTodoData>();
services.AddSingleton<TodoViewModel>();

如果我的视图模型构造函数接受 ITodoDataAccess 作为参数,Blazor DI 服务会在构造 viewmodel 时为我传入正确的实例。

视图模型的完整实现可以在 这里找到。一些亮点:

手动验证

如果我不使用内置控件并需要手动验证,我可以轻松地访问与服务器端控制器使用的相同的验证服务。这是输入属性的代码。它被放在一个对象中并传递给验证。如果存在错误,我会将它们加载到视图模型并引发属性更改通知。

public string NewDescription
{
   get => _newDescription;
   set
   {
      if (value != _newDescription)
      {
         _newDescription = value;
         _newTodo.Description = value;
          var context = new ValidationContext(_newTodo);
          var results = new List<ValidationResult>();
          Errors.Clear();
          if (!Validator.TryValidateObject(_newTodo, context, results))
          {
             foreach(var item in results)
             {
                Errors.Add(item.ErrorMessage);
             }
             RaisePropChange(nameof(Errors));
          }
          RaisePropChange(nameof(NewDescription));
      }
   }
}

视图组件使用这些属性来更改输入字段的类,并在验证错误存在时显示它们。

处理异步操作

viewmodel 使用一个简单的方法来通知何时正在等待异步操作完成。我使用这个模式已经有十多年了,它适用于 90% 的用例。操作开始时会简单地增加一个计数,完成后会减少。这样,多个并行操作可以协同工作,并在它们全部完成后更改状态。这里是实现此目的的主要代码:

private int _asyncCount = 0;
public bool Loading
{
   get => _asyncCount > 0;
}
private void StartAsyncOperation()
{
   var cur = Loading;
   _asyncCount++;
   if (cur != Loading)
   {
      RaisePropChange(nameof(Loading));
   }
}
private void EndAsyncOperation()
{
   var cur = Loading;
   _asyncCount--;
   if (cur != Loading)
   {
      RaisePropChange(nameof(Loading));
   }
}

只有在 Loading 状态改变时才会引发属性更改通知。我甚至可以进一步将异步调用包装在一个自动执行增量/减量的方法中,但这似乎有点小题大做。像这样对调用进行首尾呼应已经足够容易了:

public async Task AddNewAsync()
{
   if (!string.IsNullOrWhiteSpace(NewDescription) && !ValidationHasErrors)
   {
      var newItem = new To do { Description = NewDescription };
      StartAsyncOperation();
      await _dataAccess.AddAsync(newItem);
      EndAsyncOperation();
      NewDescription = string.Empty;
      RaisePropChange(nameof(TodosAsync));
   }
}

注意它首先检查验证,然后在开始和结束操作之间进行等待。

管理列表

属性更改通知的好处是它们的作用域限定在特定属性。我不需要不断地获取新列表,而是可以简单地响应影响它的更改。我创建了一个辅助方法,根据一个简单的标志有条件地引发 TodosAsync 通知。

private void RaisePropChange(string property, bool includeTodos = false)
{
   PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
   if (includeTodos)
   {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TodosAsync)));
   }
}

任何过滤器选项都传递 true,视图组件会监听事件并相应地获取新列表。

在完成 viewmodel 后,我能够验证我有一个可用的应用程序。我测试了添加、排序、标记项目完成和删除它们。在这些功能正常工作后,下一步是实现 ITodoDataAccess 接口,该接口使用一个进行真实 OData 调用的类。这有多难?

简单 OData 客户端

当我在俄罗斯圣彼得堡参加一个组织得非常好的会议 DotNext 时,我有机会认识了 Vagif Abilov (@ooobject)

Vagif 指导了我国际观众的期望,并提供了宝贵的反馈来帮助我准备我的演讲。我万万没想到,当我开始寻找一个简单的 OData 客户端时,我竟然会遇到他维护的如此出色的作品。好像他读懂了我的心思并回到了过去,我很快就找到了这个他维护的解决方案:

这个客户端拥有我想要的一切。它会检查 OData 客户端的元数据端点,根据返回的信息构建内部模型,并为所有内容提供手动、流畅和动态的接口。最简单的方法就是解释我如何构建了我命名的服务客户端的实现,即 TodoSimpleOData

首先,因为 Blazor 需要一个特别配置的 HttpClient 版本,它能够识别浏览器限制,所以我利用客户端的设置来传递我想要使用的实例。然后,通过依赖注入将该实例传递到服务中(它在内部连接起来,对 Blazor 代码可用)。

public TodoSimpleOData(HttpClient client)
{
   client.BaseAddress = new Uri("https://:5000/odata/");
   var settings = new ODataClientSettings(client);
   _client = new Simple.OData.Client.ODataClient(settings);
}

在这里,我硬编码了基本 URL 地址以保持演示的简单性。你可以将其设置为从标准的 .NET Core 配置设置读取,并在构建过程中生成端点。另外,我通常使用简单的 dotnet run 从 shell 运行 OData 客户端,默认端口是 5000。如果你从 Visual Studio 作为启动项目运行它,端口可能会有所不同,所以请记住这一点并相应地进行更新。

,我有一个小秘密。那个端点是我唯一需要指定 URL 或端点的地方,仅此而已。一旦客户端“知道”了基本地址,它就可以自动请求所有可用的实体、它们的端点以及如何对它们执行任何操作。拭目以待!

目前,我还有很多方法抛出“未实现”异常。为了处理列表,我需要添加新项目,所以我将首先这样做。我从不相信验证已经发生,因此该方法的大部分内容是构建一个验证上下文并验证模型是否有效,然后将其传递给客户端(该客户端将在服务器上进一步验证它)。

public async Task<To do> AddAsync(Todo itemToAdd)
{
   var results = new List<ValidationResult>();
   var validation = new ValidationContext(itemToAdd);
   if (Validator.TryValidateObject(itemToAdd, validation, results))
   {
      return await _client.For<To do>().Set(itemToAdd).InsertEntryAsync();
   }
   else
   {
      throw new ValidationException();
   }
}

感觉太容易了!主要代码在这里:

return await _client.For<To do>().Set(itemToAdd).InsertEntryAsync();

我可以运行它并在网络活动中查看后台发生了什么。

Post Request

OData POST 请求

响应不返回对象,但头信息提供了新创建实体的位置。这就是简单 OData 客户端如何返回带有服务器生成的 ID 的更新版本。

Post Request

OData POST 响应

很酷。让我们实现删除。你准备好了吗?

public async Task<bool> DeleteAsync(Todo item)
{
   await _client.For<To do>().Key(item.Id).DeleteEntryAsync();
   return true;
}

我默认返回 true,但对于生产应用程序,我还会捕获错误、记录它们并在失败时返回 false。运行删除:

OData Delete

OData 删除

最后,我最喜欢实现的方法:搜索。这是考虑过滤和条件排序的完整实现:

public async Task<IEnumerable<Todo>> GetAsync(
   bool showAll, bool byCreated, bool byCompleted)
{
   var helper = _client.For<Todo>();
   if (!showAll)
   {
      helper.Filter(todo => !todo.Complete);
   }
   if (showAll && byCompleted)
   {
      helper.OrderByDescending(todo => todo.MarkedComplete)
         .ThenByDescending(todo => todo.Created);
   }
   else if (byCreated)
   {
      helper.OrderByDescending(todo => todo.Created);
   }
   else
   {
      helper.OrderBy(todo => todo.Description);
   }
   return await helper.FindEntriesAsync();
}

简洁流畅!请注意,我可以像任何其他 IQueryable 接口上的 LINQ 一样,有条件地链接选项。这是默认查询:

OData filtered query

OData 过滤查询

以及结果。请注意,它们是从服务器返回的,并且不包含已标记为完成的“two”任务。

OData filtered results

OData 过滤结果

这只是一个简单的示例。如果你深入研究客户端,你会发现你可以处理大量数据,并使用诸如skiptake 之类的功能来实现分页。

整合

依赖注入的好处是,我可以在 Startup.cs 中更改注册的服务版本,从“内存中”切换到实时。完整的代码库可以在线找到:

本文演示了:

  • 在客户端和服务器之间共享模型,包括业务逻辑。
  • 在多个级别上使用相同的验证,而无需在任何地方重写它。
  • 使用特殊控制器上的一些简单属性来实现 OData 端点。
  • 如何利用 Blazor 客户端中的依赖注入。
  • 一些较新的 Blazor 功能,例如管理 HTML 元素的属性。
  • Blazor 中 MVVM 模式的示例。
  • 处理异步操作。
  • 使用 简单 OData 客户端通过流畅的 C# 接口操作数据。

想了解更多信息吗?我正在构建一个完整的 Blazor 文章系列,所以请务必查看!

观看另一个 Blazor 和 OData 视频

你的想法、评论和反馈是什么?你还想看什么?请在下方分享你的评论。

此致,

Jeremy Likness

使用 Blazor 和 OData 快速构建数据驱动的 Web 应用 - CodeProject - 代码之家
© . All rights reserved.