NOSQL قسمت سوم
EF Code First #3
ASP.NET MVC #22
روش Controller as در AngularJs
ولی در نهایت همان روش سنتی استفاده از رشتهها، هر چند کمی طولانیتر است، اما بهترین کارآیی و کمترین سربار را هم دارد.
مطلبی هم که من در اینجا عنوان کردم در مورد دیباگ یک سیستم مبتنی بر NHibernate هست و بدیهی است قرار نیست در محیط کاری از آن استفاده شود.
تغییرات کتابخانه DNTFrameworkCore
public interface ICreationTracking { DateTime CreatedDateTime { get; set; } } public interface IModificationTracking { DateTime? ModifiedDateTime { get; set; } }
public interface IHasRowIntegrity { string Hash { get; set; } } public interface IHasRowVersion { byte[] Version { get; set; } }
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot where TKey : IEquatable<TKey> { private readonly List<IDomainEvent> _events = new List<IDomainEvent>(); public IReadOnlyCollection<IDomainEvent> Events => _events.AsReadOnly(); protected virtual void AddDomainEvent(IDomainEvent newEvent) { _events.Add(newEvent); } public virtual void ClearEvents() { _events.Clear(); } }
public static class EventBusExtensions { public static Task TriggerAsync(this IEventBus bus, IEnumerable<IDomainEvent> events) { var tasks = events.Select(async domainEvent => await bus.TriggerAsync(domainEvent)); return Task.WhenAll(tasks); } public static async Task PublishAsync(this IEventBus bus, IAggregateRoot aggregateRoot) { await bus.TriggerAsync(aggregateRoot.Events); aggregateRoot.ClearEvents(); } }
public interface IKeyValueService : IApplicationService { Task SetValueAsync(string key, string value); Task<Maybe<string>> LoadValueAsync(string key); Task<bool> IsTamperedAsync(string key); } public class KeyValue : Entity, IModificationTracking, ICreationTracking, IHasRowIntegrity { public string Key { get; set; } [Encrypted] public string Value { get; set; } public string Hash { get; set; } public DateTime CreatedDateTime { get; set; } public DateTime? ModifiedDateTime { get; set; } }
public class ExceptionOptions { public List<ExceptionMapItem> Mappings { get; } = new List<ExceptionMapItem>(); [Required] public string DbException { get; set; } [Required] public string DbConcurrencyException { get; set; } [Required] public string InternalServerIssue { get; set; } public bool TryFindMapping(DbException dbException, out ExceptionMapItem mapping) { mapping = null; var words = new HashSet<string>(Regex.Split(dbException.ToStringFormat(), @"\W")); var mappingItem = Mappings.FirstOrDefault(a => a.Keywords.IsProperSubsetOf(words)); if (mappingItem == null) { return false; } mapping = mappingItem; return true; } }
"Exception": { "Mappings": [ { "Message": "به دلیل وجود اطلاعات وابسته امکان حذف وجود ندارد", "Keywords": [ "DELETE", "REFERENCE" ] }, { "Message": "یک تسک با این عنوان قبلا در سیستم ثبت شده است", "MemberName": "Title", "Keywords": [ "Task", "UIX_Task_NormalizedTitle" ] } ], "DbException": "امکان ذخیرهسازی اطلاعات وجود ندارد؛ دوباره تلاش نمائید", "DbConcurrencyException": "اطلاعات توسط کاربری دیگر در شبکه تغییر کرده است", "InternalServerIssue": "متأسفانه مشکلی در فرآیند انجام درخواست شما پیش آمده است!" }
_session.UserId _session.UserId<long>() _session.UserId<int>() _session.UserId<Guid>()
public static class UserClaimTypes { public const string UserName = ClaimTypes.Name; public const string UserId = ClaimTypes.NameIdentifier; public const string SerialNumber = ClaimTypes.SerialNumber; public const string Role = ClaimTypes.Role; public const string DisplayName = nameof(DisplayName); public const string BranchId = nameof(BranchId); public const string BranchName = nameof(BranchName); public const string IsHeadOffice = nameof(IsHeadOffice); public const string TenantId = nameof(TenantId); public const string TenantName = nameof(TenantName); public const string IsHeadTenant = nameof(IsHeadTenant); public const string Permission = nameof(Permission); public const string PackedPermission = nameof(PackedPermission); public const string ImpersonatorUserId = nameof(ImpersonatorUserId); public const string ImpersonatorTenantId = nameof(ImpersonatorTenantId); }
/// <summary> /// Adds a file logger named 'File' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> public static ILoggingBuilder AddFile(this ILoggingBuilder builder) { builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>(); return builder; } /// <summary> /// Adds a file logger named 'File' to the factory. /// </summary> /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param> /// <param name="configure">Configure an instance of the <see cref="FileLoggerOptions" /> to set logging options</param> public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action<FileLoggerOptions> configure) { builder.AddFile(); builder.Services.Configure(configure); return builder; }
public interface ITenantResolutionStrategy { string TenantId(); } public interface ITenantStore { Task<Tenant> FindTenantAsync(string tenantId); }
public interface ITenantSession : IScopedDependency { /// <summary> /// Gets current TenantId or null. /// This TenantId should be the TenantId of the <see cref="IUserSession.UserId" />. /// It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in. /// </summary> string TenantId { get; } /// <summary> /// Gets current TenantName or null. /// This TenantName should be the TenantName of the <see cref="IUserSession.UserId" />. /// It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in. /// </summary> string TenantName { get; } /// <summary> /// Represents current tenant is head-tenant. /// </summary> bool IsHeadTenant { get; } /// <summary> /// TenantId of the impersonator. /// This is filled if a user with <see cref="IUserSession.ImpersonatorUserId" /> performing actions behalf of the /// <see cref="IUserSession.UserId" />. /// </summary> string ImpersonatorTenantId { get; } }
public static class SystemTime { public static Func<DateTime> Now = () => DateTime.UtcNow; public static Func<DateTime, DateTime> Normalize = (dateTime) => DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); }
public interface IClock : ITransientDependency { DateTime Now { get; } DateTime Normalize(DateTime dateTime); } internal sealed class Clock : IClock { public DateTime Now => SystemTime.Now(); public DateTime Normalize(DateTime dateTime) { return SystemTime.Normalize(dateTime); } }
public class Result { private static readonly Result _ok = new Result(false, string.Empty); private readonly List<ValidationFailure> _failures; protected Result(bool failed, string message) : this(failed, message, Enumerable.Empty<ValidationFailure>()) { Failed = failed; Message = message; } protected Result(bool failed, string message, IEnumerable<ValidationFailure> failures) { Failed = failed; Message = message; _failures = failures.ToList(); } public bool Failed { get; } public string Message { get; } public IEnumerable<ValidationFailure> Failures => _failures.AsReadOnly(); [DebuggerStepThrough] public static Result Ok() => _ok; [DebuggerStepThrough] public static Result Fail(string message) { return new Result(true, message); } //... }
services.AddFramework() .WithModelValidation() .WithFluentValidation() .WithMemoryCache() .WithSecurityService() .WithBackgroundTaskQueue() .WithRandomNumber();
تغییرات کتابخانه DNTFrameworkCore.EFCore
public abstract class TrackableEntity<TKey> : Entity<TKey>, ITrackable where TKey : IEquatable<TKey> { [NotMapped] public TrackingState TrackingState { get; set; } [NotMapped] public ICollection<string> ModifiedProperties { get; set; } }
public static class ConfigurationBuilderExtensions { public static IConfigurationBuilder AddEFCore(this IConfigurationBuilder builder, IServiceProvider provider) { return builder.Add(new EFConfigurationSource(provider)); } }
protected DbContextCore(DbContextOptions options, IEnumerable<IHook> hooks) : base(options) { _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks)); }
public void IgnoreHook(string hookName) { _ignoredHookList.Add(hookName); }
internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity> { public override string Name => HookNames.RowIntegrity; public override int Order => int.MaxValue; public override EntityState HookState => EntityState.Unchanged; protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow) { metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity); } }
//DbContextCore : IUnitOfWork public string EntityHash<TEntity>(TEntity entity) where TEntity : class { var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash && !p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) && !p.Metadata.IsShadowProperty()); return EntityHash<TEntity>(row); } protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class { var json = JsonConvert.SerializeObject(row, Formatting.Indented); using (var hashAlgorithm = SHA256.Create()) { var byteValue = Encoding.UTF8.GetBytes(json); var byteHash = hashAlgorithm.ComputeHash(byteValue); return Convert.ToBase64String(byteHash); } }
IsTamperedAsync HasTamperedEntryAsync TamperedEntryListAsync
public const string CreatedDateTime = nameof(ICreationTracking.CreatedDateTime); public const string CreatedByUserId = nameof(CreatedByUserId); public const string CreatedByBrowserName = nameof(CreatedByBrowserName); public const string CreatedByIP = nameof(CreatedByIP); public const string ModifiedDateTime = nameof(IModificationTracking.ModifiedDateTime); public const string ModifiedByUserId = nameof(ModifiedByUserId); public const string ModifiedByBrowserName = nameof(ModifiedByBrowserName); public const string ModifiedByIP = nameof(ModifiedByIP);
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.NormalizeDateTime(); modelBuilder.NormalizeDecimalPrecision(precision: 20, scale: 6); base.OnModelCreating(modelBuilder); }
MigrateDbContext<TContext>(this IHost host)
services.AddEFCore<ProjectDbContext>() .WithTrackingHook<long>() .WithDeletedEntityHook() .WithRowIntegrityHook() .WithNumberingHook(options => { options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption { Prefix = "Task", FieldNames = new[] {nameof(Task.BranchId)} }; });
تغییرات کتابخانه DNTFrameworkCore.Web.Tenancy
services.AddTenancy() .WithTenantSession() .WithStore<InMemoryTenantStore>() .WithResolutionStrategy<HostResolutionStrategy>();
app.UseTenancy();
سایر کتابخانهها تغییرات خاصی نداشتند و صرفا نحوه معرفی سرویسهای آنها ممکن است تغییر کند و یا وابستگیهای آنها به آخرین نسخه موجود ارتقاء داده شده باشند که در پروژه DNTFrameworkCore.TestAPI اعمال شدهاند.
PM> Install-Package DNTFrameworkCore PM> Install-Package DNTFrameworkCore.EFCore PM> Install-Package DNTFrameworkCore.EFCore.SqlServer PM> Install-Package DNTFrameworkCore.Web PM> Install-Package DNTFrameworkCore.FluentValidation PM> Install-Package DNTFrameworkCore.Web.Tenancy PM> Install-Package DNTFrameworkCore.Licensing
ماهیت این پایگاه داده وب سرویسی مبتنی بر 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