目录
介绍
背景
两类异常
通过Http传递状态
七个步骤
数据回复的通用格式
JSON格式的数据回复
C#中的数据回复
数据回复状态
数据回复消息
如何使用DataReplyMessages?
通过步骤
第1步
第2步
第 3 步
第 4 步
第 5 步
第 6 步
第 7 步
结论
Artisan.Orm中的数据回复
关于源代码
- 下载源代码 - 96.1 KB
假设我们有一个三层结构的Web应用程序:
- Web SPA客户端,使用Ajax从Web服务器请求数据
- Web服务器和ASP.NET应用程序
- SQL Server数据库
一旦Web客户端请求获取或保存数据,数据回复就会通过如下方式从数据库传回UI:
这样一来,各种意外都可能发生。SQL Server上的数据库中可能会出现错误,或者Web服务器上的ASP.NET应用程序中可能会发生异常。最终的接收者,一个网络客户端,应该以某种方式被告知发生了什么:请求是正确执行的还是出错了。
本文试图制作一种方便且通用的数据回复格式,它提供了通过上述管道将异常情况的详细信息从数据库传递到Web客户端的可能性。
背景开始之前的一些想法和主张。
两类异常错误和异常可以分为两类:预期的和意外的。
- 意外异常是代码错误或设备故障的结果,我们希望这些事情永远不会发生在一个完美的词中。对此类异常的通常反应:通知用户和管理员有关致命的应用程序错误。
- 预期异常是由于数据保存不一致、请求不及时或其他用户能够自行解决问题的活动的结果。例子:
- Web服务器或数据库中的数据验证,例如用户登录或电子邮件的唯一性。
- 数据并发,当两个用户同时编辑数据库中的同一条记录时。
- 数据丢失,当第一个用户在第二个用户保存记录之前删除它。
- 数据访问拒绝,当数据回复取决于数据库中计算的用户访问级别时。
Web客户端如何区分发生了什么样的异常?有必要将状态传递给它。
如何将此状态从Web服务器传递到Web客户端?想到的第一个想法是使用http状态代码...
事实证明这个想法毫无价值……这就像要求机长通过用于着陆和起飞的官方无线电频率通知您的妈妈您遇到麻烦,尽管您只是将午餐盒忘在厨房桌子上。
Http状态代码用于通知接收者有关传输状态的信息。浏览器使用该状态做出反应。混合传输状态和数据回复状态迟早会导致我们遇到意外的浏览器反应或无法找到适合您需求的代码的问题。
我猜,更好的想法是制作一种通用的数据回复格式,就像一个包装对象,其中Data和Status是属性。
七个步骤通过数据回复管道传递异常详情的任务可以分为几个步骤:
- 在数据库中,找出一个例外情况并输出必要的数据。
- 在存储库中,识别特殊情况并阅读有关它的数据。
- 在存储库中,抛出异常,以便数据服务可以以C#良好实践方式处理它。
- 在数据服务中,获取正常数据或捕获异常,并创建通用数据回复。
- 在ASP.NET Web API控制器中,将数据回复序列化为JSON格式。
- 在Web客户端数据服务中,获取JSON数据、定义数据回复的状态、采取适当的操作。
- 在SPA控制器中,获取数据回复,定义数据回复的状态,采取适当的行动。
理想的数据包装器或DataReply对象,在ASP.NET Web API控制器将其序列化为JSON后,应具有以下形式:
dataReply: {
status: "ok",
data: {...},
messages: [...]
}
因此,用于序列化的C#对象必须具有相同的public属性。经过一系列实验,我找到了C#中DataReply类的最佳结构,至少对我来说是这样。
该DataReply基类只有两个属性:Status和Messages。
派生DataReply添加了Data属性。
这是DataReply类及其属性的图表:
这是C#代码:
- DataReplyStatus
- DataReplyMessage
- DataReply
- DataReply
public enum DataReplyStatus
{
Ok ,
Fail ,
Missing ,
Validation ,
Concurrency ,
Denial ,
Error
}
[DataContract]
public class DataReplyMessage
{
[DataMember]
public String Code;
[DataMember(EmitDefaultValue = false)]
public String Text;
[DataMember(EmitDefaultValue = false)]
public Int64? Id;
[DataMember(EmitDefaultValue = false)]
public Object Value;
}
[DataContract]
public class DataReply {
[DataMember]
public DataReplyStatus Status { get; set; }
[DataMember(EmitDefaultValue = false)]
public DataReplyMessage[] Messages { get; set; }
public DataReply()
{
Status = DataReplyStatus.Ok;
Messages = null;
}
public DataReply(DataReplyStatus status)
{
Status = status;
Messages = null;
}
public DataReply(DataReplyStatus status, string code, string text)
{
Status = status;
Messages = new [] { new DataReplyMessage { Code = code, Text = text } };
}
public DataReply(DataReplyStatus status, DataReplyMessage message)
{
Status = status;
if (message != null)
Messages = new [] { message };
}
public DataReply(DataReplyStatus status, DataReplyMessage[] messages)
{
Status = status;
if (messages?.Length > 0)
Messages = messages;
}
public DataReply(string message)
{
Status = DataReplyStatus.Ok;
Messages = new [] { new DataReplyMessage { Text = message } };
}
public static DataReplyStatus? ParseStatus (string statusCode)
{
if (IsNullOrWhiteSpace(statusCode))
return null;
DataReplyStatus status;
if (Enum.TryParse(statusCode, true, out status))
return status;
throw new InvalidCastException(
$"Cannot cast string '{statusCode}' to DataReplyStatus Enum. " +
$"Available values: {Join(", ", Enum.GetNames(typeof(DataReplyStatus)))}");
}
}
[DataContract]
public class DataReply: DataReply {
[DataMember(EmitDefaultValue = false)]
public TData Data { get; set; }
public DataReply(TData data)
{
Data = data;
}
public DataReply()
{
Data = default(TData);
}
public DataReply(DataReplyStatus status, string code, string text, TData data)
:base(status, code, text)
{
Data = data;
}
public DataReply(DataReplyStatus status, TData data) :base(status)
{
Data = data;
}
public DataReply(DataReplyStatus status) :base(status)
{
Data = default(TData);
}
public DataReply(DataReplyStatus status, string code, string text)
:base(status, code, text)
{
Data = default(TData);
}
public DataReply(DataReplyStatus status, DataReplyMessage replyMessage)
:base(status, replyMessage)
{
Data = default(TData);
}
public DataReply(DataReplyStatus status, DataReplyMessage[] replyMessages)
:base(status, replyMessages)
{
Data = default(TData);
}
}
这Enum包含我发现在我的项目中有用的状态,并且没有限制来减少或扩展它。
状态的含义:
代码
用法
Ok
一切都按预期执行时的默认状态
Fail
当未达到查询目标时
Missing
当查询没有找到带有Id参数的记录时
Validation
当查询发现对数据完整性的威胁时
Concurrency
当两个或多个用户同时更新同一条记录时
Denial
在数据库中计算数据访问时,用户无权查看请求的数据,您想告知用户原因
Error
所有意外错误和异常
数据回复消息这些消息用于传递有关异常情况的附加信息。
DataReplyMessage 具有以下属性:
代码
用法
Code
string消息的标识符
Id
数据库中导致异常情况的问题记录的整数标识符
Text
用于日志或其他需要的任何人类可读信息
Value
导致异常情况的值
该DataReply对象有一个数组DataReplyMessages,足以描述任何类型的例外情况细节。
如何使用DataReplyMessages?想象一下,用户提交了一个包含许多字段的表单。客户端验证没有发现错误,但服务器验证发现了错误。
例如,服务器验证发现登录名和电子邮件不是唯一的。然后DataReply将有Status=DataReplyStatus.Validation并且一个数组DataReplyMessages将包含两个项目:
Code
ID
Text
Value
NON_UNIQUE_LOGIN
15
登录名已存在
Admin
NON_UNIQUE_EMAIL
15
电子邮件已经存在
admin@mail.com
数据服务能够记录此异常,并且UI能够处理它并将此信息用于错误突出显示和适当的操作。
本DataReplyMessage类有四个属性,但只有Code是强制性的。所以如果用[DataMember(EmitDefaultValue = false)]装饰其他属性,它们将不会被序列化为JSON。
通过步骤 第1步在数据库中,找出一个例外情况并输出必要的数据。
数据库操作可能会引发错误并在C#代码中抛出SqlException。SQL Server raiserror命令能够输出ErrorNumber, ErrorMessage, ServerState。这很好,但还不够。很多时候,客户想知道关于错误的更多细节。
为了以某种DataReply格式输出错误详细信息,需要为DataMessages创建一个特殊的用户定义表类型。
create type dbo.DataReplyMessageTableType as table
(
Code varchar(50) not null ,
[Text] nvarchar(4000) null ,
Id bigint null ,
[Value] sql_variant null ,
unique (Code, Id)
);
然后在存储过程中,我们可以输出异常情况的状态及其详细信息。例如,这里是检查数据并发性和有效性的SaveUser存储过程的一部分:
declare
@UserId int ,
@Login varchar(20) ,
@Name nvarchar(50) ,
@Email varchar(50) ,
@Concurrency varchar(20) = 'Concurrency',
@Validation varchar(20) = 'Validation',
@DataReplyStatus varchar(20) ,
@DataReplyMessages dbo.DataReplyMessageTableType;
begin transaction;
if exists -- concurrency
(
select * from dbo.Users u with (tablockx, holdlock)
inner join @User t on t.Id = u.Id and t.[RowVersion] u.[RowVersion]
)
begin
select DataReplyStatus = @Concurrency;
rollback transaction;
return;
end
begin -- validation
begin -- check User.Login uniqueness
select top 1
@UserId = u.Id,
@Login = u.[Login]
from
dbo.Users u
inner join @User t on t.[Login] = u.[Login] and t.Id u.Id;
if @Login is not null
begin
set @DataReplyStatus = @Validation;
insert into @DataReplyMessages
select Code ='NON_UNIQUE_LOGIN', 'Login is not unique', @UserId, @Login;
end;
end;
begin -- check User.Email uniqueness
select top 1
@UserId = u.Id,
@Email = u.Email
from
dbo.Users u
inner join @User t on t.Email = u.Email and t.Id u.Id
if @Email is not null
begin
set @DataReplyStatus = @Validation;
insert into @DataReplyMessages
select Code ='NON_UNIQUE_EMAIL', 'User email is not unique', @UserId, @Email;
end;
end;
select DataReplyStatus = @DataReplyStatus;
if @DataReplyStatus is not null
begin
select * from @DataReplyMessages;
rollback transaction;
return;
end
end;
-- save the user
-- output the saved user
请注意特殊情况的输出模式:
select DataReplyStatus = @DataReplyStatus;
if @DataReplyStatus is not null
begin
select * from @DataReplyMessages;
rollback transaction;
return;
end
首先是状态输出。如果状态不为空,则消息输出,回滚并返回。
第2步在存储库中,识别异常情况并读取有关它的数据
在存储库方法中,我们遵循上述输出模式,从存储过程中抛出Status和 Messages的DataReplyException:
public User SaveUser(User user)
{
return GetByCommand(cmd =>
{
cmd.UseProcedure("dbo.SaveUser");
cmd.AddTableRowParam("@User", user);
return cmd.GetByReader(dr =>
{
var statusCode = dr.ReadTo(getNextResult: false);
var dataReplyStatus = DataReply.ParseStatus(statusCode);
if (dataReplyStatus != null )
{
if (dr.NextResult())
throw new DataReplyException(dataReplyStatus.Value,
dr.ReadToArray());
throw new DataReplyException(dataReplyStatus.Value);
}
dr.NextResult();
var savedUser = reader.ReadTo()
return savedUser;
});
});
}
上面的存储库方法是用Artisan.Orm ADO.NET扩展方法编写的。但是可以重写代码以使用常规的ADO.NET方法。
第 3 步在存储库中,抛出异常,以便数据服务可以以C#良好实践方式处理它。
在上面的代码示例DataReplyException中是具有两个附加属性,自定义异常Status和Messages:
public class DataReplyException: Exception
{
public DataReplyStatus Status { get; } = DataReplyStatus.Error;
public DataReplyMessage[] Messages { get; set; }
public DataReplyException(DataReplyStatus status, DataReplyMessage[] messages)
{
Status = status;
Messages = messages;
}
}
所以SaveUser存储库方法:
- 在正常情况下返回一个User对象
- 在预期的异常情况下返回DataReplyException状态和消息
- 在意外的例外情况下返回常规SqlException
在数据服务中,获取正常数据或捕获异常,并创建通用数据回复。
数据服务是一个层,在该层中,存储库中的所有异常都被拦截、记录并转换为Web API控制器的适当格式。
数据服务是来自存储库方法的数据用DataReply。
public DataReply SaveUser(User user)
{
try
{
var user = repository.SaveUser(user);
return new DataReply(user);
}
catch (DataReplyException ex)
{
// log exception here, if necessary
return new DataReply(ex.Status, ex.Messages);
}
catch (Exception ex)
{
var dataReplyMessages = new []
{
new DataReplyMessage { Code = "ERROR_MESSAGE" , Text = ex.Message },
new DataReplyMessage { Code = "STACK_TRACE" ,
Text = ex.StackTrace.Substring(0, 500) }
};
// log exception here, if necessary
return new DataReply(DataReplyStatus.Error, dataReplyMessages);
}
}
上述SaveUser方法:
- 在正常情况下,返回一个DataReply带有Status=DataReplyStatus.Ok和Data=User的对象;
- 在预期的特殊情况下,返回DataReply带有Status来自DataReplyStatus Enum列表和Messages来自存储过程;
- 在意外的异常情况下,返回一个DataReply对象带有Status=DataReplyStatus.Error 并且Messages包含原始异常Message和StackTrace。
当然,发送StackTrace到Web客户端是一个坏主意,您永远不应该在生产阶段这样做,但在开发阶段——这是有用的。
第 5 步在ASP.NET Web API控制器中,将数据响应序列化为JSON格式。
ASP.NET Web API 控制器中的SaveUser方法如下所示:
[HttpPost]
public DataReply SaveUser(User user)
{
using (var service = new DataService())
{
return service.SaveUser(user);
}
}
由于DataReply的Data和Messages属性的注释属性[DataMember(EmitDefaultValue = false)],如果它们是Null,它们被省略。所以:
- 在正常情况下,JSON字符串是:
{
"status" : "ok",
"data" : {"id":1,"name":"John Smith"}
}
- 在使用Status=DataReplyStatus.Validation的预期例外情况下,JSON字符串为:
{
"status" : "validation",
"messages" : [
{"code":"NON_UNIQUE_LOGIN", "text":"Login is not unique",
"id":"1","value":"admin"},
{"code":"NON_UNIQUE_EMAIL", "text":"User email is not unique",
"id":"1","value":"admin@mail.com"}
]
}
- 在带有Status=DataReplyStatus.Error并包含原始异常Message和StackTrace的意外异常Messages情况下,JSON字符串为:
{
"status": "error",
"messages" : [
{"code":"ERROR_MESSAGE", "text":"Division by zero"},
{"code":"STACK_TRACE", "text":"Tests.DAL.Users.Repository.
c__DisplayClass8_0.b__0(SqlCommand cmd) ..."}
]
}
在Web客户端数据服务中,获取JSON数据、定义数据回复的状态、采取适当的操作。
下面是Web客户端dataService的JavaScript和AngularJs代码示例。因为所有的数据请求和回复都经过单一的dataService及其方法,所以使用统一的方法很容易处理某些状态的数据回复。
(function () {
"use strict";
angular.module("app")
.service('dataService', function ( $q , $http ) {
var error = {status : "error"};
var allowedStatuses =
["ok", "fail", "validation", "missing", "concurrency", "denial"];
this.save = function (url, savingObject) {
var deferred = $q.defer();
$http({
method: 'Post',
url: url,
data: savingObject
})
.success(function (reply, status, headers, config) {
if ($.inArray((reply || {}).status, allowedStatuses) > -1) {
deferred.resolve(angular.fromJson(reply) );
} else {
showError(reply, status, headers, config);
deferred.resolve(error);
}
})
.error(function (data, status, headers, config) {
showError(data, status, headers, config);
deferred.resolve(error);
});
return deferred.promise;
};
function showError(data, status, headers, config) {
// inform a user about unexpected exceptional case
};
});
})();
在SPA控制器中,获取数据回复,定义数据回复的状态,采取适当的行动。
最后,这里是如何在Web客户端控制器中处理DataReply的示例:
function save() {
userService.save('api/users' $scope.user)
.then(function (dataReply) {
if (dataReply.status === 'ok') {
var savedUser = dataReply.data;
$scope.user = savedUser;
}
else if (dataReply.status === 'validation' ){
for (var i = 0; i < dataReply.messages.length; i++) {
if (dataReply.messages[i].code === 'NON_UNIQUE_LOGIN') {
// highlight the login UI control
}
else if (dataReply.messages[i].code === 'NON_UNIQUE_EMAIL') {
// highlight the email UI control
}
}
}
else if (dataReply.status === 'concurrency' ){
// message about concurrency
}
});
}
这个DataReply想法是由于在复杂对象图保存期间迫切需要将有关异常情况的更多详细信息传输到Web客户端。
在保存复杂对象时,其中的任何部分都可能出现不一致或其他问题。一次收集和交付有关数据的所有可能问题的任务需要适当的传输。DataReply成为这样的解决方案。
这个想法在几个实际项目中尝试过,可以说证明了它的普遍性、有效性和生命权。
Artisan.Orm中的数据回复DataReply,DataReplyStatus Enum,DataReplyMessages和DataReplyException现在是Artisan.Orm的一部分。
如果您对该项目感兴趣,请访问Artisan.Orm GitHub页面及其文档wiki。
Artisan.Orm也可用作NuGet 包。
关于源代码随附的存档包含在Visual Studio 2015中创建的GitHub Artisan.Orm解决方案副本,其中包含三个项目:
- Artisan.Orm——包含Artisan.Orm类的DLL项目
- Database——SSDT项目为SQL Server 2016创建数据库,为测试提供数据
- Tests——带有代码使用示例的测试项目
为了安装数据库并运行测试,请将文件Artisan.publish.xml和App.config中的连接字符串更改为您的连接字符串。
https://www.codeproject.com/Articles/1181182/Artisan-Way-of-Data-Reply