8

.NET 6: Top 6 New Features in the Upcoming .NET Version

 3 years ago
source link: https://rubikscode.net/2021/08/30/net-6-top-6-new-features-in-the-upcoming-net-6-version/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Subscribe and receive free guide - Ultimate Data Visualization Guide with Python

* indicates required

Email Address *

Microsoft .NET 6 is set to be released on November 9th. Over the course of a previous couple of months, we got several preview releases. Seven of them to be more exact. Each preview delivered various features of .NET 6 for developing server/cloud, desktop, IoT, and mobile applications.

We are expecting two more previews until the release, however, I thought that we already have a nice set of features that we can discuss. In this article, I went through 6 features that I think are really neat. I haven’t covered topics such as performance in-depth. However, I tried to cover performance topics that are affecting the development process directly.  Ok, let’s dive into it.

This bundle of e-books is specially crafted for beginners.
Everything from Python basics to the deployment of Machine Learning algorithms to production in one place.
Become a Machine Learning Superhero TODAY!

1. .NET 6 new Project Templates

With the .NET 6 preview 7  came several new project templates, which are really interesting. In essence, these templates are based on the new changes that C# 10 brings. The templates are much cleaner and simpler. It is important to note that engineers from Microsoft choose this path with templates, to suggest new features to users, but not to enforce them. For example, from now on when you create a console application, you will get just simple one-liner:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
Fashion MNIST Visual

This is great news. I mean, it just makes more sense than getting all this for one effective line of code, as it is in the current version:

using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

By enabling these features via project templates, we’re getting the best of both worlds: new code starts with these features enabled but existing code isn’t impacted when you upgrade.

Richard Lander

Program manager on the .NET team, Microsoft

So, what happened here? Well, the .NET 6 has three new features which make this happen:

  • Top-level Statements – This feature already exists in C# 9, but here it is utilized for templates. In a nutshell, from C#9 you don’t have to explicitly include the Main method in a console application project, but you can use the top-level statements feature. This is exactly what this template uses and minimizes the code dramatically.
  • Global Usings – Every C# file starts with a list of usings that are necessary for the implementation. However, sometimes this is redundant. C# 10 introduces the keyword global. Using this keyword, you are able to define global usings for the whole project in a separate file which will contain these imports (e.g. usings.cs). With this other files in the project can be simplified. C# designers refer to this as the elimination of vertical waste.
  • Implicit using directives – This feature forces the compiler to automatically import a set of usings based on the project type. In essence, for each project type, an implicit set of global using is defined, so it doesn’t need to be explicitly stated in each file. For console applications these are:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;

1.1  ASP.NET 6 Web Template

Using the same features and similar processes the template for web applications is minimized as well. Now it contains just several lines of code:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/", () => "Hello World!");

app.Run();

1.2  ASP.NET 6 MVC Template

MVC Template is modernized as well:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

2. Package Validation Tooling

Package validation tooling is one of the features that immediately caught my eye as something that is going to be very usefull.  This feature was presented with .NET 6 preview 5 and it is a great example of an in-house tool that can be shipped to all users. By this, I mean that in dotnet/runtime repo there is already tooling which ensures that multi-targeted packages are well-formed.

Coding Visual

Package validation tooling allows developers to validate that their packages are consistent and well-formed during package development. It allows developers to validate packages against their previous versions, against versions of the framework, and against runtime. With the growing .NET ecosystem, this turned out to be a big problem, especially with cross-targeting packages and with new platforms where the community is not strong enough yet. With this tooling you can not even create a package which is not validated, making it safer for the whole .NET ecosystem.

We have seen this issue in the wild, even with 1st parties, for example, the Azure AD libraries.

Immo Landwerth

Program manager on the .NET team, Microsoft

At the moment, package validation tooling can be used as an MSBuild SDK package. This means you need to include it in your project like this:

<Project Sdk="Microsoft.NET.Sdk">

  <Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />

  <PropertyGroup>
  <TargetFrameworks>net5;net6.0</TargetFrameworks>
  </PropertyGroup>

</Project>

Note that this is also new <Sdk> syntax. The above example describes a package that targets .NET 5 and .NET 6. This will create a set of tasks that will be run during donet pack or donet build commands which will perform validation of the package. As we mentioned, there is three validation that can be done with this tooling, so let’s check them out.

2.1  Package Validation – Compatible Frameworks

Package Validation Tooling in general caches errors during pack time. In this particular example, this tooling needs to verify that code compiled against one framework can run against another framework. As we could saw in the previous example, we might want a package that targets .NET 5 and .NET 6. When building such a package, we might try something like this:

public class TestClass
{
      #if NET6_0_OR_GREATER
      public void DoSomething(ReadOnlySpan<char> input)
      {
              //Some .NET 6 Operations
      }
      #else
      public void DoSomething(string input)
      {
              //Some .NET 5 Operations
      }
      #endif
}

If we try to create a package using dotnet pack on the code above we receive the following error:

C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error : net5.0 assembly api surface area should be compatible with net6.0 assembly surface area so we can compile against net5.0 
and run on net6.0 .framework. [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]
C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error : API Compatibility errors between lib/net5.0/TestPackage.dll (left) and lib/net6.0/TestPackage.dll (right): [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]
C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error CP0002: Member 'TestPackage.TestClass.DoSomething(string)' exists on the left but not on the right [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj]

What happened? Well, your code is not compatible with both frameworks. From these messages, we can understand that we shouldn’t exclude DoSomething with the string input, just provide the additional one from .NET 6 with ReadOnlySpan<char>. Like this:

public class TestClass
{
    #if NET6_0_OR_GREATER
    public void DoStringManipulation(ReadOnlySpan<char> input)
    {
        // use spans to do string operations.
    }
    #endif
      
    public void DoStringManipulation(string input)
    {
      // Do some string operations.
    }
}

Run donet pack again and you see something like this:

Microsoft (R) Build Engine version 17.0.0-preview-21378-03+d592862ed for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

Determining projects to restore...
All projects are up-to-date for restore.
You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
TestPackage -> C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\Debug\net5\TestPackage.dll
TestPackage -> C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\Debug\net6.0\TestPackage.dll
Successfully created package 'C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\Debug\TestPackage.1.0.0.nupkg'.

2.2  Package Validation – Version of the Package

How many times did you break the behavior of the previous version of the package, even though from the code perspective everything was ok? For example, you could break Liskov Substitution Principle and create a binary breaking change. This happens when you change the public API of your package, so assemblies compiled against older versions of your library are no longer able to call the API. Package Validation Tooling helps you detect this. 

For example, let’s assume that we migrated our TestClass to .NET 6 and published it within the package. To ensure that your future changes don’t make any binary braking change you should use PackageValidationBaselineVersion to your project. Tooling detects any braking changes against already created previous versions of the package. The project is defined like this:

<Project Sdk="Microsoft.NET.Sdk">

  <Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />

  <PropertyGroup>
  <TargetFrameworks>net6.0</TargetFrameworks>
  <PackageVersion>2.0.0</PackageVersion>
  <PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
  </PropertyGroup>

</Project>
Programming Visual

The code of TestClass looks like this:

public class TestClass
{
  public void DoSomething(string input)
  {
    //Some .NET 6 Operations
  }
}

Now, here is what happens if we want to extend DoSomething with additional input like this:

public class TestClass
{
  public void DoSomething(string input, string input2)
  {
    //Some .NET 6 Operations
  }
}

With this, we are definitely creating binary breaking change. However, because we used PackageValidationBaselineVersion, we receive an error during pack time:

C:\Users\n.zivkovic\.nuget\packages\microsoft.dotnet.packagevalidation\1.0.0-preview.5.21302.8\build\Microsoft.DotNet.PackageValidation.targets(24,5): error : 
API Compatibility errors between lib/net5.0/TestPackage.dll (left) and lib/net6.0/TestPackage.dll (right): [C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\TestPackage.csproj] 
There are braking changes between versions. Please add or modify apis in the reicent version or supress the intentional braking changes.

As the message suggests, we need to add additional override of the DoSomething function:

public class TestClass
{
  public void DoSomething(string input)
  {
    //Some .NET 6 Operations
  }

  public void DoSomething(string input, string input2)
  {
    //Some .NET 6 Operations
  }
}

Alternatevly, the same can be achieved by using PackageValidationBaselinePath:

<Project Sdk="Microsoft.NET.Sdk">

  <Sdk Name="Microsoft.DotNet.PackageValidation" Version="1.0.0-preview.5.21302.8" />

  <PropertyGroup>
  <TargetFrameworks>net6.0</TargetFrameworks>
  <PackageVersion>2.0.0</PackageVersion>
    <PackageValidationBaselinePath>"C:\Users\n.zivkovic\source\repos\TestPackage\TestPackage\bin\DebugV1\net6.0\"</PackageValidationBaselinePath>
  </PropertyGroup>

</Project>

2.3  Package Validation – Runtime

We can decide to go one step further and decide that we want our package to work on Unix and Windows. Maybe our DoSomething method needs to do something different in these systems, or they need to execute different code. Observe the new TestClass:

public class TestClass
{
    #if Unix
    public void DoSomething(string input, bool input2)
    {
          // Unix specific stuff
    }
    #else
    public void DoSomething(string input)
    {
            // Windows specific stuff
    }
    #endif
}

However, this code contains an error. As you can see we are missing DoSomething(string input) with just one parameter for Unix. Package Validation tooling will tell us exactly this. The code should look like this:

public class TestClass
{
    #if Unix
    public void DoSomething(string input, bool input2)
    {
        // Unix specific stuff
    }

    public void DoSomething(string input)
    {
        // Unix specific stuff
    }
    #else
    public void DoSomething(string input)
    {
        // Windows specific stuff
    }
    #endif
}

3. .NET 6 Multi-platform App UI (MAUI)

Since before .NET 5 Microsoft talks a lot about “big unification”. In general, the idea is that there is only one .NET that is used to target Windows, Linux, macOS, iOS, Android, tvOS, watchOS and WebAssembly. With .NET 5 they have started this process and produced a single .NET runtime and framework that can be used everywhere and that has uniform runtime behaviors and developer experiences.

.NET 6 MAUI Capabilities

With .NET 6 they move further with this idea and include Xamarin under the umbrella along with Android and iOS. At Build 2020 Microsoft announced .NET Multi-platform App UI (MAUI), a modern UI toolkit built on top of Xamarin. This decision is of course community-driven to a degree. There has been a lot of requests to share code across your mobile and desktop apps. So with .NET MAUI, you can build applications that can run on Android, iOS, macOS, and Windows from a single code-base. 

Xamarin has always been a part of .NET, but now it ships as a core workload, shares the same base class library as other workloads like Blazor, and adopts the modern SDK Style project system for consistent tooling experiences. 

David Ortinau

Principal Program Manager, .NET Multi-platform App UI, Microsoft

The .NET 6 has a library that is called Base Class Library (BCL). This library is the base for different specific frameworks, like .NET for Android, .NET for iOS, .NET for macOS and Windows UI. All these frameworks access BCL and in turn, it abstracts the details of the platform. So, you don’t need to know these details when you write your code. It is all abstracted so you can write code that is cross-platform without stress.

Apart from that, The Xamarin.Forms toolkit and Xamarin.Essentials are at the core of .NET MAUI. These tools are extended so they can be used for developing mobile and desktop applications. Since it is already established and long-lasting technology with a large community, it was a logical choice for the base of .NET MAUI.

.NET 6 Architecture

To enable this feature in Visual Studio 2022 Preview, with following workloads: 

.NET 6 MAUI Workloads

3.1 .NET MAUI Single Project

Microsoft uses the name Single Project for the project file that collects all platform specifics under one roof. Basically, a shared project file that can target all platforms.  This project is built in the SDK-style spirit of the projects in .NET 6. This project is separated into several sections:  Resources (Images, Fonts, Icons, etc.), App Manifest, NuGet Packages, Platform-specific Code. Here is how that looks in Visual Studio 2022:

.NET MAUI Single Project

3.2 .NET MAUI Fundamentals

.NET MAUI projects use .NET Generic Host, which is a working service that is built by HostBuilder. For each platform, there is an entry point that initializes this builder. This way application is bootstrapped by a Generic Host. Once the builder is initialized, the platform calls Configure method from the Startup class. This class can be observed as the entry point of the application and it creates the initial page of the application. Here it is:

using Microsoft.Maui;
using Microsoft.Maui.Hosting;

public class Startup : IStartup
{
    public void Configure(IAppHostBuilder appBuilder)
    {
        appBuilder
            .UseMauiApp<App>();
    }
}
.NET 6 MAUI New Architecture

The App class is the class of your application. It must derive from the Application class and must override the CreateWindow method. Something like this:

using Microsoft.Maui;
using Microsoft.Maui.Controls;

public partial class App : Application
{
    protected override IWindow CreateWindow(IActivationState activationState)
    {
        return new Window(new MainPage());
    }
}

Learn more about .NET MAUI here.

4. SDK Workloads

In the .NET 6 preview 4, we were first introduced to SDK workloads. This feature allows users to install only necessary SDKs, not complete “all-in-one” SDK. So, if you want to download and install just ASP.NET Core, or just Xamarin, you can do that. In essence, this feature is just a package manager for SDKs. All this is supported with the new CLI keyword – workload.

This feature is also a part of the bigger story. When .NET Core was initially developed there was a need for an all-in-one monolithic SDK. This is especially the case for some of the .NET Core 2.x and .NET Core 3.x features. However, since supported workloads of the .NET SDK grow a new need emerged – a need for small and focused SDK. 

Programming Visual

As mentioned, this feature is supported with CLI workload command. It grew gradually through .NET 6 previews 4, 5 and 6. First, the install command was introduced.

4.1 .NET 6 SDK Workload CLI

This command allows you to install any SDK workload:

dotnet workload install name_of_the_workload

Other commands extend this Package Manager-like experience. There is a command with which you can list all installed workloads:

dotnet workload list

It is critical that .NET source build capabilities are extended to include the new optional workload model so that the community can build all workloads for their scenarios. In fact, we hope that source build will become simpler and more efficient with this model.

Immo Landwerth

Program manager on the .NET team, Microsoft

There is also the command for updating all installed SDK workloads to latest version:

dotnet workload update

You can also list workloads available to install:

dotnet workload search

Of course, you can uninstall workload:

dotnet workload uninstall

In case something went wrong (like your internet connection broke) during workload installation, you can repair it with:

dotnet workload repair

Finally, .NET 6 workload CLI provides a way to check whether new versions of the SDK and Runtimes are available with new command:

dotnet sdk check

5. Inner-Loop Performance

When there is a new version of the framework, you sort of expect that the performance is going to be improved. The same is the case with .NET. Previous versions (and this version too) of the framework brought different performance improvements to the applications that are built with this tool. In addition to that, .NET 6 focuses on improving inner-loop performance too, i.e. improving the performance of CLI, runtime, MSBuild, etc. So, the focus is put on optimizing tools and workflows that can improve the experience of developers.

Programming Visual

I love .NET and its features. Is it good for all things? Definitely no. However, I do like working with it and suggest it as a go-to platform for many solutions. This is often received with the sort of backlash from other developers who didn’t have good experiences with it. Most of these experiences are connected with these inner-loop performances.

Many other platforms have dramatically better performance of the common inner-loop operations than .NET. This is very offputting, especially to newcomers to .NET. That is why I am really happy that .NET 6 brings improvements in that regard. From preview 2 we had a lot of information about some of the improvements.

Inner-loop performance is critical to providing the developer experiences that are increasingly expected/demanded by developers. How fast developers can make code changes and see the resulting impact in their apps is directly proportional to how productive they can be.

Damian Edwards

PM Architect on the .NET team, Microsoft

It would be had to cover all the new improvements that are part of .NET 6, a completely new article would be needed for that, so I will just cover few. Still, here is the complete list of improvements:

  • Improve performance of CLI/SDK/MSBuild that relate to the developer inner-loop.
  • Improve and modernize the launch/debug/update loop
    Hot Reload application changes during development inner-loop from the .NET CLI
  • Hot Reload for Blazor Components
  • Hot Reload for ASP.NET Core
  • Reimplement the Razor compiler using source generators & introduce incrementally
  • Blazor WebAssembly Hot Restart
    Support Code Hot Reload (EnC) for Razor
  • ASP.NET Core Hot Restart
  • Xamarin developers have an improved development experience on .NET 6
  • Ensure the BCL supports Hot Reload
  • Reduce EF Core application startup time via compiled models

5.1 .NET 6 Hot Reload

Hot Reload is one of the biggest inner-loop performances that is definitely going to provide faster development. The idea is simple – allow code changes while the application is still running. This will improve developer productivity bigtime. From now on you will not have to make the change, build the solution, run it again, and get back to the point in the application that was the cause of the change in the first place. I never thought I will see the day of this happening in .NET. That is awesome!

For the .NET with all its various features, support for different platforms, different types of applications, etc. this is a big feature. Changes involve the runtimes (both coreclr and mono), the C# Roslyn compiler, the app models (for example, Blazor, .NET MAUI), and the developer tools (for example, CLI, Visual Studio).

5.2 Faster MSBuild

This improvement was presented in preview 2 and it is part of the bigger epic where CLI, SDK and MSBuild should improve. At the moment, MSBuild is improved and the faster builds will impact developer experience in a positive way. In the image below you can see how much faster are the builds in .NET 6 compared to .NET 5:

.NET 6 Performance Improvements

5.3 Minimizing JITing

Microsoft focused on eliminating unnecessary JITing in this version as well. For example, because of some of the newly added SDKs some of the assemblies are not pre-compiled. The installer is responsible for that. This caused higher JIT time. 

6. .NET 6 Preview Features – Peek into .NET 7

.NET 6 will contain two features, which are going to be in preview form. The idea is to get feedback from the community as soon as possible and improve on that. Preview features are opt-in because it is expected to change, likely in breaking ways. I think this really cool and that this will be standard practice from now on. According to Microsoft, they enabled a “preview” API mechanism, and that this will be the case.

At the moment, we can not really try out these features, since they are not a part of the current .NET 6 preview 7, however, they will be available in .NET RC1. The configuration will be available through the RequiresPreviewFeatures attribute and corresponding analyzer. I decided to include them in this article because they seem really interesting and might have a larger impact later on. So, let’s see what they are all about.

It is highly recommended that you try the feature out and provide feedback if there are scenarios or functionality you feel is missing or could otherwise be improved.

Tanner Gooding

Software Developer on the .NET team, Microsoft

In order to try them, you should configure project file like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnablePreviewFeatures>true</EnablePreviewFeatures>
    <LangVersion>preview</LangVersion>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
  </ItemGroup>

</Project>

6.1 Static Abstracts in Interfaces

Yes, I know, my head started itching when I read the above construction of words too. However, the idea is quite simple – declare static abstract methods as part of an interface and implement them in the derived type. 

This feature will not be available for other abstractions, like an abstract class, and you will not be able to access these methods through an interface. Also, and so static and virtual will not be a valid combination.

Programming Visual

Here is how that looks like in the code:

public interface ISomeInterface<TSelf>
    where TSelf : ISomeInterface<TSelf>
{
    static abstract TSelf DoSomething(string input);
}

public readonly struct SomeImplementation : ISomeInterface<Guid>
{
    public static Guid DoSomething(string input);
    {
        /* .NET 6 Implementation */
    }
}

My feeling is that this feature is sort of a “residual feature” so to say. It seems it is just a prerequisite for the next preview feature that we will cover – Generic Math.

6.2 Generic Math

Did you have a situation in which you could use operands between generic type variables, but that was just not possible? This is a feature that has been requested for a while by the .NET community, and personally, I had several situations where this could be quite usefull.

This feature is basically built by creating new static abstract interfaces. These interfaces are built for different operators available to the language. These interfaces are granular and it is possible to extend them or modify them. Here I am using code available at Microsoft because I think it is quite neat and shows the benefits of generic math. It is an implementation of Standard Deviation using generic math:

public static TResult Sum<T, TResult>(IEnumerable<T> values)
    where T : INumber<T>
    where TResult : INumber<TResult>
{
    TResult result = TResult.Zero;

    foreach (var value in values)
    {
        result += TResult.Create(value);
    }

    return result;
}

public static TResult Average<T, TResult>(IEnumerable<T> values)
    where T : INumber<T>
    where TResult : INumber<TResult>
{
    TResult sum = Sum<T, TResult>(values);
    return TResult.Create(sum) / TResult.Create(values.Count());
}

public static TResult StandardDeviation<T, TResult>(IEnumerable<T> values)
    where T : INumber<T>
    where TResult : IFloatingPoint<TResult>
{
    TResult standardDeviation = TResult.Zero;

    if (values.Any())
    {
        TResult average = Average<T, TResult>(values);
        TResult sum = Sum<TResult, TResult>(values.Select((value) => {
            var deviation = TResult.Create(value) - average;
            return deviation * deviation;
        }));
        standardDeviation = TResult.Sqrt(sum / TResult.Create(values.Count() - 1));
    }

    return standardDeviation;
}

Conclusion

In this article, we covered 6 interesting .NET 6 features, that I think will change the way we perform day-to-day development. I am really exiting about all of them and can’t wait for the 9th of November to try them out in their full glory.

Thanks for reading!

This bundle of e-books is specially crafted for beginners.
Everything from Python basics to the deployment of Machine Learning algorithms to production in one place.
Become a Machine Learning Superhero TODAY!

Nikola M. Zivkovic

Nikola M. Zivkovic is the author of the books: Ultimate Guide to Machine Learning and Deep Learning for Programmers. He loves knowledge sharing, and he is an experienced speaker. You can find him speaking at meetups, conferences, and as a guest lecturer at the University of Novi Sad.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK