数据回复的工匠方式

目录

介绍

背景

两类异常

通过Http传递状态

七个步骤

数据回复的通用格式

JSON格式的数据回复

C#中的数据回复

数据回复状态

数据回复消息

如何使用DataReplyMessages?

通过步骤

第1步

第2步

第 3 步

第 4 步

第 5 步

第 6 步

第 7 步

结论

Artisan.Orm中的数据回复

关于源代码


介绍

假设我们有一个三层结构的Web应用程序:

  • Web SPA客户端,使用AjaxWeb服务器请求数据
  • Web服务器和ASP.NET应用程序
  • SQL Server数据库

一旦Web客户端请求获取或保存数据,数据回复就会通过如下方式从数据库传回UI

这样一来,各种意外都可能发生。SQL Server上的数据库中可能会出现错误,或者Web服务器上的ASP.NET应用程序中可能会发生异常。最终的接收者,一个网络客户端,应该以某种方式被告知发生了什么:请求是正确执行的还是出错了。

本文试图制作一种方便且通用的数据回复格式,它提供了通过上述管道将异常情况的详细信息从数据库传递到Web客户端的可能性。

背景

开始之前的一些想法和主张。

两类异常

错误和异常可以分为两类:预期的和意外的。

  • 意外异常是代码错误或设备故障的结果,我们希望这些事情永远不会发生在一个完美的词中。
    对此类异常的通常反应:通知用户和管理员有关致命的应用程序错误。
  • 预期异常是由于数据保存不一致、请求不及时或其他用户能够自行解决问题的活动的结果。
    例子:
    • Web服务器或数据库中的数据验证,例如用户登录或电子邮件的唯一性。
    • 数据并发,当两个用户同时编辑数据库中的同一条记录时。
    • 数据丢失,当第一个用户在第二个用户保存记录之前删除它。
    • 数据访问拒绝,当数据回复取决于数据库中计算的用户访问级别时。

通过Http传递状态

Web客户端如何区分发生了什么样的异常?有必要将状态传递给它。

如何将此状态从Web服务器传递到Web客户端?想到的第一个想法是使用http状态代码...

事实证明这个想法毫无价值……这就像要求机长通过用于着陆和起飞的官方无线电频率通知您的妈妈您遇到麻烦,尽管您只是将午餐盒忘在厨房桌子上。

Http状态代码用于通知接收者有关传输状态的信息。浏览器使用该状态做出反应。混合传输状态和数据回复状态迟早会导致我们遇到意外的浏览器反应或无法找到适合您需求的代码的问题。

我猜,更好的想法是制作一种通用的数据回复格式,就像一个包装对象,其中DataStatus是属性。

七个步骤

通过数据回复管道传递异常详情的任务可以分为几个步骤:

  1. 在数据库中,找出一个例外情况并输出必要的数据。
  2. 在存储库中,识别特殊情况并阅读有关它的数据。
  3. 在存储库中,抛出异常,以便数据服务可以以C#良好实践方式处理它。
  4. 在数据服务中,获取正常数据或捕获异常,并创建通用数据回复。
  5. ASP.NET Web API控制器中,将数据回复序列化为JSON格式。
  6. Web客户端数据服务中,获取JSON数据、定义数据回复的状态、采取适当的操作。
  7. SPA控制器中,获取数据回复,定义数据回复的状态,采取适当的行动。

数据回复的通用格式

JSON格式的数据回复

理想的数据包装器或DataReply对象,在ASP.NET Web API控制器将其序列化为JSON后,应具有以下形式:

dataReply: {
    status: "ok",
    data: {...}, 
    messages: [...]
}

因此,用于序列化的C#对象必须具有相同的public属性。经过一系列实验,我找到了C#DataReply类的最佳结构,至少对我来说是这样。

C#中的数据回复

DataReply基类只有两个属性:StatusMessages

派生DataReply<TData>添加了Data属性。

这是DataReply类及其属性的图表:

这是C#代码:

  • DataReplyStatus
  • DataReplyMessage
  • DataReply
  • DataReply<TData>

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<TData>: 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#代码中抛出SqlExceptionSQL Server raiserror命令能够输出ErrorNumberErrorMessageServerState。这很好,但还不够。很多时候,客户想知道关于错误的更多细节。

为了以某种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 MessagesDataReplyException

public User SaveUser(User user)
{
    return GetByCommand(cmd =>
    {
        cmd.UseProcedure("dbo.SaveUser");

        cmd.AddTableRowParam("@User", user);

        return cmd.GetByReader(dr => 
        {
            var statusCode = dr.ReadTo<string>(getNextResult: false);

            var dataReplyStatus = DataReply.ParseStatus(statusCode);

            if (dataReplyStatus != null )
            {
                if (dr.NextResult())
                    throw new DataReplyException(dataReplyStatus.Value, 
                                                 dr.ReadToArray<DataReplyMessage>());

                throw new DataReplyException(dataReplyStatus.Value);
            }

            dr.NextResult();
            
            var savedUser = reader.ReadTo<User>()

            return savedUser;
        });
    });
}

上面的存储库方法是用Artisan.Orm ADO.NET扩展方法编写的。但是可以重写代码以使用常规的ADO.NET方法。

3

在存储库中,抛出异常,以便数据服务可以以C#良好实践方式处理它。

在上面的代码示例DataReplyException中是具有两个附加属性,自定义异常StatusMessages

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

4

在数据服务中,获取正常数据或捕获异常,并创建通用数据回复。

数据服务是一个层,在该层中,存储库中的所有异常都被拦截、记录并转换为Web API控制器的适当格式。

数据服务是来自存储库方法的数据用DataReply

public DataReply<User> SaveUser(User user)
{
    try 
    {
        var user = repository.SaveUser(user);

        return new DataReply<User>(user);
    }
    catch (DataReplyException ex)
    {
        // log exception here, if necessary

        return new DataReply<User>(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<User>(DataReplyStatus.Error, dataReplyMessages);
    }
}

上述SaveUser方法:

  • 正常情况下,返回一个DataReply带有Status=DataReplyStatus.OkData=User的对象;
  • 预期的特殊情况下,返回DataReply带有Status来自DataReplyStatus Enum列表和Messages来自存储过程;
  • 意外的异常情况下,返回一个DataReply对象带有Status=DataReplyStatus.Error 并且Messages包含原始异常MessageStackTrace

当然,发送StackTraceWeb客户端是一个坏主意,您永远不应该在生产阶段这样做,但在开发阶段——这是有用的。

5

ASP.NET Web API控制器中,将数据响应序列化为JSON格式。

 ASP.NET Web API 控制器中的SaveUser方法如下所示:

[HttpPost]
public DataReply<User> SaveUser(User user) 
{ 
    using (var service = new DataService()) 
    {
        return service.SaveUser(user);
    } 
}

由于DataReplyDataMessages属性的注释属性[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并包含原始异常MessageStackTrace意外异常Messages情况下,JSON字符串为:

{
    "status": "error",
    "messages" : [
        {"code":"ERROR_MESSAGE", "text":"Division by zero"},
        {"code":"STACK_TRACE", "text":"Tests.DAL.Users.Repository.
                 <>c__DisplayClass8_0.<SaveUser>b__0(SqlCommand cmd) ..."}
  ]
}       

6

Web客户端数据服务中,获取JSON数据、定义数据回复的状态、采取适当的操作。

下面是Web客户端dataServiceJavaScriptAngularJs代码示例。因为所有的数据请求和回复都经过单一的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
        
        };

    });

})();

7

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中的数据回复

DataReplyDataReplyStatus EnumDataReplyMessagesDataReplyException现在是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.xmlApp.config的连接字符串更改为您的连接字符串。

https://www.codeproject.com/Articles/1181182/Artisan-Way-of-Data-Reply

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值