DDD与批量操作

原文链接:https://enterprisecraftsmanship.com/posts/ddd-bulk-operations/

将批量操作与领域驱动设计相结合是一个困难的问题。在这篇文章中,我们将看看为什么会这样,并讨论如何结合两个。

本文也是对读者提问的回应。这个问题包含一个有趣的例子,我将在本文中使用:

Hi Vladimir!

你有关于DDD环境下批量操作的文章吗?我没有发现任何有用的东西。

请考虑以下示例:

给定一个任务列表,我想为所有与所选月份和类别匹配的任务设置一个执行日期,

另外,我不能为已经完成的任务设置执行日期,

对于给定的月份和类别,最多可以有30000个任务。

目前,我创建了一个SetExecutionDateDomainService

查询tasksRepository.GetBy(month, category)

对于每个任务,检查task.CanSetExecutionDate()

如果为true则调用taskRepository.Update(task)

关于如何处理这个问题有什么意见/建议吗?

有三种方法可以处理此问题:

  • 逐个处理对象(问题作者的处理方式),

  • 依赖SQL批量更新,

  • 结合使用规约和命令模式。

前两种选择有权衡。我特别喜欢第三个。

顺序处理

处理此问题最直接的方法是检索所有合适的对象,然后依次更新它们:

public class Task
{public int Month { get; private set; }public string Category { get; private set; }public bool IsCompleted { get; private set; }public DateTime? ExecutionDate { get; private set; }public bool CanSetExecutionDate(){return IsCompleted == false;}public void SetExecutionDate(DateTime executionDate){Guard.Require(CanSetExecutionDate(), "CanSetExecutionDate()");ExecutionDate = executionDate;}
}public class SetExecutionDateService
{public void SetExecutionDate(int month, string category, DateTime executionDate){IReadOnlyList<Task> tasks = _repository.GetBy(month, category);foreach (Task task in tasks){if (task.CanSetExecutionDate()){task.SetExecutionDate(executionDate);_repository.Update(task);}}}
}

该解决方案的主要优点是所有领域知识都包含在领域模型中。具体来说,执行日期何时可以设置的知识(CanSetExecutionDate方法)。

这里的缺点是缺乏性能:单独处理和更新任务需要大量的数据库往返——每次更新一次。

在OLTP类型的操作(少量数据的事务处理)之外,DDD通常不能很好地工作。对于批量更新大量任务的用例也是如此——它不属于DDD的“舒适区”。

批量操作(或批量更新)是在一次数据库往返中更新大量数据。

使用原始SQL

如果DDD不能很好地与批量更新配合使用,那该怎么办?这就是原始SQL的闪光点。SQL专门设计用于处理大量相关数据,我们也可以将其用于我们的场景:

UPDATE dbo.Task
SET ExecutionDate = @ExecutionDate
WHERECategory = @Category ANDMonth = @Month ANDIsCompleted = 0 -- 领域知识重复

这种方法既快速又简单,但违反了DRY原则:您必须将哪些任务有资格设置执行日期的知识同时放到SQL(IsCompleted=0行)和应用程序代码(CanSetExecutionDate方法)中。

使用原始SQL可能不是一个坏的选择,特别是在简单的项目中,但是有更好的方法。

使用规约模式

简而言之,规约模式 是关于将一段领域知识封装到单个单元(称为规约)中,然后在三种场景中重用该单元:

  • 数据检索

  • 内存验证

  • 创建新对象(下图中的“按顺序施工”)。

我还写过,虽然这个模式的想法看起来很有趣,但它与CQRS模式相反,因此应该被丢弃。原因是CQRS提供了另一个好处——松耦合,在绝大多数情况下比DRY更重要。

CQRS通过将单个统一模型拆分为两个来实现松耦合:一个用于读取(数据检索,原始SQL查询的范围),另一个用于写入(内存验证,DDD的范围)。这种分离就是矛盾所在:规约模式主张保持统一的模型。

那么,规约模式如何在批量更新的场景中提供帮助呢?

事实证明,您不仅可以使用规约来查询数据库,还可以更新数据库。首先让我展示这个模式的一个典型用法。然后我将演示如何为批量更新用例扩展它。

在上述设置任务执行日期的用例中,我们需要以下三个规约:

public sealed class TaskIsCompletedSpecification : Specification<Task>
{public override Expression<Func<Task, bool>> ToExpression(){return task => task.IsCompleted;}
}public sealed class TaskMonthSpecification : Specification<Task>
{private readonly int _month;public TaskMonthSpecification(int month){_month = month;}public override Expression<Func<Task, bool>> ToExpression(){return task => task.Month == _month;}
}// + TaskCategorySpecification, which is the same as TaskMonthSpecification

您可以在此GitHub仓储中找到基本Specification类和所有其他支持类的源代码。

根据这些规约,Task如下所示:

public class Task
{public int Month { get; private set; }public string Category { get; private set; }public bool IsCompleted { get; private set; }public DateTime? ExecutionDate { get; private set; }public bool CanSetExecutionDate(){var spec = new TaskIsCompletedSpecification(); '1return spec.IsSatisfiedBy(this) == false;      '1}public void SetExecutionDate(DateTime executionDate){Guard.Require(CanSetExecutionDate(), "CanSetExecutionDate()");ExecutionDate = executionDate;}
}

请注意'1'中TaskIsCompletedSpecification的使用。它看起来可能是多余的(毕竟,此规约检查同一任务实例的IsCompleted字段),但在应用程序中分配域知识时保持一致是很重要的。一旦您引入了一个规约来保存一部分知识,所有其他类也应该开始使用它来遵守DRY原则。

以下是领域服务:

public class SetExecutionDateService
{public void SetExecutionDate(int month, string category, DateTime executionDate){var monthSpec = new TaskMonthSpecification(month);var categorySpec = new TaskCategorySpecification(category);var isNotCompletedSpec = new TaskIsCompletedSpecification().Not();Specification<Task> spec = monthSpec.And(categorySpec).And(isNotCompletedSpec); '1IReadOnlyList<Task> tasks = _repository.GetList(spec); '2foreach (Task task in tasks){if (task.CanSetExecutionDate()){task.SetExecutionDate(executionDate);_repository.Update(task);}}}
}

领域服务组合了三个规约(第1行)并将它们传递给仓储(“2”)。仓储如下所示(我使用的是NHibernate,但实体框架的代码是相同的):

public IReadOnlyList<Task> GetList(Specification<Task> specification)
{return _session.Query<Task>().Where(specification.ToExpression()).ToList();
}

这段代码依赖于复杂的ORM功能,它遍历规约的表达式树并将其转换为SQL。例如,此组合规约

var monthSpec = new TaskMonthSpecification(month);
var categorySpec = new TaskCategorySpecification(category);
var isNotCompletedSpec = new TaskIsCompletedSpecification().Not();
Specification<Task> spec = monthSpec.And(categorySpec).And(isNotCompletedSpec);

被翻译成

Month = @Month AND Category = @Category AND NOT(IsCompleted = 1)

C#表达式与ORM的结合是一对强大的组合。但即使是他们也只能带你走这么远。ORMs允许您使用表达式来查询数据库,但不能更新它。为了实现批量更新功能(将执行日期设置为一次数据库往返中的所有任务),我们需要更新数据库。

那么,该怎么办呢?

好消息是,使用规约模式处理数据库不必依赖ORMs或C#表达式。表达式树是一个方便的工具,可以简化规约的实现,但它们只是这样一个工具。

另一个工具是原始SQL本身。实际上,您可以将这两种方法结合起来:使用表达式树进行内存验证和查询数据库,使用原始SQL进行批量更新。其思想是,除了ToExpression方法外,每个规约还必须实现ToSql(),以便为updatesql查询生成适当的过滤器。

下面是基本规约类的外观(同样,请查看GitHub仓储以获取完整的源代码):

public abstract class Specification<T>
{public bool IsSatisfiedBy(T entity){Func<T, bool> predicate = ToExpression().Compile();return predicate(entity);}public abstract Expression<Func<T, bool>> ToExpression();/* And(), Or(), Not() methods */
}

您需要添加两个新的抽象方法:

public abstract string ToSql();
public abstract IEnumerable<SqlParameter> ToSqlParameters();

ToSql将规约转换为SQL,ToSqlParameters为该SQL提供必需的参数。

现在您需要在所有规约子类中实现这两个方法。举个例子:

public sealed class TaskMonthSpecification : Specification<Task>
{private readonly int _month;public TaskMonthSpecification(int month){_month = month;}public override Expression<Func<Task, bool>> ToExpression(){return task => task.Month == _month;}public override string ToSql(){return "[Month] = @Month";}public override IEnumerable<SqlParameter> ToSqlParameters(){yield return new SqlParameter("Month", _month);}
}

最后,批量更新是这样的:

// Domain service
public void SetExecutionDate(int month, string category, DateTime executionDate)
{var monthSpec = new TaskMonthSpecification(month);var categorySpec = new TaskCategorySpecification(category);var isNotCompletedSpec = new TaskIsCompletedSpecification().Not();Specification<Task> spec = monthSpec.And(categorySpec).And(isNotCompletedSpec);_repository.UpdateExecutionDate(executionDate, spec);
}// TaskRepository
public void UpdateExecutionDate(DateTime executionDate, Specification<Task> specification)
{string sql = @"UPDATE dbo.TaskSET ExecutionDate = @ExecutionDateWHERE " + specification.ToSql();using (DbCommand command = _session.Connection.CreateCommand()){command.CommandText = sql;command.Parameters.AddRange(specification.ToSqlParameters().ToArray());command.Parameters.Add(new SqlParameter("ExecutionDate", executionDate));command.ExecuteNonQuery();}
}

这种规约模式的使用带来了第四种场景,批量更新:

注意,这个用例并不与CQRS相矛盾:用于内存验证和批量更新的领域知识的重用发生在应用程序的写部分。因此,我想收回我先前的说法,即规约只在简单的场景中有用(在这种场景中松耦合并不是那么重要)。批量更新是这种模式的一个非常有效的用例,这种用例可以出现在任何复杂的应用程序中。

在上述实现中,有关如何为批量更新选择任务的业务需求都位于域层。这些要求是三个前提条件的组合,所有这些条件都包含在规约中:

  • 特定月份的任务,

  • 具有特定类别的任务,

  • 未完成的任务。

那么,问题解决了?还没有。虽然我们已经封装了哪些任务适合更新的知识,但更新本身仍然分散在Task领域类和TaskRepository之间('1和'2):

public class Task
{/* Month, Category, IsCompleted, ExecutionDate properties */public bool CanSetExecutionDate(){var spec = new TaskIsCompletedSpecification();return spec.IsSatisfiedBy(this) == false;}public void SetExecutionDate(DateTime executionDate){Guard.Require(CanSetExecutionDate(), "CanSetExecutionDate()");ExecutionDate = executionDate; '1}
}// TaskRepository
public void UpdateExecutionDate(DateTime executionDate, Specification<Task> specification)
{string sql = @"UPDATE dbo.TaskSET ExecutionDate = @ExecutionDate  '2WHERE " + specification.ToSql();using (DbCommand command = _session.Connection.CreateCommand()){command.CommandText = sql;command.Parameters.AddRange(specification.ToSqlParameters().ToArray());command.Parameters.Add(new SqlParameter("ExecutionDate", executionDate));command.ExecuteNonQuery();}
}

这是领域逻辑重复的另一个实例。为了解决这个问题,我们需要另一块拼图:命令模式。

遇见命令模式

上面清单中的重复似乎不是什么大事,因为它只是一个字段的赋值。但事实上,这是一件大事 — 还有一个先决条件要求任务不能完成,才能有执行日期:

public void SetExecutionDate(DateTime executionDate)
{/* 此前提条件是执行日期分配的固有部分 */Guard.Require(CanSetExecutionDate(), "CanSetExecutionDate()");ExecutionDate = executionDate;
}

设置执行日期的行为是整个SetExecutionDate方法,而不仅仅是其中的赋值操作(=)。该方法的前提条件也存在于SQL查询TaskRepository生成的:

UPDATE dbo.Task
SET ExecutionDate = @ExecutionDate
WHERE [Month] = @MonthAND Category = @CategoryAND NOT(IsCompleted = 1) -- 前提条件

问题是没有任何东西可以阻止TaskRepository在未查询此前提条件的情况下设置执行日期。IsCompletedExecutionDate字段之间的连接是一项重要的领域知识,您必须记住这一点,并在TaskTaskRepository中复制它们。

想象一下,不必指定DateTime这样的基本类型,而必须指定一个包含多个字段的值对象。让TaskTaskRepository中的逻辑不同步变得非常容易。

那么,如何克服这个问题,避免赋值逻辑的重复呢?这就是命令模式发挥作用的地方。

命令模式本质上与规约的作用相同,但是命令不检查领域对象的属性,而是更改这些属性。您可以将这两种模式之间的差异想象为:

  • 规约模式封装了要更新哪些数据的知识。

  • 命令模式封装了如何更新数据的知识。

另外,虽然您可以在4种场景中使用规约,但命令仅在两种情况下有用:内存更新和批量更新。

Command基类的如下:

public abstract class Command<T>
{/* 先决条件之外的限制 */protected readonly IReadOnlyList<Specification<T>> _restrictions;  '1protected Command(IReadOnlyList<Specification<T>> restrictions){_restrictions = restrictions;}/* Command's 前提条件 */protected abstract IEnumerable<Specification<T>> GetPreconditions();  '2private Specification<T> CombinedSpecification =>GetPreconditions().Concat(_restrictions).Aggregate(Specification<T>.All, (x, y) => x.And(y));protected abstract void ExecuteCore(T entity);protected abstract string GetTableName();protected abstract string ToSqlCore();protected abstract IEnumerable<SqlParameter> ToSqlParametersCore();/* 内存更新 */public bool CanExecute(T entity){return CombinedSpecification.IsSatisfiedBy(entity);}public void Execute(T entity){if (CanExecute(entity) == false)throw new InvalidOperationException();ExecuteCore(entity);}/* 用于批量更新的SQL */public string ToSql(){return @"UPDATE " + GetTableName() + @"SET " + ToSqlCore() + @"WHERE " + CombinedSpecification.ToSql();}/* 用于批量更新的SQL参数 */public IReadOnlyList<SqlParameter> ToSqlParameters(){return CombinedSpecification.ToSqlParameters().Concat(ToSqlParametersCore()).ToArray();}
}

这个类看起来有点大,但背后的想法很简单 — 将前提条件放到命令中,这样就连省略这些前提条件的选项都没有了。除了先决条件(第2行)之外,还可以对命令施加其他限制(“1”)。

下面是我们的批量更新Command:

public class SetExecutionDateCommand : Command<Task>
{private readonly DateTime _executionDate;public SetExecutionDateCommand(DateTime executionDate, params Specification<Task>[] restrictions): base(restrictions){_executionDate = executionDate;}protected override IEnumerable<Specification<Task>> GetPreconditions(){yield return new TaskIsCompletedSpecification().Not();}protected override void ExecuteCore(Task entity){entity.ExecutionDate = _executionDate;}protected override string GetTableName(){return "dbo.Task";}protected override string ToSqlCore(){return "ExecutionDate = @ExecutionDate";}protected override IEnumerable<SqlParameter> ToSqlParametersCore(){yield return new SqlParameter("ExecutionDate", _executionDate);}
}

用法如下:

// SetExecutionDateService
public void SetExecutionDate(int month, string category, DateTime executionDate)
{var monthSpec = new TaskMonthSpecification(month);          '1var categorySpec = new TaskCategorySpecification(category); '1var command = new SetExecutionDateCommand(executionDate, monthSpec, categorySpec);_repository.BulkUpdate(command);
}// TaskRepository
public void BulkUpdate(SetExecutionDateCommand command)
{using (DbCommand dbCommand = _session.Connection.CreateCommand()){dbCommand.CommandText = command.ToSql();dbCommand.Parameters.AddRange(command.ToSqlParameters().ToArray());dbCommand.ExecuteNonQuery();}
}

请注意,规约限制('1)是可选的(您可以将它们应用于命令,也可以不应用于命令),但规约前提条件是必需的。事实上,您甚至没有指定该前提条件的选项 — 它被放到命令本身中。这就是封装的本质:你不能总是相信自己会做正确的事情;你必须消除做错事的可能性。

另外请注意,我不熟悉应用程序的细节,并假设月份和类别限制是可选的。如果不是,您也应该将它们移到GetPreconditions方法,在这种情况下,命令和领服务将变得更加简单:

public class SetExecutionDateCommand : Command<Task>
{private readonly DateTime _executionDate;private readonly int _month;private readonly string _category;public SetExecutionDateCommand(DateTime executionDate, int month, string category): base(new Specification<Task>[0]){_category = category;_month = month;_executionDate = executionDate;}protected override IEnumerable<Specification<Task>> GetPreconditions(){yield return new TaskIsCompletedSpecification().Not();yield return new TaskMonthSpecification(_month);yield return new TaskCategorySpecification(_category);}/* 剩下的一样 */
}// SetExecutionDateService
public void SetExecutionDate(int month, string category, DateTime executionDate)
{var command = new SetExecutionDateCommand(executionDate, month, category);_repository.BulkUpdate(command);
}

同样,由于其简单性,原始SQL可能仍然是大多数项目的更好选择,即使它不遵守 DRY 原则。但是,规约和命令模式的组合对于具有复杂域逻辑的项目可能很有用,您希望在内存中更新和批量更新之间重用这些逻辑。

总结

  • DDD适合于事务处理少量数据(OLTP),不能很好地处理批量操作。

  • 批量操作(或批量更新)是在一次数据库往返中更新大量数据。

  • 有三种方法可以处理批量更新:

    • 顺序处理(遵循干燥原则,不利于性能),

    • 使用原始SQL(有利于性能,违反了DRY原则)

    • 结合使用规约和命令模式(坚持DRY和良好的性能)。

  • 除了内存验证、查询数据库和创建新对象之外,批量操作是规约模式的第四个用例。

  • 规约模式封装了要更新哪些数据的知识。命令模式封装了如何更新数据的知识。这两种模式都允许您在领域模型和批量操作之间重用这些知识。

  • 命令使用规约作为

    • 内存更新,

    • 批量更新。

欢迎关注我的个人公众号”My IO“

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

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

相关文章

乔布斯死后的300亿遗产终于被败光了,没想到竟是干了这件事

全世界只有3.14 %的人关注了青少年数学之旅前段时间&#xff0c;果粉们翘首以待的科&#xff08;ping&#xff09;技&#xff08;guo&#xff09;界&#xff08;fa&#xff09;春&#xff08;bu&#xff09;晚&#xff08;hui&#xff09;又如期而至。每年一到新款 iPhone 发布…

博客群发(2)--实现登陆

模板方法 python也是一种面向对象的语言&#xff0c;所以在实现群发的时候&#xff0c;会登陆不同的网站&#xff0c;但是登陆的方法什么的不尽相同&#xff0c;所以这里想到的是模板方法。 模板方法模式&#xff1a; 应用特性&#xff1a;重复做相同逻辑的事情&#xff0c;但是…

flask取mysql数据很慢_[flask 优化] 由flask-bootstrap,flask-moment引起的访问速度慢的原因及解决办法...

一周时间快速阅读了400页的《javascript基础教程》&#xff0c;理解了主要概念。解决了一个很久之前的疑问。我的网站是使用flask框架搭建的&#xff0c;介绍flask web的一本著名的书(之前提到过)作者搭建个人博客时&#xff0c;向读者推荐了flask-bootstrap,flask_moment这两个…

运维管理工具-- Deploy Assistant

Deploy Assistant 好用的运维管理工具&#xff0c;目前只支持了docker,后续会继续开发手机端&#xff0c;以及其他的功能&#xff0c;如k8s,mysql,redis等。系统采用ssh登录Linux系统&#xff0c;调用docker api的模式开发&#xff0c;支持账号密码&#xff0c;证书登录 支持do…

空调冷冻水系统控制

空调冷冻水系统控制 张红霞 摘要&#xff1a;通过某大厦空调冷冻水系统控制的介绍&#xff0c;表明只有在空调设计人员提供了准确的控制、测量 参数的基础下&#xff0c;才能实现冷水机组自动控制的目的&#xff0c;满足空调的设计要求。 Control of Refrigerating Water Syste…

用Python进行数据探索,探索竞赛优胜方案

全世界只有3.14 %的人关注了青少年数学之旅AI这个词相信大家都非常熟悉&#xff0c;近几年来人工智能圈子格外热闹&#xff0c;光是AlphoGo就让大家对它刮目相看。随着大数据时代信息科技的快速发展&#xff0c;各种各样的数据充斥着我们的生活。而我们又当如何有效利用数据&am…

微结构设计能力看国产CPU发展

当下&#xff0c;国内CPU公司可以大致分为泾渭分明的两条技术路线&#xff0c;分别是自主研发路线和技术引进路线&#xff0c;从实践上看&#xff0c;自主CPU架构改进能力和IPC提升能力要明显优于技术引进CPU&#xff0c;在研发上更具发展后劲。 FT CPU IPC进步相对有限2013年的…

看到这块Google的“墓地”,心中作何感想?| 今日最佳

世界只有3.14 % 的人关注了青少年数学之旅&#xff08;图源网络&#xff0c;侵权删&#xff09;

轻松实现突破网管限制(SoftEther实际应用)

因为公司限制办公室里的机器上网&#xff0c;只能使用一台内网的HTTP代理服务器浏览网站&#xff0c;而且还只能浏览端口为80的网站&#xff0c;想在天空软件站下个软件还只能通过四川的一个服务器下载。实在很郁闷。找了几天的代理工具。先后用过了“通通通”和“SocksOnline”…

看完这些,孩子的学习效率提高10倍

全世界只有3.14 % 的人关注了青少年数学之旅在工作之余&#xff0c;我们大量的碎片时间被手机占据。无意识的刷手机打发无聊&#xff0c;不如有趣又高品质的积累。我们特意精选了在不同领域的几个高品质公众号代表&#xff0c;希望让你在快乐打发闲暇时光的同时&#xff0c;也能…

中国唯一一位女性 Apache Member 潘娟:我们还是要走出自己与众不同的 My Way

The Apache Way 是一种参照&#xff0c;但我们还是要走出自己的与众不同的 My Way。———潘娟ApacheCon 是 Apache 软件基金会&#xff08;ASF&#xff09;的官方全球系列大会。作为久负盛名的开源盛宴&#xff0c;ApacheCon 在开源界备受关注&#xff0c;也是开源运动早期的知…

python seaborn 热图_python – 在seaborn中结合两张热图

在图中并排显示两个seaborn热图的一种可能方式是将它们绘制成单独的子图.可以将子图之间的空间设置为非常小(wspace 0.01),并将相应的颜色条和标记标记定位在该间隙之外.import matplotlib.pyplot as pltimport numpy as npimport pandas as pdimport seaborn as snsdf pd.Da…

Impala入门笔记

From:http://tech.uc.cn/?p817 问题背景&#xff1a; 初步了解Impala的应用重点测试Impala的查询速度是否真的如传说中的比Hive快3~30倍写作目的&#xff1a; 了解Impala的安装过程初步了解Impala的使用比较Impala与Hive的性能测试适合阅读对象&#xff1a; 想了解Impala安装的…

ASP.NET Core 中做集成测试的三种方案

学习进步老张的哲学不定期更新的日常在平时的开发中&#xff0c;我们很少会关注到测试的问题&#xff0c;更别说集成测试了&#xff0c;除非是公司有硬性要求或者是自己的开源项目中&#xff0c;为了整体架构的完整性&#xff0c;需要用测试来做辅助点缀&#xff0c;而更多的也…

爱泼斯坦事件发酵,MIT师生发起抗议逼迫校长Rafael Reif辞职

全世界只有3.14 %的人关注了青少年数学之旅爱泼斯坦自杀引发的美国学术界地震持续发酵&#xff0c;其中涉及最深的无疑是MIT。继MIT媒体实验室主任Joi Ito和计算机科学家Richard Stallman先后迫于压力辞职后&#xff0c;现任MIT校长Rafael Reif正面临越来越大的辞职压力。现年6…

java file_java开发之File类详细使用方法介绍

File类简介在 Java 中&#xff0c;File 类是 java.io 包中唯一代表磁盘文件本身的对象。File 类定义了一些与平台无关的方法来操作文件&#xff0c;File类主要用来获取或处理与磁盘文件相关的信息&#xff0c;像文件名、 文件路径、访问权限和修改日期等&#xff0c;还可以浏览…

使用 Blazor 开发内部后台(二):了解 Blazor 组件

James: 转载技术社区中一位朋友最新的文章&#xff0c;介绍自己为公司的 WebForm 遗留系统使用 Blazor 重写前端 UI 的经历。什么是Blazor组件Blazor 应用是使用 Razor 组件构建的。组件是用户界面 (UI) 的自包含部分&#xff0c;具有用于启用动态行为的处理逻辑。组件可以嵌套…

Eclipse/Myeclipse生成serialVersionUID方法

serialVersionUID作用&#xff1a;   序列化时为了保持版本的兼容性&#xff0c;即在版本升级时反序列化仍保持对象的唯一性。 如果你修改代码重新部署后出现序列化错误&#xff0c;可以考虑给相应的类增加serialVersionUID字段。 一般来说有两种生成方式&#xff1a;  …

Framework 1.0/1.1中NotifyIcon的不足

.NET Framework 1.0/1.1中给我们提供了一个NotifyIcon类&#xff0c;使用这个类我们可以非常方便的实现系统托盘(SystemTray)图标。可是不知道微软是为了兼容性还是为了偷懒&#xff0c;只实现了NOTIFYICONDATA结构的v5.0之前版本&#xff0c;也就是说不支持5.0及以后的balloon…