پیاده سازی Audit trail و نسخه بندی داده ها با استفاده از MVC

شنبه 6 خرداد 1396

در این مقاله ، توضیحاتی در مورد نحوه ی پیاده سازی یک سیستم که از audit-trail استفاده می کند ، به شما ارائه داده خواهد شد. سیستمی که در آن می توانید تمامی عملیات هایی که در یک بازه زمانی مشخص ، بر روی داده ها انجام می شود را همراه با جزئیات دقیق مشاهده کنید.

 پیاده سازی Audit trail  و نسخه بندی داده ها با استفاده از MVC

در یک دامنه ی مشخص، نیاز متداولی به پیاده سازی داده ها وجود دارد.یک مثال خوب از این موضوع می تواند یک برنامه ی رکوردهای پزشکی باشد، که داده ها در آن بسیار حیاتی و مهم هستند، و هر تغییری روی این داده ها هم می تواند موجب جریمه های قانونی شود و هم می تواند عواقبی بر سلامتی بیماران داشته باشد. این مقاله یک پیاده سازی ساده ولی موثر از یک سیستم نسخه بندی و SQL را ارائه می کند.برای ذخیره سازی داده ها در این سیستم از پایگاه داده ی #C با استفاده از audit-trail استفاده می کنیم.

تنظیم برخی از موارد:

تنظیم SQL 

برای تنظیم و تست، ما دو جدول در پایگاه داده ایجاد می کنیم، که یکی اطلاعات "Person" را نگهداری می کند، و جدول دیگر، اطلاعات "Audit trail / version" را ذخیره می کند:

Person "SampleData"

ID    int    
FirstName    nvarchar(10)    
LastName    nvarchar(10)  
DateOfBirth    date  
Deleted    bit

در این جدول که به صورت نمونه ساخته شده است، ما با استفاده از فیلد  "deleted" ، مشخص می کنیم که یک رکورد پاک شده است یا هنوز موجود است. از دیدگاه مدیریت داده ای ، این کار می تواند به شیوه ی بهتری نیز انجام بشود. 

داده های "Audit trail"

ID    int    
KeyFieldID    int    
AuditActionTypeENUM    int   
DateTimeStamp    datetime    
DataModel    nvarchar(100)   
Changes    nvarchar(MAX)    
ValueBefore    nvarchar(MAX)   
ValueAfter    nvarchar(MAX)

در جدول  audit trail ، ما از فیلد ها به شرح زیر استفاده می کنیم:

"KeyFieldID" : یک ارتباط با Person-SampleData.ID را ایجاد و نگهداری می کند.

 "AuditActionTypeENUM" این فیلد برای ما نوع رکورد audit را مشخص می کند. (create,edit,delete..)

"DateTimeStamp"  زمان رخداد را برای ما مشخص می کند.

"DataModel"  نام Data-Model/View-Model ای است که ما بر روی آن در حال انجام تغییرات هستیم. 

"Changes" یک متن از نوع XML یا JSON که میزان تغییراتی که داده قبلی با داده جدید دارد را نگه می دارد.

"ValueBefore/ValueAfter" داده های از نوع  XML/JSON که قبل و بعد از تغییر رکورد وجود دارند را ذخیره می کند. 

ValueBefore/After به صورت اختیاری است، می توانید بر اساس میزان پیچیدگی سیستم ، این فیلد را مورد استفاده قرار بدهید. این فیلد شما را قادر می کند تا بتوانید در سطح پایه، اطلاعات خودتان را بازیابی و یا دوباره سازی کنید.

اصول پایه ای

برای تست کردن سیستمی که طراحی کردیم، ما یک برنامه ی MVC  ساده با استفاده از  Entity Framework  ایجاد کردیم. ما تعدادی controller  و data model method هایی را طراحی کردیم تا بتوانیم با استفاده از آن ها، عملیات crud  را انجام بدهیم.

view model هایی که استفاده کردیم به شرح زیر هستند:

public class SampleDataModel
    {
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string lastname { get; set; }
        public DateTime DateOfBirth { get; set; }
        public bool Deleted { get; set; }
...
}

Controllers

public ActionResult Edit(int id)
        {
            SampleDataModel SD = new SampleDataModel();
            return View(SD.GetData(id));
        }

public ActionResult Create()
        {
            SampleDataModel SD = new SampleDataModel();
            SD.ID = -1; // indicates record not yet saved
            SD.DateOfBirth = DateTime.Now.AddYears(-25);
            return View("Edit", SD);
        }

public void Delete(int id)
        {
            SampleDataModel SD = new SampleDataModel();
            SD.DeleteRecord(id);
        }

public ActionResult Save(SampleDataModel Rec)
        {
            SampleDataModel SD = new SampleDataModel();
            if (Rec.ID == -1)
            {
                SD.CreateRecord(Rec);
            }
            else
            {
                SD.UpdateRecord(Rec);
            }
            return Redirect("/");
        }

متدهای CRUD

public void CreateRecord(SampleDataModel Rec)
        {

            AuditTestEntities ent = new AuditTestEntities();
            SampleData dbRec = new SampleData();
            dbRec.FirstName = Rec.FirstName;
            dbRec.LastName = Rec.lastname;
            dbRec.DateOfBirth = Rec.DateOfBirth;
            ent.SampleData.Add(dbRec);
            ent.SaveChanges(); // save first so we get back the dbRec.ID for audit tracking
       }

public bool UpdateRecord(SampleDataModel Rec)
        {
            bool rslt = false;
            AuditTestEntities ent = new AuditTestEntities();
            var dbRec = ent.SampleData.FirstOrDefault(s => s.ID == Rec.ID);
            if (dbRec != null) {
                dbRec.FirstName = Rec.FirstName;
                dbRec.LastName = Rec.lastname;
                dbRec.DateOfBirth = Rec.DateOfBirth;
                ent.SaveChanges();

                rslt = true;

            }
            return rslt;
        }

public void DeleteRecord(int ID)
        {
            AuditTestEntities ent = new AuditTestEntities();
            SampleData rec = ent.SampleData.FirstOrDefault(s => s.ID == ID);
            if (rec != null)
            {
                rec.Deleted = true;
                ent.SaveChanges();
            }
        }

برای راحتی کار، ما از bootstrap  که به صورت پیش فرض بر روی MVC  قرار دارد استفاده می کنیم. 



index view با استفاده از قاعده ی  MVC Razor  نوشته شده است و با استفاده از bootstrap نمایش داده شده است. همچنین سه action button نیز در کنار هر فیلد وجود دارند که آن ها را در تصویر می بینید.

می خواهیم فیلد "Deleted" را دوباره فراخوانی کنیم. برای این کار ما controller  و model مربوط به آن را برای بارگذاری داده ها فراخوانی می کنیم، و یک لیست از رکورد هایی که مقدار Deleted آنها false یا true باشد برمیگردانیم.

public List<SampleDataModel> GetAllData(bool ShowDeleted)
        {
            List<SampleDataModel> rslt = new List<SampleDataModel>();
            AuditTestEntities ent = new AuditTestEntities();
            List<SampleData> SearchResults = new List<SampleData>();

            if (ShowDeleted)
                SearchResults = ent.SampleData.ToList();
            else SearchResults = ent.SampleData.Where(s => s.Deleted == false).ToList();

            foreach (var record in SearchResults)
            {
                SampleDataModel rec = new SampleDataModel();
                rec.ID = record.ID;
                rec.FirstName = record.FirstName;
                rec.lastname = record.LastName;
                rec.DateOfBirth = record.DateOfBirth;
                rec.Deleted = record.Deleted;
                rslt.Add(rec);
            }
            return rslt;
        }

زمانی که از قاعده ی Razor  استفاده می کنیم، می توانیم رنگ فیلد هایی که در آن ، رکورد های پاک شده را وجود دارد را کمی تغییر بدهیم :

<table  class='table table-condensed' >
        <thead></thead>
                   @foreach (var rec in Model)
                   {
                  <tr id="@rec.ID" @(rec.Deleted == false ? String.Empty : "class=alert-danger" )>                    

                       <td><a href="/home/edit/@rec.ID">Edit</a> 
                       <a href="#" onClick="DeleteRecord(@rec.ID)">Delete</a> </td>
                            <td>
                                @rec.FirstName
                            </td>
                            <td>
                                @rec.lastname
                            </td>
                            <td>
                                @rec.DateOfBirth.ToShortDateString()
                            </td>
                            <td><a href="#" onClick="GetAuditHistory(@rec.ID)">Audit</a></td>
                        </tr>
                }
</table>

خروجی کار به صورت زیر خواهد بود:

بررسی 

یک بار که ما عمل scaffolding  را پیاده سازی کردیم، می توانیم بخشauditing را نیز انجام بدهیم. مفهوم این کار ساده است، قبل از این که ما داده ها را برای تغییر در پایگاه داده ارسال کنیم، ما یک دانش "before"  و  "after" از وضعیت داده ها داریم. (دانش قبلی/بعدی) بخاطر این که ما در حال استفاده از زبان #C هستیم، می توانیم از reflection  برای ارزیابی شی داده ا ی که در پایگاه داده داریم استفاده کنیم و آن را با داده ای که در حال فرستادن (Post) از طریق صفحه هستیم مقایسه کنیم و تفاوت بین این دو بخش را ببینیم. 

ما از nuget package Compare net objects برای این کار استفاده می کنیم. این پکیج، شی ها را به صورت بازگشتی با هم مقایسه می کند که به این ترتیب می توانیم شی هایی با ساختار پیچیده را نیز مدیریت کنیم. این پکیج، بسیار کاربردی و مفید است و تا میزان بسیار زیادی به صرفه جویی در زمان کمک می کند. 

با استفاده از CompareObjects ، کد اصلی که اطلاعات audit  را فراهم می کند را در اختیار داریم. این اطلاعات به درون پایگاه داده نیز فرستاده می شوند. 

در متد  "CreateAuditTrail" ، ما پارامتر های زیر را ارسال می کنیم:

AuditActionType = Create/Delete/Update... 

KeyFieldID = لینکی به جدول این audit  است

OldObject / NewObject = دیتابیس موجود و همچنین ViewModel جدیدی که داده ها در درون آن قرار دارند. (قبل از این که این داده ها در پایگاه داده ذخیره شوند.)

public void CreateAuditTrail (AuditActionType Action, int KeyFieldID, Object OldObject, Object NewObject)

اولین کاری که ما درون متد انجام می دهیم، مقایسه ی دو شی و پیدا کردن تفاوت بین آن ها است. برای این که بتوانید همه ی تفاوت هایی که بین این دوشی وجود دارند را ببینید، باید همان طور که در کد زیر می بینید، تعداد تفاوت ها را بر حسب نیازتان تعریف کنید. ما در این مقاله ، آن را بر روی 99 قرار داده ایم. 

// get the differance
CompareLogic compObjects = new CompareLogic();
compObjects.Config.MaxDifferences = 99;

قدم بعدی، مقایسه شی ها است، جایی که تفاوت های آن ها نیز تشخیص داده می شود. 

ComparisonResult compResult = compObjects.Compare(OldObject, NewObject);
List<AuditDelta> DeltaList = new List<AuditDelta>();

برای این که تغییرات را ذخیره کنیم، ما از دو کلاس helper  استفاده کرده ایم. "AuditDelta" که تفاوت های بین دو وضعیت field-level-value را باز می گرداند(قبل و بعد از ارسال) . و  "AuditChange" که یک ترتیب کلی برای تغییرات است. به عنوان مثال، فرض کنید که ما یک رکورد با تغییرات زیر داریم :

در این مورد، ما می توانیم یک AuditChange داشته باشیم، با یک DateTimeStamp از زمان جاری ، و دو  change delta، یکی برای تغییر نام ، و دیگری برای تغییر نام خانوادگی. 

کلاس های زیر تغییرات و Delta ها را نشان می دهند:

public class AuditChange {
   public string DateTimeStamp { get; set; }
    public AuditActionType AuditActionType { get; set; }
    public string AuditActionTypeName { get; set; }
    public List<AuditDelta> Changes { get; set; }
    public AuditChange()
    {
        Changes = new List<AuditDelta>();
    }
}

public class AuditDelta {
    public string FieldName { get; set; }
    public string ValueBefore { get; set; }
    public string ValueAfter { get; set; }
}

یک بار که CompareObjects  استفاده بشود، ما می توانیم نتایج را ارزیابی کنیم و جزئیاتی که نیاز داریم را استخراج کنیم. 

foreach (var change in compResult.Differences)
            {
                AuditDelta delta = new AuditDelta();
                if (change.PropertyName.Substring(0, 1) == ".")
                    delta.FieldName = change.PropertyName.Substring(1, change.PropertyName.Length - 1);
                delta.ValueBefore = change.Object1Value;
                delta.ValueAfter = change.Object2Value;
                DeltaList.Add(delta);
            }

زمانی که لیستی از delta ها در اختیار داشته باشیم، می توانیم آن ها را در درون پایگاه داده ذخیره کنیم، یا لیستی از آن ها را به صورت serialize شده دربیاوریم. در این مقاله، ما از JSON.net برای Serialize  استفاده می کنیم :

AuditTable audit = new AuditTable();
 audit.AuditActionTypeENUM = (int)Action;
 audit.DataModel = this.GetType().Name;
 audit.DateTimeStamp = DateTime.Now;
 audit.KeyFieldID = KeyFieldID;
 audit.ValueBefore = JsonConvert.SerializeObject(OldObject);
 audit.ValueAfter = JsonConvert.SerializeObject(NewObject);
 audit.Changes = JsonConvert.SerializeObject(DeltaList);

 AuditTestEntities ent = new AuditTestEntities();
 ent.AuditTable.Add(audit);
 ent.SaveChanges();

هر بار که ما تغییری بر روی داده ها انجام می دهیم، نیاز داریم تا متد CreateAuditTrail  را فراخوانی کنیم و پارامترهایی مثل نوع عملیات (ایجاد،حذف و ویرایش)یا پارامترهایی مثل مقادیر قبل و بعد از تغییرات را به این متد ارسال کنیم.

در UpdateRecord ما در رکورد جدیدیک پارامتر می فرستیم و داده های قدیمی را بازیابی می کنیم و هر دوی این اطلاعات را به متد CreateAuditTrail  به عنوان شی های generic ارسال می کنیم. 

public bool UpdateRecord(SampleDataModel Rec)
{
    bool rslt = false;
    AuditTestEntities ent = new AuditTestEntities();
    var dbRec = ent.SampleData.FirstOrDefault(s => s.ID == Rec.ID);
    if (dbRec != null) {
        // audit process 1 - gather old values
        SampleDataModel OldRecord = new SampleDataModel();
        OldRecord.ID = dbRec.ID; // copy data from DB to "OldRecord" ViewModel
        OldRecord.FirstName = dbRec.FirstName;
        OldRecord.lastname = dbRec.LastName;
        OldRecord.DateOfBirth = dbRec.DateOfBirth;
        // update the live record
        dbRec.FirstName = Rec.FirstName;
        dbRec.LastName = Rec.lastname;
        dbRec.DateOfBirth = Rec.DateOfBirth;
        ent.SaveChanges();

        CreateAuditTrail(AuditActionType.Update, Rec.ID, OldRecord, Rec);

        rslt = true;
    }
    return rslt;
}

اگر در موردی، داده های قبل از تغییر را نداشتیم، کافی است تا یک شی خالی به متد ارسال کنیم. 

public void CreateRecord(SampleDataModel Rec)
{

    AuditTestEntities ent = new AuditTestEntities();
    SampleData dbRec = new SampleData();
    dbRec.FirstName = Rec.FirstName;
    dbRec.LastName = Rec.lastname;
    dbRec.DateOfBirth = Rec.DateOfBirth;
    ent.SampleData.Add(dbRec);
    ent.SaveChanges(); // save first so we get back the dbRec.ID for audit tracking
    SampleData DummyObject = new SampleData(); 

    CreateAuditTrail(AuditActionType.Create, dbRec.ID, DummyObject, dbRec);

}

public void DeleteRecord(int ID)
{
    AuditTestEntities ent = new AuditTestEntities();
    SampleData rec = ent.SampleData.FirstOrDefault(s => s.ID == ID);
    if (rec != null)
    {
        SampleData DummyObject = new SampleData();
        rec.Deleted = true;
        ent.SaveChanges();
        CreateAuditTrail(AuditActionType.Delete, ID, rec, DummyObject);
    }
}

Hansel و Gretel

حالا ما داریم داده های audit  را نیز به درون پایگاه داده می فرستیم، ما نیاز داریم در این بخش ، مسیری را که در حال طی آن هستیم، به کاربر نشان بدهیم.

در سمت سرور ، ما یک متد ایجاد می کنیم که برای  record-id داده شده ، audit-history را استخراج می کند و داده ها را مطابق آخرین تغییرات ، مرتب می کند. 

public List<AuditChange> GetAudit(int ID)
{
    List<AuditChange> rslt = new List<AuditChange>();
    AuditTestEntities ent = new AuditTestEntities();
    var AuditTrail = ent.AuditTable.Where(s => s.KeyFieldID == ID).OrderByDescending(s => s.DateTimeStamp);
    var serializer = new XmlSerializer(typeof(AuditDelta));
    foreach (var record in AuditTrail)
    {
        AuditChange Change = new AuditChange();
        Change.DateTimeStamp = record.DateTimeStamp.ToString();
        Change.AuditActionType = (AuditActionType)record.AuditActionTypeENUM;
        Change.AuditActionTypeName = Enum.GetName(typeof(AuditActionType),record.AuditActionTypeENUM);
        List<AuditDelta> delta = new List<AuditDelta>();
        delta = JsonConvert.DeserializeObject<List<AuditDelta>>(record.Changes);
        Change.Changes.AddRange(delta);
        rslt.Add(Change);
    }
    return rslt;
}

همچنین ما یک controller method ایجاد کرده ایم تا این داده ها را به صورت Json ارسال کند.

public JsonResult Audit(int id)
{
    SampleDataModel SD = new SampleDataModel();
    var AuditTrail = SD.GetAudit(id);
    return Json(AuditTrail, JsonRequestBehavior.AllowGet);
}

در سمت کاربر ، ما یک modal popup به وسیله ی bootstrap  ایجاد کرده ایم که شامل یک DIV  به نام "audit" است که ما داده ها را در درون آن تزریق می کنیم:

<div id="myModal" class="modal fade">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                <h4 class="modal-title">Audit history</h4>
            </div>
            <div class="modal-body">
                <div id="audit"></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>

در درون هر سطر از داده ، یک تابع جاوا اسکریپت است که کد سمت سرور را با استفاده از AJAX فراخوانی می کند. 

<a href="#" onClick="GetAuditHistory(@rec.ID)">Audit</a>

کد جاوا اسکریپت ، controller  سمت سرور را فراخوانی می کند ،  record ID  سطر انتخاب شده را به آن پاس می دهد و یک آرایه ی JSON  دریافت می کند. و آن را در داخل یک فرم modal به شکل زیبایی به نمایش می گذارد. 

function GetAuditHistory(recordID) {
    $("#audit").html("");

    var AuditDisplay = "<table class='table table-condensed' cellpadding='5'>";
    $.getJSON( "/home/audit/"+ recordID, function( AuditTrail ) {

        for(var i = 0; i < AuditTrail.length; i++ )
        {
            AuditDisplay = AuditDisplay + "<tr class='active'><td colspan='2'>Event date: " + AuditTrail[i].DateTimeStamp + "</td>";
            AuditDisplay = AuditDisplay + "<td>Action type: " + AuditTrail[i].AuditActionTypeName + "</td></tr>";
            AuditDisplay = AuditDisplay + "<tr class='text-warning'><td>Field name</td><td>Before change</td><td>After change</td></tr>";
            for(var j = 0; j < AuditTrail[i].Changes.length; j++ )
            {
                AuditDisplay = AuditDisplay + "<tr>";
                AuditDisplay = AuditDisplay + "<td>" + AuditTrail[i].Changes[j].FieldName + "</td>";
                AuditDisplay = AuditDisplay + "<td>" + AuditTrail[i].Changes[j].ValueBefore + "</td>";
                AuditDisplay = AuditDisplay + "<td>" + AuditTrail[i].Changes[j].ValueAfter + "</td>";
                AuditDisplay = AuditDisplay + "</tr>";
            }
        }
        AuditDisplay = AuditDisplay + "</table>">

        $("#audit").html(AuditDisplay);
        $("#myModal").modal('show');


    });
}

در اینجا ما فرآیندهای مربوط به یک داده را نمایش داده ایم. 

خلاصه

این مقاله ، توضیحات مفیدی در مورد پیاده سازی یک سیستم  audit-trail در درون #C ارائه داده است. کاربرد اصلی این بخش، برای حفظ امنیت داده های کاربر است و به شما این امکان را می دهد تا فرآیندهای انجام شده بر روی یک داده ها را در طول یک زمان مشخص ببینید. 

آموزش asp.net mvc

فایل های ضمیمه

برنامه نویسان

نویسنده 3355 مقاله در برنامه نویسان

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید