模型验证组件 FluentValidation

FluentValidation 是 .NET 下的模型验证组件,和 ASP.NET MVC 基于Attribute 声明式验证的不同处,其利用表达式语法链式编程,使得验证组件与实体分开。正如 FluentValidation 的 介绍:

A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.

使用后,只能用一句话来形容:真乃神器也!

项目地址:http://fluentvalidation.codeplex.com/

想体验 Lambda Expression 流畅的感觉吗,下面 let's go!

首先,你需要通过 NuGet 获取 FluentValidation、FluentValidation.MVC3 包,我当前使用的版本如下:

<?xml version="1.0" encoding="utf-8"?>
<packages><package id="FluentValidation" version="3.3.1.0" /><package id="FluentValidation.MVC3" version="3.3.1.0" />
</packages>

快速入门

1. 建立模型类

为了演示,我这里建了一个 Person 类,并且假设有下面这些 Property(属性)。

/// <summary>
/// 个人
/// </summary>
public class Person
{/// <summary>/// 姓/// </summary>public string Surname { get; set; }/// <summary>/// 名/// </summary>public string Forename { get; set; }/// <summary>/// 公司/// </summary>public string Company { get; set; }/// <summary>/// 地址/// </summary>public string Address { get; set; }/// <summary>/// 邮政编码/// </summary>public string Postcode { get; set; }/// <summary>/// 个人空间的地址的别名,比如:bruce-liu-cnblogs、cnblogs_bruce_liu/// </summary>public string UserZoneUrl { get; set; }
}

根据 FluentValidation 的使用方法,我们直接可以在 Person 类上面直接标记对应的 Validator,比如: [Validator(typeof(PersonValidator))]。但如果我们的模型层(Model Layer)不允许修改(假设),并且你像我一样喜欢干净的模型层,不想要标记太多业务型的 Attribute 时,我们就使用继承的方式来标记,在派生类上标记。下面我们建一个 Customer 类,继承自 Person 类,并且再增加 2 个 Property(属性),最后标记 Validator Attribute。

[Validator(typeof(CustomerValidator))]
public class Customer : Person
{/// <summary>/// 是否有折扣/// </summary>public bool HasDiscount { get; set; }/// <summary>/// 折扣/// </summary>public float Discount { get; set; }}

2. 建立模型类相应的 FluentValidation 验证类

public class CustomerValidator : AbstractValidator<Customer>
{public CustomerValidator(){// 在这里写验证规则,比如:// Cascade(FluentValidation.CascadeMode.StopOnFirstFailure) 可以指定当前 CustomerValidator 的验证模式,可重写全局验证模式RuleFor(customer => customer.Surname).Cascade(FluentValidation.CascadeMode.StopOnFirstFailure).NotEmpty().Length(3, int.MaxValue).WithLocalizedName(() => "姓").WithLocalizedMessage(() => "亲,{PropertyName}不能为空字符串,并且长度大于{0}!!!");// 更多...// 更多...}
}

3. 在 Global.asax 里面的 Application_Start 中配置 FluentValidation

默认情况下,FluentValidation 使用的验证错误消息是英文的,且官方自带的语言包中没有中文,于是我自己就手动翻译,建立了一个资源文件 FluentValidationResource.resx,并且在 Global.asax 中配置。

protected void Application_Start()
{ConfigureFluentValidation();
}protected void ConfigureFluentValidation()
{// 设置 FluentValidation 默认的资源文件提供程序 - 中文资源ValidatorOptions.ResourceProviderType = typeof(FluentValidationResource);/* 比如验证用户名 not null、not empty、length(2,int.MaxValue) 时,链式验证时,如果第一个验证失败,则停止验证 */ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure; // ValidatorOptions.CascadeMode 默认值为:CascadeMode.Continue// 配置 FluentValidation 模型验证为默认的 ASP.NET MVC 模型验证FluentValidationModelValidatorProvider.Configure();
}

FluentValidationResource 代码中的 Key-Value 如下(PS:由于不知道怎么贴 Resource 文件中的代码,我就用截图了):

翻译得不好,请多多包涵!从这里下载

4. 客户端调用

本来用控制台程序就可以调用的,由于笔者建立的项目是 ASP.NET MVC 项目,本文的重点也是 FluentValidation 在 ASP.NET MVC 中使用,于是就在 Action 里面验证了。在 HomeController 的 Index 方法里面的代码如下:

public ActionResult Index()
{/* 下面的例子验证 FluentValidation 在 .net 中的使用,非特定与 ASP.NET MVC */Customer customer = new Customer(); // 我们这里直接 new 了一个 Customer 类,看看模型验证能否通过CustomerValidator validator = new CustomerValidator();ValidationResult results = validator.Validate(customer); // 或者抛出异常 validator.ValidateAndThrow(customer);bool validationSucceeded = results.IsValid;IList<ValidationFailure> failures = results.Errors;StringBuilder textAppender = new StringBuilder();if (!results.IsValid){foreach (var failureItem in failures){textAppender.Append("<br/>==========================================<br/>");textAppender.AppendFormat("引起失败的属性值为:{0}<br/>", failureItem.AttemptedValue);textAppender.AppendFormat("被关联的失败状态为:{0}<br/>", failureItem.CustomState);textAppender.AppendFormat("错误消息为:{0}<br/>", failureItem.ErrorMessage);textAppender.AppendFormat("Property(属性)为:{0}<br/>", failureItem.PropertyName);textAppender.Append("<br/>==========================================<br/>");}}ViewBag.Message = textAppender.ToString();return View();
}

最后,运行就能看到效果!

进阶篇

1. 属性类(Property Class)的验证

既然是顾客,那么顾客就可能会有订单,我们建立一个 Order 类,把 Customer 类作为 Order 类的一个 Property(属性)。

/// <summary>
/// 订单
/// </summary>
[Validator(typeof(OrderValidator))]
public class Order
{public Customer Customer { get; set; }/// <summary>/// 价格/// </summary>public decimal Price { get; set; }
}

相应的,我们还需要建立一个验证类 OrderValidator。为了共用 CustomerValidator 类,我们需要在 OrderValidator 类的构造函数中,为 Order 类的 Customer 属性指定 Validator。

/// <summary>
/// 订单验证类
/// </summary>
public class OrderValidator : AbstractValidator<Order>
{public OrderValidator() {RuleFor(order => order.Price).NotNull().GreaterThanOrEqualTo(0m).WithLocalizedName(() => "价格");// 重用 CustomerValidatorRuleFor(order => order.Customer).SetValidator(new CustomerValidator());}
}

在 ASP.NET MVC 中使用时,在 Action 方法的参数上,可以像使用 Bind Attribute 一样:

public ActionResult AddCustomer([Bind(Include = "Company", Exclude = "Address")]Customer customer)

使用 CustomizeValidator Attribute,来指定要验证的 Property(属性):

[HttpGet]
public ActionResult AddCustomer()
{return View(new Customer());
}[HttpPost]
public ActionResult AddCustomer([CustomizeValidator(Properties="Surname,Forename")] Customer customer)
{/* 在 Action 的参数上标记  CustomizeValidator 可以指定 Interceptor(拦截器)、Properties(要验证的属性,以逗号分隔)。如果指定了 Properties (要验证的属性,以逗号分隔),请注意是否别的属性有客户端验证,导致客户端提交不了,而服务器端又可以不用验证。*/if (!ModelState.IsValid){return View(customer);}return Content("验证通过");
}

由此可见,FluentValidation 真是用心良苦,这都想到了,不容易啊!

扩展篇

1. 完善 CustomerValidator

接下来,我们继续 完善 CustomerValidator ,增加更多的验证规则。

public class CustomerValidator : AbstractValidator<Customer>
{public CustomerValidator(){// CascadeMode = CascadeMode.StopOnFirstFailure; 可以指定当前 CustomerValidator 的验证模式,可重写全局验证模式RuleFor(customer => customer.Surname).Cascade(FluentValidation.CascadeMode.StopOnFirstFailure).NotEmpty().Length(3, int.MaxValue).WithLocalizedName(() => "姓").WithLocalizedMessage(() => "亲,{PropertyName}不能为空字符串,并且长度大于{0}!!!");// 注意:调用 Cascade(FluentValidation.CascadeMode.StopOnFirstFailure) 表示当一个验证条件失败后,不再继续验证RuleFor(customer => customer.Forename).NotEmpty().WithLocalizedName(() => "名").WithLocalizedMessage(() => "{PropertyName} 一定要不为空,Do you know ?");RuleFor(customer => customer.Company).NotNull().WithLocalizedName(() => "公司名称").WithMessage(string.Format("{{PropertyName}} 不能 \"{0}\",下次记住哦,{1}!", "为空", "呵呵"));RuleFor(customer => customer.Discount).NotEqual(0).WithLocalizedName(() => "折扣").When(customer => customer.HasDiscount);RuleFor(customer => customer.Address).Length(20, 250).WithLocalizedName(() => "地址").Matches("^[a-zA-Z]+$").WithLocalizedMessage(() => "地址的长度必须在 20 到 250 个字符之间,并且只能是英文字符!");RuleFor(customer => customer.Postcode).Must(BeAValidPostcode).WithLocalizedName(() => "邮政编码").WithMessage("请指定一个合法的邮政编码");// 注意:如果用了 Must 验证方法,则没有客户端验证。Custom((customer, validationContext) =>{bool flag1 = customer.HasDiscount;bool flag2 = !validationContext.IsChildContext;return flag1 && flag2 && customer.Discount > 0 ? null : new ValidationFailure("Discount", "折扣错误", customer.Discount);});}/// <summary>/// 检查是否是合法的邮政编码/// </summary>/// <param name="postcode"></param>/// <returns></returns>private bool BeAValidPostcode(string postcode){if (!string.IsNullOrEmpty(postcode) && postcode.Length == 6){return true;}return false;}
}

当我想要给 Customer.UserZoneUrl(个人空间的地址的别名) 写验证规则的时候,我发现它的验证规则可以提取出来,方便下次有类似的功能需要用到。那能不能像调用 NotNull() 、NoEmpty() 方法那样,调用我们写的 EntryName() 呢?答案:当然可以!

这样调用怎么样?

RuleFor(customer => customer.UserZoneUrl).EntryName();

其中 EntryName() 是一个扩展方法。

using FluentValidation;public static class FluentValidatorExtensions
{public static IRuleBuilderOptions<T, string> EntryName<T>(this IRuleBuilder<T, string> ruleBuilder){return ruleBuilder.SetValidator(new EntryNameValidator());}
}

我们看到,调用 EntryName 扩展方法其实是调用另外一个 Validator - EntryNameValidator。

public class EntryNameValidator : PropertyValidator, IRegularExpressionValidator
{private readonly Regex regex;const string expression = @"^[a-zA-Z0-9][\w-_]{1,149}$";public EntryNameValidator(): base(() => ExtensionResource.EntryName_Error){regex = new Regex(expression, RegexOptions.IgnoreCase);}protected override bool IsValid(PropertyValidatorContext context){if (context.PropertyValue == null) return true;if (!regex.IsMatch((string)context.PropertyValue)){return false;}return true;}public string Expression{get { return expression; }}
}

这里我们的 EntryNameValidator 除了继承自 PropertyValidator,还实现了 IRegularExpressionValidator 接口。为什么要实现 IRegularExpressionValidator 接口 呢?是因为可以共享由 FluentValidation 带来的好处,比如:客户端验证等等。

其中 ExtensionResource 是一个资源文件,我用来扩展 FluentValidation 时使用的资源文件。

2. 复杂验证

下面我们再建立一个 Pet(宠物)类,为 Customer 类增加一个 public List<Pet> Pets { get; set; } 属性。

/// <summary>
/// 顾客类
/// </summary>
[Validator(typeof(CustomerValidator))]
public class Customer : Person
{/// <summary>/// 是否有折扣/// </summary>public bool HasDiscount { get; set; }/// <summary>/// 折扣/// </summary>public float Discount { get; set; }/// <summary>/// 一个或多个宠物/// </summary>public List<Pet> Pets { get; set; }}/// <summary>
/// 宠物类
/// </summary>
public class Pet
{public string Name { get; set; }
}

那 FluentValidation 对集合的验证,该如何验证呢?下面我们要求顾客的宠物不能超过 10 个。你一定想到了用下面的代码实现:

Custom(customer =>
{return customer.Pets.Count >= 10? new ValidationFailure("Pets", "不能操作 10 个元素"): null;
});

或者我们写一个自定义的 Property(属性)验证器 ListMustContainFewerThanTenItemsValidator<T>,让它继承自 PropertyValidator

public class ListMustContainFewerThanTenItemsValidator<T> : PropertyValidator
{public ListMustContainFewerThanTenItemsValidator(): base("属性 {PropertyName} 不能超过 10 个元素!"){// 注意:这里的错误消息也可以用资源文件}protected override bool IsValid(PropertyValidatorContext context){var list = context.PropertyValue as IList<T>;if (list != null && list.Count >= 10){return false;}return true;}
}

应用这个属性验证器就很容易了,在 Customer 的构造函数中:

RuleFor(customer => customer.Pets).SetValidator(new ListMustContainFewerThanTenItemsValidator<Pet>());

再或者为了公用,写一个扩展方法,扩展 IRuleBuilder<T, IList<TElement>> 类

/// <summary>
/// 定义扩展方法,是为了方便调用。
/// </summary>
public static class MyValidatorExtensions
{public static IRuleBuilderOptions<T, IList<TElement>> MustContainFewerThanTenItems<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder){return ruleBuilder.SetValidator(new ListMustContainFewerThanTenItemsValidator<TElement>());}
}

调用也像上面调用 EntryName() 一样,直接调用:

RuleFor(customer => customer.Pets).MustContainFewerThanTenItems();

3. 与 IoC 容器(Autofac、Unity、StructureMap等)集成

下面以 Autofac 为例进行演示

1. 创建自己的 ValidatorFactory

比如我这里创建为 AutofacValidatorFactory,继承自 FluentValidation.ValidatorFactoryBase,而 ValidatorFactoryBase 本身是实现了 IValidatorFactory 的。IValidatorFactory 的代码如下:

// 摘要:
//     Gets validators for a particular type.
public interface IValidatorFactory
{// 摘要://     Gets the validator for the specified type.IValidator<T> GetValidator<T>();//// 摘要://     Gets the validator for the specified type.IValidator GetValidator(Type type);
}

ValidatorFactoryBase 的代码如下:

public abstract class ValidatorFactoryBase : IValidatorFactory
{protected ValidatorFactoryBase();public abstract IValidator CreateInstance(Type validatorType);public IValidator<T> GetValidator<T>();public IValidator GetValidator(Type type);
}

我们看到 ValidatorFactoryBase 其实是把 IValidatorFactory 接口的 2 个方法给实现了,但核心部分还是抽象出来了,那我们的 AutofacValidatorFactory 需要根据 Autofac 的使用方法进行编码,代码如下:

public class AutofacValidatorFactory : ValidatorFactoryBase
{private readonly IContainer _container;public AutofacValidatorFactory(IContainer container){_container = container;}/// <summary>/// 尝试创建实例,返回值为 NULL 表示不应用 FluentValidation 来做 MVC 的模型验证/// </summary>/// <param name="validatorType"></param>/// <returns></returns>public override IValidator CreateInstance(Type validatorType){object instance;if (_container.TryResolve(validatorType, out instance)){return instance as IValidator;}return null;}
}

2. 在 Application_Start 中注册 Autofac

protected void Application_Start()
{RegisterAutofac();
}protected void RegisterAutofac()
{// 注册 IoCContainerBuilder builder = new ContainerBuilder();builder.RegisterNewsManagement();// 创建 containerIContainer _container = builder.Build();// 在 NewsManagement 模型下设置 container_container.SetAsNewsManagementResolver();ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(new AutofacValidatorFactory(_container)));DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
}

其中上面那 2 个方法(RegisterNewsManagement、SetAsNewsManagementResolver)是扩展方法,代码如下:

public static class AutofacExtensions
{public static void RegisterNewsManagement(this ContainerBuilder builder){builder.RegisterType<NewsCategoryValidator>().As<IValidator<NewsCategoryModel>>();builder.RegisterType<NewsValidator>().As<IValidator<NewsModel>>();builder.RegisterControllers(typeof(MvcApplication).Assembly);}public static void SetAsNewsManagementResolver(this IContainer contaner){DependencyResolver.SetResolver(new AutofacDependencyResolver(contaner));}
}

至此,我们的模型上面就可以注释掉对应的 Attribute 了。

/// <summary>
/// 文章表模型
/// </summary>//[Validator(typeof(NewsValidator))]
public class NewsModel : NewsEntity
{}


---------------------
作者:惟楚有才
来源:CSDN
原文:https://blog.csdn.net/qq289523052/article/details/23739243
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/284224.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二届中国PWA开发者日

点击蓝字关注我们活动介绍为加速推动渐进式 Web 应用 (PWA) 在中国的发展&#xff0c;微软与英特尔携手举办“第二届中国 PWA 开发者日”。本次活动邀请一众业界大咖围绕 PWA 展开分享&#xff0c;探讨最新技术进展&#xff0c;及 PWA 生态的实践与落地。期待与您线上相聚。活动…

【GlobalMapper精品教程】018:提取影像数据的范围生成矢量图层

文章目录 1. 加载影像数据2. 生成边界3. 导出矢量范围4. 背景影响边界解决办法1. 加载影像数据 以DSM为例,加载如下所示: 2. 生成边界 在影像图层上右键→图层→【边界框/覆盖-创建图层覆盖框/多边形区要素】,如下图所示: 选择【否】。 边界创建完成。 3. 导出矢量范围 …

MPMoviePlayerController属性方法简介

属性说明property (nonatomic, copy) NSURL *contentURL播放媒体URL&#xff0c;这个URL可以是本地路径&#xff0c;也可以是网络路径property (nonatomic, readonly) UIView *view播放器视图&#xff0c;如果要显示视频必须将此视图添加到控制器视图中property (nonatomic, re…

在Leangoo里怎么设置看板周期?

设置看板周期有两种方式&#xff1a; 1&#xff09;点击看板上的看板周期时间直接修改 2&#xff09;通过菜单 设置看板周期 浏览器访问官网链接&#xff1a;www.leangoo.com 转载于:https://www.cnblogs.com/shineshine/p/5663104.html

consul部署多节点和consul-template部署

一.consul的介绍 1.1consul是什么&#xff1f; Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 Consul是分布式的、高可用的、可横向扩展的。它具备以下特性 : service discovery:consul通过DNS或者HTTP接口使服务注册和服务发现变的很容易,一些外…

基于ABP实现DDD

什么是DDD呢&#xff1f;领域驱动设计[DDD]是一种针对复杂需求的软件开发方法。将软件实现与不断发展的模型联系起来&#xff0c;专注于核心领域逻辑&#xff0c;而不是基础设施细节。DDD适用于复杂领域和大规模应用&#xff0c;而不是简单的CRUD应用。它有助于建立一个灵活、模…

二、通过工厂方法来配置bean

调用静态工厂方法创建 Bean是将对象创建的过程封装到静态方法中. 当客户端需要对象时, 只需要简单地调用静态方法, 而不同关心创建对象的细节. 要声明通过静态方法创建的 Bean, 需要在 Bean 的 class 属性里指定拥有该工厂的方法的类, 同时在 factory-method 属性里指定工厂方法…

【GlobalMapper精品教程】019:基于DSM提取离散随机点的高程信息

本文讲解在globalmapper中,基于DSM提取离散随机点的高程信息,配套数据为data019.rar。 文章目录 1. 离散点创建2. 提取离散点高程信息3. 高程标注1. 离散点创建 本文在ArcGIS中,根据给定的范围,随机生成离散点,如下图: 拓展阅读: ArcGIS根据范围创建随机点教程:【ArcG…

shell脚本注意点

2019独角兽企业重金招聘Python工程师标准>>> 直接命令行写脚本的时候&#xff0c;可以用 ; 分割&#xff0c;或 也可以直接回车&#xff0c;然后在继续写脚本在使用 方括号[ ] 的时候&#xff0c;里面空格两边都必须要有空格&#xff0c;比如 [ $a -gt 3 ] 在方括号…

C语言编程规范--------2 注释

2.1 注释的原则 注释的目的是解释代码的目的、功能和采用的方法&#xff0c;提供代码以外的信息&#xff0c;帮助读者理解代码&#xff0c;防止没必要的重复注释信息。 示例&#xff1a;如下注释意义不大。 /* if receive_flag is TRUE */ if (receive_flag) 而如下的注释则给出…

备战金九银十:RabbitMQ有5种工作模式(6)

RabbitMQ是实现了高级消息队列协议&#xff08;AMQP&#xff09;的开源消息代理软件&#xff08;亦称面向消息的中间件&#xff09;。RabbitMQ服务器是用Erlang语言编写的&#xff0c;而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客…

【GlobalMapper精品教程】020:Lidar点云数据分类(自动分类、手动分类)案例详解

航测点云通常跟DSM一致,即包含植被、房屋等信息,必须进行点云分类、过滤,才能生成准确的高程点、等高线和DEM等地形数据。本文以案例的形式详细讲解globalmapper23中点云工具及使用方法。 文章目录 1. 点云分类2. 创建地面高程格网3. 地形绘制4. 格网转点云5. 点云抽稀6. 点…

社交网络图中结点的“重要性“计算(Dijkstra + SPFA + Floyd + 模板)

题目链接&#xff1a; 无 题目大意&#xff1a; 求一个点到其他所有点的最短距离和&#xff0c;保证图连通。 解题过程&#xff1a; 刚开始用 Floyd 水过的&#xff0c;后来用换了几种方法&#xff0c;不错的模板题&#xff0c;Floyd 的时候&#xff0c;要用 vector 存边&#…

web布局固定宽度+变化宽度实现思路

前言 页面当中常规布局我想大家都会的&#xff0c;但有些布局是常规布局中实现不了的&#xff0c;比如变宽和固宽结合的&#xff0c;需要实现(300px)&#xff0b;(100%&#xff0d;300px)的两列布局。以下样式代码前提均为盒模型为border-sizing 的前提下。 html部分 <div c…

CSS3 nth 伪类选择器

考察下面的 HTML 代码片段&#xff1a; <div><section>section 1</section><section>section 2</section><ul><li>item 1</li><li><ul><li>sub item 1</li><li>sub item 2</li><li>…

RedisCluster的安装、部署、扩容和 Java客户端调用

Redis下载 官网地址&#xff1a;http://redis.io/ 中文官网地址&#xff1a;http://www.redis.cn/ 下载地址&#xff1a;http://download.redis.io/releases/ 安装 # &#xff08;三台&#xff09;安装 C 语言需要的 GCC 环境 yum install -y gcc-c yum install -y wget # 下…

【CloudCompare教程】001:CloudCompare中文版下载与安装图文教程

CloudCompare是一款功能强大的点云后处理软件,本文讲解CloudCompare中文版下载与安装方法。 文章目录 一、CloudCompare下载地址二、CloudCompare安装教程三、CloudCompare中文设置一、CloudCompare下载地址 官方下载地址:http://www.danielgm.net/cc/release/ 二、CloudComp…

ML.NET相关资源整理

在人工智能领域&#xff0c;无论是机器学习&#xff0c;还是深度学习等&#xff0c;Python编程语言都是绝对的主流&#xff0c;尽管底层都是C实现的&#xff0c;似乎人工智能和C#/F#编程语言没什么关系。在人工智能的工程实现&#xff0c;通常都是将Python训练好的人工智能模型…

带参数的宏替换

带参数的宏替换因各种需求叠加&#xff0c;替换规则很怪异&#xff1a; 1、首先将实参替换形参&#xff0c;并展开宏 2、如果1步展开后&#xff0c;有#或者##&#xff0c;那么停止替换。 3、如果1步展开后&#xff0c;没有#或者##&#xff0c;且参数也是宏&#xff0c;那么继续…