public class ETagAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request); } } public class ETagFilter : MemoryStream { private HttpResponseBase _response = null; private HttpRequestBase _request; private Stream _filter = null; public ETagFilter(HttpResponseBase response, HttpRequestBase request) { _response = response; _request = request; _filter = response.Filter; } private string GetToken(Stream stream) { var checksum = new byte[0]; checksum = MD5.Create().ComputeHash(stream); return Convert.ToBase64String(checksum, 0, checksum.Length); } public override void Write(byte[] buffer, int offset, int count) { var data = new byte[count]; Buffer.BlockCopy(buffer, offset, data, 0, count); var token = GetToken(new MemoryStream(data)); var clientToken = _request.Headers["If-None-Match"]; if (token != clientToken) { _response.AddHeader("ETag", token); _filter.Write(data, 0, count); } else { _response.SuppressContent = true; _response.StatusCode = 304; _response.StatusDescription = "Not Modified"; _response.AddHeader("Content-Length", "0"); } } }
هدف از این مطلب، ارائه راه حلی برای تولید خودکار کد یا شماره یکتا و ترتیبی در زمان ثبت رکورد جدید به صورت یکپارچه با EF Core، میباشد. به عنوان مثال فرض کنید در زمان ثبت سفارش، نیاز است بر اساس یکسری تنظیمات، یک شماره منحصر به فرد برای آن سفارش، تولید شده و در فیلدی تحت عنوان Number قرار گیرد؛ یا به صورت کلی برای موجودیتهایی که نیاز به یک نوع شماره گذاری منحصر به فرد دارند، مانند: سفارش، طرف حساب و ...
یک مثال واقعی
در زمان ثبت یک Task، کاربر میتواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شمارهای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین میکند که شماره یکتایی را ایجاد کند.
public interface INumberedEntity { string Number { get; set; } }
foreach (var entityType in builder.Model.GetEntityTypes() .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType))) { builder.Entity(entityType.ClrType) .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50); if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType)) { builder.Entity(entityType.ClrType) .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId)) .HasName( $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}") .IsUnique(); } else { builder.Entity(entityType.ClrType) .HasIndex(nameof(INumberedEntity.Number)) .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}") .IsUnique(); } }
public class NumberedEntity : Entity, IMultiTenantEntity { public string EntityName { get; set; } public long NextNumber { get; set; } public long TenantId { get; set; } }
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity> { public void Configure(EntityTypeBuilder<NumberedEntity> builder) { builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false); builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique(); builder.ToTable(nameof(NumberedEntity)); } }
شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت میتوان آخرین شماره ثبت شدهی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:
- ایجاد Gap مابین شمارههای تولید شده، که مدنظر ما نمیباشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شمارهای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)
پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number
internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity> { private readonly IUnitOfWork _uow; private readonly IOptions<NumberingConfiguration> _configuration; public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration) { _uow = uow ?? throw new ArgumentNullException(nameof(uow)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata) { if (!entity.Number.IsNullOrEmpty()) return; bool retry; string nextNumber; do { nextNumber = GenerateNumber(entity); var exists = CheckDuplicateNumber(entity, nextNumber); retry = exists; } while (retry); entity.Number = nextNumber; } private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber) { //... } private string GenerateNumber(INumberedEntity entity) { //... } }
ابتدا بررسی میشود اگر پراپرتی Number مقداردهی شدهاست، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شمارهای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت میگیرد.
بررسی متد GenerateNumber
private string GenerateNumber(INumberedEntity entity) { var option = _configuration.Value.NumberedEntityOptions[entity.GetType()]; var entityName = $"{entity.GetType().FullName}"; var lockKey = $"Tenant_{_uow.TenantId}_" + entityName; _uow.ObtainApplicationLevelDatabaseLock(lockKey); var nextNumber = option.Start.ToString(); var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName); if (numberedEntity == null) { _uow.ExecuteSqlCommand( "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName, option.Start + option.IncrementBy, _uow.TenantId); } else { nextNumber = numberedEntity.NextNumber.ToString(); _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ", numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id); } if (!string.IsNullOrEmpty(option.Prefix)) nextNumber = option.Prefix + nextNumber; return nextNumber; }
ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد میکنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنشهای همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید میباشد، با توجه به اینکه INumberedEntity میباشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.
پیاده سازی متد مذکور به شکل زیر میباشد:
public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource) { uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000); }
با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیتهای INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونهها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی میشود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود.
گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری میباشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام میگیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.
ساختار NumberingConfiguration
public class NumberingConfiguration { public bool Enabled { get; set; } public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } = new Dictionary<Type, NumberedEntityOption>(); }
public class NumberedEntityOption { public string Prefix { get; set; } public int Start { get; set; } = 1; public int IncrementBy { get; set; } = 1; }
با استفاده از دوکلاس بالا، امکان تنظیم الگوی تولید برای موجودیتها را خواهیم داشت.
گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیتها
public static void AddNumbering(this IServiceCollection services, IDictionary<Type, NumberedEntityOption> options) { services.Configure<NumberingConfiguration>(configuration => { configuration.Enabled = true; configuration.NumberedEntityOptions.AddRange(options); }); services.AddTransient<IPreActionHook, NumberingPreInsertHook>(); }
و استفاده از این متد الحاقی در Startup پروژه
services.AddNumbering(new Dictionary<Type, NumberedEntityOption> { [typeof(Task)] = new NumberedEntityOption { Prefix = "T_", Start = 1000, IncrementBy = 5 } });
و موجودیت Task
public class Task : TrackableEntity, IAggregateRoot, INumberedEntity { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 1024; public string Title { get; set; } public string NormalizedTitle { get; set; } public string Description { get; set; } public TaskState State { get; set; } = TaskState.Todo; public byte[] RowVersion { get; set; } public string Number { get; set; } }
با خروجیهای زیر
ویژگی های C# 9.0
ایجاد یک پروژهی Blazor WASM جدید
یک پوشهی جدید دلخواه را به نام BlazorWasmSQLite ایجاد کرده و با اجرای دستور dotnet new blazorwasm، یک پروژهی Blazor Web Assembly خالی جدید را در آن آغاز میکنیم. همانطور که از دستور نیز مشخص است، این پروژه از نوع hosted که به همراه Web API هم هست، نمیباشد.
افزودن Context و مدل EF-Core به برنامه
مدل برنامه به صورت زیر در پوشهی Models آن قرار میگیرد:
namespace BlazorWasmSQLite.Models; public class Car { public int Id { get; set; } public string Brand { get; set; } public int Price { get; set; } }
using Microsoft.EntityFrameworkCore; using BlazorWasmSQLite.Models; namespace BlazorWasmSQLite.Data; public class ClientSideDbContext : DbContext { public DbSet<Car> Cars { get; set; } = default!; public ClientSideDbContext(DbContextOptions<ClientSideDbContext> options) : base(options) { } }
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <!-- EF Core and Sqlite --> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" /> </ItemGroup> </Project>
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using BlazorWasmSQLite; using Microsoft.EntityFrameworkCore; using BlazorWasmSQLite.Data; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<HeadOutlet>("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); // Sets up EF Core with Sqlite builder.Services.AddDbContextFactory<ClientSideDbContext>(options => options .UseSqlite($"Filename=DemoData.db") .EnableSensitiveDataLogging()); await builder.Build().RunAsync();
ثبت تعدادی رکورد در بانک اطلاعاتی
در ادامه سعی میکنیم در فایل Index.razor، تعدادی رکورد را به بانک اطلاعاتی اضافه کنیم:
@page "/" @using Microsoft.Data.Sqlite @using Microsoft.EntityFrameworkCore @using BlazorWasmSQLite.Data @using BlazorWasmSQLite.Models <PageTitle>Index</PageTitle> <h1>Hello, world!</h1> Welcome to your new app. <SurveyPrompt Title="How is Blazor working for you?" /> @code { [Inject] private IDbContextFactory<ClientSideDbContext> _dbContextFactory { get; set; } = default!; protected override async Task OnInitializedAsync() { await using var db = await _dbContextFactory.CreateDbContextAsync(); await db.Database.EnsureCreatedAsync(); // create seed data if (!db.Cars.Any()) { var cars = new[] { new Car { Brand = "Audi", Price = 21000 }, new Car { Brand = "Volvo", Price = 11000 }, new Car { Brand = "Range Rover", Price = 135000 }, new Car { Brand = "Ford", Price = 8995 } }; await db.Cars.AddRangeAsync(cars); await db.SaveChangesAsync(); } await base.OnInitializedAsync(); } }
اولین سعی در اجرای برنامه
در ادامه سعی میکنیم تا برنامه را اجرا کنیم. با خطای زیر متوقف خواهیم شد:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. System.TypeInitializationException: The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.DllNotFoundException: e_sqlite3 at SQLitePCL.SQLite3Provider_e_sqlite3.SQLitePCL.ISQLite3Provider.sqlite3_libversion_number()
رفع مشکل کار با SQLite با کامپایل ویژهی آن
برای دسترسی به کدهای native در Blazor WASM و مرورگر، باید آنها را توسط کامپایلر emcc به صورت زیر کامپایل کرد:
$ git clone https://github.com/cloudmeter/sqlite $ cd sqlite $ emcc sqlite3.c -shared -o e_sqlite3.o
مرحلهی بعد، معرفی این object file تولید شده به برنامه است. برای اینکار ابتدا باید dotnet workload install wasm-tools را نصب کرد (مهم). سپس به فایل csproj برنامه مراجعه کرده و فایل e_sqlite3.o را به آن معرفی میکنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <!-- EF Core and Sqlite --> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" /> <NativeFileReference Include="Data\e_sqlite3.o" /> </ItemGroup> </Project>
سعی در اجرای مجدد برنامه
پس از نصب wasm-tools و ذکر NativeFileReference فوق، اکنون اگر برنامه را اجرا کنیم، برنامه بدون مشکل اجرا خواهد شد:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmSQLite.zip
public class EfTagService : ITagService { IUnitOfWork _uow; readonly IDbSet<Tag> _tags; public EfTagService(IUnitOfWork uow) { _uow = uow; _tags = _uow.Set<Tag>(); } public bool CreateTag(Tag tag) { // ... } public List<Tag> GetTagsByName(string[] tagNames) { // return ... } }
public class EfPostService : IPostService { IUnitOfWork _uow; readonly IDbSet<Post> _posts; //این قسمت private EfTagService _tagService; public EfPostService(IUnitOfWork uow) { _uow = uow; _posts = uow.Set<Post>(); //این قسمت _tagService = new EfTagService(_uow); } public void AddTagsToPost(Post post, string[] tagsList) { if (post.Tags != null && post.Tags.Any()) post.Tags.Clear(); //این قسمت var listOfActualTags = _tagService.GetTagsByName(tagsList); var listOfActualTagNames = listOfActualTags.Select(x => x.Name.ToLower()).ToList(); foreach (var tag in tagsList) { if (!listOfActualTagNames.Contains(tag.ToLowerInvariant().Trim())) { Tag newTag = new Tag() { Name = tag }; //این قسمت _tagService.CreateTag(newTag); post.Tags.Add(newTag); } } // ... } }
ASP.NET MVC #8
ارائه یک رشته به صورت وهلهای از IHtmlString سبب خواهد شد تا زمانیکه از
<%: %>
این سورس Html.Raw است:
public IHtmlString Raw(string value) { return new HtmlString(value); } public IHtmlString Raw(object value) { return new HtmlString(value == null ? null : value.ToString()); }
namespace System.Web.Mvc { public sealed class MvcHtmlString : HtmlString { [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "MvcHtmlString is immutable")] public static readonly MvcHtmlString Empty = Create(String.Empty); private readonly string _value; public MvcHtmlString(string value) : base(value ?? String.Empty) { _value = value ?? String.Empty; } public static MvcHtmlString Create(string value) { return new MvcHtmlString(value); } public static bool IsNullOrEmpty(MvcHtmlString value) { return (value == null || value._value.Length == 0); } } }
کاربرد MvcHtmlString عموما در بازگرداندن مقادیر از helper methods است؛ زمانیکه میدانید خروجی آن دارای تگهای html است. مثلا قرار است یک دکمه را نمایش دهد. به این ترتیب خروجی متد از encode شدن خودکار محافظت میشود.
در MVC، سیستم خروجی، رشتههایی از جنس IHtmlString را encode نمیکند. این بیشتر یک قرار داد است در اینجا.
Roslyn #3
زمانیکه صحبت از Syntax میشود، منظور نمایش متنی سورس کدها است. برای بررسی و آنالیز آن، نیاز است این نمایش متنی، به ساختار دادهای ویژهای به نام Syntax tree تبدیل شود و این Syntax tree مجموعهای است از tokenها. Tokenها بیانگر المانهای مختلف یک زبان، شامل کلمات کلیدی، عملگرها و غیره هستند.
در تصویر فوق، مراحل تبدیل یک قطعه کد #C را به مجموعهای از tokenهای معادل آن مشاهده میکنید. علاوه بر اینها، Roslyn syntax tree شامل موارد ویژهای به نام Trivia نیز هست. برای مثال در حین نوشتن کدها، در ابتدای سطرها تعدادی space یا tab وجود دارند و یا در این بین ممکن است کامنتی نوشته شود. هرچند این موارد از دیدگاه یک کامپایلر بیمعنا هستند، اما ابزارهای Refactoring ایی که به Trivia دقت نداشته باشند، خروجی کد به هم ریختهای را تولید خواهند کرد و سبب سردرگمی استفاده کنندگان میشوند.
در تصویر فوق، اشارهگر ادیتور پس از تایپ semicolon قرار گرفتهاست. در این حالت میتوانید دو نوع trivia مخصوص فضای خالی و کامنتها را در syntax visualizer، مشاهده کنید.
به علاوه پس از هر token بازهای از اعداد را مشاهده میکنید که بیانگر محل قرارگیری آنها در سورس کد هستند. این محلها جهت ارائهی خطاهای دقیق مرتبط با آن نقاط، بسیار مفید هستند.
یک Syntax tree از مجموعهای از syntax nodes تشکیل میشود و هر node شامل مواردی مانند تعاریف، عبارات و امثال آن است. در افزونهی Syntax visualizer نودهایی که رنگ قرمز متمایل به قهوهای دارند، بیانگر نودهای Trivia، نودهای آبی، Syntax nodes و نودهای سبز، Syntax token هستند.
مفاهیم این رنگها را با کلیک بر روی دکمهی Legend هم میتوان مشاهده کرد.
تفاوت Syntax با Semantics
در Roslyn امکان کار با Syntax و Semantics کدها وجود دارد.
یک Syntax، از گرامر زبان خاصی پیروی میکند. در Syntax اطلاعات بسیار زیادی وجود دارند که معنای برنامه را تغییر نمیدهند؛ مانند کامنتها، فضاهای خالی و فرمت ویژهی کدها. البته فضاهای خالی در زبانهایی مانند پایتون دارای معنا هستند؛ اما در سیشارپ خیر. همچنین در Syntax، توافق نامهای وجود دارد که بیانگر تعدادی واژهی از پیش رزرو شده، مانند کلمات کلیدی هستند.
اما Semantics در نقطهی مقابل Syntax قرار میگیرد و بیانگر معنای سورس کد است. برای مثال در اینجا تقدم و تاخر عملگرها مفهوم پیدا میکنند و یا اینکه Type system چیست و چه نوعهایی را میتوان به دیگری نسبت داد و تبدیل کرد. عملیات Binding در این مرحله رخ میدهد و مفهوم identifierها را مشخص میکند. برای مثال x در این قسمت از سورس کد، به چه معنایی است و به کجا اشاره میکند؟
خواص ویژهی Syntax tree در Roslyn
- تمام اجزای کد را شامل عناصر سازندهی زبان و همچنین Trivia، به همراه دارد.
- API آن توسط کتابخانههای ثالث قابل دسترسی است.
- Immutable طراحی شدهاست. به این معنا که زمانیکه syntax tree توسط Roslyn ایجاد شد، دیگر تغییر نمیکند. به این ترتیب امکان دسترسی همزمان و موازی به آن بدون نیاز به انواع قفلهای مسایل همزمانی وجود دارد. اگر کتابخانهی ثالثی به Syntax tree ارائه شده دسترسی پیدا میکند، میتواند کاملا مطمئن باشد که این اطلاعات دیگر تغییری نمیکنند و نیازی به قفل کردن آنها نیست. همچنین این مساله امکان استفادهی مجدد از sub treeها را در حین ویرایش کدها میسر میکند. به آنها mutating trees نیز گفته میشود.
- مقاوم است در برابر خطاها. اگر از قسمت اول به خاطر داشته باشید، Roslyn میبایستی جایگزین کامپایلر دومی به نام کامپایلر پس زمینهی ویژوال استودیو که خطوط قرمزی را ذیل سطرهای مشکل دار ترسیم میکند، نیز میشد. فلسفهی طراحی این کامپایلر، مقاوم بودن در برابر خطاهای تایپی و هماهنگی آن با تایپ کدها توسط برنامه نویس بود. Syntax tree در Roslyn نیز چنین خاصیتی را دارد و اگر مشغول به تایپ شوید، باز هم کار کرده و اینبار خطاهای موجود را نمایش میدهد که میتواند توسط ابزارهای نمایش دهندهی ویژوال استودیو یا سایر ابزارهای ثالث استفاده شود.
برای نمونه در تصویر فوق، تایپ semicolon فراموش شدهاست؛ اما همچنان Syntax tree در دسترس است و به علاوه گزارش میدهد که semicolon مفقود است و تایپ نشدهاست.
Parse سورس کد توسط Roslyn
ابتدا یک پروژهی کنسول سادهی دات نت 4.6 را در VS 2015 آغاز کنید. سپس از طریق خط فرمان نیوگت، دستور ذیل را صادر نمائید:
PM> Install-Package Microsoft.CodeAnalysis
سپس کدهای ذیل را به آن اضافه کنید:
using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Roslyn01 { class Program { static void Main(string[] args) { parseText(); } static void parseText() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }"); Console.WriteLine(tree.ToString()); Console.WriteLine(tree.GetRoot().NormalizeWhitespace().ToString()); var res = SyntaxFactory.ClassDeclaration("Foo") .WithMembers(SyntaxFactory.List<MemberDeclarationSyntax>(new[] { SyntaxFactory.MethodDeclaration( SyntaxFactory.PredefinedType( SyntaxFactory.Token(SyntaxKind.VoidKeyword) ), "Bar" ) .WithBody(SyntaxFactory.Block()) })) .NormalizeWhitespace(); Console.WriteLine(res); } } }
کار Parse سورس کد دریافتی، بر اساس سرویسهای زبان متناظر با آنها آغاز میشود. برای مثال سرویسهایی مانند VisualBasicSyntaxTree و یا CSharpSyntaxTree مثال فوق که سورس کد مورد آنالیز آن، از نوع سیشارپ است.
این کلاسهای Factory، دارای دو متد Create و ParseText هستند. کار متد ParseText آن مشخص است؛ یک قطعهی متنی از کد را آنالیز کرده و معادل Syntax Tree آنرا تولید میکند. متد Create آن، اشیایی مانند نودهای Syntax visualizer را دریافت کرده و بر اساس آنها یک Syntax tree را تولید میکند.
کار با متد Create آنچنان ساده نیست. به همین جهت یکی از اعضای تیم Roslyn برنامهای را به نام Roslyn Quoter ایجاد کردهاست که نسخهی آنلاین آنرا در اینجا و سورس کد آنرا در اینجا میتوانید بررسی کنید.
جهت آزمایش، همان قطعهی متنی سورس کد مثال فوق را در نسخهی آنلاین آن جهت آنالیز و تولید ورودی متد Create، وارد کنید. خروجی آنرا میتوان مستقیما در متد Create بکار برد.
فرمت کردن خودکار کدها به کمک Roslyn
اگر بر روی tree حاصل، متد ToString را فراخوانی کنیم، خروجی آن مجددا سورس کد مورد آنالیز است. اگر علاقمند بودید که Roslyn به صورت خودکار کدهای ورودی را فرمت کند و تمام آنها را در یک سطر نمایش ندهد، متد NormalizeWhitespace را بر روی ریشهی Syntax tree فراخوانی کنید:
tree.GetRoot().NormalizeWhitespace().ToString()
class Foo { void Bar(int x) { } }
کوئری گرفتن از سورس کد توسط Roslyn
در ادامه قصد داریم با سه روش مختلف کوئری گرفتن از Syntax tree، آشنا شویم. برای این منظور متد ذیل را به پروژهای که در ابتدای برنامه آغاز کردیم، اضافه کنید:
static void querySyntaxTree() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar() {} }"); var node = (CompilationUnitSyntax)tree.GetRoot(); // Using the object model foreach (var member in node.Members) { if (member.Kind() == SyntaxKind.ClassDeclaration) { var @class = (ClassDeclarationSyntax)member; foreach (var member2 in @class.Members) { if (member2.Kind() == SyntaxKind.MethodDeclaration) { var method = (MethodDeclarationSyntax)member2; // do stuff } } } } // Using LINQ query methods var bars = from member in node.Members.OfType<ClassDeclarationSyntax>() from member2 in member.Members.OfType<MethodDeclarationSyntax>() where member2.Identifier.Text == "Bar" select member2; var res = bars.ToList(); // Using visitors new MyVisitor().Visit(node); }
روش اول کوئری گرفتن از Syntax tree، استفاده از object model آن است. در اینجا هربار، نوع و Kind هر نود را بررسی کرده و در نهایت به اجزای مدنظر خواهیم رسید. شروع کار هم با دریافت ریشهی syntax tree توسط متد GetRoot و تبدیل نوع آن نود به CompilationUnitSyntax میباشد.
روش دوم استفاده از روش LINQ است؛ با توجه به اینکه ساختار یک Syntax tree بسیار شبیه است به LINQ to XML. در اینجا یک سری نود، ریشه و فرزندان آنها را داریم که با روش LINQ بسیار سازگار هستند. برای نمونه در مثال فوق، در ریشهی Parse شده، در تمام کلاسهای آن، به دنبال متد یا متدهایی هستیم که نام آنها Bar است.
و در نهایت روش مرسوم و متداول کار با Syntax trees، استفاده از الگوی Visitors است. همانطور که در کدهای دو روش قبل مشاهده میکنید، باید تعداد زیادی حلقه و if و else نوشت تا به جزء و المان مدنظر رسید. راه سادهتری نیز برای مدیریت این پیچیدگی وجود دارد و آن استفاده از الگوی Visitor است. کار این الگو ارائهی متدهایی قابل override شدن است و فراخوانی آنها، در طی حلقههایی پشت صحنه که این Visitor را اجرا میکنند، صورت میگیرد. بنابراین در اینجا دیگر برای رسیدن به یک متد، حلقه نخواهید نوشت. تنها کاری که باید صورت گیرد، override کردن متد Visit المانی خاص در Syntax tree است.
هر نود در syntax tree دارای متدی است به نام Accept که یک Visitor را دریافت میکند. همچنین Visitorهای نوشته شده نیز دارای متد Visit یک نود هستند.
نمونهای از این Visitors را در کلاس ذیل مشاهده میکنید:
class MyVisitor : CSharpSyntaxWalker { public override void VisitMethodDeclaration(MethodDeclarationSyntax node) { if (node.Identifier.Text == "Bar") { // do stuff } base.VisitMethodDeclaration(node); } }
کلاس پایهی CSharpSyntaxWalker از کلاس CSharpSyntaxVisitor مشتق شدهاست و به تمام امکانات آن دسترسی دارد. علاوه بر آنها، کلاس CSharpSyntaxWalker به Tokens و Trivia نیز دسترسی دارد.
نحوهی استفاده از Visitor سفارشی نوشته شده نیز به صورت ذیل است:
new MyVisitor().Visit(node);
نام علمی لحظهی سال تحویل، Vernal Equinox است. Equinox به معنای نقطهای است که یک فصل، به فصلی دیگر تبدیل میشود:
Equinox واژهای است لاتین به معنای «شبهای مساوی» و به این نکته اشاره دارد که در Equinox، طول شب و روز یکی میشوند. هر سال دارای دو Equinox است: vernal equinox و autumnal equinox (بهاری و پائیزی). البته باید درنظر داشت که Equinox بهاری در نیم کرهی شمالی بیشتر معنا پیدا میکند؛ زیرا در نیم کرهی جنوبی در همین زمان، پائیز شروع میشود.
بنابراین میتوان enum زیر را برای تعریف این چهار ثابت رخدادهای خورشیدی تعریف کرد:
public enum SunEvent { /// <summary> /// march equinox /// </summary> VernalEquinox, /// <summary> /// june solstice /// </summary> SummerSolstice, /// <summary> /// september equinox /// </summary> AutumnalEquinox, /// <summary> /// december solstice /// </summary> WinterSolstice }
در ادامه برای محاسبهی زمان equinox از فصل 27 کتاب Astronomical Algorithms کمک گرفته شده و تمام اعداد و ارقام و جداولی را که ملاحظه میکنید از این کتاب استخراج شدهاند.
/// <summary> /// Based on Jean Meeus book _Astronomical Algorithms_ /// </summary> public static class EquinoxCalculator { /// <summary> /// Degrees to Radians conversion factor. /// </summary> public static readonly double Deg2Radian = Math.PI / 180.0; public static bool ApproxEquals(double d1, double d2) { const double epsilon = 2.2204460492503131E-16; if (d1 == d2) return true; var tolerance = ((Math.Abs(d1) + Math.Abs(d2)) + 10.0) * epsilon; var difference = d1 - d2; return (-tolerance < difference && tolerance > difference); } /// <summary> /// Calculates time of the Equinox and Solstice. /// </summary> /// <param name="year">Year to calculate for.</param> /// <param name="sunEvent">Event to calculate.</param> /// <returns>Date and time event occurs as a fractional Julian Day.</returns> public static DateTime GetSunEventUtc(this int year, SunEvent sunEvent) { double y; double julianEphemerisDay; if (year >= 1000) { y = (Math.Floor((double)year) - 2000) / 1000; switch (sunEvent) { case SunEvent.VernalEquinox: julianEphemerisDay = 2451623.80984 + 365242.37404 * y + 0.05169 * (y * y) - 0.00411 * (y * y * y) - 0.00057 * (y * y * y * y); break; case SunEvent.SummerSolstice: julianEphemerisDay = 2451716.56767 + 365241.62603 * y + 0.00325 * (y * y) - 0.00888 * (y * y * y) - 0.00030 * (y * y * y * y); break; case SunEvent.AutumnalEquinox: julianEphemerisDay = 2451810.21715 + 365242.01767 * y + 0.11575 * (y * y) - 0.00337 * (y * y * y) - 0.00078 * (y * y * y * y); break; case SunEvent.WinterSolstice: julianEphemerisDay = 2451900.05952 + 365242.74049 * y + 0.06223 * (y * y) - 0.00823 * (y * y * y) - 0.00032 * (y * y * y * y); break; default: throw new NotSupportedException(); } } else { y = Math.Floor((double)year) / 1000; switch (sunEvent) { case SunEvent.VernalEquinox: julianEphemerisDay = 1721139.29189 + 365242.13740 * y + 0.06134 * (y * y) - 0.00111 * (y * y * y) - 0.00071 * (y * y * y * y); break; case SunEvent.SummerSolstice: julianEphemerisDay = 1721233.25401 + 365241.72562 * y + 0.05323 * (y * y) - 0.00907 * (y * y * y) - 0.00025 * (y * y * y * y); break; case SunEvent.AutumnalEquinox: julianEphemerisDay = 1721325.70455 + 365242.49558 * y + 0.11677 * (y * y) - 0.00297 * (y * y * y) - 0.00074 * (y * y * y * y); break; case SunEvent.WinterSolstice: julianEphemerisDay = 1721414.39987 + 365242.88257 * y + 0.00769 * (y * y) - 0.00933 * (y * y * y) - 0.00006 * (y * y * y * y); break; default: throw new NotSupportedException(); } } var julianCenturies = (julianEphemerisDay - 2451545.0) / 36525; var w = 35999.373 * julianCenturies - 2.47; var lambda = 1 + 0.0334 * Math.Cos(w * Deg2Radian) + 0.0007 * Math.Cos(2 * w * Deg2Radian); var sumOfPeriodicTerms = getSumOfPeriodicTerms(julianCenturies); return JulianToUtcDate(julianEphemerisDay + (0.00001 * sumOfPeriodicTerms / lambda)); } /// <summary> /// Converts a fractional Julian Day to a .NET DateTime. /// </summary> /// <param name="julianDay">Fractional Julian Day to convert.</param> /// <returns>Date and Time in .NET DateTime format.</returns> public static DateTime JulianToUtcDate(double julianDay) { double a; int month, year; var j = julianDay + 0.5; var z = Math.Floor(j); var f = j - z; if (z >= 2299161) { var alpha = Math.Floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.Floor(alpha / 4); } else a = z; var b = a + 1524; var c = Math.Floor((b - 122.1) / 365.25); var d = Math.Floor(365.25 * c); var e = Math.Floor((b - d) / 30.6001); var day = b - d - Math.Floor(30.6001 * e) + f; if (e < 14) month = (int)(e - 1.0); else if (ApproxEquals(e, 14) || ApproxEquals(e, 15)) month = (int)(e - 13.0); else throw new NotSupportedException("Illegal month calculated."); if (month > 2) year = (int)(c - 4716.0); else if (month == 1 || month == 2) year = (int)(c - 4715.0); else throw new NotSupportedException("Illegal year calculated."); var span = TimeSpan.FromDays(day); return new DateTime(year, month, (int)day, span.Hours, span.Minutes, span.Seconds, span.Milliseconds, new GregorianCalendar(), DateTimeKind.Utc); } /// <summary> /// These values are from Table 27.C /// </summary> private static double getSumOfPeriodicTerms(double julianCenturies) { return 485 * Math.Cos(Deg2Radian * 324.96 + Deg2Radian * (1934.136 * julianCenturies)) + 203 * Math.Cos(Deg2Radian * 337.23 + Deg2Radian * (32964.467 * julianCenturies)) + 199 * Math.Cos(Deg2Radian * 342.08 + Deg2Radian * (20.186 * julianCenturies)) + 182 * Math.Cos(Deg2Radian * 27.85 + Deg2Radian * (445267.112 * julianCenturies)) + 156 * Math.Cos(Deg2Radian * 73.14 + Deg2Radian * (45036.886 * julianCenturies)) + 136 * Math.Cos(Deg2Radian * 171.52 + Deg2Radian * (22518.443 * julianCenturies)) + 77 * Math.Cos(Deg2Radian * 222.54 + Deg2Radian * (65928.934 * julianCenturies)) + 74 * Math.Cos(Deg2Radian * 296.72 + Deg2Radian * (3034.906 * julianCenturies)) + 70 * Math.Cos(Deg2Radian * 243.58 + Deg2Radian * (9037.513 * julianCenturies)) + 58 * Math.Cos(Deg2Radian * 119.81 + Deg2Radian * (33718.147 * julianCenturies)) + 52 * Math.Cos(Deg2Radian * 297.17 + Deg2Radian * (150.678 * julianCenturies)) + 50 * Math.Cos(Deg2Radian * 21.02 + Deg2Radian * (2281.226 * julianCenturies)) + 45 * Math.Cos(Deg2Radian * 247.54 + Deg2Radian * (29929.562 * julianCenturies)) + 44 * Math.Cos(Deg2Radian * 325.15 + Deg2Radian * (31555.956 * julianCenturies)) + 29 * Math.Cos(Deg2Radian * 60.93 + Deg2Radian * (4443.417 * julianCenturies)) + 28 * Math.Cos(Deg2Radian * 155.12 + Deg2Radian * (67555.328 * julianCenturies)) + 17 * Math.Cos(Deg2Radian * 288.79 + Deg2Radian * (4562.452 * julianCenturies)) + 16 * Math.Cos(Deg2Radian * 198.04 + Deg2Radian * (62894.029 * julianCenturies)) + 14 * Math.Cos(Deg2Radian * 199.76 + Deg2Radian * (31436.921 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 95.39 + Deg2Radian * (14577.848 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 287.11 + Deg2Radian * (31931.756 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 320.81 + Deg2Radian * (34777.259 * julianCenturies)) + 9 * Math.Cos(Deg2Radian * 227.73 + Deg2Radian * (1222.114 * julianCenturies)) + 8 * Math.Cos(Deg2Radian * 15.45 + Deg2Radian * (16859.074 * julianCenturies)); } }
خروجی این الگوریتم را برای سالهای 2014 تا 2022 به صورت ذیل مشاهده میکنید:
2014 -> 1392/12/29 20:28:08 2015 -> 1394/01/01 02:16:29 2016 -> 1395/01/01 08:01:21 2017 -> 1395/12/30 14:00:00 2018 -> 1396/12/29 19:46:10 2019 -> 1398/01/01 01:29:29 2020 -> 1399/01/01 07:21:03 2021 -> 1399/12/30 13:08:41 2022 -> 1400/12/29 19:04:37
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
Equinox.zip
ASP.NET MVC #11
public class Project { [ForeignKey("FK_ProjectType_Id")] public ProjectType ProjectType{get;set;} public int FK_ProjectType_Id{get;set;} } public class ProjectType { public int id{get;set;} public string TypeName{get;set;} }
public class ProjectViewModel { public ProjectType ProjectType {get;set;} }