using System; using System.ComponentModel.DataAnnotations; namespace Test { [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class CompareAttribute : ValidationAttribute { public CompareAttribute(string originalProperty, string confirmProperty) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } protected override ValidationResult IsValid(object value, ValidationContext ctx) { if (value == null) return new ValidationResult("لطفا فیلدها را تکمیل نمائید"); var confirmProperty = ctx.ObjectType.GetProperty(ConfirmProperty); if (confirmProperty == null) throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", ConfirmProperty)); var confirmValue = confirmProperty.GetValue(ctx.ObjectInstance, null) as string; if (string.IsNullOrWhiteSpace(confirmValue)) return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", ConfirmProperty)); var originalProperty = ctx.ObjectType.GetProperty(OriginalProperty); if (originalProperty == null) throw new InvalidOperationException(string.Format("لطفا فیلد {0} را تعریف نمائید", OriginalProperty)); var originalValue = originalProperty.GetValue(ctx.ObjectInstance, null) as string; if (string.IsNullOrWhiteSpace(originalValue)) return new ValidationResult(string.Format("لطفا فیلد {0} را تکمیل نمائید", OriginalProperty)); return originalValue == confirmValue ? ValidationResult.Success : new ValidationResult("مقادیر وارد شده یکسان نیستند"); } } }
public class Personel { [Key] public int PersonelID { get; set; } [MaxLength(15)] public string Name { get; set; } [MaxLength(25)] public string Family { get; set; } [MaxLength(10)] public string CodeMelli { get; set; } }
public class PersonalContext : DbContext { public DbSet<Personel> Personel { get; set; } public override int SaveChanges() { return base.SaveChanges(); } }
Install-Package EntityFramework.BulkInsert-ef6
public ActionResult Insert() { int Counter = 1000; List<Personel> Lst = new List<Personel>(); // شبیه سازی خواندن رکوردها از فایل اکسل for (int i = 0; i < Counter; i++) { Lst.Add(new Personel { CodeMelli = "0000000000", Family = "Karimi", Name = "Mohammad" }); } PersonalContext db = new PersonalContext(); db.BulkInsert(Lst); db.SaveChanges(); return View(); }
public class BaseEntity : IBaseEntity { [JsonIgnore] int Id { get; set; } [JsonIgnore] string? Audit { get; set; } }
public class AuditSourceValues { [JsonProperty("hn")] public string? HostName { get; set; } [JsonProperty("mn")] public string? MachineName { get; set; } [JsonProperty("rip")] public string? RemoteIpAddress { get; set; } [JsonProperty("lip")] public string? LocalIpAddress { get; set; } [JsonProperty("ua")] public string? UserAgent { get; set; } [JsonProperty("an")] public string? ApplicationName { get; set; } [JsonProperty("av")] public string? ApplicationVersion { get; set; } [JsonProperty("cn")] public string? ClientName { get; set; } [JsonProperty("cv")] public string? ClientVersion { get; set; } [JsonProperty("o")] public string? Other { get; set; } }
public class EntityAudit<TEntity> { [JsonProperty("type")] [JsonConverter(typeof(StringEnumConverter))] public EntityEventType EventType { get; set; } [JsonProperty("user", NullValueHandling = NullValueHandling.Include)] public int? ActorUserId { get; set; } [JsonProperty("at")] public DateTime ActDateTime { get; set; } [JsonProperty("sources")] public AuditSourceValues? AuditSourceValues { get; set; } [JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)] public TEntity NewEntity { get; set; } = default!; public string? SerializeJson() { return JsonSerializer.Serialize(this, options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true }); } }
دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفادهی خاصی را از آن داشته باشیم، بهراحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیرهی آن پرداخته میشود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار میشود.
public enum EntityEventType { Create = 0, Update = 1, Delete = 2 }
public interface IAuditSourcesProvider { AuditSourceValues GetAuditSourceValues(); }
public class AuditSourcesProvider : IAuditSourcesProvider { protected readonly IHttpContextAccessor HttpContextAccessor; public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor) { HttpContextAccessor = httpContextAccessor; } public virtual AuditSourceValues GetAuditSourceValues() { var httpContext = HttpContextAccessor.HttpContext; return new AuditSourceValues { HostName = GetHostName(httpContext), MachineName = GetComputerName(httpContext), LocalIpAddress = GetLocalIpAddress(httpContext), RemoteIpAddress = GetRemoteIpAddress(httpContext), UserAgent = GetUserAgent(httpContext), ApplicationName = GetApplicationName(httpContext), ClientName = GetClientName(httpContext), ClientVersion = GetClientVersion(httpContext), ApplicationVersion = GetApplicationVersion(httpContext), Other = GetOther(httpContext) }; } protected virtual string? GetUserAgent(HttpContext httpContext) { return httpContext.Request?.Headers["User-Agent"].ToString(); } protected virtual string? GetRemoteIpAddress(HttpContext httpContext) { return httpContext.Connection?.RemoteIpAddress?.ToString(); } protected virtual string? GetLocalIpAddress(HttpContext httpContext) { return httpContext.Connection?.LocalIpAddress?.ToString(); } protected virtual string GetHostName(HttpContext httpContext) { return httpContext.Request.Host.ToString(); } protected virtual string GetComputerName(HttpContext httpContext) { return Environment.MachineName; } protected virtual string? GetApplicationName(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Name; } protected virtual string? GetApplicationVersion(HttpContext httpContext) { return Assembly.GetEntryAssembly()?.GetName().Version.ToString(); } protected virtual string? GetClientVersion(HttpContext httpContext) { return httpContext.Request?.Headers["client-version"]; } protected virtual string? GetClientName(HttpContext httpContext) { return httpContext.Request?.Headers["client-name"]; } protected virtual string? GetOther(HttpContext httpContext) { return null; } }
حالا برای تامین اطلاعات کلاس EntityAudit کار مشابهی میکنیم. ابتدا اینترفیس IEntityAuditProvider را به صورت زیر تعریف میکنیم:
public interface IEntityAuditProvider { string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null); }
و سپس کلاس EntityAuditProvider را ایجاد میکنیم:
public class EntityAuditProvider : IEntityAuditProvider { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuditSourcesProvider _auditSourcesProvider; #region Constructor Injections public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider) { _httpContextAccessor = httpContextAccessor; _auditSourcesProvider = auditSourcesProvider; } #endregion public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null) { var httpContext = _httpContextAccessor.HttpContext; int? userId; var user = httpContext.User; if (!user.Identity.IsAuthenticated) userId = null; else userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt(); var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues(); var auditJArray = new JArray(); // Update & Delete if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete) { auditJArray = JArray.Parse(previousJsonAudit!); } // Delete => No NewValues if (eventType == EntityEventType.Delete) { newEntity = null; } JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?> { EventType = eventType, ActorUserId = userId, ActDateTime = DateTime.Now, AuditSourceValues = auditSourceValues, NewEntity = newEntity }, new JsonSerializer { NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }); auditJArray.Add(newAuditJObject); return auditJArray.SerializeToJson(true); } }
public class AuditSaveChangesInterceptor : SaveChangesInterceptor { private readonly IEntityAuditProvider _entityAuditProvider; #region Constructor Injections public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider) { _entityAuditProvider = entityAuditProvider; } #endregion public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChanges(eventData, result); } public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = new CancellationToken()) { ApplyAudits(eventData.Context.ChangeTracker); return base.SavingChangesAsync(eventData, result, cancellationToken); } private void ApplyAudits(ChangeTracker changeTracker) { ApplyCreateAudits(changeTracker); ApplyUpdateAudits(changeTracker); ApplyDeleteAudits(changeTracker); } private void ApplyCreateAudits(ChangeTracker changeTracker) { var addedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { if (addedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity); } } } private void ApplyUpdateAudits(ChangeTracker changeTracker) { var modifiedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit); } } } private void ApplyDeleteAudits(ChangeTracker changeTracker) { var deletedEntries = changeTracker.Entries() .Where(x => x.State == EntityState.Deleted); foreach (var modifiedEntry in deletedEntries) { if (modifiedEntry.Entity is IBaseEntity entity) { entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit); } } } }
و سپس آن را به سیستم معرفی میکنیم:
services.AddDbContext<ATADbContext>((serviceProvider, options) => { options .UseSqlServer(...) // Interceptors var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>(); options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider)); });
نمونهی کامل فیلد Audit که در JsonFormatter قرار داده شده است، بعد از ایجاد شدن و یکبار آپدیت و سپس حذف نرم رکورد:
[ { "type":"Create", "user":1, "at":"2020-11-24T23:05:54.2692711+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Farshad" } }, { "type":"Update", "user":1, "at":"2020-11-24T23:06:20.0838188+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":{ "Name":"Edited Farshad" } }, { "type":"Delete", "user":null, "at":"2020-11-24T23:06:28.601837+03:30", "sources":{ "hn":"localhost:44398", "mn":"DESKTOP-N1GAV2U", "rip":"::1", "lip":"::1", "ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36", "an":"Server.Api", "av":"1.0.0.0" }, "newValues":null } ]
ولی روش گفته شده در این مقاله، همین عملیات را به صورت کاملتری و فقط بر روی یک ستون همان جدول انجام میدهد که باعث ذخیرهی دیتای کمتر، یکپارچگی بهتر و دسترسیپذیری و راحتی استفاده از آن میشود.
ObservableCollection در Entity Framework
public abstract class BaseEntity { [ColumnInfo("کد",pWidth:70)] public int Id { get; set; } [ColumnInfo("",pIsVisible:false,pIsEditable:false)] [NotMapped] public bool IsDeleted { get; set; } }
اعتبارسنجی Domain Model
public class Customer { public string Email { get; private set; } public string Name { get; private set; } private Customer(email, name) { Email = email; Name = name; } public static Result<Customer> New(string email, string name, INewCustomerPolicy policy) { var isUnique = policy.IsUnique(email); if (!isUnique) { return Result.Fail<Customer>("Customer with this email already exists."); } var customer = new Customer(email, name); //customer.AddDomainEvent(new CustomerRegistered(customer)); return Result.Ok(customer); } }
public static class CommonExtensionMethods { public static List<SelectListItem> CreateSelectListItem<T>( this List<T> items, object selectedItem = null, bool addChooseOneItem = true, string firstItemText = "انتخاب کنید", string firstItemValue = 0 ) { var modelType = items.First().GetType(); var idProperty = modelType.GetProperty("Id"); var titleProperty = modelType.GetProperty("Title"); if (idProperty is null || titleProperty is null) throw new ArgumentNullException( $"{typeof(T).Name} must have ```Id``` and ```Title``` propeties"); var result = new List<SelectListItem>(); if (addChooseOneItem) result.Add(new SelectListItem(firstItemText, firstItemValue)); foreach (var item in items) { var id = idProperty.GetValue(item)?.ToString(); var text = titleProperty.GetValue(item)?.ToString(); var selected = selectedItem?.ToString() == id; result.Add(new SelectListItem(text, id, selected)); } return result; } }
public class ShowCategory { public int Id { get; set; } public string Title { get; set; } }
public async Task<IActionResult> Add() { var categories = await _categoryService.AllMainCategories(); ViewBag.MainCategories = categories.ToList().CreateSelectListItem(firstItemText: "خودش سر دسته باشد"); return View(); } [HttpPost, ValidateAntiForgeryToken] public async Task<IActionResult> Add(AddCategoryViewModel model) { if (!ModelState.IsValid) { var categories = await _categoryService.AllMainCategories(); ViewBag.MainCategories = categories.ToList() .CreateSelectListItem(model.ParentId, firstItemText: "خودش سر دسته باشد"); ModelState.AddModelError(string.Empty, PublicConstantStrings.ModelStateErrorMessage); return View(model); } await _categoryService.AddAsync(new Category() { Title = model.Title, ParentId = model.ParentId == 0 ? null : model.ParentId }); await _uow.SaveChangesAsync(); return RedirectToAction(nameof(Index)); }
public class EditEmployeeModel { public string Email { get; set; } [CompareProperty("Email", ErrorMessage = "Email and Confirm Email must match")] public string ConfirmEmail { get; set; } }
Webgrid گرید توکار asp.net
mvc 3 است که در سری آموزشهای mvc جناب نصیری به خوبی بررسی شده است . WebGrid از طریق مجموعه ای از خواص امکان استایل دهی
به ستونها و ردیفها را به توسعه دهنده میدهد . اما در این بخش مشکلی وجود دارد که
در ادامه به آن خواهم پرداخت . کدهای زیر را در نظر بگیرید
مدلها :
public class Customer { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string Website { get; set; } public string Phone { get; set; } } public class Customers { public IList<Customer> GetList() { return new List<Customer>() { new Customer() { Id=1, Name="mohsen.d", Email="email@domain.com", Website="domain.com", Phone="213214321" } }; } public IList<Customer> GetEmptyList() { return new List<Customer>(); } }
public class HomeController : Controller { public ActionResult List() { var model = new Customers().GetList(); return View(model); } public ActionResult EmptyList() { var model = new Customers().GetEmptyList(); return View("list", model); } }
تابع کمکی برای ایجاد گرید :
@helper GenerateList(IEnumerable<object> items, List<WebGridColumn> columns) { var grid = new WebGrid(items); <div> @grid.GetHtml( tableStyle: "list", headerStyle: "list-header", footerStyle: "list-footer", alternatingRowStyle: "list-alt", selectedRowStyle: "list-selected", rowStyle: "list-row", htmlAttributes: new { id = "listItems" }, mode: WebGridPagerModes.All, columns: columns ) </div> }
@model IEnumerable<WebGridHeaderStyle.Models.Customer> @{ ViewBag.Title = "List"; } <h2>List</h2> @_List.GenerateList( Model, new List<WebGridColumn>() { new WebGridColumn(){ ColumnName="Id", Header="Id", Style="list-small-field" }, new WebGridColumn(){ ColumnName="Name", Header="Name", Style="list-long-field" }, new WebGridColumn(){ ColumnName="Email", Header="Email", Style="list-mid-field" }, new WebGridColumn(){ ColumnName="Website", Header="Website", Style="list-mid-field" }, new WebGridColumn(){ ColumnName="Phone", Header="Phone", Style="list-mid-field" } } )
خوب چندان بد نیست . با استفاده از استایلهای تعریف شده برای فیلدها و ردیفها ، لیست ساختار مناسبی دارد . اما حالا به Home/EmptyList می رویم :
همانطور که میبینید استایل هایی که برای هر ستون تعریف کرده بودیم اعمال نشده اند. مشکل هم همین
جاست . WebGrid استایل تعریف شده را تنها به ستونهای درون tbody
اعمال میکند و thead از این تنظیمات بی نصیب میماند ( WebGrid از table برای ساختن لیست استفاده میکند ) و در زمانی که رکوردی وجود نداشته باشد فرمت طراحی شده اعمال نمیشود .
در وب ترفندهایی را برای این مشکل پیدا
کردم که اصلا جالب نبودند . در نهایت راه حل زیر به نظرم رسید :
در زمان ساختن
گرید ، استایلهای تعریف شده را در یک فیلد hidden ذخیره و سپس با
استفاده از jquery این استایلها را به ستونهای header اعمال میکنیم .
تابع ساختن فیلد hidden :
@helper SetHeaderColumnsStyle(IEnumerable<WebGridColumn> columns) { var styles = new List<string>(); foreach(var col in columns) { styles.Add(col.Style); } <input id="styles" type="hidden" value="@string.Join("#",styles)" /> }
@SetHeaderColumnsStyle(columns)
<script> $(document).ready(function () { var styles = $("#styles").attr("value").split('#'); var $cols = $("#listItems th"); $cols.each(function (i) { $(this).addClass(styles[i]); }); }); </script>
ماهیت این پایگاه داده وب سرویسی مبتنی بر REST است و فرمت اطلاعاتی که از سرور دریافت میشود، JSON است.
گام اول: باید آخرین نسخه RavenDB را دریافت کنید. همان طور که مشاهده میکنید، ویرایشهای مختلف کتابخانه هایی که برای نسخه Client و همچنین Server طراحی شده است، دراین فایل قرار گرفته است.
برای راه اندازی Server باید فایل Start را اجرا کنید، چند ثانیه بعد محیط مدیریتی آن را در مرورگر خود مشاهده میکنید. در بالای صفحه روی لینک Databases کلیک کنید و در صفحه باز شده گزینه New Database را انتخاب کنید. با دادن یک نام دلخواه حالا شما یک پایگاه داده ایجاد کرده اید. تا همین جا دست نگه دارید و اجازه دهید با این محیط دوست داشتنی و قابلیتهای آن بعدا آشنا شویم.
در گام دوم به Visual Studio میرویم و نحوه ارتباط با پایگاه داده و استفاده از دستورات آن را فرا میگیریم.
گام دوم:
با یک پروژه Test شروع میکنیم که در هر گام تکمیل میشود و میتوانید پروژه کامل را در پایان این پست دانلود کنید.
برای استفاده از کتابخانههای مورد نیاز دو راه وجود دارد:
- استفاده از NuGet : با استفاده از دستور زیر Package مورد نیاز به پروژه شما افزوده میشود.
PM> Install-Package RavenDB -Version 1.0.919
- اضافه کردن کتابخانهها به صورت دستی : کتابخانههای مورد نیاز شما در همان فایلی که دانلود شده بود و در پوشه Client قرار دارند.
کتابخانه هایی را که NuGet به پروژه من اضافه کرد، در تصویر زیر مشاهده میکنید :
با Newtonsoft.Json در اولین بخش بحث آشنا شدید. NLog هم یک کتابخانه قوی و مستقل برای مدیریت Log است که این پایگاه داده از آن بهره برده است.
" دلیل اینکه از پروژه تست استفاده کردم ؛ تمرکز روی کدها و مشاهده تاثیر آنها ، مستقل از UI و لایههای دیگر نرم افزار است. بدیهی است که استفاده از آنها در هر پروژه امکان پذیر است. "
برای شروع نیاز به آدرس Server و نام پایگاه داده داریم که میتوانید در App.config به عنوان تنظیمات نرم افزار شما ذخیره شود و هنگام اجرای نرم افزار مقدار آنها را خوانده و در متغییرهای readonly ذخیره شوند.
<appSettings> <add key="ServerName" value="http://SorousH-HP:8080/"/> <add key="DatabaseName" value="TestDatabase" /> </appSettings>
هنگامی که صفحه Management Studio در مرورگر باز است، میتوانید از نوار آدرس مرورگر خود آدرس سرور را به دست آورید.
[TestClass] public class BeginnerTest { private readonly string serverName; private readonly string databaseName; public BeginnerTest() { serverName = ConfigurationManager.AppSettings["ServerName"]; databaseName = ConfigurationManager.AppSettings["DatabaseName"]; } }
برای برقراری ارتباط با پایگاه داده نیاز به یک شئ از جنس DocumentStore و جهت انجام عملیات مختلف ( ذخیره، حذف و ... ) نیاز به یک شئ از جنس IDocumentSession است. کد زیر، نحوه کار با آنها را به شما نشان میدهد :
[TestClass] public class BeginnerTest { private readonly string serverName; private readonly string databaseName; private DocumentStore documentStore; private IDocumentSession session; public BeginnerTest() { serverName = ConfigurationManager.AppSettings["ServerName"]; databaseName = ConfigurationManager.AppSettings["DatabaseName"]; } [TestInitialize] public void TestStart() { documentStore = new DocumentStore { Url = serverName }; documentStore.Initialize(); session = documentStore.OpenSession(databaseName); } [TestCleanup] public void TestEnd() { session.SaveChanges(); documentStore.Dispose(); session.Dispose(); } }
در طراحی این پایگاه داده از اگوی Unit Of Work استفاده شده است. به این معنی که تمام تغییرات در حافظه ذخیره میشوند و به محض اجرای دستور ;()session.SaveChanges ارتباط برقرار شده و تمام تغییرات ذخیره خواهند شد.
هنگام شروع ( تابع : TestStart ) متغییر session مقدار دهی میشود و در پایان کار ( تابع : TestEnd ) تغییرات ذخیره شده و منابعی که توسط این دو شئ در حافظه استفاده شده است، رها میشود.
البته بر مبنای طراحی شما، دستور ;()session.SaveChanges میتواند پس از انجام هر عملیات اجرا شود.
class User { public int Id { get; set; } public string Name { get; set; } public string Address { get; set; } public int Zip { get; set; }اهی }
[TestMethod] public void Insert() { var user = new User { Id = 1, Name = "John Doe", Address = "no-address", Zip = 65826 }; session.Store(user); }
لحظهی لذت بخشی است...
یکی از روشهای خواندن اطلاعات هم به صورت زیر است:
[TestMethod] public void Select() { var user = session.Load<User>(1); }
تا این جا، سادهترین مثالهای ممکن را مشاهده کردید و حتما در بحث بعد مثالهای جالبتر و دقیقتری را بررسی میکنیم و همچنین نگاهی به جزئیات طراحی و قراردادهای از پیش تعیین شده میاندازیم.
- نسخه بدون کتابخانههای موردنیاز ( 2 مگابایت ) : RavenDBTest_Small.zip
- نسخه کامل ( 15 مگابایت ) : RavenDBTest.zip