کتابخانه GMap.Net
declare @t table(id int, name nvarchar(max), active bit) insert @t values (1, 'Group 1', 1), (2, 'Group 2', 0)
select '[' + STUFF(( select ',{"id":' + cast(id as varchar(max)) + ',"name":"' + name + '"' + ',"active":' + cast(active as varchar(max)) +'}' from @t t1 for xml path(''), type ).value('.', 'varchar(max)'), 1, 1, '') + ']'
[{"id":1,"name":"Group 1","active":1},{"id":2,"name":"Group 2","active":0}]
declare @group table(id int, name nvarchar(max), active bit) insert @group values (1, 'Group 1', 1), (2, 'Group 2', 0) declare @member table(id int, groupid int,name nvarchar(max)) insert @member values (1, 1,'Ali'), (2, 1,'Mojtaba'),(3,2,'Hamid') select '[' + STUFF(( select ',{"id":' + cast(g.id as varchar(max)) + ',"name":"' + g.name + '"' + ',"members": { "children": [' + (select + STUFF(( select ',{"id":' + cast(m.id as varchar(max)) + ',"name":"' + m.name + '"}' from @member m where m.groupid = g.id for xml path(''), type ).value('.', 'varchar(max)'), 1, 1, '') + ']}' + ',"active":' + cast(g.active as varchar(max)) +'}') from @group g for xml path(''), type ).value('.', 'varchar(max)'), 1, 1, '') + ']'
[{"id":1,"name":"Group 1","members": { "children": [{"id":1,"name":"Ali"},{"id":2,"name":"Mojtaba"}]} ,"active":1}, {"id":2,"name":"Group 2","members": { "children": [{"id":3,"name":"Hamid"}]} ,"active":0}]
شرایطی را در نظر بگیرید که نیاز است از تغییرات یک Entity در سیستم آگاه شویم. برای مثلا در زمان ثبت سفارش جدید در فروشگاه، ایمیلی به مدیر فروشگاه ارسال شود، یک Business Rule نیز چک شود و همچنین بنابر نیاز مشتری، تعداد آنها روز به روز ممکن است افزایش یابد و چه بسا در اعمال این Ruleها، موجودیتهای مختلفی درگیر باشند. در این صورت است که خواسته یا ناخواسته اتصال بین کلاسها خیلی افزایش خواهد یافت. یکی از راه حلهای رهایی از این پیچیدگی و اتصال بالا، استفاده از Event میباشد.
هدف طراحی و پیاده سازی زیرساختی برای استفاده از DomainEventها میباشد. کدهای کامل این مطلب را میتوانید از اینجا دریافت کنید.
Domain Event چیست؟
چیزی که در یک Domain خاصی رخ داده است و هدف از آن آگاه کردن سایر بخشهای آن Domain میباشد تا بتوانند واکنش مناسبی را نشان دهند. با بهره گیری از این نوع رویدادها، میتوان Separation Of Concerns خوبی را بین کلاسهای موجود در آن Domain اعمال کرد و به طراحی ای با Coupling پایین رسید. این رویدادها عموما داخل پروسه Raise میشوند.
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEvent : ITransientDependency { } }
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEventHandler<in T> : ITransientDependency where T : IDomainEvent { bool IsAdvisable { get; } void Handle(T domainEvent); } }
- متد Raise مربوط به Engine برای رویداد خاصی فراخوانی میشود.
- با استفاده از یک IOC Container، تمام هندلرهای مربوط به رویداد جمع آوری میشود.
- متد Handle مربوط به تک تک هندلرها، فراخوانی خواهد شد.
namespace DomainEventsSample.Framework.Eventing.DomainEvents { public interface IDomainEventEngine : ISingletonDependency { void Raise<T>(T domainEvent) where T : IDomainEvent; } } namespace DomainEventsSample.Framework.Eventing.DomainEvents { public class DomainEventEngine : IDomainEventEngine { private readonly IContainer _container; public DomainEventEngine(IContainer container) { _container = container; } public void Raise<T>(T domainEvent) where T : IDomainEvent { foreach (var handler in _container.GetAllInstances<IDomainEventHandler<T>>()) try { handler.Handle(domainEvent); } catch (Exception) { if (domainEvent.IsAdvisable && handler.IsAdvisable) throw; } } } }
namespace DomainEventsSample.Framework.Domain.Events { public abstract class EntityDomainEvent<TEntity> : IDomainEvent where TEntity : Entity { protected EntityDomainEvent(TEntity entity) { Entity = entity; } public TEntity Entity { get; } } }
کلاس بالا به عنوان کلاس پایه یکسری رویداد مشترک مابین Entityهای سیستم در نظر گرفته شده است.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityCreatingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityCreatingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityCreatedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityCreatedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ایجاد یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityEditingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityEditingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityEditedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityEditedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ویرایش یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntityDeletingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityDeletingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntityDeletedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntityDeletedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از حذف یک Entity میباشند.
namespace DomainEventsSample.Framework.Domain.Events { public class EntitySavingEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntitySavingEvent(TEntity entity) : base(entity) { } } } namespace DomainEventsSample.Framework.Domain.Events { public class EntitySavedEvent<TEntity> : EntityDomainEvent<TEntity> where TEntity : Entity { public EntitySavedEvent(TEntity entity) : base(entity) { } } }
این رویدادها مربوط به زمان قبل و بعد از ذخیره (ایجاد و ویرایش) یک Entity میباشند.
نکته: برای اسکن کردن تمام هندلرها لازم است کد زیر را به تنظیمات StructureMap اضافه کنید:
Scan(scan => { scan.ConnectImplementationsToTypesClosing(typeof(IDomainEventHandler<>)); });
public class ProductCreatedEventHandler : IDomainEventHandler<EntityCreatedEvent<Product>> { public bool IsAdvisable => false; public void Handle(EntityCreatedEvent<Product> domainEvent) { //todo: notify users } }
در متد Create مربوط به ProductApplicationService و بعد از عملیات ذخیره سازی به شکل زیر میبایست عمل کرد:
public class ProductApplicationService : IProductApplicationService { private readonly IDomainEventEngine _eventEngine; private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; public ProductApplicationService(IDomainEventEngine eventEngine,IMapper mapper,IUnitOfWork unitOfWork) { _eventEngine = eventEngine; _mapper=mapper; _unitOfWork=unitOfWork; } [Transactional] public void Create(ProductCreateViewModel model) { var entity=_mapper.Map<Product>(model); _unitOfWork.Set<Product>().Add(entity); _unitOfWork.SaveChanges(); _eventEngine.Raise(new EntityCreatedEvent<Product>(entity)); } }
البته بهتر است برای Raise کردن این نوع رویدادها از مکانیزم Hook استفاده کرد و در زمان ذخیره سازی و فراخوانی متد SaveChange، این عملیات به صورت خودکار صورت گیرند.
در مقاله بعدی با استفاده از Hookها این عملیات را انجام خواهیم داد.
کدهای این قسمت را میتوانید از اینجا دریافت کنید.
Get-Process | Where-Object { $_.Name -eq 'Dropbox' }
PS /Users/sirwanafifi/Desktop> $block = { >> $newVar = 10 >> Write-Host $newVar >> }
PS /Users/sirwanafifi/Desktop> & $block
PS /Users/sirwanafifi/Desktop> & "Get-Process"
PS /Users/sirwanafifi/Desktop> & "1 + 1" or PS /Users/sirwanafifi/Desktop> & "Get-Process -Name Slack"
Function Add-Something { Write-Host "$_ World" } "Hello" | Add-Something
Function Add-Something { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [string]$Name ) Write-Host "$Name World" } "Hello" | Add-Something
$API_KEY = "...." Function Read-WeatherData { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [string]$CityName ) $Url = "https://api.openweathermap.org/data/2.5/forecast?q=$CityName&cnt=40&appid=$API_KEY&units=metric" Try { Write-Verbose "Reading weather data for $CityName" $Response = Invoke-RestMethod -Uri $Url $Response.list | ForEach-Object { Write-Verbose "Processing $($_.dt_txt)" [PSCustomObject]@{ City = $Response.city.name DateTime = [DateTime]::Parse($_.dt_txt) Temperature = $_.main.temp Humidity = $_.main.humidity Pressure = $_.main.pressure WindSpeed = $_.wind.speed WindDirection = $_.wind.deg Cloudiness = $_.clouds.all Weather = $_.weather.main WeatherDescription = $_.weather.description } } | Where-Object { $_.DateTime.Date -eq (Get-Date).Date } Write-Verbose "Done processing $CityName" } Catch { Write-Error $_.Exception.Message } }
Read-WeatherData -CityName "London" -Verbose
PS /> [Parameter]::new() ExperimentName : ExperimentAction : None Position : -2147483648 ParameterSetName : __AllParameterSets Mandatory : False ValueFromPipeline : False ValueFromPipelineByPropertyName : False ValueFromRemainingArguments : False HelpMessage : HelpMessageBaseName : HelpMessageResourceId : DontShow : False TypeId : System.Management.Automation.ParameterAttribute
Function Ping-Website { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidatePattern('^www\..*')] [string[]]$Websites, [ValidateRange(1, 3)] [int]$Count = 3 ) $Results = @() $Websites | ForEach-Object { $Website = $_ $Result = Test-Connection -ComputerName $Website -Count $Count -Quiet $ResultText = $Result ? 'Success' : 'Failed' $Results += @{ Website = $Website Result = $ResultText } Write-Verbose "The result of pinging $Website is $ResultText" } $Results | ForEach-Object { $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $Count }; } } }
Function Ping-Website { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ If (-Not ($_ | Test-Path) ) { Throw "File or folder does not exist" } If (-Not ($_ | Test-Path -PathType Leaf) ) { Throw "The Path argument must be a file. Folder paths are not allowed." } If ($_ -NotMatch "(\.json)$") { throw "The file specified in the path argument must be either of type json" } Return $true })] [Alias("src", "source", "file")] [System.IO.FileInfo]$Path, [int]$Count = 1 ) $Results = [System.Collections.ArrayList]@() $Urls = Get-Content -Path $Path | ConvertFrom-Json $Urls | ForEach-Object -Parallel { $Website = $_.url $Result = Test-Connection -ComputerName $Website -Count $using:Count -Quiet $ResultText = $Result ? 'Success' : 'Failed' $Item = @{ Website = $Website Result = $ResultText } $null = ($using:Results).Add($Item) } $Results | ForEach-Object -Parallel { $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $using:Count }; } } }
Function Ping-Website { [CmdletBinding()] Param( # As before ) # As before $Urls | ForEach-Object -Parallel { $DebugPreference = $using:DebugPreference $VerbosePreference = $using:VerbosePreference $InformationPreference = $using:InformationPreference # As before } # As before }
$scriptBlock = { $logOutput = { param($message) Write-Host $message } [int]$someVariable = 10 $doSomeWork = { & $logOutput -message "Some variable value: $someVariable" } $someVariable = 20 & $doSomeWork }
$scriptBlock = { $logOutput = { param($message) Write-Host $message } [int]$someVariable = 10 $doSomeWork = { & $logOutput -message "Some variable value: $someVariable" }.GetNewClosure() $someVariable = 20 & $doSomeWork }
begin process end dynamicparam
function Show-Pipeline { begin { Write-Host "Pipeline start" } process { Write-Host "Pipeline process $_" } end { Write-Host "Pipeline end $_" } }
PS /> 1..2 | Show-Pipeline Pipeline start Pipeline process 1 Pipeline process 2 Pipeline end 2
PS /> "www.google.com", "www.yahoo.com" | Ping-Website Website Result Number Of Attempts ------- ------ ------------------ www.yahoo.com Success 3
Function Ping-Website { [CmdletBinding()] Param( # As before ) process { # As before } }
PS /> "www.google.com", "www.yahoo.com" | Ping-Website Website Result Number Of Attempts ------- ------ ------------------ www.google.com Success 3 www.yahoo.com Success 3
PS /> Read-Csv ./users.csv -Columns name
using namespace System.Management.Automation Function Read-Csv { Param ( [Parameter(Mandatory = $true, Position = 0)] [string]$Path ) DynamicParam { $firstLine = Get-Content $Path | Select-Object -First 1 [String[]]$headers = $firstLine -split ', ' $parameters = [RuntimeDefinedParameterDictionary]::new() $parameter = [RuntimeDefinedParameter]::new( 'Columns', [String[]], [Attribute[]]@( [Parameter]@{ Mandatory = $false; Position = 1 } [ValidateSet]::new($headers) ) ) $parameters.Add($parameter.Name, $parameter) Return $parameters } Begin { $csvContent = Import-Csv $Path If ($PSBoundParameters.ContainsKey('Columns')) { $columns = $PSBoundParameters['Columns'] $csvContent | Select-Object -Property $columns } Else { $csvContent } } }
در این قسمت مدلهای مربوط به بخش انجمن را تکمیل کرده و همچنین سیستم نظرسنجی را نیز بررسی خواهیم کرد.
همکاران این قسمت:
سلمان معروفی
سید مجبتی حسینی
مدل پستهای انجمن
/// <summary> /// Represents The Post of Forum /// </summary> public class ForumPost : AuditBaseEntity { #region Ctor /// <summary> /// create one instance of <see cref="ForumPost"/> /// </summary> public ForumPost() { CreatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets body of this post /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets Count of this post's reports /// </summary> public virtual int ReportsCount { get; set; } /// <summary> /// gets or sets information of User-Agent /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets rating values /// <remarks>is a complex type</remarks> /// </summary> public virtual Rating Rating { get; set; } /// <summary> /// gets or sets author's ip address /// </summary> public virtual string CreatorIp { get; set; } /// <summary> /// gets or sets status of this post /// </summary> public virtual ForumPostStatus Status { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets ParentPost of this post /// </summary> public virtual ForumPost Reply { get; set; } /// <summary> /// gets or sets ParentPost's Id of this post /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets /// </summary> public virtual ICollection<ForumPost> Children { get; set; } /// <summary> /// gets or sets Topic That Associated with this Post /// </summary> public virtual ForumTopic Topic { get; set; } /// <summary> /// gets or sets Id of Topic That Associated with this Post /// </summary> public virtual long TopicId { get; set; } /// <summary> /// get or sets Histories of this Post's Updates /// </summary> public virtual ICollection<ForumPostHistory> Histories { get; set; } /// <summary> /// gets or sets Forum that this post created in it . used for retrive posts count /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets id of Forum that this post created in it . used for retrive posts count /// </summary> public virtual long ForumId { get; set; } #endregion } public enum ForumPostStatus { /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */ [Display(Name = "تأیید شده")] Approved = 0, [Display(Name = "در انتظار بررسی")] Pending = 1, [Display(Name = "جفنگ")] Spam = 2, [Display(Name = "زباله دان")] Trash = -1 }
مدل بالا مشخص کنندهی پستهایی که در پاسخ به تاپیکها ارسال میشوند، میباشد. ساختار درختی آن به منظور امکان پاسخ به پستها در نظر گرفته شده است. در هر تاپیک چندین پست ارسال میشود که اولین پست ارسال شده، همان محتوای اصلی تاپیک میباشد. بدین منظور خصوصیت Topic را در مدل بالا تعریف کردهایم. برای این پستهای ارسالی امکان امتیاز دهی و اخطار دادن نیز خواهیم داشت که به ترتیب خصوصیات Rating و ReportsCount (بحث شده در مقالات قبل) را در مدل بالا تعریف کردهایم. خصوصیت Status به منظور اعمال مدیریتی در نظر گرفته شده است که از نوع ForumPostStatus میباشد و در بالا تعریف آن نیز آمده است.
نکته : خصوصیتی از نوع مدل Forum نیز در مدل بالا تعریف شده است. هدف از آن افزایش سرعت ویرایش خصوصیات ApprovedPostsCount و UnApprovedPostsCount موجود در مدل Forum میباشد. در واقع هنگام درج پست جدید یا حذف پستی و یا ... ، لازم است خصوصیات مذکور به روز شوند.
علاوه بر این موارد ، لازم است تاریخچهی تغییرات پستهای ارسالی را هم نگهداری کرد تا در صورت نیاز به آنها استناد کنیم. از طرفی پستهای ارسالی را میتوان چندین بار ویرایش کرد. به همین دلیل خصوصیت Histories را که لیستی از مدل ForumPostHistory میباشد، در مدل بالا تعریف کردهایم.
مدل تاریخچهی تغییرات پست
/// <summary> /// Represents History Of Post's Updates /// </summary> public class ForumPostHistory { #region Ctor /// <summary> /// create one instance of <see cref="ForumPostHistory"/> /// </summary> public ForumPostHistory() { Id = SequentialGuidGenerator.NewSequentialGuid(); CreatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets Identifier of this history /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets Reason of update /// </summary> public virtual string Reason { get; set; } /// <summary> /// gets or sets DateTime that this record added /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets body of this post /// </summary> public virtual string Body { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Post /// </summary> public virtual ForumPost Post { get; set; } /// <summary> /// gets or sets Id Of Post /// </summary> public virtual long PostId { get; set; } /// <summary> /// gets or sets User that modified this Record /// </summary> public virtual User Modifier { get; set; } /// <summary> /// gets or sets if of User that modified this Record /// </summary> public virtual long ModifierId { get; set; } #endregion }
اگر خصوصیت ModifyLocked مربوط به مدل ForumPost که آن را از کلاس پایه AuditBaseEntity به ارث برده است، دارای مقدار true باشد، این امکان وجود خواهد داشت تا بتوان پست مورد نظر را ویرایش کرده و اطلاعات قبلی، در قالب یک رکورد در جدول حاصل از مدل بالا ثبت شوند.
- Reason : دلیل این ویرایش به عمل آماده
- Body : محتوای پست یا تاپیک
- Modifier : کاربر انجام دهندهی این ویرایش
- CreatedOn : زمانی که این ویرایش انجام شده است
مدل ردیابی انجمن ها
public class ForumTracker { #region Ctor /// <summary> /// create one instance of <see cref="ForumTracker"/> /// </summary> public ForumTracker() { LastMarkedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets DateTime Of Las Visit by User /// </summary> public virtual DateTime LastMarkedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Forum that Tracked /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets Id of Forum tath Tracked /// </summary> public virtual long ForumId { get; set; } /// <summary> /// gets or sets User that tracked The forum /// </summary> public virtual User Tracker { get; set; } /// <summary> /// gets or sets Id Of User that Tracked the forum /// </summary> public virtual long TrackerId { get; set; } #endregion } public class ForumTopicTracker { #region Ctor /// <summary> /// create one instance of <see cref="ForumTopicTracker"/> /// </summary> public ForumTopicTracker() { LastVisitedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets DateTime Of Las Visit by User /// </summary> public virtual DateTime LastVisitedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets topc that Tracked /// </summary> public virtual ForumTopic Topic { get; set; } /// <summary> /// gets or sets Id of topic that Tracked /// </summary> public virtual long TopicId { get; set; } /// <summary> /// gets or sets User that tracked The topic /// </summary> public virtual User Tracker { get; set; } /// <summary> /// gets or sets Id Of User that Tracked the topic /// </summary> public virtual long TrackerId { get; set; } /// <summary> /// gets or sets Forum /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets Identifier of Forum . used for delete /// </summary> public virtual long ForumId { get; set; } #endregion }
از مدل ForumTopicTracker هم برای مشخص کردن اینکه کاربر کدام تاپیک را و در چه تاریخی آخرین بار مشاهده کرده است، کمک میگیریم. برای این منظور از خصوصیت LastVisitedOn استفاده میشود.
البته نیاز است هنگام واکشی انجمنها و تاپیکها، یکسری بررسیهایی را بر اساس این جداول انجام داد که تشریح این بررسیها را قصد دارم هنگام پیاده سازی سیستم انجام دهم.
این قسمت از کار کمی پیچیده است و برای خودم نیز چالش داشت. سعی کردم انجمنهای سورس باز PHP را بررسی کنم تا در نهایت به تحلیل بالا دست یافتم. مدلهای ارائه شده انجمن تا این قسمت، نیازهای مورد نظر ما را برآورده خواهند کرد.
مدل سیستم نظرسنجی
public class Poll : BaseContent { #region Ctor /// <summary> /// create one instance of <see cref="Poll"/> /// </summary> public Poll() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or set Date that this Poll will Expire /// </summary> public virtual DateTime? ExpireOn { get; set; } /// <summary> ///indicating this poll allow to select multi item /// </summary> public virtual bool IsMultiSelect { get; set; } /// <summary> /// gets or sets Count of this poll's votes /// </summary> public virtual long VotesCount { get; set; } /// <summary> /// indicate this Poll is approved by admin if Poll.Moderate==true /// </summary> public virtual bool IsApproved { get; set; } #endregion #region NavigationProperties /// <summary> /// get or set comments of this poll /// </summary> public virtual ICollection<PollComment> Comments { get; set; } /// <summary> /// get or set Options Of Poll For selection /// </summary> public virtual ICollection<PollOption> Options { get; set; } /// <summary> /// get or set Users List That vote for this poll /// </summary> public virtual ICollection<User> Voters { get; set; } #endregion }
- ExpireOn : زمان اتمام فرصت رای دهی که اگر نال باشد در آن صورت زمان انقضا نخواهد داشت.
- IsMultiSelect : اگر انتخاب چندگزینهای مجاز باشد، این خصوصیت، با مقدار true مقدار دهی میشود.
- VotesCount : به منظور افزایش کارآیی در نظر گرفته شده است و تعداد کل رایهای داده شدهی به نظرسنجی را در بر میگیرد.
- Voters : برای جلوگیری از رای دهی چند بارهی کاربر به یک نظرسنجی، یک ارتباط چند به چند بین کاربر و نظرسنجی برقرار کردهایم. هر کاربر به چند نظر سنجی میتواند پاسخ دهد و به هر نظرسنجی توسط چندین کاربر رای داده میشود.
- PollOptions : هر نظر سنجی تعدادی گزینهی انتخابی هم خواهد داشت که برای همین منظور و اعمال ارتباط یک به چند بین نظرسنجی و گزینههای انتخابی، لیستی از PollOption را در مدل بالا تعریف کردهایم.
مدل گزینههای نظرسنجی
public class PollOption { #region Properties /// <summary> /// gets or sets identifier of this polloption /// </summary> public virtual long Id { get; set; } /// <summary> /// gets or sets Title of this polloption /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets count of votes /// </summary> public virtual long VotesCount { get; set; } /// <summary> /// gets or sets Description of this Option for more details /// </summary> public virtual string Description { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the poll that assosiated with this Polloption /// </summary> public virtual Poll Poll { get; set; } /// <summary> /// gets or sets the id of poll that assosiated with this Polloption /// </summary> public virtual long PollId { get; set; } #endregion }
- Title: عنوان گزینهی مورد نظر
- Description: توضیح بیشتر برای گزینهی مورد نظر
- VotesCount: تعداد باری که یک گزینه در نظر سنجی انتخاب شده است.
مدل نظرات سیستم نظرسنجی
public class PollComment : BaseComment { #region Ctor public PollComment() { CreatedOn = DateTime.Now; Rating = new Rating(); } #endregion #region NavigationProperties /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual PollComment Reply { get; set; } /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual ICollection<PollComment> Children { get; set; } /// <summary> /// gets or sets poll that this comment sent to it /// </summary> public virtual Poll Poll { get; set; } /// <summary> /// gets or sets poll'Id that this comment sent to it /// </summary> public virtual long PollId { get; set; } #endregion }
در مقالهی بعد به بررسی سیستم پیام رسانی و همچنین بخشی از سیستم تحت عنوان Collections (امکان ساخت گروههای شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسیهای مختلف) خواهیم پرداخت.
نتیجه تا این قسمت
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
ارائهی AuthenticationState به تمام کامپوننتهای یک برنامهی Blazor WASM
در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامههای Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب میشود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننتهای یک برنامهیBlazor ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائهی یکسری اطلاعات، به تمام زیر کامپوننتهای یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آنها را در بالاترین سطح سلسله مراتب کامپوننتهای تعریف شده تعریف کردیم، در تمام زیر کامپوننتهای آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامهی Blazor را بر اساس وضعیت اعتبارسنجی و نقشهای کاربر جاری، میسر میکند.
روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <Authorizing> <p>Please wait, we are authorizing the user.</p> </Authorizing> <NotAuthorized> <p>Not Authorized</p> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
مشکل! برخلاف برنامههای Blazor Server، برنامههای Blazor WASM به صورت پیشفرض به همراه تامین کنندهی توکار AuthenticationState نیستند.
اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامههای Blazor Server، در قسمتهای 21 تا 23، پیشتر مشاهده کردهاید. همان مفاهیم، در برنامههای Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامهی سمت کلاینت WASM Blazor، از برنامهی Web API سمت سرور، نیاز است یک تامین کنندهی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامههای Blazor Server، این مورد به صورت خودکار مدیریت نمیشود و با ASP.NET Core Identity سمت سروری که JWT تولید میکند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار میکند. به همین جهت در ابتدا یک JWT Parser را طراحی میکنیم که رشتهی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim میکند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار میدهیم تا اطلاعات مورد نیاز کامپوننتهای CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.
نیاز به یک JWT Parser
در قسمت 25، پس از لاگین موفق، یک JWT تولید میشود که به همراه قسمتی از مشخصات کاربر است. میتوان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی میرسیم و حاوی claims تعریف شدهاست:
{ "iss": "https://localhost:5001/", "iat": 1616396383, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir", "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c", "DisplayName": "vahid@dntips.ir", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1616396383, "exp": 1616397583, "aud": "Any" }
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); return claims; } private static byte[] ParseBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } } }
تامین AuthenticationState مبتنی بر JWT مخصوص برنامههای Blazor WASM
پس از داشتن لیست Claims دریافتی از یک رشتهی JWT، اکنون میتوان آنرا تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بستهی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامهی کلاینت اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" /> </ItemGroup> </Project>
namespace BlazorWasm.Client.Services { public class AuthStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; private readonly ILocalStorageService _localStorage; public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage)); } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken); if (token == null) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); return new AuthenticationState( new ClaimsPrincipal( new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ) ); } } }
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده میکنید که مقدار token خودش را از Local Storage دریافت میکند که کلید متناظر با آنرا در پروژهی BlazorServer.Common به صورت زیر تعریف کردهایم:
namespace BlazorServer.Common { public static class ConstantKeys { // ... public const string LocalToken = "JWT Token"; } }
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفادهی در برنامهی Blazor WASM کردهایم.
پس از تعریف یک AuthenticationStateProvider سفارشی، باید آنرا به همراه Authorization، به سیستم تزریق وابستگیهای برنامه در فایل Program.cs اضافه کرد:
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddAuthorizationCore(); builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>(); // ... } } }
@using Microsoft.AspNetCore.Components.Authorization
تهیهی سرویسی برای کار با AccountController
اکنون میخواهیم در برنامهی سمت کلاینت، از AccountController سمت سرور که آنرا در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر میکند:
namespace BlazorWasm.Client.Services { public interface IClientAuthenticationService { Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication); Task LogoutAsync(); Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration); } }
namespace BlazorWasm.Client.Services { public class ClientAuthenticationService : IClientAuthenticationService { private readonly HttpClient _client; private readonly ILocalStorageService _localStorage; public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage) { _client = client; _localStorage = localStorage; } public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication) { var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent); if (response.IsSuccessStatusCode) { await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token); await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return new AuthenticationResponseDTO { IsAuthSuccessful = true }; } else { return result; } } public async Task LogoutAsync() { await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken); await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails); _client.DefaultRequestHeaders.Authorization = null; } public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration) { var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent); if (response.IsSuccessStatusCode) { return new RegisterationResponseDTO { IsRegisterationSuccessful = true }; } else { return result; } } } }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>(); // ... } } }
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی میکند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویسهای دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالتهایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد میشود و به این ترتیب میتوان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمتهای مختلف برنامه را به او نشان داد. نمونهی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامهی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ میدهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامهی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.
- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامههای سمت کلاینت Blazor، طول عمر تمام سرویسها، صرفنظر از نوع طول عمری که برای آنها مشخص میکنیم، همواره Singleton هستند (ماخذ).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه میکنید. همانطور که عنوان شد، سرویسهای Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویسهای HttpClient تزریق شدهی به سایر سرویسهای برنامه نیز بلافاصله تاثیرگذار خواهد بود.
- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال میکند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواستهای ارسالی به منابع محافظت شدهی سمت سرور کفایت میکند.
- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال میکند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-31.zip
در این پست به شرح کلاس Rectangle جهت رسم مستطیل و Square جهت رسم مربع میپردازیم
using System.Drawing; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Rectangle /// </summary> public class Rectangle : Shape { #region Constructors (2) /// <summary> /// Initializes a new instance of the <see cref="Rectangle" /> class. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> /// <param name="zIndex">Index of the z.</param> /// <param name="foreColor">Color of the fore.</param> /// <param name="thickness">The thickness.</param> /// <param name="isFill">if set to <c>true</c> [is fill].</param> /// <param name="backgroundColor">Color of the background.</param> public Rectangle(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor) : base(startPoint, endPoint, zIndex, foreColor, thickness, isFill, backgroundColor) { ShapeType = ShapeType.Rectangle; } /// <summary> /// Initializes a new instance of the <see cref="Rectangle" /> class. /// </summary> public Rectangle() { ShapeType = ShapeType.Rectangle; } #endregion Constructors #region Methods (1) // Public Methods (1) /// <summary> /// Draws the specified g. /// </summary> /// <param name="g">The g.</param> public override void Draw(Graphics g) { if (IsFill) g.FillRectangle(BackgroundBrush, StartPoint.X, StartPoint.Y, Width, Height); g.DrawRectangle(Pen, StartPoint.X, StartPoint.Y, Width, Height); base.Draw(g); } #endregion Methods } }
کلاس بعدی کلاس Square میباشد، که از کلاس بالا (Rectangle) ارث بری داشته است، کدهای این کلاس را در زیر مشاهده میکنید.
using System; using System.Drawing; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Square /// </summary> public class Square : Rectangle { #region Constructors (2) /// <summary> /// Initializes a new instance of the <see cref="Square" /> class. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> /// <param name="zIndex">Index of the z.</param> /// <param name="foreColor">Color of the fore.</param> /// <param name="thickness">The thickness.</param> /// <param name="isFill">if set to <c>true</c> [is fill].</param> /// <param name="backgroundColor">Color of the background.</param> public Square(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor) { float x = 0, y = 0; float width = Math.Abs(endPoint.X - startPoint.X); float height = Math.Abs(endPoint.Y - startPoint.Y); if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y) { x = startPoint.X; y = startPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y) { x = endPoint.X; y = endPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y) { x = endPoint.X; y = startPoint.Y; } else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y) { x = startPoint.X; y = endPoint.Y; } StartPoint = new PointF(x, y); var side = Math.Max(width, height); EndPoint = new PointF(x+side, y+side); ShapeType = ShapeType.Square; Zindex = zIndex; ForeColor = foreColor; Thickness = thickness; BackgroundColor = backgroundColor; IsFill = isFill; } /// <summary> /// Initializes a new instance of the <see cref="Square" /> class. /// </summary> public Square() { ShapeType = ShapeType.Square; } #endregion Constructors } }
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4#
در این قسمت بر روی توابع Topcount, bottomcount , toppercent, bottompercent, topsum, bottomsum تمرکز خواهیم داشت.
در ابتدا تصور کنید بخواهیم میزان فروش اینترنتی را برای پنج ردیف از دسته بندیهای محصولات واکشی کنیم.
Select [Measures].[Internet Sales Amount] on columns, non empty( topcount([Product].[Product Categories].[Subcategory],5) ) on rows From [Adventure Works]
در تابع بالا پنج ردیف ابتدایی (به صورت فیزیکی) برگردانده میشوند.
در اینجا تابع topcount دارای دو پارامتر می باشد که پارامتر دوم آن مشخص کنندهی تعداد ردیف واکشی شده و پارامتر اول آن، مشخص کنندهی دایمنشنی میباشد که عمل واکشی برای آن صورت میگیرد. همچنین در بالا از تابع Non empty برای حذف ردیفهای دارای مقدار Null استفاده شده است. حال تصور کنید بخواهیم پنج دسته بندی محصولی را دریافت کنیم که دارای بیشترین میزان فروش اینترنتی میباشند.
Select [Measures].[Internet Sales Amount] on columns, non empty( topcount( [Product].[Product Categories].[Subcategory], 5, [Measures].[Internet Sales Amount] ) ) on rows From [Adventure Works]
خروجی بر اساس میزان فروش اینترنتی به صورت نزولی مرتب شده است.
تابع Topcount به عنوان پارامتر سوم میتواند نام یک Measure را دریافت کند و خروجی را براساس آن شاخص، برگرداند. امکان واکشی و مرتب سازی در تابع Topcount
برای یک شاخص متفاوت از شاخص واکشی شده در یک محور دیگر نیز وجود دارد به مثال زیر دقت کنید:
Select [Measures].[Internet Sales Amount] on columns, topcount( [Product].[Product Categories].[Subcategory], 5, [Measures].[Reseller Sales Amount] ) on rows From [Adventure Works]
همانطور که مشخص میباشد، پنج دسته بندی محصولاتی که دارای بیشترین میزان فروش نمایندگان فروش میباشند، در خروجی واکشی شدهاند؛ در حالیکه در محور ستون میزان فروش اینترنتی واکشی شده است.
برای درک بیشتر همین کوئری را دوباره بازنویسی کرده اما اینبار در محور ستون هر دو شاخص [Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount] را واکشی میکنیم.
Select {[Measures].[Internet Sales Amount],[Measures].[Reseller Sales Amount]} on columns, topcount( [Product].[Product Categories].[Subcategory], 5, [Measures].[Reseller Sales Amount] ) on rows From [Adventure Works]
با بررسی خروجی دو کوئری بالا تفاوت واکشی را متوجه خواهید شد. در هر دو کوئری واکشی براساس شاخص [Measures].[Reseller Sales Amount] انجام شده است
اما واکشی در محور ستون متفاوت میباشد. (دقیقا مانند T/SQL که میتوانستیم، مرتب سازی براساس فیلدی باشد که در قسمت Projection حاضر نبوده و در این حالت در برخی موارد ظاهرا خروجی مرتب نمیباشد)
حال تصور کنید بخواهیم 30 دسته بندی محصولاتی را داشته باشیم که دارای کمترین میزان فروش اینترنتی میباشند. برای این منظور از تابع bottomcount استفاده میکنیم
Select [Measures].[Internet Sales Amount] on columns, bottomcount( [Product].[Product Categories].[Subcategory], 30, [Measures].[Internet Sales Amount] ) on rows From [Adventure Works]
ردیف هایی که دارای مقدار Null می باشند هم در خروجی قرار می گیرند
Select [Measures].[Internet Sales Amount] on columns, non empty bottomcount( [Product].[Product Categories].[Subcategory], 30, [Measures].[Internet Sales Amount] )on rows From [Adventure Works]
در مثال بالا ردیفهای دارای مقدار Null را از خروجی حذف کرده ایم.
گاهی نیاز میباشد که تعداد دسته بندیهای محصولاتی را واکشی کنیم که دارای بیشترین یا کمترین میزان فروش اینترنتی میباشند و سرجمع فروش اینترنتی آنها بیشتر یا کمتر از X درصد از فروش اینترنتی کل میباشد را داشته باشند. به عنوان مثال میخواهیم ببینیم کدام دسته بندی محصولات شامل بیشترین میزان فروش اینترنتی میباشند و سرجمع فروش آنها 53 در صد از کل فروش اینترنتی میباشند.
Select [Measures].[Internet Sales Amount] on columns, { toppercent( [Product].[Product Categories].[Subcategory], 53, [Measures].[Internet Sales Amount] ), [Product].[Product Categories] } on rows From [Adventure Works]
و یا واکشی دسته محصولاتی که دارای کمترین میزان فروش اینترنتی میباشند و سرجمع فروش اینترنتی آنها کمتر از 1 درصد کل میزان فروش اینترنتی میباشد.
Select [Measures].[Internet Sales Amount] on columns, non empty bottompercent( [Product].[Product Categories].[Subcategory], --0.01, 1, [Measures].[Internet Sales Amount] ) on rows From [Adventure Works]
کاربرد تابع Topsum در کوئری زیر نمایش داده شده است
Select [Measures].[Internet Sales Amount] on columns, topsum( [Product].[Product Categories].[Subcategory], 25000000, [Measures].[Internet Sales Amount] ) on rows From [Adventure Works]
در این کوئری از تابع TopSum استفاده شده است که عملا حداکثر تعداد دسته بندی محصولاتی را بازیابی میکند که دارای بیشترین میزان فروش بوده اند و همچنین در مجموع بیش از 25000000 فروش داشته باشند .
تابع bottomsum عملا تعداد دسته بندی محصولاتی را که دارای کمترین میزان فروش بوده اند و همچنین سرجمع میزان فروش اینترنتی آنها 100000 بوده است را بر می گرداند. البته خروجی توسط non empty ، فیلتر شده است و خروجی هایی که کاملا Null می باشند، حذف گردیده اند.
Select [Measures].[Internet Sales Amount] on columns, non empty bottomsum( [Product].[Product Categories].[Subcategory], 100000, [Measures].[Internet Sales Amount] )on rows From [Adventure Works]