Service Layer
نقش لایهی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایهی Facade نیز میشناسند. این لایه، دادهها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایهی Presentation فراهم میکند. کلاس View Model یک Strongly Typed محسوب میشود که نماهای خاصی از دادهها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه مینماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.
الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interfaceها و زیر سیستمهای پیچیده ارائه میکند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.
کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductViewModel { Public int ProductId {get; set;} public string Name { get; set; } public string Rrp { get; set; } public string SellingPrice { get; set; } public string Discount { get; set; } public string Savings { get; set; } }
برای اینکه کلاینت با لایهی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه میشود و پارامترهای مورد نیاز را فراهم میکند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Service { public class ProductListRequest { public CustomerType CustomerType { get; set; } } }
در شی Response نیز بررسی میکنیم که درخواست به درستی انجام شده باشد، دادههای مورد نیاز را برای کلاینت فراهم میکنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال مینماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductListResponse { public bool Success { get; set; } public string Message { get; set; } public IList<ProductViewModel> Products { get; set; } }
به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما میتوانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمیباشند. بنابراین بهترین انتخاب، استفاده از Extension Methodها میباشد که باید برای کلاس Product و در لایهی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public static class ProductMapperExtensionMethods { public static ProductViewModel ConvertToProductViewModel(this Model.Product product) { ProductViewModel productViewModel = new ProductViewModel(); productViewModel.ProductId = product.Id; productViewModel.Name = product.Name; productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP); productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice); if (product.Price.Discount > 0) productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount); if (product.Price.Savings < 1 && product.Price.Savings > 0) productViewModel.Savings = product.Price.Savings.ToString(“#%”); return productViewModel; } public static IList<ProductViewModel> ConvertToProductListViewModel( this IList<Model.Product> products) { IList<ProductViewModel> productViewModels = new List<ProductViewModel>(); foreach(Model.Product p in products) { productViewModels.Add(p.ConvertToProductViewModel()); } return productViewModels; } }
حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد مینماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:
public class ProductService { private Model.ProductService _productService; public ProductService(Model.ProductService ProductService) { _productService = ProductService; } public ProductListResponse GetAllProductsFor( ProductListRequest productListRequest) { ProductListResponse productListResponse = new ProductListResponse(); try { IList<Model.Product> productEntities = _productService.GetAllProductsFor(productListRequest.CustomerType); productListResponse.Products = productEntities.ConvertToProductListViewModel(); productListResponse.Success = true; } catch (Exception ex) { // Log the exception… productListResponse.Success = false; // Return a friendly error message productListResponse.Message = ex.Message; } return productListResponse; } }
کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال میکند. همچنین این لایه محل مناسبی برای Log کردن خطاها میباشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer میپردازیم.
Data Layer
برای ذخیره سازی محصولات، یک بانک اطلاعاتی با نام Shop01 ایجاد کنید که شامل جدولی به نام Product با ساختار زیر باشد:
برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده میکنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه میکنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.
از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.
اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using SoCPatterns.Layered.Model; namespace SoCPatterns.Layered.Repository { public class ProductRepository : IProductRepository { public IList<Model.Product> FindAll() { var products = from p in new ShopDataContext().Products select new Model.Product { Id = p.ProductId, Name = p.ProductName, Price = new Model.Price(p.Rrp, p.SellingPrice) }; return products.ToList(); } } }
در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایهی Data هم به پایان رسید و در ادامه به کدنویسی لایهی Presentation و UI میپردازیم.
Presentation Layer
به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده میکنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; public interface IProductListView { void Display(IList<ProductViewModel> Products); Model.CustomerType CustomerType { get; } string ErrorMessage { set; } }
این Interface توسط Web Formهای ASP.NET و یا Win Formها باید پیاده سازی شوند. کار با Interfaceها موجب میشود تا تست Viewها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:
using SoCPatterns.Layered.Service; namespace SoCPatterns.Layered.Presentation { public class ProductListPresenter { private IProductListView _productListView; private Service.ProductService _productService; public ProductListPresenter(IProductListView ProductListView, Service.ProductService ProductService) { _productService = ProductService; _productListView = ProductListView; } public void Display() { ProductListRequest productListRequest = new ProductListRequest(); productListRequest.CustomerType = _productListView.CustomerType; ProductListResponse productResponse = _productService.GetAllProductsFor(productListRequest); if (productResponse.Success) { _productListView.Display(productResponse.Products); } else { _productListView.ErrorMessage = productResponse.Message; } } } }
کلاس Presenter وظیفهی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایهی Presentation به پایان رسیده است. از مزایای وجود لایهی Presentation این است که تست نویسی مربوط به نمایش دادهها و تعامل بین کاربر و سیستم به سهولت انجام میشود بدون آنکه نگران دشواری Unit Test نویسی Web Formها باشید. حال میتوانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.
در دات نت فریم ورک امکان کامپایل پویای یک قطعه کد دریافت شده از یک رشته، توسط فضای نام CodeDom مهیا است که قدرت قابل توجهی را در اختیار برنامه نویس قرار میدهد.
مثال یک:
رشته زیر را کامپایل کرده و تبدیل به یک فایل exe کنید:
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
using System;
using System.Collections.Generic;
//دو فضای نامی که برای این منظور اضافه شدهاند
using Microsoft.CSharp;
using System.CodeDom.Compiler;
namespace compilerTest
{
class Program
{
static void compileIt1()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
//تعیین اینکه خروجی یک فایل اجرایی است بعلاوه مشخص سازی محل ذخیره سازی فایل نهایی
CompilerParameters compilerParams = new CompilerParameters
{
OutputAssembly = "D:\\Foo.EXE",
GenerateExecutable = true
};
//عملیات کامپایل در اینجا صورت میگیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);
//اگر خطایی وجود داشته باشد نمایش داده خواهد شد
Console.WriteLine("Number of Errors: {0}", results.Errors.Count);
foreach (CompilerError err in results.Errors)
{
Console.WriteLine("ERROR {0}", err.ErrorText);
}
}
static void Main(string[] args)
{
compileIt1();
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
کد مورد نظر را به صورت یک فایل dll کامپایل کنید.
برای این منظور تمامی مراحل مانند قبل است فقط GenerateExecutable ذکر شده به false تنظیم شده و نام خروجی نیز به foo.dll باید تنظیم شود.
مثال 3:
کد مورد نظر را در حافظه کامپایل کرده (خروجی dll یا exe نمیخواهیم)، سپس متد SayHello آن را به صورت پویا فراخوانی نموده و خروجی را نمایش دهید.
در این حالت روش کار همانند مثال 1 است با این تفاوت که GenerateInMemory = true و GenerateExecutable = false تنظیم میشوند. همچنین جهت دسترسی به متد کلاس ذکر شده، از قابلیتهای ریفلکشن موجود در دات نت فریم ورک استفاده خواهد شد.
using System;
using System.Collections.Generic;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
namespace compilerTest
{
class Program
{
static void compileIt2()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);
//نحوه تعیین مشخص سازی کامپایل در حافظه
CompilerParameters compilerParams = new CompilerParameters
{
GenerateInMemory = true,
GenerateExecutable = false
};
//عملیات کامپایل در اینجا صورت میگیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);
// اگر خطایی در کامپایل وجود نداشت متد دلخواه را فراخوانی میکنیم
if (results.Errors.Count == 0)
{
//استفاده از ریفلکشن برای دسترسی به متد و فراخوانی آن
Type type = results.CompiledAssembly.GetType("Foo.Bar");
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(null, null);
}
}
static void Main(string[] args)
{
compileIt2();
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مثال:
اگر رشته سورس ما به صورت زیر بوده و از اسمبلی System.Drawing.Dll نیز کمک گرفته باشد،
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}
public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
var r = new System.Drawing.Rectangle(0,0,100,100);
System.Console.WriteLine(r);
}
}
}
";
Number of Errors: 1
ERROR The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?)
برای رفع این مشکل و معرفی این اسمبلی، سطر زیر باید پس از تعریف compilerParams اضافه شود.
compilerParams.ReferencedAssemblies.Add("System.Drawing.Dll");
نمونهای دیگر از این دست، استفاده از LINQ میباشد. در این حالت اسمبلی System.Core.Dll نیز به روش ذکر شده باید معرفی گردد تا مشکلی در کامپایل کد رخ ندهد.
کاربردها:
1- استفاده در ابزارهای تولید کد (برای مثال در برنامه Linqer از این قابلیت استفاده میشود)
2- استفادههای امنیتی (ایجاد روشهای تولید یک سریال به صورت پویا و کامپایل پویای کد مربوطه در حافظهای محافظت شده)
3- استفاده جهت مقاصد محاسباتی پیشرفته
4- دادن اجازهی کد نویسی به کاربران برنامهی خود (شبیه به سیستمهای ماکرو و اسکریپت نویسی موجود)
و ...
- Get the message "Working on it" when switching to a tab
- Visual Studio Feedback - new Web-Browser based Version crashes due to proxy failure
- Unable to build xamarin.ios library project with .xib interface declaration on specific version of VS
- Cannot generate shim for System.DateTime
- IntelliCode suggestion, filtering compilation errors
- Inserting .NET Core SDK 3.1.403 into Visual Studio 2019
- Fixed a bug that affects debugging https enabled Service Fabric Application.
- Support Service Fabric nuget packages with external dependencies.
سیر تکاملی پایتون در سال های اخیر
According to several websites, Python is one of the most popular coding languages of 2015.
The programming language is currently being used by a number of high-traffic websites including Google, Yahoo Groups, Yahoo Maps, Linux Weekly News, Shopzilla and Web Therapy. Likewise, Python also finds great use for creating gaming, financial, scientific and educational applications
public class PostDto : BaseDto<PostDto, Post, long> { public string Title { get; set; } public string Text { get; set; } public int CategoryId { get; set; } public string CategoryName { get; set; } //=> Category.Name }
- کلاس PostDto خودش را به عنوان اولین پارامتر جنریک BaseDto معرفی میکند.
- به عنوان پارامتر دوم، باید کلاس Entity ایی که قرار است به آن نگاشت شود (Post) را معرفی کنیم.
- پارامتر سوم، نوع فیلد Id است که در اینجا خاصیت Id کلاسهای Post و PostDto ما، از نوع long است.
- نهایتا خواصی را که برای نگاشت لازم داریم، تعریف میکنیم مثل Title و...
- همچنین میتوانیم خواصی برای نگاشت با خواص Navigation Propertyهای Post هم تعریف کنیم؛ مانند CategoryName که به خاصیت Name از Category پست مربوطه اشاره میکند و AutoMapper به صورت هوشمندانه آنها را به هم نگاشت میکند.
public abstract class BaseDto<TDto, TEntity, TKey> where TDto : class, new() where TEntity : BaseEntity<TKey>, new() { [Display(Name = "ردیف")] public TKey Id { get; set; } public TEntity ToEntity() { return Mapper.Map<TEntity>(CastToDerivedClass(this)); } public TEntity ToEntity(TEntity entity) { return Mapper.Map(CastToDerivedClass(this), entity); } public static TDto FromEntity(TEntity model) { return Mapper.Map<TDto>(model); } protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance) { return Mapper.Map<TDto>(baseInstance); } }
- نوع TDto به کلاس Dto ما اشاره میکند؛ مثلا PostDto
- نوع TEntity به کلاس Entity ما اشاره میکند؛ مثلا Post
- نوع TKey به نوع خاصیت Id اشاره میکند.
- شرط لازم برای نوع TEntity این است که از <BaseEntity<TKey ارث بری کرده باشد (نوع پایهای که تمام Entityهای ما از آن ارث بری میکنند).
- متدهای کمکی ToEntity و FromEntity، کار نگاشت اشیاء را برای ما راحتتر میکنند.
public abstract class BaseEntity<TKey> { public TKey Id { get; set; } } public class Post : BaseEntity<long> { public string Title { get; set; } public string Text { get; set; } public int CatgeoryId { get; set; } public Category Category { get; set; } }
var postDto = new PostDto(); var post = postDto.ToEntity();
var post = // finded by id var updatePost = postDto.ToEntity(post);
var postDto = PostDto.FromEntity(post);
public static class AutoMapperConfiguration { public static void InitializeAutoMapper() { Mapper.Initialize(configuration => { configuration.ConfigureAutoMapperForDto(); }); //Compile mapping after configuration to boost map speed Mapper.Configuration.CompileMappings(); } public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config) { config.ConfigureAutoMapperForDto(Assembly.GetEntryAssembly()); } public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var dtoTypes = GetDtoTypes(assemblies); var mappingTypes = dtoTypes .Select(type => { var arguments = type.BaseType.GetGenericArguments(); return new { DtoType = arguments[0], EntityType = arguments[1] }; }).ToList(); foreach (var mappingType in mappingTypes) config.CreateMappingAndIgnoreUnmappedProperties(mappingType.EntityType, mappingType.DtoType); } public static void CreateMappingAndIgnoreUnmappedProperties(this IMapperConfigurationExpression config, Type entityType, Type dtoType) { var mappingExpression = config.CreateMap(entityType, dtoType).ReverseMap(); //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } } public static IEnumerable<Type> GetDtoTypes(params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); var dtoTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType != null && type.BaseType.IsGenericType && (type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,>) || type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,,>))); return dtoTypes; } }
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; AutoMapperConfiguration.InitializeAutoMapper(); }
public static IEnumerable<Type> GetDtoTypes(params Assembly[] assemblies) { var allTypes = assemblies.SelectMany(a => a.ExportedTypes); var dtoTypes = allTypes.Where(type => type.IsClass && !type.IsAbstract && type.BaseType != null && type.BaseType.IsGenericType && (type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,>) || type.BaseType.GetGenericTypeDefinition() == typeof(BaseDto<,,>))); return dtoTypes; }
- در خط اول ابتدا تمامی نوعهای قابل دسترس از بیرون (ExportedTypes) از assemblyهای دریافتی واکشی میشود.
- سپس توسط Where، نوعهایی که کلاس بوده، abstract نیستند و از BaseDto ارث بری کردهاند، فیلتر شده و بازگردانده میشوند.
public static void ConfigureAutoMapperForDto(this IMapperConfigurationExpression config, params Assembly[] assemblies) { var dtoTypes = GetDtoTypes(assemblies); var mappingTypes = dtoTypes .Select(type => { var arguments = type.BaseType.GetGenericArguments(); return new { DtoType = arguments[0], EntityType = arguments[1] }; }).ToList(); foreach (var mappingType in mappingTypes) config.CreateMappingAndIgnoreUnmappedProperties(mappingType.EntityType, mappingType.DtoType); }
public static void CreateMappingAndIgnoreUnmappedProperties(this IMapperConfigurationExpression config, Type entityType, Type dtoType) { var mappingExpression = config.CreateMap(entityType, dtoType).ReverseMap(); //Ignore mapping to any property of entity (like Post.Categroy) that dose not contains in dto (like PostDto.CategoryName) //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value foreach (var property in entityType.GetProperties()) { if (dtoType.GetProperty(property.Name) == null) mappingExpression.ForMember(property.Name, opt => opt.Ignore()); } }
public class SearchDTO { public string TableName { get; set; } public string ColumnName { get; set; } public string searchPhrase { get; set; } = string.Empty; public DateTimeOffset? searchDateFrom { get; set; } public DateTimeOffset? searchDateTo { get; set; } public int Include { get; set; } = 1; }
// Join the tables dynamically based on the table names for (int i = 1; i < filterList.Count; i++) { var joinEntityType = entityTypes.FirstOrDefault(t => t.ClrType.Name == filterList[0].TableName)?.ClrType; if (entityType == null) { return (null, 0, 0); } var joinEntityQuery = (IQueryable<object>)Activator.CreateInstance(typeof(DbSet<>).MakeGenericType(joinEntityType), _dbContext); query = query.Join(joinEntityQuery.ToList(), x => x.GetType().GetProperty($"{filterList[i - 1].TableName}.{filterList[i - 1].TableName}Id").GetValue(x), y => y.GetType().GetProperty($"{filterList[i].TableName}.{filterList[i].TableName}Id").GetValue(y), (x, y) => x); } // Apply the conditions dynamically based on the column names and conditions for (int i = 0; i < filterList.Count; i++) { if (!string.IsNullOrEmpty(filterList[i].searchPhrase)) { var parameter = Expression.Parameter(entityType, "x"); var condition = Expression.Call( typeof(string).GetMethod("Contains", new[] { typeof(string) }), Expression.PropertyOrField(parameter, filterList[i].ColumnName), Expression.Constant(filterList[i].searchPhrase) ); var lambda = Expression.Lambda<Func<object, bool>>(condition, parameter); query = query.Where(lambda); } if (filterList[i].searchDateFrom.HasValue) { //must write expression for date constraint } } // Select the specified columns dynamically ResourceManager resourceManager = new ResourceManager(typeof(TablePropertiesResources)); var columnNames = resourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true) .OfType<DictionaryEntry>() .Select(entry => entry.Key.ToString()) .ToList(); var selectColumns = columnNames.ToArray(); var selectedData = query .Select(x => new { // Dynamically select the desired properties Result = selectColumns.ToDictionary(column => column, column => x.GetType().GetProperty(column).GetValue(x)) }) .ToList();
IIS 10.0 Express منتشر شد
Internet Information Services (IIS) 10.0 Express is a free, simple and
self-contained version of IIS that is optimized for developers. IIS 10.0
Express makes it easy to use the most current version of IIS to develop
and test websites. IIS 10.0 Express has all the core capabilities of
IIS 10.0 and additional features to ease website development.
The benefits of using IIS 10.0 Express include:
- The same web server that runs on your production server is now available on your development computer.
- Most tasks can be done without the need for administrative privileges.
- IIS Express runs on Windows 7 Service Pack 1 and all later versions of Windows.
- Many users can work independently on the same computer.