European ASP.NET 4.5 Hosting BLOG

BLOG about ASP.NET 4, ASP.NET 4.5 Hosting and Its Technology - Dedicated to European Windows Hosting Customer

European ASP.NET Core Hosting - HostForLIFE.eu :: Read and Write a CSV File in ASP.NET Core

clock March 31, 2020 11:06 by author Peter
For this blog, my agenda is to provide a step-by-step solution to read and write CSV files in ASP.NET Core 3.0, and CSVHelper. The Same Logic will work for Web Application, Windows Application, and Console Application. In this post, I'm going to create a sample Console Application to show you the process.

Steps:
  1. Create a Console Application project
  2. Create a student class inside the project
  3. Install the CSVHelper from NuGet Package Manager
  4. Add Mappers folder and inside add a mapper Class for Student
  5. Add Services Folder in the project and Add StudentService Class inside it
  6. Write the Logic inside the main method of Program file as its starting point of the application
Create a Console Application project
Give the Name to The Project as "ReadWriteCSVFile". You can give any name.
 
Create a student class inside the project 
Write the below code inside the Student class:

namespace ReadWriteCSVFile {  
    public class Student {  
        public int RollNo {  
            get;  
            set;  
        }  
        public string Name {  
            get;  
            set;  
        }  
        public string Course {  
            get;  
            set;  
        }  
        public decimal Fees {  
            get;  
            set;  
        }  
        public string Mobile {  
            get;  
            set;  
        }  
        public string City {  
            get;  
            set;  
        }  
    }  

In the next step, we are going to install the CSVHelper package so that it will help us to read and write the CSV file in an easy way.

Now Install the CSVHelper from NuGet Package Manager --Version (12.2.1),

  • Step 1 - Right Click on the Project
  • Step 2 - Go To "Manage NuGet Packages..."
  • Step 3 - Go To Browse Tab then move to the Search area
  • Step 4 - Type "CSVHelper" in the search box

Here you will see a list of packages. Choose the first one and move it to the right-side panel. You will see one option as Version: (If not installed, if you already installed, then you will see both Installed and Version, two options). Select Version 12.2.1 and click on the install button and follow the steps to install successfully.

Add "Mappers" folder and inside it add mapper Class for Student

  • Here you can give any name to the folder and class
  • Give the proper name to the class as "StudentMap"
  • Make this class as sealed

Write the below code inside the StudentMap Class:

namespace ReadWriteCSVFile.Mappers {  
    public sealed class StudentMap: ClassMap < Student > {  
        public StudentMap() {  
            Map(x => x.RollNo).Name("RollNo");  
            Map(x => x.Name).Name("Name");  
            Map(x => x.Course).Name("Course");  
            Map(x => x.Fees).Name("Fees");  
            Map(x => x.Mobile).Name("Mobile");  
            Map(x => x.City).Name("City");  
        }  
    }  

Add "Services" Folder in the project and Add StudentService Class inside it:

  • Here, you can give any name to the folder and class
  • Give the proper name to the class as "StudentService"

Write the below Code inside the StudentService Class

namespace ReadWriteCSVFile.Services {  
    public class StudentService {  
        public List < Student > ReadCSVFile(string location) {  
            try {  
                using(var reader = new StreamReader(location, Encoding.Default))  
                using(var csv = new CsvReader(reader)) {  
                    csv.Configuration.RegisterClassMap < StudentMap > ();  
                    var records = csv.GetRecords < Student > ().ToList();  
                    return records;  
                }  
            } catch (Exception e) {  
                throw new Exception(e.Message);  
            }  
        }  
        public void WriteCSVFile(string path, List < Student > student) {  
            using(StreamWriter sw = new StreamWriter(path, false, new UTF8Encoding(true)))  
            using(CsvWriter cw = new CsvWriter(sw)) {  
                cw.WriteHeader < Student > ();  
                cw.NextRecord();  
                foreach(Student stu in student) {  
                    cw.WriteRecord < Student > (stu);  
                    cw.NextRecord();  
                }  
            }  
        }  
    }  

Write the Logic inside the main method of Program file as its starting point of the application.
Change the code inside the Main method of Program class as shown below:

 

namespace ReadWriteCSVFile {  
    class Program {  
        static void Main(string[] args) {  
            Console.WriteLine("Start CSV File Reading...");  
            var _studentService = new StudentService();  
            var path = @ "D:\Tutorials\Student.csv";  
            //Here We are calling function to read CSV file  
            var resultData = _studentService.ReadCSVFile(path);  
            //Create an object of the Student class  
            Student student = new Student();  
            student.RollNo = 5;  
            student.Name = "Lucy";  
            student.Course = "B.Tech";  
            student.Fees = 75000;  
            student.Mobile = "7788990099";  
            student.City = "Pune";  
            resultData.Add(student);  
            //Here We are calling function to write file  
            _studentService.WriteCSVFile(@ "D:\Tutorials\NewStudentFile.csv", resultData);  
            //Here D: Drive and Tutorials is the Folder name, and CSV File name will be "NewStudentFile.csv"  
            Console.WriteLine("New File Created Successfully.");  
        }  
    }  

 



European ASP.NET Core Hosting - HostForLIFE.eu :: 9 Tips to Increase Your ASP.NET Core 3.0 Applications

clock March 31, 2020 09:56 by author Scott

Performance is very important; it is a major factor for the success of any web application. ASP.NET Core 3.0 includes several enhancements that scale back memory usage and improve turnout. In this blog post, I provide 10 tips to help you improve the performance of ASP.NET Core 3.0 applications by doing the following:

Avoid synchronous and use asynchronous

Try to avoid synchronous calling when developing ASP.NET Core 3.0 applications. Synchronous calling blocks the next execution until the current execution is completed. While fetching data from an API or performing operations like I/O operations or independent calling, execute the call in an asynchronous manner.

Avoid using Task.Wait and Task.Result, and try to use await. The following code shows how to do this.

public class WebHost
{
    public virtual async Task StartAsync(CancellationToken cancellationToken = default)
    { 

        // Fire IHostedService.Start
        await _hostedServiceExecutor.StartAsync(cancellationToken).ConfigureAwait(false); 

        // More setup
        await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false); 

        // Fire IApplicationLifetime.Started
        _applicationLifetime?.NotifyStarted(); 

        // Remaining setup
    }
}

Entity Framework 3.0 Core also provides a set of async extension methods, similar to LINQ methods, that execute a query and return results.

Asynchronous querying

Asynchronous queries avoid blocking a thread while the query is executed in the database. Async queries are important for quick, responsive client applications.

Examples:

  • ToListAsync()
  • ToArrayAsync()
  • SingleAsync()

public async Task<List> GetBlogsAsync()
{
    using (var context = new BloggingContext())
    {
        return await context.Blogs.ToListAsync();
    }
}

Asynchronous saving

Asynchronous saving avoids a thread block while changes are written to the database. It provides DbContext.SaveChangesAsync() as an asynchronous alternative to DbContext.SaveChanges().

public static async Task AddBlogAsync(string url)
{
    using (var context = new BloggingContext())
    {
        var blogContent = new BlogContent { Url = url };
        context.Blogs.Add(blogContent);
        await context.SaveChangesAsync();
    }
}

Optimize data access

Improve the performance of an application by optimizing its data access logic. Most applications are totally dependent on a database. They have to fetch data from the database, process the data, and then display it. If it is time-consuming, then the application will take much more time to load.

Recommendations:

  • Call all data access APIs asynchronously.
  • Don’t try to get data that is not required in advance.
  • Try to use no-tracking queries in Entity Framework Core when accessing data for read-only purposes.
  • Use filter and aggregate LINQ queries (with .Where, .Select, or .Sum statements), so filtering can be performed by the database.

You can find approaches that may improve performance of your high-scale apps in the new features of EF Core 3.0.

Use response caching middleware

Middleware controls when responses are cacheable. It stores responses and serves them from the cache. It is available in the Microsoft.AspNetCore.ResponseCaching package, which was implicitly added to ASP.NET Core.

In Startup.ConfigureServices, add the Response Caching Middleware to the service collection.

public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCaching();
    services.AddRazorPages();
}

Use JSON serialization

ASP.NET Core 3.0 uses System.Text.Json for JSON serialization by default. Now, you can read and write JSON asynchronously. This improves performance better than Newtonsoft.Json. The System.Text.Json namespace provides the following features for processing JSON:

  • High performance.
  • Low allocation.
  • Standards-compliant capabilities.

  • Serializing objects to JSON text and deserializing JSON text to objects.

Reduce HTTP requests

Reducing the number of HTTP requests is one of the major optimizations. Cache the webpages and avoid client-side redirects to reduce the number of connections made to the web server.

Use the following techniques to reduce the HTTP requests:

  • Use minification.
  • Use bundling.
  • Use sprite images.

By reducing HTTP requests, these techniques help pages load faster.

Use exceptions only when necessary

Exceptions should be rare. Throwing and catching exceptions will consume more time relative to other code flow patterns.

  • Don’t throw and catch exceptions in normal program flow.

  • Use exceptions only when they are needed.

Use response compression

Response compression, which compresses the size of a file, is another factor in improving performance. In ASP.NET Core, response compression is available as a middleware component.

Usually, responses are not natively compressed. This typically includes CSS, JavaScript, HTML, XML, and JSON.

  • Don’t compress natively compressed assets, such as PNG files.
  • Don’t compress files with a size of 150-1,000 bytes.
  • Don’t compress small files; it may produce a compressed file larger than the uncompressed file.

Package: Microsoft.AspNetCore.ResponseCompression is implicitly included in ASP.NET Core apps.

The following sample code shows how to enable Response Compression Middleware for the default MIME types and compression providers.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddResponseCompression();
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseResponseCompression();
    }
}

These are the providers:

public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.Providers.Add<BrotliCompressionProvider>();
        options.Providers.Add<GzipCompressionProvider>();
        options.Providers.Add<CustomCompressionProvider>();
        options.MimeTypes =
            ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "image/svg+xml" });
    });
}

HttpContext accessibility improvements

HttpContext accessibility is only valid as long as there is an active HTTP request in ASP.NET Core. Here are some suggestions for accessing HttpContext from Microsoft’s documentation:

Client-side improvements

Client-side optimization is one important aspect of improving performance. When creating a website using ASP.Net Core, consider the following tips:

Bundling

Bundling combines multiple files into a single file, reducing the number of server requests. You can use multiple individual bundles in a webpage.

Minification

Minification removes unnecessary characters from code without changing any functionality, also reducing file size. After applying minification, variable names are shortened to one character and comments and unnecessary whitespace are removed.

Loading JavaScript at last

Load JavaScript files at the end. If you do that, static content will show faster, so users won’t have to wait to see the content.

Use a content delivery network

Use a content delivery network (CDN) to load static files such as images, JS, CSS, etc. This keeps your data close to your consumers, serving it from the nearest local server.

Conclusion

Now you know 10 tips to help improve the performance of ASP.NET Core 3.0 applications. I hope you can implement most of them in your development.

 



European ASP.NET Core Hosting - HostForLIFE.eu :: How to Use AutoMapper with ASP.NET Core 3

clock March 19, 2020 09:20 by author Scott

AutoMapper is well known in the .NET community. It bills itself as "a simple little library built to solve a deceptively complex problem - getting rid of code that maps one object to another," and it does the job nicely.

In the past, I've used it exclusively with ASP.NET APIs. However, the method for utilizing it via dependency injection has changed. So let's review how to get started, how to define mappings and how to inject our mappings into ASP.NET Core APIs.

Getting Started

Like most .NET libraries, we can install the AutoMapper package from Nuget.

Install-Package AutoMapper

For our purposes, we'll focus on two classes that are related; User and UserDTO.

public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string FavoriteFood { get; set; }
    public DateTime BirthDate { get; set; }
}

public class UserDTO
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int BirthYear { get; set; }
}

These classes will serve as source and destination types that we can work with.

Default Mappings

Without specific configuration, AutoMapper will match properties based on their name. By default, it will ignore null reference exceptions when mapping source and destination types. Below is a snippet mapping the source and destination types using the default configuration.

var config = new MapperConfiguration(cfg => cfg.CreateMap<User, UserDTO>());

var user = new User()
{
    Id = Guid.NewGuid(),
    Name = "Wayne Curry",
    FavoriteFood = "Sushi",
    BirthDate = new DateTime(1986, 10, 12)
};

var mapper = config.CreateMapper();
UserDTO userDTO = mapper.Map<UserDTO>(user);

The above will create a UserDTO object with an Id and Name that matches the original user object, but no error is thrown as a result of not having the FavoriteFood property on the UserDTO type. Also, the BirthYear property of the UserDTO will be zero.

Custom Mappings

We can use projection to translate properties as they are mapped. For instance, the code snippet below shows how we can map the BirthDate property of the User type to the BirthYear property of the UserDTO type.

var config = new MapperConfiguration(cfg =>
    cfg.CreateMap<User, UserDTO>()
        .ForMember(dest => dest.BirthYear,
                   opt => opt.MapFrom(src => src.BirthDate.Year));

var user = new User()
{
    Id = Guid.NewGuid(),
    Name = "Wayne Curry",
    FavoriteFood = "Sushi",
    BirthDate = new DateTime(1986, 10, 12)
};

var mapper = config.CreateMapper();
UserDTO userDTO = mapper.Map<UserDTO>(user);

The resulting userDTO object will be similar to our first example, but this time it will
include the BirthYear property of 2000.

Profiles

A clean way to organize and maintain our mapping configurations is with profiles. Many times these Profile classes will encapsulate business areas (e.g. Ordering, Shipping). To start, we'll create a class that inherits from Profile and put the configuration in the constructor.

public class UserManagementProfile : Profile
{
    public UserManagementProfile()
    {
        CreateMap<User, UserDTO>()
            .ForMember(dest => dest.BirthYear,
            opt => opt.MapFrom(src => src.BirthDate.Year));

        // Configurations for other classes in this business
        // area can be included here as well, like below:

        // CreateMap<Role, RoleDTO>();
        // CreateMap<Permission, PermissionDTO>();
    }
}

For added isolation, we can create a project just for our Profile configurations. Using profiles helps us keep configurations more manageable as our application grows.

Dependency Injection

Dependency injection is baked into ASP.NET Core, but to use AutoMapper with it we'll need additional configuration and an additional Nuget package.

Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

Register AutoMapper

Once installed, we can define the configuration using profiles. In the Startup.ConfigureServices method, we can use the AddAutoMapper extension method on the IServiceCollection object as shown below:

// By Marker
services.AddAutoMapper(typeof(ProfileTypeFromAssembly1) /*, ...*/);

// or by Assembly
services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/);

Inject AutoMapper

With AutoMapper registered and its configurations set, we can now inject an IMapper into our controllers.

public class UsersController
{
    private readonly IMapper _mapper;

    public UsersController(IMapper mapper) => _mapper = mapper;

    // use _mapper.Map
}

With the IMapper we can map our objects to their DTO equivalents using the .Map method.

Wrap It Up

Now that ASP.NET Core is injecting AutoMapper to our controllers, we can add configurations to our profiles or create new profiles for new business areas and still map appropriately without further configuration.

Of course, we didn't cover all of the features of AutoMapper so I'd suggest checking out their documentation for more information about their capabilities. Hopefully this post gave you enough information to start trying AutoMapper yourself. Let me know in the comments how you use AutoMapper in your applications.



ASP.NET Core Hosting - HostForLIFE.eu :: Progressive Retry for Network Calls

clock March 18, 2020 11:55 by author Peter

In today's mobile world, many calls across the internet or network could fail for many reasons. Some of the reasons could be the service is busy, the network is slow and many more. For these types of calls, it’s advisable to retry the call if there is an error. The current code base I am working on connects to Salesforce to retrieve and update data. Salesforce is one of those backend services that could experience these types of issues.

To help with these types of network issues, I have added a new method to my open-source code called ProgressiveRetry(). This method will try the call a given number of times. If there is an error, it will wait for a given milliseconds that increases with each error. Below is the code for this call.

    /// <summary> 
    /// Progressive retry for a function call. 
    /// </summary> 
    /// <param name="operation">The operation to perform.</param> 
    /// <param name="retryCount">The retry count (default 3).</param> 
    /// <param name="retryWaitMilliseconds">The retry wait milliseconds (default 100).</param> 
    /// <returns>System.Int32.</returns> 
    public static int ProgressiveRetry(Action operation, byte retryCount = 3,  
                                       int retryWaitMilliseconds = 100) 
    { 
        Encapsulation.TryValidateParam<ArgumentNullException>(operation != null); 
        Encapsulation.TryValidateParam<ArgumentOutOfRangeException>(retryCount > 0); 
        Encapsulation.TryValidateParam<ArgumentOutOfRangeException>(retryWaitMilliseconds > 0); 
      
        var attempts = 0; 
      
        do 
        { 
            try 
            { 
                attempts++; 
     
                 operation(); 
                return attempts; 
     
            } 
            catch (Exception ex) 
            { 
                if (attempts == retryCount) 
                { 
                    throw; 
                } 
      
                Debug.WriteLine(ex.GetAllMessages()); 
      
                Task.Delay(retryWaitMilliseconds * attempts).Wait(); 
                   } 
            } while (true); 
        } 
    } 


Here is example code on how to use ProgressiveRetry().
    var result = false; 
    var count = ExecutionHelper.ProgressiveRetry(() => 
    { 
        result = NetworkHelper.IsHostAvailable("wordpress.com"); 
    } 
    , retryCount: 3, retryWaitMilliseconds: 225); 
    Console.WriteLine($"Host available {result}. Tried call {count} times.");

HostForLIFE.eu ASP.NET Core Hosting

European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



ASP.NET Core Hosting - HostForLIFE.eu :: Using Redis To Delay Execution In ASP.NET Core

clock March 3, 2020 11:07 by author Peter

Due to some business requirements, many operations should not begin to execute right away;  they should begin after some seconds, minutes or hours. For example, say there is a task, and it contains two steps. When we finish the first step, the second step should begin after 5 minutes.
 

How can we solve this problem?
Thread.Sleep() and Task.Delay() is a very easy solution that we can use. But it may block our applictions.
 
And in this article, I will introduce a solution based on keyspace notifications of Redis.
 
Here we will use expired events to do it, but this solution also has some limitations , because it may have a significant delay. That means that delay of execution may have smoe error, and will not be very accurate!
 
Let's take a look at this solution.
 
Set Up Redis
Keyspace notifications is a feature available since 2.8.0, so the version of Redis should not be less than 2.8.0.
 
We should modify an important configuration so that we can enable this feature.
    ############################# Event notification ############################## 
    
    # Redis can notify Pub/Sub clients about events happening in the key space. 
    # This feature is documented at http://redis.io/topics/notifications 
    # 
    # ......... 
    # 
    #  By default all notifications are disabled because most users don't need 
    #  this feature and the feature has some overhead. Note that if you don't 
    #  specify at least one of K or E, no events will be delivered. 
    notify-keyspace-events "" 


The default value of notify-keyspace-events is empty, we should modify it to Ex.
    notify-keyspace-events "Ex" 

Then we can startup the Redis server.

Create Project
Create a new ASP.NET Core Web API project and install CSRedisCore.
    <Project Sdk="Microsoft.NET.Sdk.Web"> 
     
      <PropertyGroup> 
        <TargetFramework>netcoreapp3.1</TargetFramework> 
      </PropertyGroup> 
     
      <ItemGroup> 
        <PackageReference Include="CSRedisCore" Version="3.4.1" /> 
      </ItemGroup> 
     
    </Project> 

Add an interface named ITaskServices and a class named TaskServices.
    public interface ITaskServices 
    { 
        void SubscribeToDo(string keyPrefix); 
     
        Task DoTaskAsync(); 
    } 
     
    public class TaskServices : ITaskServices 
    { 
        public async Task DoTaskAsync() 
        { 
            // do something here 
            // ... 
     
            // this operation should be done after some min or sec 
            var taskId = new Random().Next(1, 10000); 
            int sec = new Random().Next(1, 5); 
     
            await RedisHelper.SetAsync($"task:{taskId}", "1", sec); 
            await RedisHelper.SetAsync($"other:{taskId + 10000}", "1", sec); 
        } 
     
        public void SubscribeToDo(string keyPrefix) 
        { 
            RedisHelper.Subscribe( 
                ("__keyevent@0__:expired", arg => 
                    { 
                        var msg = arg.Body; 
                        Console.WriteLine($"recive {msg}"); 
                        if (msg.StartsWith(keyPrefix)) 
                        { 
                            // read the task id from expired key 
                            var val = msg.Substring(keyPrefix.Length); 
                            Console.WriteLine($"Redis + Subscribe {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} begin to do task {val}"); 
                        } 
                    }) 
            ); 
        } 
    } 


As you can see, we set a redis key with expiration. The expiration is the delay time.
 
For the delay execution, we can find it in SubscribeToDo method. It subscribes a channel named __keyevent@0__:expired.
 
When a key is expired, redis server will publish a message to this channel, and subscribers will receive it.
 
After reciving the notification, the client will begin to do the job.
 
Before the client receives the notification, the delay job will not be executed, so that it can help us to do the job for a delay.
 
Here is the entry of this operation.
    [ApiController] 
    [Route("api/tasks")] 
    public class TaskController : ControllerBase 
    { 
        private readonly ITaskServices _svc; 
     
        public TaskController(ITaskServices svc) 
        { 
            _svc = svc; 
        } 
     
        [HttpGet] 
        public async Task<string> Get() 
        { 
            await _svc.DoTaskAsync(); 
            System.Console.WriteLine("done here"); 
            return "done"; 
        } 
    } 


Put the subscriber to a BackgroundService, so that it can run in the background.
    public class SubscribeTaskBgTask : BackgroundService 
    { 
        private readonly ILogger _logger; 
        private readonly ITaskServices _taskServices; 
     
        public SubscribeTaskBgTask(ILoggerFactory loggerFactory, ITaskServices taskServices) 
        { 
            this._logger = loggerFactory.CreateLogger<RefreshCachingBgTask>(); 
            this._taskServices = taskServices; 
        } 
     
        protected override Task ExecuteAsync(CancellationToken stoppingToken) 
        { 
            stoppingToken.ThrowIfCancellationRequested(); 
     
            _taskServices.SubscribeToDo("task:"); 
     
            return Task.CompletedTask; 
        } 
    } 


At last, we should register the above services in startup class.
    public class Startup 
    { 
        // ... 
         
        public void ConfigureServices(IServiceCollection services) 
        { 
            var csredis = new CSRedis.CSRedisClient("127.0.0.1:6379"); 
            RedisHelper.Initialization(csredis); 
     
            services.AddSingleton<ITaskServices, TaskServices>(); 
            services.AddHostedService<SubscribeTaskBgTask>(); 
     
            services.AddControllers(); 
        } 
    } 


Here is the result after running this application.

HostForLIFE.eu ASP.NET Core Hosting

European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



About HostForLIFE.eu

HostForLIFE.eu is European Windows Hosting Provider which focuses on Windows Platform only. We deliver on-demand hosting solutions including Shared hosting, Reseller Hosting, Cloud Hosting, Dedicated Servers, and IT as a Service for companies of all sizes.

We have offered the latest Windows 2016 Hosting, ASP.NET Core 2.2.1 Hosting, ASP.NET MVC 6 Hosting and SQL 2017 Hosting.


Tag cloud

Sign in