.NET 移动开发(二)

原文:Mobile Development with .NET

协议:CC BY-NC-SA 4.0

六、定制 Xamarin.Forms

Xamarin 的。 表单允许开发人员以各种方式修改 ui 呈现基础设施。 开发人员引入的自定义可以针对某个控件元素上的某个平台特性,也可以创建一个全新的视图控件。 这些定制可以在 Xamarin 上进行。 窗体层或目标本机平台上。

在本章中,我们将介绍定制 Xamarin 所涉及的步骤和过程。 在性能或用户体验(UX)方面不打折扣。 我们将从定义定制的开发领域开始。 从与平台无关的自定义开始,比如行为、样式和 XAML 扩展,我们将通过实现特定于平台的功能和自定义进入本机域。 最后,我们将看看自定义呈现器和自定义控件。

下面几节将介绍 Xamarin 的不同开发领域。 形式的定制:

  • Xamarin 的。 形式发展领域
  • Xamarin 的。 形式共享域
  • 自定义本机域
  • 创建自定义控件

到本章结束时,您将能够在 Xamarin 中自定义视图元素。 形式和本土边界。 此外,您还可以向这些元素添加行为修改。

技术要求

你可以通过 GitHub 上的https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter06找到本章的代码。

Xamarin。 形式发展领域

在本节中,我们将从定义 Xamarin 的开发领域开始。 表单应用。 我们将创建一个象限平面,将共享和本地作为一个轴,业务逻辑和 UI 作为另一个轴。 然后,我们将根据它们的实现和利用在这些象限中放置各种定制选项。

到目前为止,在本书中,您将注意到使用 Xamarin 进行应用开发。 表单框架在多个域上执行。 而 Xamarin 的。 表单层创建了一个共享开发域,该域将用于目标本机平台,目标平台仍然可以用于特定于平台的实现。

如果我们要分离一颗 Xamarin。 根据发展战略和应用领域类别将应用划分为四个象限,它看起来是这样的:

<image/Figure_6.01_B16381.jpg>

图 6.1 Xamarin 定制域

在这个设置中,象限 I(即共享业务逻辑)表示应用的核心逻辑实现。 这个域将包含视图模型、域数据描述和服务客户端实现。 最重要的是,特定于平台的 api 的抽象(即将在本机平台上实现的接口)应该在这个域中创建,以便其他域以及这个域中的视图模型可以使用它们。

象限 II 和 III 表示我们需要实现的 UI 定制,以便为应用创建所需的 UX。 到目前为止,我们一直只使用象限 II 中的来创建可视化树。 简单的数据驱动应用和业务线(Line-of-Business)(LOB)应用可以单独使用这个域。 然而,如果我们要创建面向消费者的应用,那么遵循品牌 UX 需求并创建直观的 UI 应该是我们的主要目标。 在这个场景中,我们可以为 Xamarin 创建自定义。 使用象限 III 形成视图。

在这个范例中,象限 I 仅使用数据绑定和转换器实现与象限 II 连接。 象限 II 负责将交付的数据传播到象限 III。

在象限 II 中,针对开发人员的定制选项主要限于 Xamarin 提供的开箱即用视图提供的可扩展性选项。 形式框架。 这些视图的组合和行为修改可以提供高度可维护性的跨平台源代码。 通过使用样式选项、可视化状态和数据驱动模板,UX 可以满足这些需求。

当从共享平台移动到本地平台时(即从象限 II 移动到象限 III),开发人员可以使用平台细节和 Xamarin。 形成效应。 使用这些扩展点,作为开发人员,我们可以修改本地控件的行为并修改呈现的本地 UI,从而在 Xamarin 之间创建一座桥梁。 表单视图抽象和目标本机控件。 这些可扩展性特性与 Xamarin 的组合。 表单行为可以提高应用的可维护性。

特定象限 iii 的开发包括自定义渲染器和本地控件。 可以在 Xamarin 下创建和组合本地控件。 形成组合,从而降低 Xamarin 的复杂性。 形成 XAML 树(即复合控件)。

最后,象限 IV 表示特定于平台的 api,比如地理位置,外围设备的使用,比如蓝牙或 NFC,或者需要本地实现的 SaaS 集成/ sdk。

我们现在已经将我们的定制选项划分为特定的象限。 查看所有这些选项是如何位于这些象限上的,可以帮助我们识别特定场景的特定定制选项。 一般的经验法则是从象限 I 开始权衡选项,如果没有其他选项可用,就转向下一个象限。 当您移动到象限 III 和 IV 时,因为跨平台代码的数量减少了,项目的可维护性也减少了。 因此,不浪费时间,让我们从共享域定制选项开始,即象限 I 和 II。

Xamarin。 形式共享域

第五章使用 Xamarin 开发 UI中,我们使用了固有的 Xamarin。 表单控件及其样式属性来创建我们的 UI。 通过使用数据绑定和数据触发器,我们创建了数据驱动视图。 当然,可扩展性选项并不局限于此层上可用的控制属性。 呈现控件的行为和外观都可以使用标准定制和可扩展性选项进行修改。 让我们看看共享 Xamarin 中的不同定制选项。 表单域。

使用风格

在前面的一章中,在我们的 ShopAcross 应用中,当处理产品详细信息视图时,我们创建了一个简单的芯片容器来显示当前通过应用提供的项目的各种特性。

在前面的设置中,我们只对标签使用了Margin属性和VerticalTextAlignment:

<FlexLayout Direction="Row" Wrap="Wrap">
    <Label Text="Feature 1" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Gray" />
    <Label Text="Feat. 2" Margin="4" VerticalTextAlignment="Center" BackgroundColor="Lime"/>
    <!-- Additional Labels -->
</FlexLayout>

此流体布局设置创建包含特性名称的小矩形。 然而,外观和感觉与芯片的材料设计(例如,填充和圆角)略有不同。

现在让我们修改这些项目,使标签看起来更像芯片,以改善用户体验:

  1. We will start by wrapping the label in a frame and then styling the frame. Open ProductDetailsView.xaml and add a frame outside each feature label. Then, move the BackgroundColor assignment to the Frame element:

     <Frame
        BackgroundColor="Gray" 
     CornerRadius="7" 
     Padding="3" 
     Margin="4" 
     HasShadow="false">
         <Label x:Name="Feat1" Text="Feature 1" 
             VerticalTextAlignment="Center" 
             HorizontalTextAlignment="Center" />
     </Frame> 
    

    这当然会为我们的 chip 创建一个更理想的查找:

    <image/Figure_6.02_B16381.jpg>

    图 6.2 -定制芯片

    然而,请注意,将这些属性添加到每个特性将创建一个完全冗余的 XAML 结构。

    我们可以将公共属性定义提取为两个单独的样式(即,一个用于特征标签,一个用于框架本身),它们将应用于每个元素,从而减少冗余的机会。

  2. 为此,打开App.xaml并在现有的ResourceDictionary中添加以下样式定义:

    <Style TargetType="Frame">
         <Setter Property="HasShadow" Value="false" />
    </Style>
    <Style TargetType="Frame" x:Key="ChipContainer">
     <Setter Property="CornerRadius" Value="7" />
     <Setter Property="Padding" Value="3" />
     <Setter Property="Margin" Value="3" />
     </Style>
     <Style TargetType="Label" x:Key="ChipLabel">
         <Setter Property="VerticalTextAlignment" Value="Center" />
         <Setter Property="HorizontalTextAlignment" Value="Center" />
         <Setter Property="TextColor" Value="White" />
     </Style> 
    
  3. 接下来,应用【显示】这些隐式(即HasShadow="false"setter 将适用于所有应用级别上的帧)【病人】和**显式风格(注意x:Key【T2 宣言】和ChipLabel风格)FrameLabel控制:

    <FlexLayout Direction="Row" Wrap="Wrap" FlowDirection="LeftToRight" AlignItems="Start">
         <Frame BackgroundColor="Gray" Style="{StaticResource
     ChipContainer}">
             <Label x:Name="Feat1" Text="Feature 1" Style="
     {StaticResource ChipLabel}" />
         </Frame>
         <Frame BackgroundColor="Lime" 
          Style="{StaticResource ChipContainer}">
             <Label x:Name="Feat2" Text="Feat. 2"  
          Style="{StaticResource ChipLabel}" />
         </Frame>
         <!-- Additional Labels -->
     </FlexLayout> 
    ```** 
    
    

通过这样做,我们将减少 XAML 树中的杂乱和冗余。 样式可以在应用级别(如本场景所示)声明为全局样式使用App.xaml。 此外,还可以使用本地资源字典在页面和视图级别声明。

另一种样式化控件的方法是使用 CSS 样式表。 虽然这些样式表目前不支持 XAML 控件样式的全部范围,但它们可以被证明是强大的,特别是在使用 CSS 选择器时。 让我们开始学习 Xamarin 中的 CSS。 通过为我们的芯片视图重新创建样式:

  1. 首先,在ShopAcross.Mobile.Client项目中创建一个名为Resources的新文件夹。 然后,添加一个名为Styles.css的新文件。 确保此文件的构建操作设置为EmbeddedResource

  2. Next, add the following style declarations to Styles.css:

     .ChipContainerClass {
         border-radius: 7;
         padding: 3;
         margin: 3;
     }
    
     .ChipLabelClass {
         text-align: center;
         vertical-align: central;
         color: white;
     } 
    

    对于那些不熟悉 CSS 的人,我们在这里创建了两个样式类,命名为ChipContainerClassChipLabelClass

  3. 现在,使用StyleClass属性将这些样式类添加到FrameLabel控件中:

  4. In order to decrease the clutter, apply the style directly to the child label within the frame with the ChipContainerClass style class (note that we will not need to use an explicit style declaration for the Label element):

     .ChipContainerClass {
         border-radius: 7;
         padding: 3;
         margin: 3;
     }
    
     .ChipContainerClass>^label {
         text-align: center;
         vertical-align: central;
         color: white;
     }
    

    .ChipContainerClass>label.ChipContainerClass>^label的区别是,通过使用^(基类)符号,我们可以确保即使我们修改视图使用自定义控制从label推导,风格以同样的方式被应用。

  5. 现在,从Label控件中删除ChipLabelClass声明。

如您所见,样式确实有助于减少 XAML 声明中冗余的机会。 通过这样做,您创建了一个更易于维护的项目。 样式还可以使用来创建绑定到系统主题或用户首选项的主题。 样式也可以与 Xamarin 一起使用。 表单行为不仅可以修改可视化,还可以修改元素的行为。

创造行为

行为是装饰器模式的的有力使用,允许开发人员修改他们的 Xamarin。 窗体控件,而无需创建派生控件。 在本节中,我们将创建一个简单的行为来演示行为如何帮助创建数据驱动的应用 UI。

为了演示行为的使用,让我们看一下下面的用户故事:

作为一名软件开发人员,我希望将 LoginView 中的验证行为委托给 UI 元素,以便在不同的控件中重用相同的行为,而不将其复制到不同的视图模型中。

您可能还记得,在LoginView中,我们实际上使用了Command.CanExecute委托来验证字段。 在本例中,我们将分离电子邮件字段和密码字段的验证器。 通过这种方式,我们可以允许 UI 将错误输入的结果反馈给用户。 这比只禁用登录窗口更加友好。 要设置它,请遵循以下步骤:

  1. 首先,创建一个验证规则基础设施,从验证接口开始。 在ShopAcross.Mobile.Core项目中创建一个名为Common的文件夹,并添加一个名为IValidationRule:

     public interface IValidationRule<T>
     {
         string ValidationMessage { get; set; }
         bool Validate (T value);
     }
    

    的新接口

  2. In order to implement the required validation, create a new class, called RequiredValidationRule, deriving from IValidationRule. We can also add a short validation message stating that this field is a required field:

     public class RequiredValidationRule : IValidationRule<string>
     {
         public string ValidationMessage { get; set; } = "This field is a required field";
    
         public bool Validate (string value)
         {
             return !string.IsNullOrEmpty(value);
         }
     }
    

    现在,我们可以为Entry字段创建验证行为,它将使用任何给定的验证规则(从RequiredValidationRule开始,这是我们刚刚实现的)。

  3. For this, create a new folder, called Behaviors, in the ShopAcross.Mobile.Client project, and add a new class, called ValidationBehavior:

     public class ValidationBehavior : Behavior<Entry>
     {
         protected override void OnAttachedTo(Entry bindable)
         {
             base.OnAttachedTo(bindable);
    
             bindable.TextChanged += ValidateField;
         }
    
         protected override void OnDetachingFrom(Entry bindable)
         {
             base.OnDetachingFrom(bindable);
    
             bindable.TextChanged -= ValidateField;
         }
    
         private void ValidateField(object sender, 
             TextChangedEventArgs args)
         {
             if (sender is Entry entry)
             {
                 // TODO:
             }
         }
     } 
    

    在这个实现中,OnAttachedToOnDetachingFrom方法是关键的入口点和拆卸逻辑实现。 在此场景中,当行为附加到目标控件时,我们订阅TextChanged事件,当行为被删除时,我们从事件取消订阅,以便避免任何不希望的内存泄漏问题。 这个实现也很重要,因为考虑象限平面,我们通过向 Xamarin 添加这个验证行为从象限 I 跨越到象限 II。 形成Entry视图元素。

  4. The next order of business is to implement a bindable property for the validation rule so that the validation rules are dictated by the view model (or another business logic module), decoupling it from the view.

    为此,打开ValidationBehavior类并添加以下 ing 属性:

    public static readonly BindableProperty ValidationRuleProperty =
             BindableProperty.CreateAttached("ValidationRule", typeof(IValidationRule<string>), typeof(ValidationBehavior), null);
    
    public static readonly BindableProperty HasErrorProperty =
             BindableProperty.CreateAttached("HasError", typeof(bool), typeof(ValidationBehavior), false, BindingMode.TwoWay);
    
    public IValidationRule<string> ValidationRule 
    {
        get { return this.GetValue(ValidationRuleProperty) as IValidationRule<string>; }
        set { this.SetValue(ValidationRuleProperty, value); }
    }
    
    public bool HasError
    {
        get { return (bool) GetValue(HasErrorProperty); }
        set { SetValue(HasErrorProperty, value); }
    }
    
  5. 现在我们有了验证规则的出口和输出字段(这样我们就可以给它附加额外的 UX 逻辑),为ValidateField方法添加以下实现:

     private void ValidateField(object sender, TextChangedEventArgs 
     args)
     {
         if (sender is Entry entry && ValidationRule != null)
         {
             if (!ValidationRule.Validate(args.NewTextValue))
             {
                 entry.BackgroundColor = Color.Crimson;
                 HasError = true;
             }
             else
             {
                 entry.BackgroundColor = Color.White;
                 HasError = false;
             }
         }
     }
    
  6. 接下来,使用适当的规则属性(在本例中为UserNameValidation)扩展类:

  7. Now, bind the behavior to the validation rule that’s exposed from the view model. Then, observe the Entry field behavior according to the text input:

    <Entry x:Name="usernameEntry" Placeholder="username" Text="{Binding UserName, Mode=OneWayToSource}" >
         <Entry.Behaviors>
     <behaviors:ValidationBehavior x:Name="UserNameValidation" 
     ValidationRule="{Binding 
     BindingContext.UserNameValidation, 
     Source={x:Reference Root}}" />
         </Entry.Behaviors>
     </Entry> 
    

    附加行为时,您需要添加ContentPagex:Name属性声明的值在LoginView.xaml``Root(请查看前面的引用),引入 CLR 命名空间参考B【5】。

    这里,主要的好处是我们不需要修改Entry字段,实现的行为可以作为一个单独的模块进行维护。

    重要提示

    行为的绑定上下文与页面布局或视图不同,这就是为什么验证规则的绑定值源必须引用页面本身,并使用BindingContext作为绑定路径的一部分。

  8. 要扩展这个实现,添加一个验证错误消息标签,该标签将与HasError可绑定属性一起显示(可以在页面布局的任何地方,只要UserNameValidation元素是可访问的):

  9. 结果将类似如下:

<image/Figure_6.03_B16381.jpg>

图 6.3 - Username 字段中的自定义行为

在这里,我们实现了一个验证行为,该行为可以绑定到应用核心层中的验证实现。 换句话说,我们已经创建了另一个象限二世之间的桥梁,即这种方式,实现可以使用每当我们需要实现一个类似的验证在项目中的任何条目,以及视图模型只是负责定义验证需求。 然而,使用这种实现,控件上的行为声明可能会再次导致较大窗体的混乱。 您可以使用附加属性来减少启用和禁用行为时重复代码的数量。 在下一节中,我们将带您完成附加属性的创建。

附加属性

另一种改变默认控件行为的方法是通过为现有控件声明一个可绑定的扩展来使用附加属性。 这种方法通常用于小的行为调整,例如启用/禁用其他行为和添加/删除效果。

我们可以通过使用附加属性(而不是行为)重新创建前面的场景来演示此实现。 让我们开始:

  1. In order to implement such a behavior, first, we need to create a bindable property that will be used with the Xamarin.Forms view elements. For this, create a new folder, called Extensions, in the ShopAcross.Mobile.Client project, and add a new class, called Validations:

     public static class Validations
     {
         public static readonly BindableProperty ValidateRequiredProperty =
             BindableProperty.CreateAttached(
                 "ValidateRequired",
                 typeof(bool),
                 typeof(RequiredValidationRule),
                 false, 
                 propertyChanged: OnValidateRequiredChanged);
    
         public static bool GetValidateRequired(BindableObject view)
         {
             return (bool)view.GetValue(ValidateRequiredProperty);
         }
    
         public static void SetValidateRequired(BindableObject view, 
         bool value)
         {
             view.SetValue(ValidateRequiredProperty, value);
         }
    
         private static void OnValidateRequiredChanged(
                 BindableObject bindable, object oldValue, object
                 newValue)
         {
             // TODO:
         }
     } 
    

    通过附加行为,可以直接访问静态类,以便它将附加属性设置到当前控件(而不是创建和添加行为)。

  2. 现在,删除行为声明并将附加属性设置到usernameEntry视图:

     <Entry x:Name="usernameEntry" Placeholder="username" 
        Text="{Binding UserName, Mode=OneWayToSource}" 
        extensions:Validations.ValidateRequired="true" >
    
  3. 接下来,实现属性更改处理程序ValidateRequired。 这样,我们就可以将附加的属性插入和删除所需的验证到各种Entry视图:

     private static void OnValidateRequiredChanged(
         BindableObject bindable, 
         object oldValue, 
         object newValue)
     {
         if(bindable is Entry entry)
         {
             if ((bool)newValue)
             {
                 entry.Behaviors.Add(new ValidationBehavior() 
     { 
     ValidationRule = new RequiredValidationRule() 
     });
             }
             else
             {
     var behaviorToRemove = entry.Behaviors
     .OfType<ValidationBehavior>()
     .FirstOrDefault(
     item => item.ValidationRule is 
     RequiredValidationRule);
    
                 if (behaviorToRemove != null)
                 {
                     entry.Behaviors.Remove(behaviorToRemove);
                 }
             }
         }
     } 
    

这里,我们创建了一个附加属性来修改 Xamarin 添加的行为。 表单元素。 记住,附加属性也可以用来修改视图元素的其他属性。

XAML 标记扩展

在本节中,您将学习如何使用并创建另一个非常实用的自定义选项:标记扩展

到目前为止,当我们创建 XAML 视图时,我们使用了 Xamarin 所支持的几个标记扩展。 表单框架或 XAML 名称空间本身。 其中一些扩展如下:

  • x:Reference:用于指同一页面上的另一个视图。
  • Binding:这在整个视图模型实现中都被使用。
  • StaticResource:用于指样式。

这些都是由 Xamarin 中相关的服务实现解析的标记扩展。 形式框架。

为了满足应用中的特定需求,可以实现自定义标记扩展,以创建更具维护性的 XAML 结构。 要创建标记扩展,需要实现IMarkupExtension<T>类。 这取决于需要提供的类型。

例如,在前面的示例中,错误标签和字段描述符被硬编码到 XAML 视图中。 如果应用需要支持多个本地化,这可能会产生问题。

让我们使用下面的用户故事来实现一个自定义标记扩展:

作为用户,我希望将应用的 UI 翻译成我手机的语言设置,这样我就可以轻松地浏览包含本地语言内容的 UI。

这里,我们将实现LoginView的本地化。 以下步骤将指导你完成这个过程:

  1. First, create a markup extension that will translate the associated text values. Then, create a class in the Extensions folder, called TranslateExtension:

     [ContentProperty("Text")]
     public class TranslateExtension : IMarkupExtension<string>
     {
         public string Text { get; set; }
    
         public string ProvideValue(IServiceProvider serviceProvider)
         {
             // TODO:
             return Text;
         }
    
         object IMarkupExtension.ProvideValue(IServiceProvider 
         serviceProvider)
         {
             return (this as
             IMarkupExtension<string>).ProvideValue(serviceProvider);
         }
     } 
    

    注意,Text属性被设置为ContentProperty,这允许开发人员通过手动为扩展添加一个值来为该扩展提供一个值。

  2. 将扩展并入 e XAML 结构LoginView.xaml:

    <Label Text="{extensions:Translate LblUsername}" />
    <Entry x:Name="usernameEntry" Placeholder="username" 
        Text="{Binding UserName, Mode=OneWayToSource}" >
        <Entry.Behaviors>
            <behaviors:ValidationBehavior x:Name="UserNameValidation" ValidationRule="{Binding BindingContext.UserNameValidation, Source={x:Reference Root}}" />
        </Entry.Behaviors>
    </Entry>
    <Label Text="{extensions:Translate LblRequiredError}" 
        FontSize="12" TextColor="Gray" 
        IsVisible="{Binding HasError, Source={x:Reference UserNameValidation}}"/> 
    
  3. The ProvideValue method will, therefore, need to translate the LblUsername and LblRequiredError keys. Use the following implementation to achieve this:

     public string ProvideValue(IServiceProvider serviceProvider)
     {
         switch (Text)
         {
             case "LblRequiredError":
                 return "This a required field";
             case "LblUsername":
                 return "Username";
             default:
                 return Text;
         }
     } 
    

    这里,我们使用硬编码的值进行翻译。 但是,在实际实现中,您可以根据系统的当前区域性设置从 web 服务或资源文件加载值。

在本节中,我们为象限 II(即共享域)创建了各种定制。 我们演示了对风格、行为和附加属性的使用。 我们甚至创建了一个标记扩展来方便地翻译字符串资源。 您可能已经注意到,到目前为止,我们还没有接触本地项目。 我们的完整实现是在共享 UI 和核心项目上完成的。 在下一节中,我们将继续讨论象限 III 和自定义本地控件。

自定义本机域

UI 控件的本地自定义可以从简单的特定于平台的调整到创建完全自定义的本地控件来替换现有的平台渲染器。 在本节中,我们将在象限 III 上实现自定义,这与平台无关。 我们将进一步研究平台细节和 Xamarin 效果。

平台细节

而 UI 由 Xamarin 提供。 表单对于大多数 UX 需求来说是足够可定制的,可能需要额外的本机行为。 对于某些本机控件行为,可以使用目标控件的IElementConfiguration接口实现来访问特定于平台的配置。 例如,为了更改UpdateMode选择器(即ImmediatelyWhenFinished),您可以使用On<iOS>方法访问特定于平台的行为:

 var picker = new Xamarin.Forms.Picker();
 picker.On<iOS>().SetUpdateMode(UpdateMode.WhenFinished);

同样的可以在 XAML 中使用Xamarin.Forms.PlatformConfiguration.iOSSpecific命名空间实现:

<ContentPage 
    ... 
 xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"> 
    <!-- ... -->
        <Picker ios:Picker.UpdateMode="WhenFinished">
        <!-- Removed for brevity -->          
        </Picker>
    <!-- ... -->
</ContentPage>

类似的平台配置可用于相同名称空间(即Xamarin.Forms.PlatformConfiguration)中的其他控件和平台。 这些特定于平台的附加属性通过公开可以在特定平台上操作本地控件的方法,在象限 III 和象限 II 之间创建了一座桥梁。 但是,如果我们想要修改的本地属性没有由特定的平台公开该怎么办? 我们可能需要使用效应

Xamarin。 形式的影响

Xamarin 的。 形式效果是在跨平台域(象限 II)和本地域(象限 III)之间的完美桥梁。效果通常用于通过共享域暴露特定的平台行为或给定本地控件的实现。 这是为了确保实现不需要一个全新的自定义本地控件。

类似于 Xamarin。 表单视图/控件,影响分别存在于共享域和本地域的抽象和实现上。 虽然共享域用于创建路由效果,但本机项目负责使用它。

例如,让我们假设我们收到的产品项的详细信息实际上包含一些 HTML 数据,我们希望在应用中显示这些数据:

作为产品所有者,我希望在本地移动平台上重用 web 应用中基于 html 的产品描述。 这就使得内容在推送到移动平台之前不需要经过清理。

在这个场景中,我们知道 Xamarin 上的Label元素。 表单在 iOS 中使用UILabel呈现,在 Android 中使用TextView呈现。 虽然UILabel提供了AttributedString属性(可以从 HTML 创建),但 Android 平台提供了一个用于解析 HTML 的固有模块。 我们可以使用效果公开这些特定于平台的特性,因此启用 Xamarin。 表单抽象来接受 HTML 输入。 让我们开始:

  1. 创建路由效果,为平台效果提供数据。 然后,在ShopAcross.Mobile.Client项目中创建一个新文件夹,ca 命名Effects,以及一个类HtmlTextEffect:

     public class HtmlTextEffect: RoutingEffect
     {
         public HtmlTextEffect(): base("ShopAcross.HtmlTextEffect")
         {
         }
    
         public string HtmlText { get; set; }
     } 
    
  2. Now, we can use this effect in our XAML. Open ProductDetailsView.xaml and add the following effect to the Description label:

     <Label Text="{Binding Description}">
      <Label.Effects>
     <effects:HtmlTextEffect 
     HtmlText="&lt;b&gt;Here&lt;/b&gt; is some &lt;u&gt;HTML&lt;/u&gt;" />
      </Label.Effects>
     </Label> 
    

    要注意,HtmlText属性中的内容应该进行编码,以避免 XAML 编译问题,这点很重要。 另外,如果平台没有实现这种路由效果,标签仍然会显示绑定数据。

    现在,我们需要实现 iOS 效果,它将解析我们的效果的HtmlText属性。

  3. Create a new folder, called Effects, in the ShopAcross.Mobile.Client.iOS project, and create a new class, called HtmlTextEffect:

     [assembly: ResolutionGroupName("ShopAcross")]
     [assembly: ExportEffect(typeof(HtmlTextEffect), "HtmlTextEffect")]
     namespace ShopAcross.Mobile.Client.iOS.Effects
     {
         public class HtmlTextEffect: PlatformEffect
         {
     protected override void OnAttached()
     {
     }
    
     protected override void OnDetached()
     {
     }
         }
     } 
    

    平台效应主要由两个主要组成部分组成:注册和实现。 在运行时环境中使用ExportEffect属性的ResolutionGroupName注册的效果来解决在第一步中实现的路由效果。 另外,请注意,ExportEffect属性使用对特定于 ios 的属性的引用,因此您需要为当前命名空间(即ShopAcross.Mobile.Client.iOS.Effects)添加using语句。

    为了修改本机控件,您现在可以使用PlatformEffectControl属性。 Element属性指 Xamarin。 需要这种效果的窗体控件。

  4. 现在,实现OnAttached方法(该方法将在PlatformEffect解决时执行),如果附加的控件上存在PlatformEffect,则添加AttributedText:

     protected override void OnAttached()
     {
     var htmlTextEffect = Element.Effects
     .OfType<Client.Effects.HtmlTextEffect>
     ().FirstOrDefault();
    
         if(htmlTextEffect != null && Control is UILabel label)
         {
             var documentAttributes = new NSAttributedStringDocumentAttributes();
             documentAttributes.DocumentType = NSDocumentType.HTML;
             var error = new NSError();
    
     label.AttributedText = new NSAttributedString(htmlTextEffect.HtmlText, documentAttributes, ref error);
         }
     } 
    
  5. Android 平台的一个类似的实现将创建控件的 HTML 渲染。 使用OnAttached属性添加 HTML 内容:

     protected override void OnAttached()
     {
     var htmlTextEffect = Element.Effects
     .OfType<Client.Effects.HtmlTextEffect>
     ().FirstOrDefault();
    
         if (htmlTextEffect != null && Control is TextView label)
         {
             label.SetText(
     Html.FromHtml(htmlTextEffect.HtmlText, 
     FromHtmlOptions.ModeLegacy), 
     TextView.BufferType.Spannable);
         }
     } 
    

结果屏幕应该显示硬编码文本,而不是提供的视图模型数据:

<image/Figure_6.04_B16381.jpg>

图 6.4 -使用效果修复 HTML 内容

虽然我们已经成功地在 Xamarin 上显示 HTML 内容。 在表单视图中,我们使用的值仍然是不可绑定的。 通过少量的重组和使用附加属性(即附加行为),我们可以同时使用数据绑定和效果。

复合定制

当行为和效果一起使用时,可以为常见的本地元素需求创建有说服力的解决方案,而无需求助于自定义控件和渲染器。

现在让我们扩展我们的效果,使用视图模型提供的数据:

  1. Before we begin, let’s first extend our ProductViewModel class with an IsHtml property and add some sample data to HomeViewModel:

    yield return new ProductViewModel {
        Title = "First Item",
        IsHtml = true,
        Description = "<b/>Here</b> is some <u>HTML</u>;",
        Image = "https://picsum.photos/800?image=0" };
    

    并且,从我们结束的HtmlText效果的地方继续,让我们创建一个附加的行为,它将允许我们打开或关闭 HTML 呈现。

  2. ShopAcross.Mobile.Client项目的Extensions文件夹中创建一个类HtmlText

  3. The behavior of this attached property will result in the addition or removal of the HTML effect, depending on the IsHtml property declaration. Add the following code to OnHtmlPropertyChanged:

    if (newValue is bool isHtml && isHtml)
    {
        view.Effects.Add(new HtmlTextEffect());
    }
    else
    {
        var htmlEffect = view.Effects
            .FirstOrDefault(e => e is HtmlTextEffect);
    
        if (htmlEffect != null)
        {
            view.Effects.Remove(htmlEffect);
        }
    }
    

    现在,我们可以修改 HTML 效果,以便它使用表单视图上现有的文本赋值,分别为 iOS 和 Android 平台创建NSAttributedTextISpannable

  4. Copy and replace the existing effects for both iOS and Android with the following code:

     public class HtmlTextEffect: PlatformEffect
     {
         protected override void OnAttached()
         {
             SetHtmlText();
         }
    
         protected override void OnDetached()
         {
             // TODO: Remove formatted text
         }
    
         protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
         {
             base.OnElementPropertyChanged(args);
    
             if (args.PropertyName == Label.TextProperty.PropertyName)
             {
                 SetHtmlText();
             }
         }
    
         private void SetHtmlText()
         {
             // Removed for brevity
         }
     } 
    

    请注意,我们还使用了OnElementPropertyChanged方法来侦听Text属性值的任何更改。 这将是绑定数据的主要访问点。

  5. 对于SetHtmlText方法,在 iOS 中使用下面的方法,类似地,修改 Android 效果,使其使用Label元素的Text属性,而不是解决附加的效果:

    private void SetHtmlText()
    {
        if (Control is UILabel label && Element is Label formLabel)
        {
            var documentAttributes = new NSAttributedStringDocumentAttributes();
            documentAttributes.DocumentType = NSDocumentType.HTML;
            var error = new NSError();
            label.AttributedText = new NSAttributedString(formLabel.Text, documentAttributes, ref error);
        }
    }
    
  6. 现在,我们将把行为添加到我们的 XAML:

     <Label Text="{Binding Description}" effects:HtmlText.IsHtml="{Binding IsHtml}" /> 
    

现在,我们可以使用视图模型上的IsHtml附加属性来控制两个平台上显示的文本属性。

在实现了这个复合定制之后,我们将结束本节。 到目前为止,我们关注的是第三象限中的自定义,并尝试创建到象限 II 的桥梁。 我们使用了平台的细节,使用效果实现了本地控件修改,最后,我们创建了利用效果和附加属性的复合定制。 如果这些定制选项都不能提供所需 UI 的实际需求,那么可以考虑使用一个完整的定制控件实现作为替代选项。 在下一节中,我们将了解创建自定义控件时的各种不同选项。

创建自定义控件

就像任何其他开发平台一样,它也可以创建自定义视图/控件,这些视图/控件在外观、行为和渲染上都与开箱即用的 Xamarin 不同。 表单控件。 然而,创建自定义控件并不意味着完整的 Xamarin。 表单呈现的基础设施需要为目标平台和共享域实现。 根据用户体验和平台的需求,可能会出现以下情况:

  • 自定义控件可以单独作为其他 Xamarin 的组合来创建。 表单控件。
  • 现有 Xamarin 的。 窗体控件可以用不同平台上的自定义呈现器进行修改。
  • Xamarin 的习俗。 可以使用自定义呈现器创建表单控件。

创建 Xamarin 表格控制

Xamarin 的。 窗体控件可以创建,原因有很多,其中之一是减少 XAML 树中的杂乱,并创建可重用的视图块。 让我们开始吧。

首先,我们将后退一步,看看我们之前为登录屏幕创建的可验证条目:

<Label x:Name="lblUserName" Text="..." />
<Entry x:Name="txtUserName" Placeholder=".." Text="..." >
    <Entry.Behaviors>
        <behaviors:ValidationBehavior x:Name="UserNameValidation" 
 ValidationRule="..." />
    </Entry.Behaviors>
</Entry>
<Label x:Name="errUserName" Text="..." IsVisible="..."/>

控制块由与条目相关的标签和错误标签组成,错误标签只有在标签中存在验证错误时才可见。 密码字段也使用了类似的结构。 通过简单地公开两个绑定数据点,可以轻松地将该块转换为自定义控件。 以下步骤将指导您通过过程从该入口块中提取自定义控件:

  1. In order to create the base control, we will use ContentView. Add a new folder in the ShopAcross.Mobile.Client project named Controls, and add a ContentView, called ValidatableEntry, using the Forms ContentView XAML template:

     <ContentView 
    
         xmlns:x=http://schemas.microsoft.com/winfx/2009/xaml
         xmlns:behaviors="clr-namespace:ShopAcross.Mobile.Client.Behaviors"
         x:Class=" ShopAcross.Mobile.Client.Controls.ValidatableEntry"
         x:Name="RootView">
         <ContentView.Content>
             <StackLayout>
                 <!-- TODO: // Insert Controls -->
             </StackLayout>
         </ContentView.Content>
     </ContentView> 
    

    注意,这里使用了名称声明来创建对控件本身的引用。 这是因为我们将在控件上创建可绑定属性,并将它们绑定到我们之前标识的子值。

  2. 没有 w,在ValidatableEntry.xaml.cs文件中创建我们的可绑定属性:

  3. We should also create accessors for these properties using the bindable properties as a backing field:

     public string Label
     {
         get
         {
             return (string)GetValue(LabelProperty);
         }
         set
         {
             SetValue(LabelProperty, value);
         }
     } 
    

    PlaceholderValueValidationRule属性重复相同的步骤。

  4. 接下来,将这些属性连接到视图元素的子属性,我们将把这些子属性添加到ValidatableEntry.xaml:

    <StackLayout>
        <Label Text="{Binding Label, Source={x:Reference RootView}}" />
        <Entry Placeholder="{Binding Placeholder, Source={x:Reference RootView}}" Text="{Binding Value, Mode=OneWayToSource, Source={x:Reference RootView}}" >
            <Entry.Behaviors>
                <behaviors:ValidationBehavior x:Name="ValidationBehavior" 
                          ValidationRule="{Binding ValidationRule, Source={x:Reference RootView}}" />
            </Entry.Behaviors>
        </Entry>
        <Label Text="{Binding ValidationRule.ValidationMessage, Source={x:Reference RootView}}" FontSize="12" TextColor="Gray" IsVisible="{Binding HasError, Source={x:Reference ValidationBehavior}}"/>
    </StackLayout> 
    

    中的ContentView.Content节点中。

  5. 最后,用我们的自定义控件

    <controls:ValidatableEntry 
        Label="{extensions:Translate LblUsername}" 
        Placeholder="{behaviors:Translate LblUsername}" 
        ValidationRule="{Binding UserNameValidation}"
        Value="{Binding UserName, Mode=OneWayToSource}"/> 
    

    替换LoginView.xaml文件中的原始入口块:

在这里,我们创建了自定义的ContentView,它将可视化树的一个节点捆绑到单个控件中。 此控件还可用于其他需要验证的输入字段。

在本节中,我们专门讨论了在象限 II 中完成的一个实现。 我们使用现有的 Xamarin。 窗体基础结构,以创建可在具有多个本机元素的本机平台上呈现的自定义控件。 接下来,我们将看看如何为 Android 创建一个自定义渲染器,这样我们就可以利用内置的验证显示和浮动标签设计概念。

创建自定义渲染器

有时候,目标平台可以通过使用 Xamarin.Forms 的自定义控件提供超出我们预期需求的开箱即开的功能。 在这些类型的情况下,替换 Xamarin 可能是一个好主意。 使用自定义实现在特定平台上形成实现。

例如,在上一节中,我们试图通过自定义实现实现的表单输入字段,如果用遵循材料设计指南的TextInputLayout实现,将看起来更适合平台:

<image/Figure_6.05_B16381.jpg>

图 6.5 -材料设计浮动标签条目

在这个布局中,我们可以将标签绑定到浮动标签,将错误文本绑定到浮动标签编辑文本的帮助文本区域。 然而,默认情况下,Xamarin。 表单使用FormsEditText(是EditText的衍生品)而不是TextInputLayout用于 Android。 为了解决这个问题,我们可以实现自己的自定义渲染器,它将使用所需的控件。 让我们看看如何做到这一点:

  1. The first step in creating a renderer is to decide whether to create a renderer deriving from ViewRenderer<TView,TNativeView> or the actual render implementation. For EntryRenderer, the Xamarin.Forms base class is ViewRenderer<Entry, FormsEditText>. Unfortunately, this means that we won’t be able to make use of the base class implementation since our renderer will need to return TextInputLayout to the native platform. Therefore, we will need to create a renderer from scratch. To do this, create a new folder, called Renderers, in the ShopAcross.Mobile.Client.Android project, and add a new class, called FloatingLabelEntryRenderer. The renderer declaration should look like this:

    public class FloatingLabelEntryRenderer : ViewRenderer<Entry, TextInputLayout>
     { 
         public FloatingLabelEntryRenderer(Context context) : 
             base(context)
         {
         }
    
         private EditText EditText => Control.EditText;
    
         protected override TextInputLayout CreateNativeControl()
         {
             // TODO:
             return null;
         }
    
         protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
         {
             // TODO:
         }
    
         protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
         {
             base.OnElementChanged(e);
    
             // TODO:
         }
     } 
    

    在这个声明中,我们首先应该处理几个覆盖方法,如下所示:

    • CreateNativeControl:它负责使用Element属性创建本机控件。
    • OnElementChanged:行为和效果方面与OnAttached相似。
    • OnElementPropertyChanged:这用于同步来自 Xamarin 的更改。 元素到本地元素。
  2. 对于CreateNativeControl,我们需要创建一个EditText控件,就像标准的渲染器一样,但是我们还希望将其封装在TextInputLayout:

    protected override TextInputLayout CreateNativeControl()
    {
        var textInputLayout = new TextInputLayout(Context);
        var editText = new EditText(Context);
        editText.SetTextSize(ComplexUnitType.Sp, (float)Element.FontSize);
        textInputLayout.AddView(editText);
        return textInputLayout;
    }
    

    中。

  3. 对于OnElementPropertyChanged,我们感兴趣的是Placeholder属性和相关的OneWay绑定(即从ElementNative)。 因此,我们将使用Placeholder值作为EditText字段的提示文本:

  4. 除了属性之外,当Element附加到渲染器(即初始同步)时,我们还想更新占位符:

  5. Another value we would like to keep in sync is the actual Text value. However, here, the synchronization should be able to support TwoWay binding.

    为了监听输入文本的变化,我们将在渲染器中实现ITextWatcher接口:

    public FloatingLabelEntryRenderer : ViewRenderer<Entry, TextInputLayout>, ITextWatcher
    {
       // … removed for brevity
       void ITextWatcher.AfterTextChanged(IEditable @string)
       {
       }
    
       void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int after)
       {
       } 
    
       void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count)
       {
          if (string.IsNullOrEmpty(Element.Text) && s.Length() == 0)
          {
             return;
          }
    
          ((IElementController)Element)
            .SetValueFromRenderer(Entry.TextProperty, s.ToString());
       }
    } 
    
  6. 现在我们可以在元素更改时引入文本观察器:

    if (e.OldElement == null)
    {
        var textView = CreateNativeControl();
        textView.EditText.AddTextChangedListener(this);
        SetNativeControl(textView);
    }
    
  7. 一旦渲染器完成,我们还需要注册渲染器,以便 Xamarin。 窗体 runtime 知道Entry控件和这个新的渲染器之间的关联:

    [assembly: ExportRenderer(typeof(Entry), typeof(FloatingLabelEntryRenderer))]
    namespace ShopAcross.Mobile.Client.Droid.Renderers 
    
  8. Now that the renderer is going to be handling both the label and the placeholder, we won’t need the additional label within ValidatableEntry, so we will only be using them for iOS:

    <ContentView>
        <OnPlatform x:TypeArguments="View">
            <On Platform="iOS">
                <Label Text="{Binding Label, Source={x:Reference RootView}}" />
            </On>
        </OnPlatform>
    </ContentView>
    <Entry Placeholder="{Binding Placeholder, Source={x:Reference RootView}}" Text="{Binding Value, Mode=OneWayToSource, Source={x:Reference RootView}}" >
        <Entry.Behaviors>
            <behaviors:ValidationBehavior x:Name="ValidationBehavior" 
                          ValidationRule="{Binding ValidationRule, Source={x:Reference RootView}}" />
        </Entry.Behaviors>
    </Entry> 
    

    重要提示

    我们用包装OnPlatform声明的原因是,即使它在语法上是正确的,但由于反射的实现方式,向具有多个子视图的父视图添加一个视图是无法呈现的。 为了解决这个问题,需要将平台特定的声明包装成带有单个子视图的良性视图。

    最终的结果是这样的:

<image/Figure_6.06_B16381.jpg>

图 6.6 -浮动标签的自定义渲染器

注意,浮动的标签在 Android 平台的用户名字段中显示,而在 iOS 上,Label视图在用户名的Entry字段上呈现。

在本节中,我们实现了一个自定义的渲染器,它代替了 Android 平台的开箱即用的实现,作为对被渲染的本地控件的回报。 同样重要的是,我们用来处理文本更改的ITextWatcher接口是一个 Java 接口。 这确实证明了我们现在已经完全移动到象限 III,并正在接近象限 IV。我们可以进一步扩展这个实现,在自定义控制中包含误差指示器。 但是,这意味着我们需要创建一个自定义控件并将一个自定义渲染器附加到它。 在下一节中,我们将使用自定义呈现器实现一个完整的自定义控件。

创建自定义 Xamarin 表格控制

要创建一个完整的自定义控件,实现需要从 Xamarin 开始。 视图的抽象形式。 这种抽象提供了与 XAML 以及与该特定视图相关联的视图模型(即业务逻辑)的集成。

因此,对于浮动标签条目,我们需要创建一个具有必需的可绑定属性的控件。 对于我们的用例,除了Entry控件属性之外,我们还需要验证错误描述和标识是否存在此类错误的标志。 让我们开始实现我们的自定义控件:

  1. 我们将从Entry本身派生自定义控件并添加附加属性开始。 为此实现,在ShopAcross.Mobile.Client项目的Controlsfolder 中创建一个新类,并使用以下类定义:

  2. 现在,修改您的FloatingLabelRenderer以使用新的控件作为TElement类型参数:

    [assembly: ExportRenderer(typeof(FloatingLabelEntry), typeof(FloatingLabelEntryRenderer))]
     namespace ShopAcross.Mobile.Client.Droid.Renderers
     {
         public class FloatingLabelEntryRenderer : ViewRenderer<FloatingLabelEntry, TextInputLayout>, ITextWatcher 
    
  3. 在渲染器中,我们需要监听任何HasErrorProperty更改,并相应地设置错误描述和错误指示器。 那么,让我们将OnElementPropertyChanged展开如下:

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
     {
         .... 
         else if (e.PropertyName == FloatingLabelEntry.HasErrorProperty.PropertyName)
         {
             if (!Element.HasError || string.IsNullOrEmpty(Element.ErrorMessage))
             {
                 EditText.Error = null;
                 Control.ErrorEnabled = false;
             }
             else
             {
                 Control.ErrorEnabled = true;
                 EditText.Error = Element.ErrorMessage;
             }
         }
         ....
     }
    
  4. ValidatableEntry中使用这个控件来代替Entry控件,将创建一个愉快的材料设计布局:

    <controls:FloatingLabelEntry
      Placeholder="{Binding Placeholder, Source={x:Reference RootView}}"
      Text="{Binding Value, Mode=OneWayToSource, Source={x:Reference RootView}}"
      ErrorMessage="{Binding ValidationRule.ValidationMessage, Source={x:Reference RootView}}"
      HasError="{Binding HasError, Source={x:Reference ValidationBehavior}}">
        <Entry.Behaviors>
            <behaviors:ValidationBehavior x:Name="ValidationBehavior" 
              ValidationRule="{Binding ValidationRule, Source={x:Reference RootView}}" />
        </Entry.Behaviors>
    </controls:FloatingLabelEntry>
    

结果页面现在应该将验证错误消息显示为中的TextInputLayout中的所谓提示:

<image/Figure_6.07_B16381.jpg>

图 6.7 -自定义浮动标签控件

重要提示

即使我们已经为 Android 和 iOS 创建并使用了这个自定义控件,因为 iOS 渲染器没有实现,iOS 仍然会显示继承树中下一个最好的东西(即EntryRenderer)。

在本节中,我们通过在 Xamarin 中实现一个自定义控件开始象限 II 中的实现。 形式共享域。 然后,我们继续创建一个自定义渲染器和一个自定义控件。

总结

总的来说,Xamarin 的。 表单对于不同的场景有许多扩展点。 然而,作为开发人员,我们应该谨慎地使用这些扩展点,以便创建健壮、简单且复杂的 ui。 在本章中,为了理解可用的定制选项,我们确定了 Xamarin 的实现域/象限。 表单应用,并介绍了每个象限的不同定制选项。 最后,我们创建了一个自定义控件来演示共享域和本地域中用户控件的完整实现。

本章结束了我们项目的 Xamarin 方面的开发工作。 在接下来的几章中,我们将继续使用。net Core 为我们的移动应用开发一个云基础设施。

七、面向移动应用的 Azure 服务

无论您是处理一个小型的启动应用,还是处理一个企业应用的大量数据,Microsoft Azure 总是一个方便的选择,因为它提供的低成本订阅模型和可伸缩性。 有许多可用的服务在不同的管理服务模式,如软件即服务(SaaS),平台即服务(PaaS)和【显示】基础设施作为服务(IaaS)。 这些功能包括通知中心、认知服务和 Azure 功能,它们可以改变用户对您的应用的印象,而不需要额外的开发时间。 本章将为您提供关于如何在开发。net Core 应用时使用这些服务的快速概述。

**在本章中,我们将使用 Azure 平台上提供的服务来设计我们的服务后端。 我们将首先浏览 Azure 平台上可用的服务,然后继续深入研究数据存储、Azure 无服务器 PaaS 产品,最后是开发服务。 以下主题将指导你完成本章:

  • Azure 服务的概述
  • 数据存储
  • Azure Serverless
  • 开发服务

在本章结束时,您将熟悉各种利用 Azure 服务的架构模型,并更好地理解如何将这些模型整合到您的移动应用项目中。 我们将仔细研究持久性服务以及 Azure 无服务器服务。 最后,我们将讨论 Azure DevOps 和 Visual Studio App Center。

Azure 服务概述

我们正生活在云计算的时代。 我们 10 年前学习并应用到应用中的许多软件范例现在已经完全过时了。 为了可维护性和性能,分布式模块已经取代了传统的 n 层应用和开发团队的简单性。

废话少说,让我们开始通过设置架构和探索 Azure 平台的概念来准备应用的范围。

分布式系统简介

在本节中,我们将讨论分布式后端系统的不同托管模型,以及这些设置的优缺点。

在本书前几章中,我们开始了客户端应用的开发; 这将需要一些额外的视图和修改。 为了继续开发,我们首先需要设置我们的后端。 对于我们的应用,我们需要一个服务后端,它将做以下工作:

  • 提供关于产品的静态元数据
  • 管理用户配置文件并维护用户特定的信息
  • 允许用户上传和公开共享数据
  • 索引和搜索用户上传和共享
  • 使用实时更新通知一组用户

现在,把这些需求和我们创建云基础设施的目标放在一边,让我们试着想象一下,我们如何通过一个本地 n 层应用设置来实现一个分布式系统:

<image/Figure_7.01_B16381.jpg>

【t】【t】

图 7.1 - n 层本地后端

在上图所示的设置中,我们有一个 web 层,它向客户端公开封闭的逻辑 n 层结构。 请注意,系统被划分为逻辑层,并且不涉及无线通信。 我们将使用一个本地服务器来维护这个结构。 如果需要扩展,具有负载平衡实现的多个服务器仍然可以工作。 在大多数情况下,同步和规范化将在数据层进行。 从部署和管理的角度来看,每个部署都将导致一个完整的更新(在多个服务器上)。 此外,每个逻辑模块的需求必须单独维护,即使它是一个整体实现,而且不应该因为这些需求而轻视对本地服务器的应用更新。 部署到不同的服务器也必须小心处理。

重要提示

Knight Capital Group 是一家美国全球金融服务公司,专门从事销售和交易的电子执行。 2012 年 8 月,该公司的资产从 4 亿美元一夜之间破产,原因是该公司运营的 8 台服务器中只有 7 台发布了一项新部署。 在这个 45 分钟的噩梦中,正确的部署与旧代码在单个服务器上竞争,导致了 4.6 亿美元的损失。

我们可以轻松地将整个 web 应用移动到云 IaaS虚拟机(虚拟机)。 然而,这种迁移只会有助于维护,并且扩展仍然必须是在系统级别而不是组件级别。 在这种情况下,瓶颈很可能是数据层,因为伸缩的应用组件只会对数据存储库施加更大的压力。

为了理解这个 n 层设置,让我们仔细看看可能涉及到的组件。 我们将使用一个 SQL 数据库来存储数据,一个消息队列(如 Rabbit MQ 或 MSMQ)和一个 ASP.NET web API 实现。 身份管理可能是一个集成的解决方案,例如 ASP。 净的身份。 通知可以是来自客户端的轮询实现,或者可以考虑在 ASP 中使用 SignalR 实现。 净的 web 应用。 搜索功能可能必须在 SQL Server 级别上才能提高性能。 所有这些都是基于这样一个假设:我们使用的是 Microsoft . net 栈,目标托管平台是 Windows 主机上的 Microsoft IIS 服务器。

接下来,让我们将逻辑模块分解为更小的服务,这些服务可以在面向服务的体系结构(SOA)生态系统中相互通信

<image/Figure_7.02_B16381.jpg>

图 7.2 -微服务设置

与图 7.1 中的相比,前面截图中的设置要轻一些,图 7.1中的每个组件都可以独立开发和部署(也就是说,与系统中的其他元素解耦)。 从维护的角度来看,每个服务可以部署到单独的服务器或虚拟机上。 反过来,它们可以相互独立地进行缩放。 而且,这些服务中的每一个现在都可以被容器化,这样我们就可以完全将服务从操作系统中分离出来。 毕竟,我们只需要有一个 web 服务器,我们的服务集可以在其中托管和服务给客户端。 在这一点上,. net Core 将把我们的应用变成一个跨平台的 web 模块,允许我们同时使用 Windows 和 Unix 容器。 这整个过程可以被标记为从 IaaS 策略迁移到 PaaS 方法。 此外,应用现在可以实现一个基础结构作为代码(IaC)结构,在这个结构中,我们不需要担心应用正在运行的服务器的当前状态。

嗯,这听起来很棒,但是它与云架构和 Azure 有什么关系呢? 创建云就绪应用的主要目的是创建具有功能独立的模块的应用,这些模块可以由适当的、可维护的和可伸缩的云资源托管。 在这一点上,我们不再谈论单个应用,而是一组为各种应用需求携手工作的资源 ts:

<image/Figure_7.03_B16381.jpg>

图 7.3 - Azure PaaS 设置

在前面的分布式模型图中,每个组件都由简单的 PaaS 服务组成,它们之间没有直接依赖关系。 这些组件是完全可扩展的,只要满足系统要求,就可以更换。 例如,如果我们从一个小的 web API 应用服务开始,它可能驻留在一个应用服务计划中。 然而,如果满足了这些需求,那么我们就可以用 Azure 的功能实现来替换这个微服务,它将改变部署模型和执行运行时,但仍然保持系统的完整性。 总的来说,在云模型中,单个组件的可替换性(只要整个系统处于检查状态)将风险和维护工作降到最低。

回到我们的需求,我们可以在关系数据库(如 SQL Server PaaS)或使用 Cosmos DB 的 NoSQL 设置之间自由选择。 此外,我们可以通过在数据存储和 web 网关之间使用 Redis 缓存来提高性能。 搜索功能可以使用 Azure 搜索索引执行,应用服务和 Azure 函数可以用于 API 层。 此外,一个简单的 ESB 实现或 Azure 持久函数可以帮助实现长时间运行的异步操作。 最后,通知可以通过使用 Azure SignalR 或通知中心来执行。

当然,资源的选择很大程度上取决于所选择的体系结构方法。

云架构

在本节中,我们将分析云托管分布式应用和相关 Azure 服务产品的几种架构模式。

在云平台中,系统的设计由单个组件组成。 虽然应该分别设计和开发每个组件,但这些组件的组合方式应该遵循特定的体系结构模式,这些模式将允许系统提供弹性、可维护性、可伸缩性和安全性。

特别是对于移动应用,下面的一些组合模型有助于应用的成功。

为了关联这些模式,我们将讨论我们的 ShopAcross 应用,所以让我们定义一个新的用户故事,我们将在接下来的部分工作:

作为产品所有者,我想引入一个拍卖功能,用户可以为他们的商品创建帖子,比如要拍卖的车辆,这样我就可以增加我的应用的目标群体。

该特性增加了对应用持久性存储的数据输入/输出,并增加了后端服务的复杂性。 现在让我们看看几个可以帮助我们实现这一目标的模型。

门户聚合

在微服务设置中,应用由多个域组成,每个域实现自己的微服务对等体。 由于域是隔离的,因此可以通过执行对后端服务的多个调用来构造客户机应用视图所需的数据。 在一个不断发展的应用生态系统中,这将及时将业务层的所有复杂性推到客户端应用中。 虽然这对于 web 应用来说仍然是可以接受的,但移动应用的性能将随着系统复杂性的增长而降低。 为了避免这个问题,可以在客户端应用和微服务之间放置一个网关服务 façade:

<image/Figure_7.04_B16381.png>

图 7.4 -网关聚合设置

让我们考虑在应用中应用相同的逻辑。 让我们假设拍卖车辆的数据由一个 API(业务项)处理,车辆的元数据由另一个 API(静态数据)处理,用户信息通过另一个 API 提供,最后,我们有一个投标 API。 虽然这种设置为微服务设置提供了必要的隔离,但它要求客户端应用执行多个服务调用来查看和/或创建单个发布。 在这样的场景中,可以使用网关来编排微服务,以便客户机应用可以免除此责任。

事实上,如果我们计划将 web 应用作为客户端来支持,那么数据模型和服务编排可能与移动应用不同。 在这种情况下,我们需要考虑为每个客户端应用创建单独的网关,从而降低单个超级网关的可维护性成本。

后端,前端

在多客户端系统中,每个客户端可能需要以某种方式聚合数据。 这将取决于目标平台资源、技术可行性和用例。 在这种场景中,需要网关 API 为不同的微服务和数据组合公开多个服务端点。 相反,每个客户端应用可以通过单独的网关提供数据,从而减少通过单个 façade 支持多个客户端应用的复杂性:

<image/Figure_7.05_B16381.jpg>

图 7.5 -前端的后端

在前面的示例中,我们设置了三个单独的网关(换句话说,是 Gate-A、Gate-B 和 Gate-C),以支持三个不同的客户机应用。 每个网关实现自己的聚合模型,而不是创建一个复杂的 façade,其中充满了特定于客户端的调整。

例如,让我们假设我们的应用开发团队在一个针对 iOS 和 Android 平台的原始移动应用之上实现了一个 UWP 应用。 在这种情况下,UWP 视图将在更大的设计范围内查看,并且数据需求将不同于移动应用。 对于一个简单的解决方案,网关 API 端点现在可以使用参数来扩展,以限制或扩展响应中返回的对象树(即,info、normal 或 extended),或者可以引入额外的Get{Entity}Extended端点。 然而,通过这种方式,在最小化客户端应用复杂性的同时,我们正在导致网关的增长,并降低了这一层的可维护性。 如果我们引入单独的网关,我们将为已经拥有单独应用生命周期的客户端分离这些 api 的生命周期。 这可以帮助创建一个更易于维护的系统。

但是,如果在整个客户机应用的执行过程中有某些重复的组合或聚合,该怎么办? 这些重复模式可以解释为数据设计问题,其中数据隔离导致性能下降。 事实上,如果微服务设置需要这些领域分离,那么我们将需要在数据存储级别上提出一个数据组合。

物化视图

某些数据维度的聚合可以在数据存储级别上完成。 事实上,作为具有 SQL 背景的开发人员,我们熟悉可以由多个关系表组成的 SQL 视图,并且可以在数据存储级别上进行索引。 虽然这提供了对可用数据的不同视角,但它甚至可以创建多个特定于领域的数据模型的聚合。 尽管如此,这些视图仍然只是一个运行时抽象。 此外,如果持久性存储分散在多个服务器上,我们将需要一个独立的进程来同步域存储和另一个聚合存储之间的数据,并持久化非规范化的数据。 换句话说,具体化视图。 类似的策略可以应用于 Cosmos 等 NoSQL 数据库。

例如,可以使用 Azure Cosmos DB 更改提要在 Cosmos DB 上执行这个数据反规范化过程。 一个文档集合上的更改可以跨多个集合进行同步,这些集合为执行各种搜索或聚合数据定量进行了优化:

<image/Figure_7.06_B16381.jpg>

图 7.6 - Azure 函数的反规范化数据

例如,回到我们的拍卖功能,当我们处理搜索功能时,我们将对多个文档集合执行搜索; 也就是说,用户将需要根据车辆、拍卖数据、出价和个人资料数据进行搜索。 换句话说,不同维度上的数据点都应该通过内部连接用于搜索执行。 这可以通过使用车辆岗位的汇总表来实现,允许可搜索字段在集合之间同步。

cache-aside 模式

缓存是另一个可以帮助提高应用性能的因素,也就是说,我们缓存的数据类型和我们缓存这些信息的应用层。 cache-aside 模式是一个多路复用器的实现,它将处理缓存存储和数据存储之间的数据一致性,这取决于传入的请求和数据寿命:

<image/Figure_7.07_B16381.jpg>

图 7.7 - Cache-aside 实现

在这种设置中,首先在缓存存储中搜索带有特定惟一标识符(例如{EntityName}_{EntityId})的传入请求,如果没有,则从数据存储中检索并插入到缓存中。 通过这种方式,下一个请求将能够从缓存中检索数据。

在缓存还是不缓存的困境中,数据熵可能是一个基本的决定因素。 例如,缓存静态引用项的数据可能是有益的; 然而,缓存拍卖信息,其中的数据是不纯的,并且对相同数据点的重复请求比静态引用更不可能,不会为系统提供额外的价值。

缓存边策略也可以在客户端使用本地存储(如 SQLite)来实现。 有时,在服务器端缓存某个没有意义的文档集合可以在客户端缓存。 例如,当前用户的特定车型和模型的车辆元数据可能是重复请求模式; 然而,考虑到该数据的熵以及其他用户对同一项的访问频率,它不会是一个服务器缓存维度。

基于队列的负载均衡

消息队列不是一个新概念,也不是云架构所独有的。 然而,在具有微服务的分布式系统中,它们可以帮助解耦服务,并允许您控制资源的利用。 为可伸缩性和性能而设计的无服务器组件,如 Azure Functions,可以为云基础设施中的工作队列提供优秀的消费者。

例如,让我们考虑一个应用用例,其中注册用户正在创建一个拍卖项目。 他们已经选择了车型和型号,添加了额外的信息,甚至还添加了几张照片。 此时,如果我们允许将拍卖项目的发布作为一个同步请求,我们将把管道中的某些模块锁定为单个请求。 首先,请求需要在数据存储中创建一个文档; 但是,系统中还会触发其他功能来处理映像、通知订阅的用户,甚至启动内容管理员的审批流程。 现在,假设这个请求由应用的多个用户执行(例如,多个注册用户创建多个帖子)。 这将导致资源利用率达到峰值,进而将应用的恢复力和可用性置于风险之中。

作为这个问题的解决方案,我们可以创建一个消息队列,由 Azure 函数使用,它将协调拍卖数据的创建。 消息队列可以是企业服务总线,也可以是 Azure sto 狂暴队列:

<image/Figure_7.08_B16381.jpg>

图 7.8 -基于队列的请求处理

这听起来很棒,但是这个实现如何影响客户端实现? 客户机应用需要实现一种轮询策略来检索异步作业的状态,或者可以使用推拉机制来通知它,在推拉机制中,服务器将在进程排队之前首先发送拍卖 ID。 然后,当它完成时,服务器可以用相同的 ID 通知客户机,允许它提取完成的服务器数据。 此时,可以存储本地版本的数据并将其提供给用户,直到实际的服务器数据可用为止。 对于这种类型的通知,可以使用 Azure SignalR 或 Notifications Hub 等通知机制。

竞争消费者

在前面的示例中,我们使用 Azure 函数作为消息队列的消费者。 这种方法已经可以作为竞争消费者的实现来接受,其中提供的消息队列由多个工作模块处理。

虽然这将提供可伸缩需求并允许性能执行,但作为产品所有者,我们将无法控制创建来使用消息队列中的事件的函数实例。 为了能够控制和管理队列,可以引入消息代理机制,该机制将控制进入队列的消息流。 消息被推入队列后,多个使用者可以检索、处理和完成消息。

发布者/订阅者的模式

让我们假设我们已经完成了代理队列的实现,并分派了一个使用者来完成一个长时间运行的操作。 此时,正如前面提到的,我们的应用正在期待一个完成信号,以便摆脱任何瞬时数据。

在一个开放的系统,就像我们在实施的过程中,每个服务可以相互通信(而不是一个封闭的系统,执行顺序处理下游),我们面对的不再是一个确定性的同步模型,然而系统的消费者仍然期望结果。 为了允许源系统(即发布者)将操作的输出传播给相关方(即订阅者),可以建立输出通道。 这个实现模式可以归因于发布者/订阅方模式(也称为发布/订阅模式)。

回到我们的异步 web 请求,输出通道然后将结果传递给通知模块,并将结果传递给客户端应用。

同样的模式实现可以通过使用服务总线的另一个消息队列或者使用 Azure 基础设施 EventGrid 上的 pub/sub 模式的实际实现来建立。 这些服务中的任何一个都可以允许长时间运行的流程的输出向相关方散开,比如一个 Azure 函数,它将在 Azure SignalR 上推送通知消息或触发消息。

断路器和重试模式

在一个云系统中,涉及多个移动的块,很难避免失败。 在这种情况下,系统的弹性取决于它从故障中恢复的速度和频率。 断路器和重试模式是微服务生态系统中通常引入的互补模式。 如果即将发生故障,可以使用断路模式来减少系统的时间和资源。 在这些类型的情况下,最好允许系统尽早失败,而不是推迟失败,以便由辅助进程处理故障或启动故障转移机制。

例如,如果我们有一个容易超时的服务(例如,负载过重或由于外部服务故障),可以实现一个断路器,以在闭路状态下连续监视传入的请求。 对于客户端应用,可以无缝地重试失败。 当后续的故障发生时,电路可以暂时处于半开或开的状态,这样下面的请求在不尝试执行的情况下立即被丢弃(知道它可能会失败,直到问题得到解决)。 在这种状态下,客户端应用可以禁用该特性,或者,如果实现了故障转移/解决方案,则可以使用该实现。 一旦电路开放状态过期,系统可以重新引入这个端点,首先是半开放状态,最后是关闭状态,系统就被说成痊愈了。

内置的 Azure 监控功能以及应用遥测功能可以提供警报和通知,这有助于维护 Azure 应用。

Azure 服务提供商和资源类型

在前面的小节中,我们分析了各种模型并提到了几种 Azure 产品。 在本部分中,您将了解 Azure 生态系统中这些服务产品是如何组织的。

Azure 生态系统及其提供的不断增长的服务集允许开发人员轻松地创建各种分布式云应用。 正如我们在云架构一节中看到的,许多 PaaS 和 SaaS 产品通过设计可伸缩和有弹性的应用,为日常问题创建了一个解决方案目录。

通过快速查看服务目录(不完整),您会注意到每个服务都是作为提供者类别的一部分提供的:

<image/Figure_7.09_B16381.jpg>

图 7.9 - Azure 服务提供

每个类别中的服务由一个或多个服务提供商提供。 这些目录中的每个服务都进行了版本管理,以便 Azure Resource Manager 能够处理这些服务的供应。

为了可视化同一屋檐下可用的提供商数量,你可以使用 Azure PowerShell 模块:

Get-AzResourceProvider -ListAvailable | Select-Object ProviderNamespace, RegistrationState 

这将返回一组可供您订阅的提供商。 这些提供商可以是微软提供的模块或第三方提供的:

<image/Figure_7.10_B16381.jpg>

图 7.10 - Microsoft Azure 提供商

重要提示

Microsoft Azure 文档提供了有用的 Azure PowerShell 命令,这些命令可以直接在 Cloud Shell 中执行,而无需使用 PowerShell(在 Windows 上)或 Bash(在 Linux 或 macOS 上)。 此外,跨平台版本 PowerShell (Core)在非 windows 操作系统上也可用,它利用了。net Core。

如果你潜水到一个特定的名称空间,例如,Microsoft.Compute提供者名称空间,你可以得到一个更好的提供的服务的概述,和这些资源的地理区域中可以看到 f 问题从截图:

<image/Figure_7.11_B16381.jpg>

图 7.11 -微软。 计算提供者服务

在 Azure 资源组中,Resource类型定义了我们真正需要的资源以及该资源的版本。 这些资源定义,如果作为资源组Azure 资源管理器(ARM)模板的一部分准备,就构成了我们的声明式 IaC。

ARM 是一个平台服务,它允许在订阅中提供资源。 它公开了一个 web API,可以使用 PowerShell、Azure CLI 和 CloudShell,以及 Azure 门户本身。 资源管理器模板中使用的声明式语法提供了一致的、幂等的部署体验,这允许开发人员和自动化工程师自信地管理基础设施生命周期。

在本节中,讨论了几种分布式应用模型以及体系结构模式。 我们还简要地看了一下 Azure 提供目录中的资源组和提供者。 在下一节中,我们将重点讨论 Azure 上可用的持久性存储服务。

数据存储

定义域并创建我们的分布式系统将要构建的体系结构,从决定持久性存储开始。 反过来,可以定义数据域,并指定访问模型。 在大多数情况下,这个决策不需要局限于单个数据存储,但是系统可以使用多种数据类型和不同的数据存储。 Azure 平台提供了具有不同数据管理概念和特性集的各种资源。 选择最适合应用需求并考虑成本和管理的数据存储模型是很重要的。 现在让我们看看这些不同的模型以及何时使用它们。

关系数据库资源

关系数据库可能是数据存储中最重要的应用。 事务一致性实现原子**,,**,持久【显示】(酸)原则为开发人员提供了一个强大的保证一致性。 然而,从可伸缩性和性能的角度来看,在大多数情况下,常见的 SQL 实现(如 MSSQL 或 MySQL)比 NoSQL 数据库(如 Mongo 和 Cosmos DB)的性能要好。 Azure SQL Database、Azure Database for MySQL 和 PostgreSQL 在 Azure 平台上都可以作为 IaaS 和 PaaS 产品。

在 PaaS 资源模型中,数据库的操作成本和可伸缩性是通过一个称为数据库事务单元**(DTU)的单元来处理的。 这个单元是一个抽象的基准测试,它使用 CPU、内存和数据 I/O 度量来计算。 换句话说,DTU 不是一个精确的度量,而是根据上述度量的标准化值。 微软提供了一个 DTU 计算器,它可以根据在实时数据库上收集的性能计数器来估计 DTU 的使用情况。

从安全性的角度来看,Azure SQL 数据库有几个高级特性。 这些安全特性可用于不同级别的数据可访问性:

  • 网络安全由防火墙维护,访问权限由 IP 和虚拟网络防火墙规则显式授予。
  • 访问管理实现包括 SQL 认证和 Azure Active Directory 认证。 安全权限可以像数据表和行一样进行粒度化。
  • 威胁保护可以通过日志分析和数据审计以及威胁检测服务提供。
  • 通过不同级别的数据屏蔽和加密进行信息保护,保护了数据本身。

正如您所看到的,作为最保守的数据模型之一,它在 Azure 平台上仍然非常流行,可以作为 IaaS 和 PaaS 产品使用。 现在我们已经讨论了关系数据库,让我们转向更“自由”的 NoSQL 数据存储模型。

Azure 存储

Azure 存储模型是云生态系统中最古老的服务之一。 它是一个 NoSQL 存储,为开发人员提供了一个持久的、可伸缩的持久层。 Azure 存储由四个不同的数据服务组成,每个服务都通过 HTTP/HTTPS 和一个完善的 REST API 进行访问。

让我们仔细看看 Azure 存储中可用的这些数据服务。

Azure 斑点

Azure Blob 存储是为非结构化数据提供的云存储。 blob 可用于存储任何类型的数据块,如文本或二进制数据。 Azure Blob 存储可以通过为创建的存储帐户提供的 URL 访问:

http://{storageaccountname}.blob.core.windows.net

每个存储帐户至少包含一个容器,用于组织创建的 blob。 三种类型的 blob 用于不同类型的数据块上传:

  1. 块斑点:这些是为大二进制数据设计的。 一个块块的大小可以达到 4.7 TB。 每个块块由更小的数据块组成,这些数据块可以单独管理。 每个块可以容纳高达 100 MB 的数据。 每个块应该定义一个块 ID,它应该符合 blob 中特定的长度。 块块可以看作是离散的存储对象,比如本地操作系统中的文件。 它们通常用于存储单个文件,如媒体内容。
  2. 页团:当需要随机读写操作时,使用。 这些斑点由 512 字节的页面组成。 一个页面 blob 可以存储高达 8tb 的数据。 为了创建一个页面 blob,应该指定一个最大的大小。 然后,通过指定与 512 字节页面边界对齐的偏移量和范围,可以在页面中添加内容。 存储在云中的 vhd 非常适合页面团的使用场景。 事实上,为 Azure VM 提供的持久磁盘是基于页面 blob 的(也就是说,它们是 Azure IaaS 磁盘)。
  3. 追加斑点:正如名称所示,这些是仅追加斑点。 它们不能被更新或删除,并且不支持对单个块的管理。 它们经常用于记录信息。 一个附加 blob 可以增长到 195 GB。

如您所见,blob 存储,特别是块 blob,是存储应用图像内容的理想选择。 Azure Storage Client 库方法为 blob 提供了对 CRUD 操作的访问,可以直接在客户端应用中使用。 然而,通常使用后端服务来执行实际的上传到 blob 存储是一种安全感知的方法,这样 Azure 安全密钥就可以保存在服务器中而不是客户机中。

Azure 文件

Azure 文件可以视为一个云托管的文件共享系统。 可以通过服务器消息块(SMB)访问它,也称为 Samba,并允许在混合(即本地和云)场景中使用存储资源。 使用网络共享文件夹(甚至本地文件)的遗留应用可以很容易地指向 Azure 文件网络存储。 Azure 文件,就像任何其他 Azure 存储数据服务一样,可以通过 REST API 和 Azure 存储客户端库访问。

Azure 队列

为了实现异步处理模式,如果您不追求高级功能和队列一致性,那么 Azure 队列可以成为服务总线之外的划算的替代方案。 Azure 队列可以更大,更容易实现和管理,以实现更简单的用例。 与服务总线类似,Azure 队列消息也可以与 Azure 函数一起使用,其中每个消息触发一个 Azure 函数来处理处理。 如果不使用触发器,那么只有轮询机制可以处理消息队列。 这是因为,与服务总线不同,它们不提供阻塞访问或事件触发机制,如服务总线上的OnMessage

Azure 表

Azure 表是一个 nosql 结构的云数据存储解决方案。 Azure 表存储的实现遵循一种键值对(KVP)方法,其中没有公共模式的结构化数据可以存储在表存储中。 Azure Table 存储数据可以很容易地在 Azure 门户上可视化,并且通过其他 Azure 存储服务等 Azure storage 客户端库支持数据操作。 然而,Azure 表存储现在是 Azure Cosmos DB 的一部分,可以使用 Cosmos DB 表 API 和 SDK 访问。

Cosmos DB

Cosmos DB 是微软在 Azure 云上提供的 multi-façade,全球分布式数据库服务。 Azure Cosmos DB 的主要优点是可伸缩性和可用性,因此它是任何基于云计算的项目的强有力的候选人。 作为一个写优化的数据库引擎,它保证在全球范围内 99%的读/写查询上的延迟小于 10 毫秒。

Cosmos DB 为开发人员提供了五种不同的一致性模型,以根据需求在性能和可用性之间实现最佳折衷。 所谓的一致性频谱定义了强一致性和最终一致性之间的不同级别,或者换句话说,更高的可用性和更高的吞吐量。

尽管它被设计为 NoSQL 存储,但它确实支持各种存储模型协议,包括 SQL。 这些存储协议支持使用现有的客户端驱动程序和 sdk,并且可以无缝地替换现有的 NoSQL 数据存储。 每个 API 模型也可以通过 gh 使用可用的 REST API:

<image/Figure_7.12_B16381.jpg>

图 7.12 - CosmosDB 访问模型

由于它提供了各种访问模型,CosmosDB 正在成为 Azure 平台上数据持久性的首选服务。 但是,如果我们要处理的数据比我们在长期持久性存储中存储的数据类型更不稳定,该怎么办呢? Azure 缓存的 Redis 可以是一个伟大的解决方案,临时数据是使用在这些类型的场景。

Azure Cache for Redis

Azure Cache for Redis 是一个提供类似 Redis 的内存数据结构存储的提供商。 通过减少实际持久性存储的负载,它有助于提高分布式系统的性能和可伸缩性。 使用 Redis,数据存储为 kvp,由于其复制特性,它也可以用作分布式队列。 Redis 支持以原子方式执行事务。

在 Azure Cache 的帮助下,我们将在应用后端使用缓存模式来实现 Redis。

在本节中,我们浏览了 Azure 上用于数据存储的 PaaS 产品。 在下一节中,我们将了解另一个对分布式系统的可维护性和成本有很大帮助的 PaaS 服务:Azure 无服务器。

Azure 无服务器

您可能已经注意到,在现代云应用中,PaaS 组件比 IaaS 资源更丰富。 在这里,应用虚拟机被更小的应用容器取代,数据库作为平台取代了集群数据库服务器。 Azure 无服务器将基础设施和平台管理向前推进了一步。 在无服务器资源模型(如 Azure Functions)中,事件驱动的应用逻辑在由平台本身提供、扩展和管理的平台上按需执行。 在 Azure 无服务器平台中,事件触发器可以从消息队列到网络钩子等多种多样,可以内在地集成到生态系统中的各种资源中。

Azure 功能

Azure 功能是管理的、事件驱动的逻辑实现,可以为云架构提供轻量级的特别解决方案。 在 Azure 函数中,工程团队不仅忽略了执行基础设施,也忽略了平台,因为 Azure 函数是用。net Core、Java 和 Python 跨平台实现的。

Azure 函数的执行从触发器开始。 Azure 功能支持多种执行模型,包括:

  1. 数据触发器:CosmosDBTriggerBlobTrigger
  2. 周期触发器:TimerTrigger
  3. 队列触发:QueueTriggerServicesBusQueueTriggerServiceBusTopicTrigger
  4. 事件触发:EventGridTriggerEventHubTrigger

Azure 函数的触发器在函数清单/配置中定义:function.json

触发器一旦实现,函数运行时就执行 Azure 函数的 run 块。 传递给运行块的请求参数(即输入绑定)由所使用的触发器决定。

例如,下面的函数实现是由 Azure 存储队列中的一个消息条目触发的:

public static class MyQueueSample 
{ 
    [FunctionName("LogQueueMessage")] 
    public static void Run(
        [QueueTrigger("%queueappsetting%")] string 
         queueItem, ILogger log) 
    { 
        log.LogInformation($"Function was called with: 
        {queueItem}");
    } 
}

输出参数(即输出绑定)也可以定义为函数声明中的out参数:

public static class MyQueueSample 
{ 
    [FunctionName("LogQueueMessage")] 
    public static void Run(
        [QueueTrigger("%queueappsetting%")] string 
         queueItem,
        [Queue("%queueappsetting%-out")] string outputItem 
        ILogger log) 
    { 
        log.LogInformation($"Function was called with: 
        {queueItem}");
    } 
}

注意,[Queue]属性在 Azure 函数的队列存储绑定中用作输出绑定,它将在另一个队列中创建一个新的消息条目。 对于 Azure 函数,还有更多类似的开箱即用绑定类型。

重要提示

在这些例子中,我们使用了 c#和。net Standard 来创建已编译的 Azure 函数。 基于脚本的 c#、Node.js 和 Python 也是使用类似方法创建函数的选项。

从概念上讲,Azure 函数可以被视为逐调用的 web 服务。 然而,作为 Azure 函数的扩展,持久性函数允许开发人员创建持久性(即有状态的)函数。 这些函数允许您使用检查点编写有状态函数,其中编排器函数可以分派无状态函数并执行工作流。

Azure 函数既可以作为在特定触发器上执行业务逻辑的单个模块使用,也可以作为一组强制工作流(使用持久函数)使用; 或者,它们可以作为Azure Logic Apps的处理单元。

Azure Logic 应用

Azure Logic 应用是声明式工作流定义,用于编排任务、流程和工作流。 与函数类似,它们可以与许多其他 Azure 资源以及外部资源集成。 逻辑应用是使用 JSON 应用定义模式创建、版本控制和配给的。 Azure 门户的和 Visual Studio 都提供了设计器:

<image/Figure_7.13_B16381.jpg>

图 7.13 -逻辑应用设计器

逻辑应用的任务不仅限于 Azure 功能,还包括所谓的商业和/或第三方连接器(例如,使用 Twilio 发送短信,使用 SendGrid 发送电子邮件,或发布 Tweet)。 此外,企业集成包(EIP)提供行业标准消息传递协议。

与 Azure 函数一样,逻辑应用的执行从触发器开始,每个输出步骤都存储在执行上下文中。 处理块,如条件、开关和foreach循环在应用流中是可用的。 此外,逻辑应用工作流可以通过Azure 事件网格事件进行调度。

Azure 事件网格

Azure Event Grid 是一个基于云的事件聚合实现,它支持发布/订阅事件路由策略。 事件网格由事件源(即发布者)和事件订阅(即使用者)组成:

<image/Figure_7.14_B16381.jpg>

图 7.14 -事件网格参与者

与 Azure 函数触发器类似,开发人员可以使用各种事件源,Azure 事件网格可用于将某些事件从一个 Azure 资源路由到另一个 Azure 资源,或将某些事件多播。 事件不需要由系统资源触发,但也可以使用 HTTP 请求创建事件,从而允许自定义模块向消费者发送事件。

使用事件网格,我们完成了本节。 在本节中,我们讨论了 Azure 函数和逻辑应用,最后讨论了事件网格,它可以用作应用中使用的其他 Azure 组件之间的中介。 现在我们已经浏览了可以在应用中使用的服务,接下来让我们看看可以帮助我们管理项目生命周期的服务。

开发服务

Azure 资源不仅仅局限于应用生命周期中提供和维护的应用需求。 它们还包括某些用于实现应用生命周期和开发管道的平台服务,例如 Azure DevOps 和 Visual Studio App Center。 在本节中,我们将了解这些免费增值服务,在本书的其余部分中,我们将使用它们来管理应用的开发和部署。

Azure DevOps

AzureDevOps(以前称为 TFS 在线或 Visual Studio 团队服务),开始在微软应用生命周期管理(ALM)本地产品套件 TFS,现在是利用最广泛的免费商业模式管理门户。 Azure DevOps 实例可以从 Azure Portal 创建,也可以通过 Azure DevOps 门户创建。 这个采购过程从创建 DevOps 组织开始。

一旦创建了组织,就可以创建一个包含源控制存储库和待办事项列表的新项目。 在项目设置的Advanced部分中,可以使用中的 ALM 过程和版本控制选项:

<image/Figure_7.15_B16381.jpg>

图 7.15 - Azure DevOps 项目设置

需要指出的是,TFVC 和 Git 存储库是同时可用的。 一个单独的 Azure DevOps 项目可能包含多个存储库。 由于跨平台支持和与 ide(如 Visual Studio Code 和 Visual Studio for Mac)的集成,以及本地 ide(如 Android Studio)的集成,Git 通常是 Xamarin 和本地移动开发者的首选存储库类型。

Azure DevOps 上的 DevOps 实现提供了广泛的特性集。 Azure 门户(除了概述)分为五个主要部分,如下:

  • 董事会(项目管理)
  • 回购(版本控制)
  • 管道(CI / CD)
  • 测试计划(测试管理)
  • 工件(包管理)

根据免费订阅模式,一个项目最多可以免费包含 5 个贡献者。 额外的团队成员将需要具有有效的 Visual Studio 或Microsoft Developer Network(MSDN)许可证,否则他们将被分配给只读的涉事者角色。

Visual Studio App Center

Visual Studio App Center 是一套工具,它将移动开发人员使用的各种开发服务(如 Xamarin、Native 和 Hybrid)捆绑到一个管理门户中。 App Center 与 Azure DevOps 紧密集成,它们可以相互配合使用。 App Center 支持多种应用平台,这些平台具有多种特性:

<image/Figure_7.16_B16381.jpg>

图 7.16 - App Center 平台

从 CI/CD 的角度来看,App Center 允许使用来自各种存储库系统(如 Azure DevOps 和 GitHub)的源工件来构建移动应用。 应用可以使用开箱即用的构建模板进行编译,应用包可以分发给指定的组,而不需要使用任何其他存储。

准备好的应用包还可以通过 UI 测试进行自动验收测试。 自动化 UI 测试支持多个测试运行时,例如 Xamarin。 外的和 Appium。

最后,可以从移动应用的 beta 版和生产版收集应用遥测和诊断数据,并将有价值的应用反馈重新引入 backlog 中。 推送通知是另一个有价值的功能,可以用来吸引应用用户。

App Center 也采用免费订阅模式,其中的构建和测试时间受到订阅的限制; 然而,CI/CD 特性的有限使用和无限的分发特性是免费的。

总结

总的来说,使用紧密集成的 Azure 模块,现在开发分布式应用要容易得多。 云应用和混合应用都可以使用可用的资源和通过。net Core 堆栈实现的模块来创建。 同样重要的是要记住,资源不应该定义应用需求; 相反,应该设计一个最佳的解决方案,同时考虑到可用的模块、需求和成本。

在本章中,我们讨论了云环境中的各种应用模型和架构模型。 我们还浏览了可用的 Azure 资源,这些资源将在接下来的章节中用于创建我们的应用后端。 在下一章中,我们将使用 Cosmos DB 创建我们的数据存储。****

八、使用 Cosmos DB 创建数据存储

创建数据存储是移动和 web 应用项目的重要组成部分。 可伸缩性、成本效益和性能是决定哪个数据库适合应用的三个关键因素。 Cosmos DB 具有广泛的可伸缩性选项和订阅模型,可以为移动应用提供理想的解决方案。 Cosmos DB 提供了多模型和多 API 范例,允许应用使用多个数据模型,同时使用最适合应用的 API 和模型(如 SQL、Cassandra、Gremlin 或 MongoDB)存储应用数据。

在本章中,我们将讨论 Cosmos DB 的基本概念,分析和实验数据访问模型,最后,我们将开始为我们的应用创建数据模型和数据存储,并实现数据访问模块。

在本章中,我们将涵盖以下主题:

  • 宇宙 DB 的基础知识
  • 数据访问模型
  • 建模数据
  • 深入了解 Cosmos DB

在本章结束时,你将会熟练地使用 Cosmos DB 提供的 SQL API 或 Mongo 访问模型来实现和访问数据模型。

技术要求

你可以在以下 GitHub 链接中找到本章使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter08

宇宙 DB 的基本原理

从基于云的应用的角度来看,Cosmos DB是另一个可用的持久性存储,您可以将其包含在资源组中。 正如我们前面所讨论的,Cosmos DB 最大的优势还构成了 Cosmos DB 的独特特性集,即全局分布、多模型和高可用性。

在我们的示例应用中,我们将使用 Cosmos DB 并围绕可用的持久性模型创建我们的数据模型。 让我们开始添加一个 Cosmos DB 实例到我们的资源组:

<image/Figure_8.01_B16381.jpg>

F 图 8.1 -创建 Cosmos 数据库帐户

在这个屏幕上,我们将设置资源组、帐户名称(它还定义了访问 URL)和与数据访问模型以及全局分布相关的其他参数。 在决定任何 Cosmos 资源属性之前,让我们先看看这些基本概念。 在接下来的小节中,我们将了解 Cosmos DB 如何使用全球分布,一致性频谱看起来像什么,以及不同的一致性要求,最后,如何根据您的需求调整定价模型。

全球分销

全局分发是处理应用的全球范围的可用选项。 如果您计划让您的应用在全球范围内可用,并且您希望在每个市场中具有相同的延迟,那么可以选择 Geo-Redundancy 以允许在多个地区分布。 创建 Cosmos DB 资源后,可以使用全局复制数据刀片:

<image/Figure_8.02_B16381.jpg>

Figure 8.2 - Cosmos DB 全局复制

太棒了! 现在,我们在四个大洲的五个不同的数据中心展示了这个应用。 换句话说,我们为我们的持久化存储启用了多宿主。

除了两地三中心冗余功能外,还可以启用多个写区域。 多个写区域允许您为分布式数据存储设置多个主服务器。 这些数据不仅可以在不同的区域进行复制,还可以提供相同的写吞吐量。

在配置全局分布区域时,您可以设置其中一个读区域,即它的故障转移区域。

一致性谱

一旦数据持久性成为全球分布式、多宿主操作,一致性概念就成为一个基本主题。 在 Cosmos DB 中,有五个定义良好的一致性级别,简单地说,就是允许开发者优化一致性和性能之间的权衡:

<image/Figure_8.03_B16381.jpg>

图 8.3 -一致性谱

默认的一致性级别可以在数据库级别上设置,也可以在使用数据的客户机会话中设置。 但是,客户端集的一致性不能设置为高于默认一致性级别的一致性。

为了理解这种一致性,我们将使用微软研究论文中使用的棒球类比。 在本例中,我们将考虑棒球比赛中的各个利益相关者以及他们如何读取和写入比分。

毫无疑问,最需要一致性的人是官方的记分员。 他们将读取当前的分数,并在任何一支队伍得分时增加分数。 然后,他们希望得到他们正在读取的数据是最新版本的保证。 然而,由于记分员是唯一将执行写操作的人,我们也许能够侥幸不一致的阅读水平,如会话一致性,它提供了单调读一致性,单调写道,“读己之所写”一致性,write-follows-reads 保证考虑到有一个作家会话。

继官方记分员之后,另一个有强一致性要求的利益相关者是裁判,在本垒板后面主持棒球比赛的人。 裁判在第九局下半局结束比赛的决定取决于主队是否领先(也就是说,如果客队没有办法扳平比分,主队就没有必要击球)。 在这个决定中,他们需要线性化保证(也就是说,他们将读取任何给定数据点的最新版本)。 从他们的角度来看,每个操作都应该以原子的方式发生,并且存在一个单一的全局状态(即真相的来源)。 在这种设置中,性能(即低延迟)会因分布式集群中的仲裁状态而受到影响。

不像裁判,一个定期的记者(例如,30 分钟)就像保证一个一致的前缀*; 换句话说,它们只依赖于数据的一致状态,直到返回写操作为止。 对他们来说,数据状态一致性比结果的延迟更重要,因为操作会定期执行,以提供全面的更新。*

*另一个不太关心延迟但关心一致状态的利益相关者是体育记者。 作者可以收到游戏的最终结果,并在第二天提供他们的评论,只要他们收到的结果是正确的。 在与此类似的场景中,最终一致性可能会返回正确的最终结果,但是当您想用延迟时间限制最终一致性承诺时,有界过时可能是一个解决方案。 事实上,裁判本可以对他们的读操作使用类似的策略,以更短的延迟。

通过将这些概念应用到我们的应用模型中,我们可以自己决定哪些模块需要哪种类型的一致性。

让我们假设一旦交易完成,我们的用户就会收到来自参与者的评论或评级。 在这个场景中,评级系统实际上并不需要一组有序的写操作,一致性也不是很重要。 如果我们以最终一致性的承诺来编写和阅读,那么我们就能够为同一用户处理每一个评论。

接下来是通知系统,它在特定的时间间隔内只向感兴趣的各方发送拍卖物品的最高出价。 在这里,只需要在承诺的情况下执行读取操作,以便保持将出价写入数据存储的顺序,换句话说,使用一致的前缀。 如果我们发送的统计数据与类似,即物品的价值在过去一小时内增加了 30%,这就变得尤为重要。 类似地,这种一致性的周期可以由读系统定义,使其成为有界过时一致性。

现在,让我们假设,用户希望在观察列表中保留一组拍卖。 这个观察列表将只由用户自己编写,重要的读取保证是读取您所写的内容。 这可以通过会话一致性来处理。 此外,创建一个新的拍卖物品或更新将再次只是会话一致。

最后,可能在设置过程中最一致的过程是实际投标(即强一致性)。 在这里,为了竞拍一个拍卖项目,以及宣布拍卖的结果,我们依赖于强烈的一致性,因为竞拍项目是一个多参与者操作,我们希望确保传入的出价以一致的方式执行。

当然,这只是一种自以为是的设置,成本和实现完全没有考虑在内。 在真实的实现中,会话一致性在降低成本的同时提供了一致性和性能之间的最佳权衡。

定价

Cosmos DB 的定价模式相当复杂。 此计算涉及许多因素,例如全局可用性、一致性和另一个称为请求单元(RU)的抽象单元。 与数据事务单元(DTU)类似,它是用于读取 1 KB 项的系统资源(例如,CPU、内存和 IO)的度量。 有很多因素会影响 RU 的使用,比如条目大小、复杂性、索引、一致性级别和执行的查询。 通过使用 DB 返回的请求收费头来跟踪 RU 的消费是可能的。

让我们看看下面的文档 DB 客户端执行:

var query = client.CreateDocumentQuery<Item>(
UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
        new FeedOptions { MaxItemCount = -1 })
                 .Where(item => !item.IsCompleted)
                 .AsDocumentQuery(); 

这将转换成一个 SQL 查询如下:

select * from Items where Items.isCompleted = false

我们可以通过使用Data Explorer刀片上的查询统计信息标签来获取请求费用:

<image/Figure_8.04_B16381.jpg>

图 8.4 - Cosmos DB 查询统计

根据数据集和应用的执行情况,请求收费变得越来越重要。 Cosmos DB 成本可以通过分析提供的遥测技术来优化应用的需求。

在本节中,我们介绍了 Cosmos DB 的基本概念,包括全局分布和一致性。 最后,讨论了基于云的数据存储的关键决策因素之一——定价模型,并对请求单元进行了分析。

数据访问模型

在创建 Cosmos DB 实例之前,可能要选择的最重要的选项是访问模型(即 API)。 在我们的应用中,我们将使用 SQL API,因为它本质上是唯一的本机访问模型,并允许使用其他特性,如触发器。 这就是为什么 SQL API 将是本节首先讨论的访问模型。 尽管如此,我们还将讨论 Mongo API,它可以提供一个可行的替代方案,其强大的社区支持以及减少了厂商锁定的风险。 本节将讨论的其他选项包括 Gremlin、Cassandra 和 Azure Table Storage。

SQL API

SQL API 允许开发人员使用 SQL 方言查询基于 json 的 NoSQL 数据结构。 与实际的 SQL 实现类似,SQL API 支持使用存储过程、触发器(即更改提要)和用户定义函数。 对 SQL 查询的支持允许(部分)使用 LINQ 和现有的客户端 sdk,比如实体框架。

The MongoDB API

由 Cosmos DB 提供的 MongoDB API 为 MongoDB 查询语言提供了广泛的支持(在本文撰写时,MongoDB 3.4 有线协议还在预览中)。 使用 MongoDB API 类型创建的 Cosmos DB 实例可以使用现有的数据管理器访问,例如 Compass、Studio 3T、RoboMongo 和 Mongoose。 这种对 MongoDB 的全面支持为开发人员提供了从现有 MongoDB 存储进行无缝迁移的选择。 Azure 门户数据提供了对 MongoDB 资源的 shell 和查询访问,以便可视化和分析数据。 为了演示这一点,让我们从 MongoDB 文档库中执行几个 MongoDB 查询。

假设我们有一个集合survey,我们将从插入调查结果的集合开始:

db.survey.insert([
 { "_id": 1, "results": [{ "product": "abc", "score": 10 }, { "product": "xyz", "score": 5 }]},
 { "_id": 2, "results": [{ "product": "abc", "score": 8 }, { "product": "xyz", "score": 7 }]},
 { "_id": 3, "results": [{ "product": "abc", "score": 7 }, { "product": "xyz", "score": 8 }]}
])

这将导致的错误消息类似如下:

ERROR: Cannot deserialize a 'BsonDocument' from BsonType 'Array'.

这是,因为insert命令在 web shell 上不完全支持。 为了能够正确地执行命令,我们需要转移到本地终端(假设 Mongo 工具集已经安装):

$ mongo handsoncrossplatformmongo.documents.azure.com:10255 -u handsoncrossplatformmongo -p {PrimaryKey} --ssl --sslAllowInvalidCertificates
MongoDB shell version v4.0.3
connecting to: mongodb://handsoncrossplatformmongo.documents.azure.com:10255/test
WARNING: No implicit session: Logical Sessions are only supported on server versions 3.6 and greater.
Implicit session: dummy session
MongoDB server version: 3.2.0
WARNING: shell and server versions do not match
globaldb:PRIMARY>show databases
sample  0.000GB
globaldb:PRIMARY>use sample
switched to db sample
globaldb:PRIMARY>db.survey.find()
globaldb:PRIMARY>db.survey.insert([{"_id":1, "results":[{"product":"abc", "score":10}, { "product":"xyz", "score":5}]}, { "_id":2, "results":[{"product":"abc", "score":8}, { "product":"xyz", "score":7}]}, { "_id":3, "results":[{"product":"abc", "score":7}, { "product":"xyz", "score":8}]} ])
BulkWriteResult({
 "writeErrors" : [ ],
 "writeConcernErrors" : [ ],
 "nInserted" : 3,
 "nUpserted" : 0,
 "nMatched" : 0,
 "nModified" : 0,
 "nRemoved" : 0,
 "upserted" : [ ]
})
globaldb:PRIMARY>db.survey.find()
{"_id":1, "results":[{"product":"abc", "score":10}, {"product":"xyz", "score":5}]}
{"_id":2, "results":[{"product":"abc", "score" : 8}, {"product":"xyz", "score":7}]}
{"_id":3, "results":[{ "product":"abc", "score" : 7}, {"product":"xyz", "score":8}]}

重要提示

MongoDB 的服务器和客户端Mongo.exe可以从 MongoDB 网站上下载。 在 macOS 上,brew install mongo命令将安装 Mongo。 个性化的连接字符串或完整的 shell 连接命令可以从 Cosmos DB 资源的 Quick Start 部分复制。

接下来,我们可以继续在云 shell 或本地mongoshell 上执行。 现在我们将执行一个find查询,其中产品应该是"xyz",并且评分应该大于或等于8:

db.survey.find(
   { results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } }
)

接下来我们将找到所有包含产品"xyz"的调查结果:

db.survey.find(
   { "results.product": "xyz" }
)

最后,我们将在产品为"abc"的地方增加第一个分数:

db.survey.update({
    "results.product" : "abc" 
 }, 
 {
     $inc : {'results.0.score' : 1}
 });

你可以在数据资源管理器的 shell 窗口中可视化结果:

<image/Figure_8.05_B16381.jpg>

图 8.5 - Data explorer shell

正如我们在这里说明的,Mongo 数据库和文档完全由 Cosmos DB 支持。 使用 Mongo 数据存储实现的应用可以很容易地在简单的 Mongo 守护进程上开发和测试,并且可以使用 Cosmos DB 部署到云环境中。 这使得 Mongo 成为 Cosmos DB 上最具吸引力的通用 NoSQL 访问模型之一。

其他

Gremlin 是一个图数据模型,Cassandra 是一个列族模型,它们都支持有线协议。 这些 api 允许与集群计算和大数据分析平台(如 Spark (GraphX))集成。 可以在 Azure hinsight 中创建 Apache Spark 集群来分析流和历史数据。

正如我们前面提到的,Cosmos DB 的最后一个成员是 Azure Table 存储,它提供对键/值对存储的访问,该存储支持数据的自动分片以及索引。

Cosmos DB 中支持的每个访问模型都可以用于特定的情况,SQL API 和 Mongo 是两个最常见的访问模型。 记住一个单一的 Cosmos DB 订阅应该使用一个特定的模型,这一点也很重要。 一旦确定了访问模型,就可以开始对数据建模。

建模数据

熟悉 Cosmos DB 提供的各种数据模型的最好方法是使用提供的 NoSQL 数据访问 api 实现固有的关系域模型。 这样,就更容易掌握不同数据模型的好处。 在本节中,我们将使用 SQL API 访问模型在 Cosmos DB 上创建域的主聚合根,并实现存储库类,这些类将在 web 应用中用于访问这些文档集合。 最后,我们还将讨论非正规化和引用数据。

对于这个练习,让我们为我们的拍卖应用创建一个关系数据模型。

在这个设置中,我们有三个大的数据集群:

  1. 车辆,包括制造商,型号,年份,发动机规格,以及描述车辆的一些附加属性
  2. 用户,由拍卖车辆的卖家和买家组成
  3. Auctions,由销售用户提供的一些关于销售的元数据,以及用户提供的车辆和出价组成

我们将使用 SQL API 描述此数据。

创建和访问文档

最微不足道的方式考虑到数据模型的设计,当处理一个 NoSQL 数据库,将想象数据转换对象(DTO)模型所需的应用。 在非 rbms 数据平台的情况下,重要的是要记住我们不受引用、唯一键或多对多关系的约束。

例如,让我们看一下最简单的模型,即User。 用户将拥有基本的概要信息,这些信息可以在应用的其余部分中使用。 现在,让我们想象一下用户对象的 DTO 是什么样的:

{
    "id": "efd68a2f-7309-41c0-af52-696ebe820199",
    "firstName": "John",
    "lastName": "Smith",
    "address": {
        "addressTypeId": 4000,
        "city": "Seattle",
        "countryCode": "USA",
        "stateOrProvince": "Washington",
        "street1": "159 S. Jackson St.",
        "street2": "Suite 400",
        "zipCode": "98101"
    },
    "email": {
        "emailTypeId": 1000,
        "emailAddress": "john.smith@test.com"
    },
    "isActive": true,
    "phone": {
        "phoneTypeId": 1000,
        "number": "+1 121-212-3333"
    },
    "otherPhones": [{
        "phoneTypeId": 3000,
        "number": "+1 111-222-3333"
    }],
    "signUpDate": "2001-11-02T00:00:00Z"
}

让我们创建名称为UsersCollection和的集合,分区键设置为/address/countryCode

接下来,将这个 data 导入我们的数据库:

<image/Figure_8.06_B16381.jpg>

图 8.6 - Cosmos DB Documents 视图

好吧; 现在,我们创建了第一个文档。 但是这些由系统添加的额外字段是什么? 这些是对存放集合文档数据的容器和项的引用:

<image/Figure_8.07_B16381.jpg>

图 8.7 -系统生成的属性

在这些字段中,最重要的字段是_etag_ts,它们都定义了实体在给定时间点的状态。 请注意,描述并不指向文档,而是指向项目和实体。 主要原因是在 Cosmos DB 中,存储桶被称为容器,而存储在这些容器中的实体被称为项目。 集合、表或图形是这些容器的实现,这取决于所使用的 API 类型。

现在,我们可以开始创建我们的数据访问层,这将是 User API 的一部分,以向我们的移动应用提供所需的数据。 让我们开始:

  1. 首先,让我们使用以下命令在您选择的新文件夹中创建一个新的解决方案文件:

  2. 现在我们可以创建我们的核心存储库项目,我们将使用它来存储我们的存储库接口:

    dotnet new classlib -o ShopAcross.Web.Repository
    
  3. 在这个新创建的项目中创建一个通用接口,允许我们检索用户提要以及单个用户

     public interface IRepository<T> where T : class
     {
         Task<T> GetItemAsync(string id);
         Task<IEnumerable<T>> GetItemsAsync();
         //Task<Document> AddItemAsync(T item);
         //Task DeleteItemAsync(string id);
         //Task<Document> UpdateItemAsync(string id, T item);
     } 
    
  4. 创建一个新项目,该项目将使用 Cosmos DB 上的 SQL API 实现存储库访问模型,如下所示:

  5. Createour iimplementation for Cosmos DB:

     public class CosmosCollection<T> : IRepository<T> where T : class
     {
         private Container _cosmosContainer;
         public CosmosCollection(string collectionName)
         {
    CollectionId = collectionName;
             var client = new CosmosClient(Endpoint, Key);
             var client = new CosmosClient(Endpoint, Key);
             var database = client.GetDatabase(DatabaseId);
             _cosmosContainer = database.GetContainer(CollectionId);
    }
        // ... Removed for brevity
     } 
    
  6. 实现获取所有条目的存储库方法:

  7. 让我们对检索单个条目执行相同的操作:

    public async Task<T>GetItemAsync(string id)
    {
        // Query for items by a property other than Id
        var queryDefinition = new QueryDefinition
            ($"select * from {CollectionId} c where c.Id = @EntityId")
            .WithParameter("@EntityId", id);
        using FeedIterator<T> resultSet = _cosmosContainer.GetItemQueryIterator<T>(queryDefinition);
        var response = await resultSet.ReadNextAsync();
        return response.FirstOrDefault();
    }
    
  8. 现在,我们已经准备好加载我们已经导入到文档集合中的文档:

    var cosmosCollection = new  
        CosmosCollection<User>("UsersCollection");
    var collection = await cosmosCollection.GetItemsAsync() 
    
  9. 您还可以传递分区键(即countryCode)以降低查询成本,否则将是一个跨分区调用:

    using FeedIterator<T> resultSet = _cosmosContainer.GetItemQueryIterator<T>(
    queryDefinition: null,
    requestOptions: new QueryRequestOptions
         {
              MaxItemCount = -1, 
              PartitionKey = new PartitionKey("USA")
         });
    
  10. 现在,为给定集合加载完整的条目集。 然而,在大多数情况下,我们将使用谓词来加载所需的集合。 因此,在查询中添加一个Where子句:

```
public async Task<IEnumerable<T>> GetItemsAsync(
Expression<Func<T, bool>> predicate)
 {
     using FeedIterator<T> resultSet =  
        _cosmosContainer.GetItemLinqQueryable<T>(
requestOptions: new QueryRequestOptions
             {
MaxItemCount = -1,
PartitionKey = new PartitionKey("USA")
             })
.Where(predicate)
         .ToFeedIterator();
    // ...
 } 
```
  1. 现在,创建相应的添加、更新和删除方法,这将为集合提供完整的 CRUD 操作:
```
public async Task<T>AddItemAsync(T item)
 {
     var resp = await _cosmosContainer.CreateItemAsync(item);
     return resp.Resource;
 }
 public async Task<T> UpdateItemAsync(string id, T item)
 {
     var resp = await _cosmosContainer.ReplaceItemAsync(item, id);
     return resp.Resource;
 }
 public async Task DeleteItemAsync(string id)
 {
     _ = await _cosmosContainer.DeleteItemAsync<T>(id, PartitionKey.None);
 }
```
  1. 最后,优先方法后,为了避免文档集每次都手动创建,可以使用初始化函数来创建数据库和收集如果他们不 exi 圣当客户是第一次创建。

现在,我们已经创建了一个完整的文档集合和基本的 CRUD 函数。 现在,我们将通过数据反规格化继续并进一步扩展我们的领域模型。

非正态化数据

数据标准化(Data normalization)是通过分解现有结构和创建替换引用来构建数据库模型的过程,以减少冗余,提高数据完整性。 然而,数据规范化本质上适用于关系数据库。 在文档集合的情况下,嵌入式数据优先于引用完整性。 此外,跨单个集合边界的数据视图还应该根据设计需求在不同的枢轴上进行复制。

让我们继续进行关于车辆和拍卖的数据模型设计。 这两个数据域将使用单独的 api 进行处理,并拥有单独的集合。 但是,在一般的提要(例如,最新的拍卖)中,我们需要检索关于拍卖的数据,以及拍卖中的汽车和用户为该特定拍卖提供的出价。 让我们看看如何做到这一点:

  1. For the vehicle’s declaration, we will need the main product information:

    { 
        "id" : "f5574e12-01dc-4639-abeb-722e8e53e64f", 
        "make" : "Volvo", 
        "model": "S60", 
        "year": 2018, 
        "engine": { 
            "displacement" : "2.0", 
            "power" : 150, 
            "torque" : 320, 
            "fuelType": { "id": "11", "name": "Diesel" } 
        }, 
        "doors": 4, 
        "driveType": { "id" : "20", "name" : "FWD" }, 
        "primaryPicture" : "",
        "pictures" : [], 
        "color": "black", 
        "features": [ "Heated Seats",  "Automatic Mirrors",  "Windscreen Defrost",  "Dimmed Mirrors",  "Blind Spot Detection" ]
    

    注意,特性数组包含从引用值列表中选择的特性列表,但是我们没有创建多对多关系表,而是选择在这里嵌入数据,这是对常规形式的一种折衷。 类似的方法也可以用于fuelTypedriveType引用,但从概念上讲,我们在这些数据点上具有多对一关系,因此它们本身作为引用数据对象嵌入。

  2. 移动,创建拍卖数据:

    {
        "id" : "7ad0d2d4-e19c-4715-921b-950387abbe50",
        "title" : "Volvo S60 for Sale",
        "description" : "..."
        "vehicle": {
            "id" : "f5574e12-01dc-4639-abeb-722e8e53e64f",
            "make" : "Volvo",
            "model": "S60",
            "year": 2018,
            "engine": {
                "displacement" : "2.0",
                "power" : 150,
                "torque" : 320,
                "fuel": { "id": "11", "name": "Diesel" }
            },
            "primaryPicture" : "",
            "color": "black"
        },
        "startingPrice": {
            "value" : 25000,
            "currency" : {
                "id" : "32",
                "name" : "USD",
                "symbol" : "$"
            } 
        },
        "created": "2019-03-01T10:00Z",        
        "countryCode": "USA"
        "user": {
            "id" : "efd68a2f-7309-41c0-af52-696ebe820199",
            "firstName": "John",
            "lastName": "Smith"
        }
    }
    
  3. If this was a relational model, this data would have been enough for identifying an auction. Nevertheless, it would decrease the number of round trips to load additional data if we embedded the highest bid (or even the most recent or highest bids) within the same structure:

        "highestBids":[
            {
                "id" : "5d669390-2ba4-467a-b7f9-26fea2d6a129",
                "offer" : {
                    "value" : 26000,
                },
                "user": {
                    "id" : "f50e4bd2-6beb-4345-9c30-18b3436e7766",
                    "firstName": "Jack",
                    "lastName": "Lemon",
                },
                "created" : "2019-03-12T11:00Z"
            }
        ],
    

    重要提示

    在这个场景中,我们还可以将完整的投标结构嵌入到拍卖模型中。 虽然这将减少数据在集合之间的冗余和分散,但出价并不像我们在汽车对象中看到的特性集那样是一个有限的集合,而且每个新的出价都需要对拍卖集合进行完整的文档替换。

我们可以说Auction表充当了清单的物化视图,而车辆和出价提供了对应用视图所需数据点的方便访问。 在这里,数据完整性的责任落在客户机应用而不是数据库本身。

参考资料

在前面的示例中,我们广泛地使用嵌入来创建优化的数据结构。 当然,这并不意味着我们没有使用任何参考。 所使用的大多数嵌入对象实际上都是引用描述。

为了可视化参考数据点,让我们标准化我们的拍卖数据:

{
    "id" : "7ad0d2d4-e19c-4715-921b-950387abbe50",
    "description" : "Volvo S60 for Sale",
"vehicleId": "f5574e12-01dc-4639-abeb-722e8e53e64f",
    "startingPrice": {
        "value" : 25000,
"currencyId" : "32"
    },
    "highestBids":[
"5d669390-2ba4-467a-b7f9-26fea2d6a129"
    ],
    "created": "2019-03-01T10:00Z",
    "countryCode": "USA",
"userId": "efd68a2f-7309-41c0-af52-696ebe820199"
}

关系在这里,我们有一个1-*``VehicleAuction,【T7 之间的关系1-*】货币和起始价值,1-*关系拍卖投标,和一个1-*关系用户和拍卖。 所有这些引用都嵌入到auction对象中,但是相互引用呢? 例如,如果我们正在实现一个用户配置视图,我们可能想要显示他们参与了多少投标,并可能从获胜的买家或卖家获得反馈值:

{
    "id": "efd68a2f-7309-41c0-af52-696ebe820199",
    "firstName": "John",
    "lastName": "Smith",
"numberOfAuctions" : 1,
 "auctions" : [
 { 
 "auctionId": "7ad0d2d4-e19c-4715-921b-950387abbe50",
 "role" : { "roledId" : "20", "roleName": "seller" },
 "auctionReview" : 1,
 "auctionState" : { "stateId" : "10", "stateName" : "Closed" }
 }
 ]
    ...
}

这些情形的类型完全取决于应用用例。 正如前面提到的,在 NoSQL 设置中,我们没有绑定到外键和约束,设计也不应该规定嵌入或引用。 Cosmos DB 提供了存储过程和触发器等特性,将数据完整性的责任分配给数据库。 此外,索引和分区策略可以提高应用的整体性能。

Cosmos DB in depth

作为一个平台,Cosmos DB 不仅仅是一个简单的数据库。 数据模型的设计以及数据访问层的实现在很大程度上取决于所使用的特性。 分区索引的设置有助于提高性能,同时也为查询策略提供了路线图。 数据触发,存储过程【显示】、改变饲料是扩展点,允许开发者实现综合语言事务 JavaScript 块,这可以极大地降低系统的整体复杂性也弥补写事务支持非正规数据妥协。

分区

Cosmos 使用了两种类型的分区——即物理分区和逻辑分区——来伸缩数据库中的单个容器(即集合)。 在创建容器时定义的分区键定义了逻辑分区。 然后将这些逻辑分区分成若干组,每个组包含一组副本,以便能够水平扩展数据库。

在这个方案中,分区键的选择成为决定查询性能的重要决策。 通过正确选择分区键,数据将被统一分片(即数据分片),这样分布式系统就不会显示所谓的热分区(即某些分区上的请求峰值,而其他分区是空闲的)。 热分区最终会导致性能下降。

UsersCollection中,我们使用/address/countryCode作为分区键。 这意味着我们期望一组用户在各个国家中呈正态分布。 然而,在现实生活中,来自某个市场的用户数量实际上取决于该市场的规模。 通俗地说,如果我们考虑到人口数量和需求,土耳其或德国的用户数量不可能与波斯尼亚-黑塞哥维那相同。

重要提示

在 Cosmos DB 中创建容器之后,就不支持更改集合的其他属性,比如 ID 或分区键。 只能更新索引策略。

分区键不一定需要是对数据的语义分析。 例如,在UsersCollection场景中,根据定义的分区键很容易的名字的第一个字母,他们签约,以及合成分区键,如生成的值从一个范围(例如 1 - 100)指定在创建的时候。 尽管如此,由于容器中某项的 ID 在该容器中是唯一的,所以容器和 ID 的组合定义了该项的索引。 为了实现更高的吞吐量,查询应该在特定的容器中执行。 换句话说,如果分区键可以在客户端查询之前计算,应用将比执行跨分区查询执行得更好:

FeedIterator<T>resultSet = _cosmosContainer
        .GetItemLinqQueryable<T>(
requestOptions: new QueryRequestOptions
                    {
                        MaxItemCount = -1,
 PartitionKey = new PartitionKey("USA")
                    })
                .Where(predicate)
                .ToFeedIterator();

例如,让我们以为例,看看这个集合的以下执行:

var cosmosCollection = new CosmosCollection<User>("UsersCollection");
await cosmosCollection.GetItemsAsync(
    (item) =>item.FirstName.StartsWith("J"));
// Calling with the partition key
await cosmosCollection.GetItemsAsync(
    (item) =>item.FirstName.StartsWith("J"), "USA");

当使用前面的表达式时,这个查询的结果(对于每个分区只有两个条目的集合)如下:

Executing Query without PartitionKey
Query: {"query":"SELECT VALUE root FROM root WHERE STARTSWITH(root[\"firstName\"], \"J\") "}
Request Charge : 2.96 RUs 
Partition Execution Duration: 218.08ms
Scheduling Response Time: 26.67ms
Scheduling Run Time: 217.45ms
Scheduling Turnaround Time: 244.65ms
Executing Query with PartitionKey
Query: {"query":"SELECT VALUE root FROM root WHERE STARTSWITH(root[\"firstName\"], \"J\") "}
Request Charge : 3.13 RUs 
Partition Execution Duration: 136.37ms
Scheduling Response Time: 0.03ms
Scheduling Run Time: 136.37ms
Scheduling Turnaround Time: 136.41ms

即使使用最小的数据集,执行结果也显示出在执行所需的总时间上有相当大的改进。

重要提示

为了检索 Cosmos 查询的指标,可以使用FeedResponse<T>.Diagnostics属性。 与以前的 SDK 不同,诊断数据收集在默认情况下是启用的。

以类似的方式,我们可以扩展汽车和拍卖的数据模型,我们可以用汽车的型号或颜色创建集合,这样我们就有了均匀分布的分区。

索引

Azure Cosmos DB,根据默认,假设项目中的每个属性都应该被索引。 当一个复杂对象被推入集合时,该对象被视为具有组成节点和值以及叶节点的属性的树。 这样,树的每个分支上的每个属性都是可查询的。 每个后续对象要么使用相同的索引树,要么使用附加属性展开它。

这种索引行为可以在任何时候对任何集合进行更改。 这可以帮助提高数据集的成本和性能。 索引定义使用通配符值来定义应该包含哪些路径和/或排除哪些路径。

例如,我们来看看我们的AuctionsCollection的索引策略:

<image/Figure_8.08_B16381.jpg>

图 8.8 -索引策略

除了被排除的_etag字段外,/*声明包含了完整的对象树。 这些索引可以使用更专门化的索引类型和路径进行优化。

例如,我们排除所有路径,并引入我们自己的索引:

"includedPaths": [
     {
         "path": "/description/?",
         "indexes": [
             {
                 "kind": "Hash",
                 "dataType": "String",
                 "precision": -1
             }
         ]
     },
     {
         "path": "/vehicle/*",
         "indexes": [
             {
                 "kind": "Hash",
                 "dataType": "String",
                 "precision": -1
             },
             {
                 "kind": "Range",
                 "dataType": "Number",
                 "precision": -1
             }
         ]
     }
 ],
 "excludedPaths": [
     {
         "path": "/*"
     }
 ]

这里,我们添加了两个索引:一个哈希索引用于描述字段的标量值(即/?),一个范围和/或哈希索引用于车辆路径及其下的所有节点(即/*)。 哈希索引类型是用于相等查询的索引,而范围索引类型用于比较或排序。

通过使用正确的索引路径和类型,可以降低查询成本,并避免扫描查询。 如果索引模式被设置为None而不是Consistent,那么数据库将在给定的集合上返回一个错误。 查询仍然可以使用EnableScanInQuery标志执行。

可编程性

Cosmos 最有用的特性之一是它的服务器端可编程性,它允许开发人员创建存储过程、函数和数据库触发器。 对于在 SQL 数据库上创建应用的开发人员来说,这些概念并不陌生,但是在 NoSQL 数据库上创建存储过程的能力,以及在 JavaScript 等客户端脚本语言上创建存储过程的能力,是前所未有的。

作为一个简单的例子,让我们实现一个触发器来计算用户配置文件的聚合值:

  1. As you may remember, we added the following reference values to UserProfile for the cross-collection partition:

     public class User
     {
         [JsonProperty("id")]
         public string Id { get; set; }
         [JsonProperty("firstName")]
         public string FirstName { get; set; }
         //...
     [JsonProperty("numberOfAuctions")]
     public int NumberOfAuctions { get; set; }
     [JsonProperty("auctions")]
     public List<BasicAuction> Auctions { get; set; }
        //...
     } 
    

    现在,让我们创建一个聚合更新函数,它将在用户配置文件上有更新时更新拍卖数量。 我们将使用这个函数拦截对集合的更新请求(即一个预执行触发器)并修改对象的内容。

  2. 函数应该首先从执行上下文中检索当前集合和文档:

    function updateAggregates(){
        // HTTP error codes sent to our callback function by server.
        var ErrorCode = {
            RETRY_WITH: 449,
        }
        var collection = getContext().getCollection();
        var collectionLink = collection.getSelfLink();
        // Get the document from request (the script runs as trigger, 
        // thus the input comes in request).
        var document = getContext().getRequest().getBody();
    
  3. 现在,让我们的函数计算更新正在推动的拍卖:

    if(document.auctions != null) {
        document.numberOfAuctions = document.auctions.length;
    }
    getContext().getRequest().setBody(document);
    
  4. We can now add this trigger to the UsersCollection as a Pre trigger on Replace calls:

    <image/Figure_8.09_B16381.png>

    图 8.9 - Cosmos DB 触发器

  5. However, the trigger function will still not execute until we explicitly add the trigger to the client request:

    var requestOption = new ItemRequestOptions();
    requestOption.PreTriggers= new []{ "updateAggregates"};
    await _client.ReplaceDocumentAsync(item, id, requestOption); 
    

    太棒了! 每次更新用户的个人资料时,都会计算用户参与拍卖的次数。 然而,为了插入一个新的拍卖(例如,当用户实际创建一个拍卖或投标时),我们需要更新整个用户配置文件(也就是说,部分更新目前在 SQL API 上不支持)。

  6. 让我们创建一个存储过程,该存储过程将在特定的用户配置文件中插入拍卖项目,以推送部分更新:

    function insertAuction(id, auction) {
        var collection = getContext().getCollection();
        var collectionLink = collection.getSelfLink();
        var response = getContext().getResponse();
    
  7. 接下来,检索需要将拍卖插入的用户配置文件对象:

    var documentFilter = 'Select * FROM r where r.id = \'' + id + '\'';
    var isAccepted = collection.queryDocuments(
    collectionLink, 
    documentFilter, 
        function (err, docs, options) {
            if (err) throw err;
            var userProfile = docs[0];
            // TODO: Insert Auction
         });
    
  8. 现在,我们可以用新的拍卖

    userProfile.auctions[userProfile.auctions.length] = auction;
    collection.replaceDocument(userProfile._self, userProfile, function (err) {
        if (err) throw err;
    });
    

    来更新文档:

  9. 最后,我们将为UserProfileRepository:

    public async Task InsertAuction(string userId, Auction auction) 
    {
        try 
        {
          _ = await _cosmosContainer.Scripts
                       .ExecuteStoredProcedureAsync<T>(
                           "insertAuction", 
                           new PartitionKey("USA"), 
                           (dynamic)userId,
                           (dynamic)auction);
        }
        catch (DocumentClientException e)
        {
            throw;
        }
    } 
    

    创建一个附加函数。

现在,将拍卖插入到用户配置文件中,并且在调用存储过程时更新聚合列。

触发器、函数和存储的过程都被限制在创建它们的集合中。 换句话说,一个集合触发器不能对另一个集合执行任何更改。 为了执行这样的更新,我们需要使用一个外部进程,比如调用者应用本身,或者一个由 Cosmos DB 上的更改提要触发的 Azure 函数。

change feed

Azure Cosmos DB 持续地监视集合中的更改,并且这些更改可以通过更改提要推送到各种服务。 通过更改 feed 推送的事件可以被 Azure Functions 和 App Services,以及流处理和数据管理流程使用。

插入、更新和软删除操作可以通过更改提要进行监视,每个更改在更改提要中只出现一次。 如果对某项进行多个更新,则更改提要中只包含最新的更改,从而使其成为健壮且易于管理的事件处理管道。

总结

Cosmos DB 为 NoSQL 数据库概念提供了新的视角,为各种场景提供了广泛的服务。 此外,使用 Cosmos DB 访问模型,与关系数据模型相比,使用者应用对参考数据完整性负有更多责任。 数据容器之间的薄弱链接可以作为微服务体系结构的优势。

在本章中,您使用 SQL API 作为访问模型创建了一个全新的 Cosmos DB 资源。 然而,我们也讨论了其他访问模型,我们也在 Mongo 上执行了示例查询。 创建 Cosmos DB 资源之后,您就创建了示例文档集合,对数据建模,并实现了简单的存储库类来访问这些数据。 现在,您了解了 Cosmos DB 的基本概念,并准备将其用作持久性存储。

在下一章中,我们将为应用套件创建服务层。*

九、创建 Azure 应用服务

Azure 应用服务是一种平台即服务(PaaS),面向移动和应用开发人员,可以托管多种不同的应用模型和服务。 虽然开发人员可以在几分钟内创建一个简单的移动应用服务来充当数据存储访问层,而无需编写一行代码,但复杂而健壮的。net Core 应用也可以通过与其他 Azure 服务的内在集成来实现。

在本章中,我们将学习 Azure App Service 的基础知识,并使用 ASP 为我们的应用创建一个简单的面向数据的后端.NET 5,认证由Azure Active Directory(Azure AD)提供。 我们也将提高我们的 web API 端点 Redis 缓存的性能。 以下部分将指导您创建我们的服务后端:

  • 选择正确的应用模式
  • 创建我们的第一个微服务
  • 集成 Redis 缓存
  • 托管服务
  • 确保应用

到本章结束时,您将完全有能力从头开始设计和创建 web 服务,并将它们集成到云基础设施中,同时还具有额外的性能和身份验证方面的非功能性需求。

技术要求

你可以通过以下 GitHub 链接找到本章使用的代码:https://github.com/PacktPublishing/Mobile-Development-with-.NET-Second-Edition/tree/master/chapter09

选择正确的应用模式

Azure 堆栈提供了多个主机 web 应用的方法,不同的从简单的基础设施即服务(IaaS)产品,如虚拟机**(【显示】vm)完全托管 PaaS 托管服务等应用服务。 因为。net Core 和 ASP 的平台无关性.NET Core、甚至 Linux 容器和容器编排服务(如 Kubernetes)都是可用的选项。******

**Azure 计算服务可以根据职责的划分分为三个主要类别,即 IaaS、PaaS 和容器即服务(CaaS),如下图所示:

【t】【t】

<image/Figure_9.01_B16381.jpg>

图 9.1 -托管主机模型

除了 IaaS 和 PaaS 之外,还有各种托管选项,每种都有自己的优点和用例。 现在我们将仔细研究这些产品。

下面几节将带领您了解 Azure 生态系统中的不同应用托管模型。

Azure 虚拟机

虚拟机(虚拟机)是微软 Azure Cloud 上最古老的 IaaS 产品之一。 简单地说,这个产品提供了一个在云上具有完全控制的托管服务器。 Azure 自动化服务为您提供很需要的工具来管理这些服务器与基础设施代码(【显示】IaC)原则使用 PowerShell**期望状态配置【病人】(**DSC)和计划运行手册。 您可以根据您的应用需求轻松地伸缩和监视它们。

有了 Windows 和 Linux 变体,Azure vm 可以成为现有应用的简单、浅层云迁移路径。

VM 的扩展是通过在 VM 级别上调整系统配置(例如,CPU、虚拟磁盘、RAM 等)或在基础架构中引入额外的 VM 来实现的。

虚拟机托管发生在托管模型频谱的最左边; 换句话说,它提供了一个几乎完全自管理的主机模型。 在管理区域,我们有集装箱服务。

Azure 中的容器

如果虚拟机是硬件的虚拟化,则容器将操作系统虚拟化。 容器为共享相同操作系统内核的应用创建隔离的沙箱。 这样,由应用及其依赖项组成的应用容器可以轻松地托管在任何满足容器需求的环境中。 容器映像可以作为多个应用容器执行,这些容器实例可以使用 Docker Swarm、Kubernetes 和 Service Fabric 等编制工具进行编排。

Azure 目前有两个托管容器编排产品:Azure Kubernetes Service(AKS)和 Service Fabric Mesh。

Azure 集装箱服务与 Kubernetes

Azure 容器服务(ACS)是一个托管容器调度环境,在这里开发人员可以轻松地部署容器,并享受自动恢复和缩放体验。

Kubernetes 是一个开放源码的容器编排系统,最初是由谷歌设计和开发的。 Azure Kubernetes 是该服务的托管实现,其中大部分配置和管理职责都委托给了平台本身。 在这种设置中,作为这个 PaaS 产品的消费者,您只负责管理和维护代理节点。

AKS 支持 Linux 容器和 Windows 操作系统虚拟容器。 Windows 容器支持目前处于私有预览状态。

服务结构网格

Azure Service FabricMesh 是一个完全受管理的容器编排平台,在这个平台中,使用者无需与底层集群的配置或维护进行任何直接交互。 所谓的多语言服务(即任何语言和任何操作系统)是在容器中运行的。 在这种设置中,开发人员只负责指定应用所需的资源,例如容器的数量及其大小、网络需求和自动伸缩规则。

一旦部署了应用容器,Service Fabric Mesh 将容器托管在一个由数千台机器组成的集群中,而集群操作对开发人员是完全隐藏的。 通过软件定义网络(SDN)的智能消息路由实现了微服务之间的业务发现和路由。

尽管服务结构网格共享相同的底层平台,但它与 Azure 服务结构有很大的不同。 虽然 Service Fabric Mesh 是一个托管解决方案,但 Azure Service Fabric 是一个完整的微服务平台,允许开发人员创建容器化或本地云应用。

Azure 服务结构的微服务

Azure Service Fabric 是一个托管和开发平台,允许开发人员创建由微服务组成的企业级应用。 Service Fabric 提供了全面的运行时和生命周期管理功能,以及使用可变状态容器(如可靠的字典和队列)的持久编程模型。

服务 Fabric 应用可以由三种不同的托管服务模型组成:容器、服务/参与者和来宾可执行文件。

与它的托管版本(即 Service Fabric Mesh)和 Azure Kubernetes 类似,Service Fabric 能够运行针对 Linux 和 Windows 容器的容器化应用。 可以很容易地将容器包含在一个 Service Fabric 应用中,该应用与其他组件捆绑在一起,并通过高密度共享计算池(称为集群)和预定义的节点进行伸缩。

可靠的服务和可靠的参与者是真正的云本地服务,它们利用了服务结构编程模型。 在本地开发集群和。net Core 以及 Java 可用的 sdk 上进行开发的方便性,允许开发人员以平台无关的方式创建有状态和无状态的微服务。

重要提示

开发环境可用于 Windows、Linux 和 Mac OS X, Linux 和 OS X 开发设置依赖于在容器中运行 Service Fabric 本身,. net Core 是创建针对这些平台的应用的首选语言。 可用的 Visual Studio Code 扩展使得在每个操作系统上开发。NET Core 服务结构应用变得很容易。

最后,客户可执行文件可以是在各种语言/框架(如 Node.js、Java 或 c++)上开发的应用。 客户可执行文件作为无状态服务进行管理和处理,可以与其他服务 Fabric 服务并排放置在集群节点上。

Azure 基于容器的资源可以为开发高度复杂的微服务生态系统提供灵活性; 但是,您为容器和容器托管选择的任何模型都需要配置和基础设施管理,并且通常需要一个陡峭的学习曲线。 如果你正在寻找一个在配置和基础设施管理方面负担更轻的替代方案,我们可以继续我们的托管模型,选择 Azure App Service 作为我们的托管模型。

Azure 应用服务

Azure 应用服务是一个完全管理的 web 应用托管服务。 App Service 可以用于托管应用,而不考虑开发平台或操作系统。 App Service 为 ASP 提供了一流的支持.NET (Core)、Java、Ruby、Node.js、PHP 和 Python。 它可以与 Azure DevOps、GitHub 和 BitBucket 等 DevOps 平台进行即时集成和部署。 通常托管环境的大多数管理功能都集成到 App Service 刀片上的 Azure 门户中,例如伸缩、CORS、应用设置、SSL 等等。 此外,WebJobs 可以用来创建后台进程,这些进程可以作为应用包的一部分定期执行。

容器 Web 应用是 Azure 应用服务的另一个特性,它允许将容器化应用部署为应用服务,并与 Kubernetes 进行协调。

移动应用服务(Mobile App Service)是一种为简单的移动应用集成所有必要功能的简单方法,如身份验证、推送通知和离线同步。 然而,移动应用服务目前正在过渡到应用中心,不支持 ASP。 净的核心。

最后,Azure Functions 提供了一个平台来创建代码片段或无状态按需函数,并托管它们,而无需显式地提供或管理基础设施。

在本节中,我们了解了不同的托管模型及其优缺点。我们从 Azure vm 开始研究,对容器做了简短的介绍,最后讨论了 Azure 应用服务和功能的 PaaS 模型。 现在我们已经讨论了关于选择模型的基础知识,接下来我们将看看如何创建我们的第一个微服务。

打造首个微服务

对于我们的移动应用,在第八章用 Cosmos DB 创建一个数据存储中,我们创建了一个简单的数据访问代理,从 Cosmos DB 中检索数据。 在这个练习中,我们将创建小型 web API 组件,这些组件将在集合上公开用于 CRUD 操作的各种方法。

初始设置

让我们开始我们的实现:

  1. First, create an ASP.NET Core project:

    <image/Figure_9.02_B16381.jpg>

    图 9.2 -创建 ASP。 网项目

  2. 创建项目后,进行快速测试,检查dotnet核心组件是否正确设置。

  3. 打开一个控制台窗口并导航到项目文件夹。 以下命令将恢复引用的包并编译应用:

  4. Once the application is built, we can use the run command and execute a GET call to the api/values endpoint:

    <image/Figure_9.03_B16381.jpg>

    图 9.3 -第一次运行 ASP.NET Core 应用

    这将导致WeatherForecastController控制器的GET方法的值输出。

    重要提示

    在前面的示例中,我们使用curl来执行一个快速 HTTP 请求。 客户端 URL(curl)是一个实用程序,可在基于 unix 的系统、macOS 和 Windows 10 上使用。

  5. Next, we will set up the Swagger endpoint so that we have a metadata endpoint, as well as a UI to execute test requests. For this purpose, we will be using the Swashbuckle NuGet packages to generate the API endpoint metadata. A basic setup of Swashbuckle requires three packages, and we can reference them together by adding the Swashbuckle.AspNetCore meta-package:

    <image/Figure_9.04_B16381.jpg>

    图 9.4 - Swashbuckle NuGet

  6. 在添加了元包和依赖项之后,修改Startup类来声明服务:

  7. 现在,运行应用并导航到{dev host}/swagger端点。 我们将看到生成的 Swagger UI 和方法声明:

<image/Figure_9.05_B16381.jpg>

图 9.5 - Swagger UI

现在我们已经准备好了样板 API 项目,这意味着我们可以继续实现我们的服务了。

实现检索操作

考虑到拍卖数据集,由于我们已经创建了 MVC 应用,我们应该在控制器中包含两个GET方法:一个用于检索拍卖的完整集合,另一个用于检索特定的拍卖。

让我们看看如何做到这一点:

  1. 在开始实现之前,让我们将WeatherForecastController重命名为AuctionsController

  2. We can simply initialize our repository and return the results for the first GET method:

    [HttpGet]
    public async Task<IEnumerable<Auction>> Get()
    {
        var result = Enumerable.Empty<Auction>();
        try
        {
            result = await _cosmosCollection.GetItemsAsync(item => 
            true);
        }
        catch (Exception ex)
        {
            // Log the error or throw depending on the requirements
        }
        return result;
    }
    

    注意,这里使用的谓词针对的是完整的拍卖集。 我们可以使用查询参数来扩展这个实现,这些查询参数使用附加的谓词来过滤集合。

  3. The GET method for retrieving a specific item would not be much different:

    [HttpGet("{id}")]
    public async Task<User> Get(string id)
    {
        User result = null;
        try
        {
            result = await _cosmosCollection.GetItemAsync(id);
        }
        catch (Exception ex)
        {
            // Log or throw error depending on the requirements
        }
    
        return result;
    }
    

    这样就可以满足需求,但是为了改进移动应用和 Cosmos 集合之间的交互,我们还可以启用OData查询,并创建一个到数据存储的透明查询管道。 为此,我们可以使用可用的。net Core 包实现 OData 控制器,或者在当前 MVC 控制器上启用查询。

    重要提示

    需要注意的是,我们用来生成 Swagger UI 的 Swashbuckle 包目前不支持 OData 控制器,因此这些 api 在 Swagger 接口上不可用。

  4. 对于 OData 的实现,我们首先在启动类中设置基础设施:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOData();
        services.AddODataQueryFilter();
        // ... Removed
    }
    
  5. 现在,为 MVC 控制器设置路由:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        app.UseMvc(builder =>
            {
                builder.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
                builder.EnableDependencyInjection();
            });
        // ... Removed
    }
    
  6. 在 Cosmos 存储库客户端上创建一个新的方法,该方法将返回一个可查询集而不是一个结果集:

    public IQueryable<T> GetItemsAsync()
    {
        var feedOptions = new FeedOptions { 
            MaxItemCount = -1, PopulateQueryMetrics = true, EnableCrossPartitionQuery = true };
        IOrderedQueryable<T> query = _client.CreateDocumentQuery<T>(
            UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId), feedOptions);
        return query;
    }
    
  7. Finally, we need to implement our query action:

    [HttpGet]
    [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
    public ActionResult<IQueryable<Auction>> Get(ODataQueryOptions<Auction> queryOptions)
    {
        var items = _cosmosCollection.GetItemsAsync();
       return Ok(queryOptions.ApplyTo(items.AsQueryable()));
    }
    

    现在,可以在集合上执行简单的 OData 查询(例如,第一层 OData 过滤器查询)。 例如,如果我们在Users端点上执行一个查询,一个简单的过滤器查询以检索一个或多个拍卖的用户将如下所示:

    http://localhost:20337/api/users?$filter=NumberOfAuctions ge 1
    

为了能够扩大查询选项,执行查询相关的实体,我们需要创建一个实体数据模型**(EDM)和注册各自 OData 控制器。**

**重要提示

支持高级搜索的另一个更合适的选项是创建一个 Azure 搜索索引,并在该索引之上公开 Azure 搜索功能。

对于内存中的 EDM 和数据上下文,我们将使用Microsoft.EntityFrameworkCore的特性和功能。 让我们从实现开始:

  1. 创建一个DbContext,它将定义我们的主数据模型和实体之间的关系:

    public class AuctionsStoreContext : DbContext
    {
        public AuctionsStoreContext(DbContextOptions
    <AuctionsStoreContext> options)
            : base(options)
        {
        }
    
       public DbSet<Auction> Auctions { get; set; }
       public DbSet<User> Users { get; set; }
       protected override void OnModelCreating(ModelBuilder
       modelBuilder)
       {
           modelBuilder.Entity<User>().OwnsOne(c => c.Address);
           modelBuilder.Entity<User>().HasMany<Auction>(c =>
           c.Auctions);
       }
    }
    
  2. 现在,让我们用在ConfigureServices方法中注册这个上下文:

    public void ConfigureServices(IServiceCollection services)
    {
        servics.AddDbContext<AuctionsStoreContext>(option => 
            option.UseInMemoryData("AuctionsContext"));
        services.AddOData();
        services.AddODataQueryFilter();
        // ... Removed
    }
    
  3. 现在,创建一个返回 EDM 的方法:

    private static IEdmModel GetEdmModel()
    {
        ODataConventionModelBuilder builder = new
        ODataConventionModelBuilder();
        var auctionsSet = builder.EntitySet<Auction>("Auctions");
        var usersSet = builder.EntitySet<User>("Users");
        builder.ComplexType<Vehicle>();
        builder.ComplexType<Engine>();
        return builder.GetEdmModel();
    }
    
  4. 最后注册一条 OData 路由,实体控制器可以通过这条路由服务:

    app.UseMvc(builder =>
        {
            builder.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
            builder.EnableDependencyInjection();
            builder.MapODataServiceRoute("odata", "odata", GetEdmModel());
        });
    
  5. Now that the infrastructure is ready, you can navigate to the $metadata endpoint to take a look at the EDM that was generated:

    <image/Figure_9.06_B16381.jpg>

    Figure 9.6 – OData EDM

  6. 现在,通过实现快速ODataController,我们可以向 OData 查询公开整个集合:

    public class AuctionsController : ODataController
    {
        private readonly CosmosCollection<Auction> _cosmosCollection;
        public AuctionsController()
        {
            _cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection");
        }
        // GET: api/Users
        [EnableQuery]
        public IActionResult Get()
        {
            var items = _cosmosCollection.GetItemsAsync();
            return Ok(items.AsQueryable());
        }
        [EnableQuery]
        public async Task<IActionResult> Get(string key)
        {
            var auction = await _cosmosCollection.GetItemsAsync(item => item.Id == key);
            return Ok(auction.FirstOrDefault());
        }
    }
    

有了给定的ODataController和用户集合的附加实现,就可以执行各种实体过滤器表达式,如以下所示:

  • http://localhost:20337/odata/auctions?$filter=Vehicle/Engine/Power%20gt%20120
  • http://localhost:20337/odata/users?$filter=Address/City%20eq%20'London'
  • http://localhost:20337/odata/users?$filter=startswith(FirstName,%20'J')
  • http://localhost:20337/odata/auctions('7ad0d2d4-e19c-4715-921b-950387abbe50')

至此,我们已经成功地使用 ASP 为 Cosmos Document DB 公开了一个 OData 端点.NET Core 和实体框架核心。 现在我们已经有了检索方法,我们需要实现处理数据创建和操作的方法。

实现更新方法

当我们处理不支持部分更新的 NoSQL 数据存储时,至少可以说,实现更新端点实际上是不同的。 根据应用需求,我们可以选择两个不同的模式。

在经典的并发模型中,我们将接收一个带有完整对象主体的PUT请求,并检查更新是否正在对象的最新版本上执行。 可以在_ts属性集合项上执行此并发检查。 时间戳属性也被 Cosmos DB 容器本身用于处理并发性问题。 让我们开始吧。

在这个模型中,传入的对象体将被验证,以检查它是否携带最新的时间戳,如果不携带,表示冲突的409响应将作为响应发送回。 如果时间戳与存储库中的时间戳相匹配,那么我们可以自由地修改实体:

[EnableQuery]
[HttpPut]
public async Task<IActionResult> Put([FromODataUri]string key, [FromBody] Auction auctionUpdate)
{
    var cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection");
    var auction = (await cosmosCollection.GetItemsAsync(item => item.Id == key)).FirstOrDefault();

    if (auction == null)
    {
        return NotFound();
    }
    if (auction.TimeStamp != auctionUpdate.TimeStamp)
    {
        return Conflict();
    }
    await cosmosCollection.UpdateItemAsync(key, auctionUpdate);

    return Accepted(auction);
}

然而,使用这种方法,随着对象树的大小增长,以及容器项的复杂性增加,更新的请求将变得更大,更难以执行(例如,更有可能发生冲突、无意识地删除属性等等)。 下面的截图显示了一个只更新拍卖文档的描述字段的请求:

<image/Figure_9.07_B16381.jpg>

图 9.7 - ASP。 网络更新请求/响应

为了减少更新调用的影响,至少对于客户端,我们可以使用PATCH方法。 在PATCH方法中,只有对象树的一部分作为增量传递,或者只有部分更新操作作为补丁操作传递。

让我们为相同的拍卖服务实现一个PATCH操作,并检查请求:

[EnableQuery]
[HttpPatch]
public async Task<IActionResult> Patch(
    string key, 
    [FromBody] JsonPatchDocument<Auction> auctionPatch)
{
    var cosmosCollection = new CosmosCollection<Auction>("AuctionsCollection");
    var auction = (await cosmosCollection.GetItemsAsync(item => item.Id == key)).FirstOrDefault();
    if (auction == null)
    {
        return NotFound();
    }
    auctionPatch.ApplyTo(auction);
    await cosmosCollection.UpdateItemAsync(key, auction);
    return Accepted(auction);
}

对给定端点的请求如下所示:

PATCH /odata/auctions('3634031a-1f45-4aa0-9385-5e2c86795c49')
[
    {"op" : "replace", "path" : "description", "value" : "Updated Description"}
]

我们用时间戳值实现的乐观并发控制机制也可以用Test操作实现:

PATCH /odata/auctions('3634031a-1f45-4aa0-9385-5e2c86795c49')
[ 
    { "op": "test", "path": "_ts", "value": 1552741629 }, 
    { "op" : "replace", "path" : "vehicle/year", "value" : 2017}, 
    { "op" : "replace", "path" : "vehicle/engine/displacement", "value" : "2.4"} 
]

在本例中,如果时间戳值与请求中的值不匹配,后续的更新操作将不会执行。

我们已经完成了UPDATEPATCH方法,现在让我们继续DELETE动作。

软删除

如果您计划将存储级操作与触发器和/或更改提要集成在一起,那么可以使用软删除实现而不是实现对象的完全删除。 在软删除方法中,我们可以使用一个特定的属性(例如isDeleted)来扩展实体模型,该属性将定义文档被消费应用删除。

在此设置中,消费应用可以使用已实现的PATCH方法或显式的DELETE方法,可以为我们的实体服务实现该方法。

让我们来看看下面的PATCH请求:

PATCH /odata/auctions('3634031a-1f45-4aa0-9385-5e2c86795c49')
[ 
    { "op": "test", "path": "_ts", "value": 1552741629 }, 
    { "op" : "replace", "path" : "isDeleted", "value" : true}, 
    { "op" : "replace", "path" : "ttl", "value" : "30"} 
]

通过此请求,我们表示应该将具有给定 ID 的拍卖实体标记为删除。 此外,通过设置Time To Live(TTL)属性,我们将触发给定实体的过期时间。 通过这种方式,触发器和更改提要都将收到关于此更新的通知,并且在给定的 TTL 内,实体将从数据存储中删除。

重要提示

TTL 是 Cosmos DB 的固有特性。 TTL 可以在容器级别设置,也可以在项目级别设置。 如果在容器级别没有设置值,那么平台将忽略项目的值集。 但是,容器的默认过期时间可以为-1,并且我们希望在某段时间后过期的项目可以声明一个大于 0 的值。 TTL 不消耗资源,不作为消耗的 RUs 的一部分计算。

通过删除实现,我们拥有一组完整的函数,可以根据 Cosmos 集合为微服务创建基本的 CRUD 结构。 我们用一个简单的。net core 风格的请求管道开始实现,我们创建了UPDATEPATCH方法,最后,我们用软删除实现完成了实现。 现在我们可以开始处理 API 的润饰了。 我们将从使用 Redis 的性能改进开始。

集成 Redis 缓存

在具有细粒度微服务架构的分布式云应用中,分布式缓存可以提供非常需要的数据一致性以及性能改进。 一般来说,基础设施的分布、数据模型和成本是决定是否使用分布式缓存实现的因素。

ASP.NET Core 提供了各种缓存选项,其中之一是分布式缓存。 可选择的分布式缓存选项如下:

  1. 分布式内存缓存
  2. 分布式 SQL 服务器缓存
  3. 分布式缓存复述,

虽然内存缓存并不是一个生产准备策略,但 SQL 和 Redis 对于使用。net Core 开发的云应用来说是可行的选择。 然而,对于 NoSQL 数据库和半结构化数据,Redis 将是一个理想的选择。 让我们来看看如何引入分布式缓存并使其准备好使用:

  1. In order to introduce a distributed cache that can be used across controllers, we would need to use the available extensions so that we can inject an appropriate implementation of the IDistributedCache interface. IDistributedCache would be our main tool for implementing the cache, aside from the pattern we mentioned previously:

    public interface IDistributedCache 
    { 
        byte[] Get(string key);
        Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken)); 
        void Set(string key, byte[] value, DistributedCacheEntryOptions options); 
        Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken)); 
        void Refresh(string key); 
        Task RefreshAsync(string key, CancellationToken token = default(CancellationToken)); 
        void Remove(string key); 
        Task RemoveAsync(string key, CancellationToken token = default(CancellationToken)); 
    }
    

    如您所见,使用注入的实例,我们将能够以字节数组的形式设置和获取数据结构。 应用将首先访问分布式缓存以检索数据。 如果我们需要的数据不存在,我们将从实际的数据存储(或服务)中检索它,并将结果存储在我们的缓存中。

  2. 在我们可以在我们的应用中实现 Redis 缓存之前,我们可以前往 Azure 门户,在我们一直使用的资源组中创建一个 Microsoft Azure Redis 缓存资源。

  3. 一旦 Redis 缓存实例被创建,记录一个可用的连接字符串。 连接字符串可以在管理键刀片下找到,可以使用概述屏幕上的显示访问键选项访问该刀片。 现在我们可以继续我们的实现。

  4. Start the implementation by installing the required Redis extension:

    <image/Figure_9.08_B16381.jpg>

    图 9.8 - Redis 扩展

  5. 在安装了扩展及其依赖项之后,使用扩展方法

        services.AddDistributedRedisCache(
            option =>
                {
                    option.Configuration = Configuration.GetConnectionString("AzureRedisConnection");
                    option.InstanceName = "master";
                });
    

    配置我们的分布式缓存服务

  6. 现在,在appsettings.json文件中插入我们从 Azure 门户检索到的连接字符串,然后转到我们的控制器,为IDistributedCache实例设置构造函数注入:

    private readonly IDistributedCache _distributedCache;
    public UsersController(IDistributedCache distributedCacheInstance)
    {
        _distributedCache = distributedCacheInstance;
    }
    

就这些了——分布式缓存已经可以使用了。 现在,我们可以将序列化的数据项作为键/值(其中值是序列化的字节数组)插入到缓存中,而无需与实际数据源通信。

与前端模式的后端结合,Redis 缓存可以将缓存的数据交付到应用网关服务,而无需联系多个微服务,提供了一个简单的成本削减解决方案,以及承诺的性能增强。

托管服务

由于我们的 web 实现使用了 ASP.NET Core,我们有多种部署和托管选项,可以跨越 Windows 和 Linux 平台。 我们将考虑的第一个选项是 Azure Web App 设置,它可以设置在 Windows 或 Linux 服务计划中。 然后我们将移动我们的 ASP。 它可以托管在我们之前提到的任何容器编排平台上,也可以简单地托管在一个运行在 Linux 服务器群上的 Azure web 应用中。

Azure Web App for App Service

此时,不需要任何额外的实现或配置,我们的微服务就可以部署了,它们已经可以作为一个完全托管的应用服务托管在 Azure 云上了。 为了将服务部署为应用服务,您可以使用 Visual Studio Azure 扩展,它允许您创建发布配置文件以及目标托管环境。

让我们看看如何做到这一点:

  1. Right-click on the project to be deployed. You will see the Pick a publish target selection window:

    <image/Figure_9.09_B16381.jpg>

    图 9.9 -发布目标

    对于完整的托管选项,我们可以选择App ServiceApp Service Linux选项,并继续创建新的应用服务。

  2. If we were to choose the App Service option, the application would be hosted on the Windows platform with a full .NET Framework profile, whereas the Linux option would use Linux operating systems with the .NET Core runtime. Selecting the Create New option allows us to select/create the resource group we want the App Service instance to be added to:

    <image/Figure_9.10_B16381.jpg>

    图 9.10 - Azure App Service

  3. 一旦发布,从站点 URL 字段复制 URL 并使用curl:

    Microsoft Windows [Version 10.0.17134.590] (c) 2018 Microsoft Corporation. All rights reserved.
    C:\Users\can.bilgin>curl https://netcoreuserapi-dev.azurewebsites.net/odata/users?$filter=FirstName%20eq%20%27Jane%27
    {"@odata.context":"https://netcoreuserapi-dev.azurewebsites  .net/odata/$metadata#Users","value":[{"Id":"7aa0c870- cb90-4f02-bf7e-867914383190","FirstName":"Jane","LastName": "Doe","NumberOfAuctions":1,"Auctions":[],"Address": {"AddressTypeId":4000,"City":"Seattle","Street1":"23  Pike St.","CountryCode":"USA"}}]} 
    C:\Users\can.bilgin>
    

    执行查询

集装箱服务

托管的另一种选择是将我们的应用容器化,这将带来作为代码原则配置的额外好处。 在容器设置中,每个服务都将被隔离在自己的沙箱中,并以高度的灵活性和性能轻松地从一个环境迁移到下一个环境。 如果将容器部署到前面提到的容器注册中心和平台(如 ACS、AKS、Service Fabric 和 Service Fabric Mesh),那么与 web 应用相比,容器还可以节省成本。

重要提示

容器是隔离的、受管理的、可移植的操作环境。 它们提供了一个位置,在这个位置上,应用可以在不影响系统其他部分的情况下运行,系统也不会影响应用。 与 vm 相比,它们提供了更高的服务器密度,因为它们不共享硬件,而是共享操作系统内核。

Docker 是一种容器技术,近年来已经几乎成为容器本身的同义词。 Docker 容器托管环境可以使用提供的免费软件在 Windows 和 macOS 上创建。 让我们开始:

  1. 为了准备一台用于 Docker 容器化的 Windows 开发机器,我们需要确保已经安装了以下设备:

  2. Docker for Windows(用于承载 Windows 和 Linux 容器)

  3. 用于 Visual Studio 的 Docker 工具(可选择安装 Visual Studio 2017 v15.8+)

  4. Now that we have the prerequisites, we can add a Docker container image definition to each microservice project using the Add | Docker Support menu item:

    <image/Figure_9.11_B16381.jpg>

    图 9.11 - Docker 支持

  5. 这将创建一个多级 Docker 文件,简单来说,它将做以下工作:

  6. 将应用的源代码复制到容器映像中。

  7. 恢复所需的。net Core 运行时组件,这取决于容器的类型(Windows 或 Linux)。

  8. 编译应用。

  9. 使用编译后的应用组件创建最终的容器映像。

  10. 这里,我们为UsersApi创建的 Docker 文件如下所示:

如你所见,定义 base 的第一个阶段是引用公有 Docker 注册表中的 microsoft 管理的容器映像。 构建映像是我们拥有 ASP 源代码的地方.NET Core 应用。 最后,构建和最终映像是将应用编译(即dotnet publish)并将其设置为容器的入口点的最后阶段。

换句话说,创建的容器映像拥有最终的应用代码以及所需的组件,而不管主机操作系统和该主机上运行的其他容器是什么。

现在,如果你在控制台或终端(取决于安装 Docker 的操作系统)导航到UsersApi项目的父目录,并执行以下命令,Docker 将构建容器镜像:

docker build -f ./NetCore.Web.UsersApi/Dockerfile -t netcore-usersapi .

一旦 Docker 守护进程构建了容器镜像,你可以使用下面的命令来检查镜像是否可以作为容器启动:

docker image ls

如果映像在可用容器映像列表中,现在可以使用暴露的端口80443运行该容器(下面的命令将容器端口80映射到主机端口8000):

docker run -p 8000:80 netcore-usersapi

当然,容器开发与 Windows 平台上的 Visual Studio 的集成程度更高。 事实上,ASP.NET Core 应用,一旦容器化,包含一个运行/调试 Docker 配置文件,可以直接从 Visual Studio UI 启动:

<image/Figure_9.12_B16381.jpg>

图 9.12 - Docker 启动设置

在这个阶段,我们的容器配置已经可以使用了,并且它已经可以部署到 Azure Web App for Containers 中了。 可以使用 Visual Studio 中的可用工具添加容器编排支持。

在本节中,我们测试了 PaaS 和 CaaS 模型,首先使用应用服务来托管应用,然后使用 Docker 将. net 应用容器化。 ASP.NET 应用包和容器可以托管在 App Service 上。 现在我们的应用已经准备好在云上托管了,让我们看看保护它的可用选项。

保护应用

在具有客户端特定后端的微服务设置中,可以使用多种身份验证策略来保护 web 应用。 ASP.NET Core 提供了所需的 OWIN 中间件组件来支持大多数这些场景。

根据网关和下游业务架构,可以在网关上实现身份验证/授权,用户身份可以转移到后端服务:

<image/Figure_9.13_B16381.jpg>

图 9.13 -网关标识

另一种方法是,每个服务可以在联合设置中使用相同的身份提供者。 在此设置中,客户端应用将使用专用的安全令牌服务(STS),并且需要在 STS 和应用服务之间建立信任关系:

<image/Figure_9.14_B16381.jpg>

图 9.14 -微服务身份

在选择身份验证和授权策略时,一定要记住,此设置中的身份使用者将是本地移动客户端。 当涉及移动应用时,选择的认证流程一般为 OAuth 2 授权码流:

<image/Figure_9.15_B16381.jpg>

图 9.15 - OAuth 阶段

再一次,这取决于您正在构建的应用,多个OpenID 身份连接(OIDC)提供者,如微软的 Live, Facebook 和谷歌,可以引入允许用户选择他们喜欢的身份。

**## NET Core 身份

ASP.NET Core Identity 是默认的成员系统,它可以提供相对简单但广泛的 STS 实现,以及登录、注册和管理 ui。 与它的前身相比,ASP.NET Core Identity 为开发人员提供了更广泛的身份验证场景,如 OAuth、双因素身份验证、基于时间的一次性密码二维码(TOTP)等。

ASP.NET Core Identity 默认情况下使用 SQL 数据库作为持久性存储,并且可以用其他存储库实现替换。 实体框架核心用于实现标准存储库功能。

由 ASP 支持的外部 OIDC 提供商.NET Core 身份是 Facebook, Twitter,谷歌和微软。 可以在第三方或社区提供的包中找到其他提供者实现。

使用 ASP.NET Core Identity,创建的 STS 可以由 Xamarin 应用通过一组简单的 HTTP 请求使用,以注册、验证和授权用户。 此外,Xamarin 应用可以利用可用的身份提供者 sdk,以及跨提供者包。

虽然这种身份管理应该足够了,因为需求只针对一个 ASP.NET 基于核心的解决方案,一旦将额外的 Azure 资源包括在分布式应用中,例如 Azure 无服务器组件,基于云的身份管理可能是一个更好的选择。

Azure AD

Azure 广告基于云的身份作为服务(IDaaS)提供,迄今为止,唯一的认证和身份管理过程与资源管理器集成的分布式应用开发的 Azure 的基础设施。 Azure AD 用于管理对资源组中任何 SaaS/PaaS 资源的访问。 它支持 OpenID Connect、OAuth 和 SAML 等协议,以提供对目录内资源的 SSO 和访问控制。

**可以使用目录中定义的标识原则来设置访问资源和资源之间的授权。 该原则可以表示具有单个组织(可能具有关联的本地活动目录)、应用(在目录或外部应用中设置的资源,如本地移动应用)的用户,或者来自不同 Azure 目录或外部身份提供者的外部身份。

一般来说,这种在组织单元中定义用户身份,并将来自其他目录的用户作为来宾引入的设置称为 Azure ADBusiness to Business(B2B)。

在中,为了使用 Azure AD B2B 建立一个应用范围的认证方案,遵循以下步骤:

  1. Create a directory that will define the organization that will be using the application suite (that is, the mobile application and associated services).

    重要提示

    任何 Azure 订阅至少都附带免费的 Azure AD(取决于订阅类型),而且在大多数情况下,不需要创建新目录。

    您可以使用 Azure 门户上的create a Resource接口来创建一个新目录。 一旦您选择了 Azure AD,您将需要声明一个组织名称和初始域名(例如netcorecrossplatform.onmicrosoft.com)。

  2. Additional custom domains can be added to this declaration at a later time:

    <image/Figure_9.16_B16381.jpg>

    图 9.16 -创建 Azure Active Directory

  3. Once the directory is created, you’ll see that the organization should be available as a domain option so that you can set up the authentication for an ASP.NET Core web application using Visual Studio:

    <image/Figure_9.17_B16381.jpg>

    图 9.17 -添加认证

    一旦应用项目被创建或使用所选的认证选项更新,它将自动向 Azure AD 添加一个应用注册表,并为应用认证添加/配置所需的中间件。 Azure AD 在应用启动时的配置看起来类似如下:

    services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)        
        .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
    

    创建的配置(匹配 Azure AD 应用注册)是,如下:

    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "Domain": "netcorecrossplatform.onmicrosoft.com",
        "TenantId": "f381eb86-1781-4732-9543-9729eef9f843",
        "ClientId": "ababb076-abb9-4426-b7df-6b9d3922f797"
    },
    

    对于 Azure AD,在客户端应用上进行身份验证(考虑到我们正在使用 Xamarin 和 Xamarin。 窗体作为开发平台)可以使用Microsoft 身份验证库(MSAL)实现。 遵循以下步骤:

  4. In order for the client application to be able to use the identity federation within this organization, register (yet) another application on Azure AD. However, this registration should be declared for a native application:

    <image/Figure_9.18_B16381.jpg>

    图 9.18 -注册一个应用到 Azure AD

  5. 创建应用注册后,使用当前目录(即租户)和客户端应用(即应用注册)设置身份验证库。 在这个设置中,身份流可以简单地定义如下:

    • 本机应用使用授权代码流检索访问令牌

    • 本机应用执行对网关服务(即我们的 ASP. net 服务)的 HTTP 请求.NET Core 服务暴露特定于移动应用的端点)

    • The gateway service verifying the token and retrieving an on-behalf-of token to call the downstream stream services

      通过这种方式,可以将用户身份传播到每个层,并且可以使用声明原则实现所需的授权过程。

  6. In order to allow identity propagation, the gateway service application registration (that is, the service principal) should be given the required identity delegation permissions to the downstream service registrations:

    <image/Figure_9.19_B16381.jpg>

    图 9.19 -添加 API 权限

  7. 现在,用户标识可以访问资源,前提是它们的标识存在于目标组织中,并且具有所需的权限。

对于一个面向业务应用(即业务(LOB)应用),B2B Azure 广告可以提供一个安全的身份管理解决方案轻松,并没有额外的自定义实现。 然而,如果应用需要面向客户端,我们将需要一个更灵活的解决方案,并提供额外的注册支持。 Azure B2C 可以为个人用户帐户提供所需的支持。

Azure AD B2C

Azure AD B2C 是面向消费者场景的身份管理服务,可以选择自定义和控制客户如何注册、登录以及在使用您的应用时如何管理他们的配置文件。 这针对各种平台。

B2C 是一种现代的联合身份管理服务,其中消费者应用(即依赖方)可以使用多个身份提供者和验证方法。

在 B2C 领域中,用于注册和登录的用户流被称为用户旅程。 如果需要,可以使用策略定制用户旅程。 身份体验框架使用这些策略来实现所需的用户流。 身份体验框架是建立多方信任并完成用户旅程步骤的底层平台。

与 Azure AD 本身类似,租户描述一个用户域,其中可以定义用户和应用之间的某些关系。 然而,在 B2C 中,域是特定于客户的,而不是特定于组织的。 换句话说,租户定义一个由策略描述和链接的标识提供者控制的访问组。

在这种设置中,多个应用可以访问多个租户,这使得 B2C 非常适合开发公司,因为它们拥有一套要发布给消费者的应用。 消费者一旦注册使用一个链接的 OIDC 身份提供商,就可以访问多个面向消费者的应用。

在本节中,我们开始使用。net Core Identity 进行安全模型研究。 虽然它为较小的使用者应用提供了最主流的需求,但如果身份管理需求的数量增加,它很快就会变得过于复杂。 正如您所看到的,Azure AD 也可以很容易地集成到 ASP 中.NET 应用,并且可以为复杂的 LOB 场景提供基础设施。 最后,我们简要地介绍了 Azure AD B2C,它提供了一个完整的联邦身份管理平台,具有灵活的扩展选项。

总结

在本章中,我们浏览了 PaaS 平台,以及可用于托管和实现 ASP 的架构方法.NET Core web 服务。 使用 ASP 提供的灵活的基础设施。 对于开发人员来说,实现使用来自 Cosmos DB 集合的数据的微服务是一种解脱。 在域对象上包含 CRUD 操作的服务可以通过 Redis 和容器化进行优化和改进,并托管在多个平台和操作系统上。 安全性是我们在分布式云架构中主要关注的问题之一,可以通过在 Azure 云堆栈上使用可用的身份基础设施和 iaas 产品(如 Azure AD 和 Azure AD B2C)来确保安全性。

在下一章中,我们将转向 Azure 无服务器,这是另一个服务平台,在它上。net Core 可以被证明是至关重要的。************

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值