mklink /j C:\android-sdk "C:\Program Files (x86)\Android\android-sdk"
<AotAssemblies>true</AotAssemblies> <EnableLLVM>true</EnableLLVM>
mklink /j C:\android-sdk "C:\Program Files (x86)\Android\android-sdk"
<AotAssemblies>true</AotAssemblies> <EnableLLVM>true</EnableLLVM>
هدف از این مطلب، ارائه راه حلی برای تولید خودکار کد یا شماره یکتا و ترتیبی در زمان ثبت رکورد جدید به صورت یکپارچه با 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)); } }
شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت میتوان آخرین شماره ثبت شدهی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:
پیاده سازی یک 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; } }
با خروجیهای زیر
• All .NET application (Console, ASP.NET 4, WinForms, WPF) • Mac and Linux applications (Mono) • UWP (Universal Windows Platform) • ASP.NET Core applications • Can use EF Core in Windows phone and Windows store app
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final", "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0" } }
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.EntityFrameworkCore.SqlServer.Design": { "version": "1.0.0", "type": "build" } }, "tools": { // same as before "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } } }
{ "dependencies": { // same as before "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0" } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDatabaseErrorPage(); }
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Person> Persons { get; set; } } }
protected DbContext() : this((DbContextOptions) new DbContextOptions<DbContext>()) { } public DbContext([NotNull] DbContextOptions options) { // … }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"... connection string ..."); } } }
{ "ConnectionStrings": { "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true" } }
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]); });
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]); } } }
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>();
using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } } }
"HealthChecksUI": { "HealthChecks": [ { "Name": "Supported Services", "Uri": "https://Ip server/uaa/health-services" }, { "Name": "Supported Databases", "Uri": "https://Ip server/uaa/health-databases" } ], "EvulationTimeInSeconds": 50 }
dotnet new worker
<Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> <UserSecretsId>dotnet-MyWorkerServiceApp-B76DB08E-FFBB-4AD1-89B5-93BF483D1BD0</UserSecretsId> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0-preview8.19405.4" /> </ItemGroup> </Project>
namespace MyWorkerServiceApp { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); }); } }
public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; public Worker(ILogger<Worker> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); await Task.Delay(1000, stoppingToken); } } }
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureServices((hostContext, services) => { //services.AddHttpClient(); services.AddHostedService<Worker>(); });
cs create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSystemd() .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); });
The C# Dev Kit leverages the core C# language services and delivers additional productivity value to developers.
افزونه های SQL Server Reporting Services Projects و RDLC Report Designer که جدیدا توسط مایکروسافت به صورت افزونه ای که میتوان به Visual Studio 2022 اضافه کرد، ارائه شده است.